commit 7238986fa1dcbf3d167dbba3f690266eb28905af Author: Bram Dingelstad Date: Sat Nov 20 11:10:20 2021 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..271f639 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.import +.DS_Store diff --git a/Dialogue.tscn b/Dialogue.tscn new file mode 100644 index 0000000..87700fd --- /dev/null +++ b/Dialogue.tscn @@ -0,0 +1,8 @@ +[gd_scene format=2] + +[node name="Dialogue" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/addons/Wol/.!11255!.DS_Store b/addons/Wol/.!11255!.DS_Store new file mode 100644 index 0000000..d0e2670 Binary files /dev/null and b/addons/Wol/.!11255!.DS_Store differ diff --git a/addons/Wol/assets/.!11279!icon.png b/addons/Wol/assets/.!11279!icon.png new file mode 100644 index 0000000..e69de29 diff --git a/addons/Wol/assets/.!11280!.DS_Store b/addons/Wol/assets/.!11280!.DS_Store new file mode 100644 index 0000000..e69de29 diff --git a/addons/Wol/assets/icon.png b/addons/Wol/assets/icon.png new file mode 100644 index 0000000..c52b1db Binary files /dev/null and b/addons/Wol/assets/icon.png differ diff --git a/addons/Wol/assets/icon.png.import b/addons/Wol/assets/icon.png.import new file mode 100644 index 0000000..76d43f9 --- /dev/null +++ b/addons/Wol/assets/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-79ec48c27aef60cbae9d34f0545addad.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Wol/assets/icon.png" +dest_files=[ "res://.import/icon.png-79ec48c27aef60cbae9d34f0545addad.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/addons/Wol/autoloads/execution_states.gd b/addons/Wol/autoloads/execution_states.gd new file mode 100644 index 0000000..259e4f2 --- /dev/null +++ b/addons/Wol/autoloads/execution_states.gd @@ -0,0 +1,234 @@ +extends Node + +#VM Execution States + +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, + + #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 +} + +enum ValueType{ + Number, + Str, + Boolean, + Variable, + Nullean#null lel +} + +func defaultValue(type): + pass + +static func token_type_name(value:int)->String: + 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] + + +#same as top one woops +func token_name(type)->String: + var string : String = "" + + for key in TokenType.keys(): + if TokenType[key] == type: + return key + return string + +func bytecode_name(bytecode): + return [ + "Label", + "JumpTo", + "Jump", + "RunLine", + "RunCommand", + "AddOption", + "ShowOptions", + "PushString", + "PushNumber", + "PushBool", + "PushNull", + "JumpIfFalse", + "Pop", + "CallFunc", + "PushVariable", + "StoreVariable", + "Stop", + "RunNode" + ][bytecode] + +#combine all the programs in the provided array +static func combine_programs(programs : Array = []): + var YarnProgram = load("res://addons/Wol/core/program/program.gd") + if programs.size() == 0: + printerr("no programs to combine - you failure") + return + var p = YarnProgram.new() + + for program in programs: + for nodeKey in program.yarnNodes.keys(): + if p.yarnNodes.has(nodeKey): + printerr("Program with duplicate node names %s "% nodeKey) + return + p.yarnNodes[nodeKey] = program.yarnNodes[nodeKey] + + return p + diff --git a/addons/Wol/core/compiler/analyzer.gd b/addons/Wol/core/compiler/analyzer.gd new file mode 100644 index 0000000..3467adc --- /dev/null +++ b/addons/Wol/core/compiler/analyzer.gd @@ -0,0 +1,20 @@ +extends Node + + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass + + +#This is a test of the keyboard omg is it working i hope so please make sure it works 12345678910 +#Do you think that it works. So maybe PPPP \ No newline at end of file diff --git a/addons/Wol/core/compiler/compiler.gd b/addons/Wol/core/compiler/compiler.gd new file mode 100644 index 0000000..9b2dbef --- /dev/null +++ b/addons/Wol/core/compiler/compiler.gd @@ -0,0 +1,463 @@ +extends Object + +const Lexer = preload("res://addons/Wol/core/compiler/lexer.gd") +const LineInfo = preload("res://addons/Wol/core/program/yarn_line.gd") +const YarnNode = preload("res://addons/Wol/core/program/yarn_node.gd") +const Instruction = preload("res://addons/Wol/core/program/instruction.gd") +const YarnProgram = preload("res://addons/Wol/core/program/program.gd") +const Operand = preload("res://addons/Wol/core/program/operand.gd") + + +#patterns +const INVALIDTITLENAME = "[\\[<>\\]{}\\|:\\s#\\$]" + +#ERROR Codes +const NO_ERROR = 0x00 +const LEXER_FAILURE = 0x01 +const PARSER_FAILURE = 0x02 +const INVALID_HEADER = 0x04 +const DUPLICATE_NODES_IN_PROGRAM = 0x08 +const ERR_COMPILATION_FAILED = 0x10 + +var _errors : int +var _lastError : int + +#-----Class vars +var _currentNode : YarnNode +var _rawText : bool +var _fileName : String +var _containsImplicitStringTags : bool +var _labelCount : int = 0 + +# +var _stringTable : Dictionary = {} +var _stringCount : int = 0 +# +var _tokens : Dictionary = {} + +static func compile_string(source: String, filename: String) -> YarnProgram: + var Parser = load("res://addons/Wol/core/compiler/parser.gd") + var Compiler = load("res://addons/Wol/core/compiler/compiler.gd") + + var compiler = Compiler.new() + compiler._fileName = filename + + #--------------Nodes + var headerSep : RegEx = RegEx.new() + headerSep.compile("---(\r\n|\r|\n)") + var headerProperty : RegEx = RegEx.new() + headerProperty.compile("(?.*): *(?.*)") + + assert(not not headerSep.search(source), "No headers found") + + var lineNumber: int = 0 + + var sourceLines : Array = source.split('\n',false) + for i in range(sourceLines.size()): + sourceLines[i] = sourceLines[i].strip_edges(false,true) + + var parsedNodes : Array = [] + + while lineNumber < sourceLines.size(): + + var title : String + var body : String + + #get title + while true: + var line : String = sourceLines[lineNumber] + lineNumber+=1 + + if !line.empty(): + var result = headerProperty.search(line) + if result != null : + var field : String = result.get_string("field") + var value : String = result.get_string("value") + + if field == "title": + title = value + + if(lineNumber >= sourceLines.size() || sourceLines[lineNumber] == "---"): + break + + + lineNumber+=1 + + #past header + var bodyLines : PoolStringArray = [] + + while lineNumber < sourceLines.size() && sourceLines[lineNumber]!="===": + bodyLines.append(sourceLines[lineNumber]) + lineNumber+=1 + + lineNumber+=1 + + body = bodyLines.join('\n') + var lexer = Lexer.new() + + var tokens : Array = lexer.tokenize(body) + var parser = Parser.new(tokens) + + var parserNode = parser.parse_node() + + parserNode.name = title + parsedNodes.append(parserNode) + while lineNumber < sourceLines.size() && sourceLines[lineNumber].empty(): + lineNumber+=1 + + #--- End parsing nodes--- + + var program = YarnProgram.new() + + #compile nodes + for node in parsedNodes: + compiler.compile_node(program, node) + + for key in compiler._stringTable: + program.yarnStrings[key] = compiler._stringTable[key] + + return program + +func compile_node(program:YarnProgram,parsedNode)->void: + if program.yarnNodes.has(parsedNode.name): + emit_error(DUPLICATE_NODES_IN_PROGRAM) + printerr("Duplicate node in program: %s" % parsedNode.name) + else: + var nodeCompiled : YarnNode = YarnNode.new() + + nodeCompiled.nodeName = parsedNode.name + nodeCompiled.tags = parsedNode.tags + + #raw text + if parsedNode.source != null && !parsedNode.source.empty(): + nodeCompiled.sourceId = register_string(parsedNode.source,parsedNode.name, + "line:"+parsedNode.name, 0, []) + else: + #compile node + var startLabel : String = register_label() + emit(YarnGlobals.ByteCode.Label,nodeCompiled,[Operand.new(startLabel)]) + + for statement in parsedNode.statements: + generate_statement(nodeCompiled,statement) + + + #add options + #todo: add parser flag + + var danglingOptions = false + for instruction in nodeCompiled.instructions : + if instruction.operation == YarnGlobals.ByteCode.AddOption: + danglingOptions = true + if instruction.operation == YarnGlobals.ByteCode.ShowOptions: + danglingOptions = false + + if danglingOptions: + emit(YarnGlobals.ByteCode.ShowOptions, nodeCompiled) + emit(YarnGlobals.ByteCode.RunNode, nodeCompiled) + else: + emit(YarnGlobals.ByteCode.Stop, nodeCompiled) + + + program.yarnNodes[nodeCompiled.nodeName] = nodeCompiled + +func register_string(text:String,nodeName:String,id:String="",lineNumber:int=-1,tags:Array=[])->String: + var lineIdUsed : String + + var implicit : bool + + if id.empty(): + lineIdUsed = "%s-%s-%d" % [self._fileName,nodeName,self._stringCount] + self._stringCount+=1 + + #use this when we generate implicit tags + #they are not saved and are generated + #aka dummy tags that change on each compilation + _containsImplicitStringTags = true + + implicit = true + else : + lineIdUsed = id + implicit = false + + var stringInfo : LineInfo = LineInfo.new(text,nodeName,lineNumber,_fileName,implicit,tags) + #add to string table and return id + self._stringTable[lineIdUsed] = stringInfo + + return lineIdUsed + +func register_label(comment:String="")->String: + _labelCount+=1 + return "L%s%s" %[ _labelCount , comment] + +func emit(bytecode,node:YarnNode=_currentNode,operands:Array=[]): + var instruction : Instruction = Instruction.new(null) + instruction.operation = bytecode + instruction.operands = operands + # print("emitting instruction to %s"%node.nodeName) + + if(node == null): + printerr("trying to emit to null node with byteCode: %s" % bytecode) + return; + node.instructions.append(instruction) + if bytecode == YarnGlobals.ByteCode.Label : + #add to label table + node.labels[instruction.operands[0].value] = node.instructions.size()-1 + + +func get_string_tokens()->Array: + return [] + +#compile header +func generate_header(): + pass + +#compile instructions for statements +#this will walk through all child branches +#of the parse tree +func generate_statement(node,statement): + # print("generating statement") + match statement.type: + YarnGlobals.StatementTypes.CustomCommand: + generate_custom_command(node,statement.customCommand) + YarnGlobals.StatementTypes.ShortcutOptionGroup: + generate_shortcut_group(node,statement.shortcutOptionGroup) + YarnGlobals.StatementTypes.Block: + generate_block(node,statement.block.statements) + YarnGlobals.StatementTypes.IfStatement: + generate_if(node,statement.ifStatement) + YarnGlobals.StatementTypes.OptionStatement: + generate_option(node,statement.optionStatement) + YarnGlobals.StatementTypes.AssignmentStatement: + generate_assignment(node,statement.assignment) + YarnGlobals.StatementTypes.Line: + generate_line(node,statement,statement.line) + _: + emit_error(ERR_COMPILATION_FAILED) + printerr("illegal statement type [%s]- could not generate code" % statement.type) + +#compile instructions for custom commands +func generate_custom_command(node,command): + #print("generating custom command") + #can evaluate command + if command.expression != null: + generate_expression(node,command.expression) + else: + var commandString = command.clientCommand + if commandString == "stop": + emit(YarnGlobals.ByteCode.Stop,node) + else : + emit(YarnGlobals.ByteCode.RunCommand,node,[Operand.new(commandString)]) + +#compile instructions for linetags and use them +# \#line:number +func generate_line(node,statement,line:String): + var num : String = register_string(line,node.nodeName,"",statement.lineNumber,[]); + emit(YarnGlobals.ByteCode.RunLine,node,[Operand.new(num)]) + +func generate_shortcut_group(node,shortcutGroup): + # print("generating shortcutoptopn group") + var end : String = register_label("group_end") + + var labels : Array = []#String + + var optionCount : int = 0 + + for option in shortcutGroup.options: + var opDestination : String = register_label("option_%s"%[optionCount+1]) + labels.append(opDestination) + + var endofClause : String = "" + + if option.condition != null : + endofClause = register_label("conditional_%s"%optionCount) + generate_expression(node,option.condition) + emit(YarnGlobals.ByteCode.JumpIfFalse,node,[Operand.new(endofClause)]) + + var labelLineId : String = ""#no tag TODO: ADD TAG SUPPORT + var labelStringId : String = register_string(option.label,node.nodeName, + labelLineId,option.lineNumber,[]) + + emit(YarnGlobals.ByteCode.AddOption,node,[Operand.new(labelStringId),Operand.new(opDestination)]) + + if option.condition != null : + emit(YarnGlobals.ByteCode.Label,node,[Operand.new(endofClause)]) + emit(YarnGlobals.ByteCode.Pop,node) + + optionCount+=1 + + emit(YarnGlobals.ByteCode.ShowOptions,node) + emit(YarnGlobals.ByteCode.Jump,node) + + optionCount = 0 + + for option in shortcutGroup.options: + emit(YarnGlobals.ByteCode.Label,node,[Operand.new(labels[optionCount])]) + + if option.node != null : + generate_block(node,option.node.statements) + emit(YarnGlobals.ByteCode.JumpTo,node,[Operand.new(end)]) + optionCount+=1 + + #end of option group + emit(YarnGlobals.ByteCode.Label,node,[Operand.new(end)]) + #clean up + emit(YarnGlobals.ByteCode.Pop,node) + + + +#compile instructions for block +#blocks are just groups of statements +func generate_block(node,statements:Array=[]): + # print("generating block") + if !statements.empty(): + for statement in statements: + generate_statement(node,statement) + + +#compile if branching instructions +func generate_if(node,ifStatement): + # print("generating if") + #jump to label @ end of every clause + var endif : String = register_label("endif") + + for clause in ifStatement.clauses: + var endClause : String = register_label("skip_clause") + + if clause.expression!=null: + generate_expression(node,clause.expression) + emit(YarnGlobals.ByteCode.JumpIfFalse,node,[Operand.new(endClause)]) + + generate_block(node,clause.statements) + emit(YarnGlobals.ByteCode.JumpTo,node,[Operand.new(endif)]) + + if clause.expression!=null: + emit(YarnGlobals.ByteCode.Label,node,[Operand.new(endClause)]) + + if clause.expression!=null: + emit(YarnGlobals.ByteCode.Pop) + + + emit(YarnGlobals.ByteCode.Label,node,[Operand.new(endif)]) + + +#compile instructions for options +func generate_option(node,option): + # print("generating option") + var destination : String = option.destination + + if option.label == null || option.label.empty(): + #jump to another node + emit(YarnGlobals.ByteCode.RunNode,node,[Operand.new(destination)]) + else : + var lineID : String = ""#tags not supported TODO: ADD TAG SUPPORT + var stringID = register_string(option.label,node.nodeName,lineID,option.lineNumber,[]) + + emit(YarnGlobals.ByteCode.AddOption,node,[Operand.new(stringID),Operand.new(destination)]) + + +#compile instructions for assigning values +func generate_assignment(node,assignment): + # print("generating assign") + #assignment + if assignment.operation == YarnGlobals.TokenType.EqualToOrAssign: + #evaluate the expression to a value for the stack + generate_expression(node,assignment.value) + else : + #this is combined op + #get value of var + emit(YarnGlobals.ByteCode.PushVariable,node,[assignment.destination]) + + #evaluate the expression and push value to stack + generate_expression(node,assignment.value) + + #stack contains oldvalue and result + + match assignment.operation: + YarnGlobals.TokenType.AddAssign: + emit(YarnGlobals.ByteCode.CallFunc,node, + [Operand.new(YarnGlobals.token_type_name(YarnGlobals.TokenType.Add))]) + YarnGlobals.TokenType.MinusAssign: + emit(YarnGlobals.ByteCode.CallFunc,node, + [Operand.new(YarnGlobals.token_type_name(YarnGlobals.TokenType.Minus))]) + YarnGlobals.TokenType.MultiplyAssign: + emit(YarnGlobals.ByteCode.CallFunc,node, + [Operand.new(YarnGlobals.token_type_name(YarnGlobals.TokenType.MultiplyAssign))]) + YarnGlobals.TokenType.DivideAssign: + emit(YarnGlobals.ByteCode.CallFunc,node, + [Operand.new(YarnGlobals.token_type_name(YarnGlobals.TokenType.DivideAssign))]) + _: + printerr("Unable to generate assignment") + + #stack contains destination value + #store the top of the stack in variable + emit(YarnGlobals.ByteCode.StoreVariable,node,[Operand.new(assignment.destination)]) + + #clean stack + emit(YarnGlobals.ByteCode.Pop,node) + + +#compile expression instructions +func generate_expression(node,expression): + # print("generating expression") + #expression = value || func call + match expression.type: + YarnGlobals.ExpressionType.Value: + generate_value(node,expression.value) + YarnGlobals.ExpressionType.FunctionCall: + #eval all parameters + for param in expression.params: + generate_expression(node,param) + + #put the num of of params to stack + emit(YarnGlobals.ByteCode.PushNumber,node,[Operand.new(expression.params.size())]) + + #call function + emit(YarnGlobals.ByteCode.CallFunc,node,[Operand.new(expression.function)]) + _: + printerr("no expression") + +#compile value instructions +func generate_value(node,value): + # print("generating value") + #push value to stack + match value.value.type: + YarnGlobals.ValueType.Number: + emit(YarnGlobals.ByteCode.PushNumber,node,[Operand.new(value.value.as_number())]) + YarnGlobals.ValueType.Str: + var id : String = register_string(value.value.as_string(), + node.nodeName,"",value.lineNumber,[]) + emit(YarnGlobals.ByteCode.PushString,node,[Operand.new(id)]) + YarnGlobals.ValueType.Boolean: + emit(YarnGlobals.ByteCode.PushBool,node,[Operand.new(value.value.as_bool())]) + YarnGlobals.ValueType.Variable: + emit(YarnGlobals.ByteCode.PushVariable,node,[Operand.new(value.value.variable)]) + YarnGlobals.ValueType.Nullean: + emit(YarnGlobals.ByteCode.PushNull,node) + _: + printerr("Unrecognized valuenode type: %s" % value.value.type) + + +#get the error flags +func get_errors()->int: + return _errors + +#get the last error code reported +func get_last_error()->int: + return _lastError + +func clear_errors()->void: + _errors = NO_ERROR + _lastError = NO_ERROR + +func emit_error(error : int)->void: + _lastError = error + _errors |= _lastError + + +static func print_tokens(tokens:Array=[]): + var list : PoolStringArray = [] + list.append("\n") + for token in tokens: + list.append("%s (%s line %s)\n"%[YarnGlobals.token_type_name(token.type),token.value,token.lineNumber]) + print("TOKENS:") + print(list.join("")) diff --git a/addons/Wol/core/compiler/lexer.gd b/addons/Wol/core/compiler/lexer.gd new file mode 100644 index 0000000..9e67436 --- /dev/null +++ b/addons/Wol/core/compiler/lexer.gd @@ -0,0 +1,428 @@ +extends Object + +const LINE_COMENT : String = "//" +const FORWARD_SLASH : String = "/" + +const LINE_SEPARATOR : String = "\n" + +const BASE : String = "base" +const DASH : String = "-" +const COMMAND : String = "command" +const LINK : String = "link" +const SHORTCUT : String = "shortcut" +const TAG : String = "tag" +const EXPRESSION : String = "expression" +const ASSIGNMENT : String = "assignment" +const OPTION : String = "option" +const OR : String = "or" +const DESTINATION : String = "destination" + +var WHITESPACE : String = "\\s*" + +var _states : Dictionary = {} +var _defaultState : LexerState + +var _currentState : LexerState + +var _indentStack : Array = [] +var _shouldTrackIndent : bool = false + + +func _init(): + create_states() + +func create_states(): + var patterns : Dictionary = {} + patterns[YarnGlobals.TokenType.Text] = ".*" + + patterns[YarnGlobals.TokenType.Number] = "\\-?[0-9]+(\\.[0-9+])?" + patterns[YarnGlobals.TokenType.Str] = "\"([^\"\\\\]*(?:\\.[^\"\\\\]*)*)\"" + patterns[YarnGlobals.TokenType.TagMarker] = "\\#" + patterns[YarnGlobals.TokenType.LeftParen] = "\\(" + patterns[YarnGlobals.TokenType.RightParen] = "\\)" + patterns[YarnGlobals.TokenType.EqualTo] = "(==|is(?!\\w)|eq(?!\\w))" + patterns[YarnGlobals.TokenType.EqualToOrAssign] = "(=|to(?!\\w))" + patterns[YarnGlobals.TokenType.NotEqualTo] = "(\\!=|neq(?!\\w))" + patterns[YarnGlobals.TokenType.GreaterThanOrEqualTo] = "(\\>=|gte(?!\\w))" + patterns[YarnGlobals.TokenType.GreaterThan] = "(\\>|gt(?!\\w))" + patterns[YarnGlobals.TokenType.LessThanOrEqualTo] = "(\\<=|lte(?!\\w))" + patterns[YarnGlobals.TokenType.LessThan] = "(\\<|lt(?!\\w))" + patterns[YarnGlobals.TokenType.AddAssign] = "\\+=" + patterns[YarnGlobals.TokenType.MinusAssign] = "\\-=" + patterns[YarnGlobals.TokenType.MultiplyAssign] = "\\*=" + patterns[YarnGlobals.TokenType.DivideAssign] = "\\/=" + patterns[YarnGlobals.TokenType.Add] = "\\+" + patterns[YarnGlobals.TokenType.Minus] = "\\-" + patterns[YarnGlobals.TokenType.Multiply] = "\\*" + patterns[YarnGlobals.TokenType.Divide] = "\\/" + patterns[YarnGlobals.TokenType.Modulo] = "\\%" + patterns[YarnGlobals.TokenType.And] = "(\\&\\&|and(?!\\w))" + patterns[YarnGlobals.TokenType.Or] = "(\\|\\||or(?!\\w))" + patterns[YarnGlobals.TokenType.Xor] = "(\\^|xor(?!\\w))" + patterns[YarnGlobals.TokenType.Not] = "(\\!|not(?!\\w))" + patterns[YarnGlobals.TokenType.Variable] = "\\$([A-Za-z0-9_\\.])+" + patterns[YarnGlobals.TokenType.Comma] = "\\," + patterns[YarnGlobals.TokenType.TrueToken] = "true(?!\\w)" + patterns[YarnGlobals.TokenType.FalseToken] = "false(?!\\w)" + patterns[YarnGlobals.TokenType.NullToken] = "null(?!\\w)" + patterns[YarnGlobals.TokenType.BeginCommand] = "\\<\\<" + patterns[YarnGlobals.TokenType.EndCommand] = "\\>\\>" + patterns[YarnGlobals.TokenType.OptionStart] = "\\[\\[" + patterns[YarnGlobals.TokenType.OptionEnd] = "\\]\\]" + patterns[YarnGlobals.TokenType.OptionDelimit] = "\\|" + patterns[YarnGlobals.TokenType.Identifier] = "[a-zA-Z0-9_:\\.]+" + patterns[YarnGlobals.TokenType.IfToken] = "if(?!\\w)" + patterns[YarnGlobals.TokenType.ElseToken] = "else(?!\\w)" + patterns[YarnGlobals.TokenType.ElseIf] = "elseif(?!\\w)" + patterns[YarnGlobals.TokenType.EndIf] = "endif(?!\\w)" + patterns[YarnGlobals.TokenType.Set] = "set(?!\\w)" + patterns[YarnGlobals.TokenType.ShortcutOption] = "\\-\\>\\s*" + + #compound states + var shortcut_option : String= SHORTCUT + DASH + OPTION + var shortcut_option_tag : String = shortcut_option + DASH + TAG + var command_or_expression : String= COMMAND + DASH + OR + DASH + EXPRESSION + var link_destination : String = LINK + DASH + DESTINATION + + _states = {} + + _states[BASE] = LexerState.new(patterns) + _states[BASE].add_transition(YarnGlobals.TokenType.BeginCommand,COMMAND,true) + _states[BASE].add_transition(YarnGlobals.TokenType.OptionStart,LINK,true) + _states[BASE].add_transition(YarnGlobals.TokenType.ShortcutOption,shortcut_option) + _states[BASE].add_transition(YarnGlobals.TokenType.TagMarker,TAG,true) + _states[BASE].add_text_rule(YarnGlobals.TokenType.Text) + + _states[TAG] = LexerState.new(patterns) + _states[TAG].add_transition(YarnGlobals.TokenType.Identifier,BASE) + + _states[shortcut_option] = LexerState.new(patterns) + _states[shortcut_option].track_indent = true + _states[shortcut_option].add_transition(YarnGlobals.TokenType.BeginCommand,EXPRESSION,true) + _states[shortcut_option].add_transition(YarnGlobals.TokenType.TagMarker,shortcut_option_tag,true) + _states[shortcut_option].add_text_rule(YarnGlobals.TokenType.Text,BASE) + + _states[shortcut_option_tag] = LexerState.new(patterns) + _states[shortcut_option_tag].add_transition(YarnGlobals.TokenType.Identifier,shortcut_option) + + _states[COMMAND] = LexerState.new(patterns) + _states[COMMAND].add_transition(YarnGlobals.TokenType.IfToken,EXPRESSION) + _states[COMMAND].add_transition(YarnGlobals.TokenType.ElseToken) + _states[COMMAND].add_transition(YarnGlobals.TokenType.ElseIf,EXPRESSION) + _states[COMMAND].add_transition(YarnGlobals.TokenType.EndIf) + _states[COMMAND].add_transition(YarnGlobals.TokenType.Set,ASSIGNMENT) + _states[COMMAND].add_transition(YarnGlobals.TokenType.EndCommand,BASE,true) + _states[COMMAND].add_transition(YarnGlobals.TokenType.Identifier,command_or_expression) + _states[COMMAND].add_text_rule(YarnGlobals.TokenType.Text) + + _states[command_or_expression] = LexerState.new(patterns) + _states[command_or_expression].add_transition(YarnGlobals.TokenType.LeftParen,EXPRESSION) + _states[command_or_expression].add_transition(YarnGlobals.TokenType.EndCommand,BASE,true) + _states[command_or_expression].add_text_rule(YarnGlobals.TokenType.Text) + + _states[ASSIGNMENT] = LexerState.new(patterns) + _states[ASSIGNMENT].add_transition(YarnGlobals.TokenType.Variable) + _states[ASSIGNMENT].add_transition(YarnGlobals.TokenType.EqualToOrAssign, EXPRESSION) + _states[ASSIGNMENT].add_transition(YarnGlobals.TokenType.AddAssign, EXPRESSION) + _states[ASSIGNMENT].add_transition(YarnGlobals.TokenType.MinusAssign, EXPRESSION) + _states[ASSIGNMENT].add_transition(YarnGlobals.TokenType.MultiplyAssign, EXPRESSION) + _states[ASSIGNMENT].add_transition(YarnGlobals.TokenType.DivideAssign, EXPRESSION) + + _states[EXPRESSION] = LexerState.new(patterns) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.EndCommand, BASE) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Number) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Str) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.LeftParen) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.RightParen) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.EqualTo) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.EqualToOrAssign) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.NotEqualTo) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.GreaterThanOrEqualTo) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.GreaterThan) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.LessThanOrEqualTo) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.LessThan) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Add) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Minus) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Multiply) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Divide) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Modulo) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.And) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Or) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Xor) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Not) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Variable) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Comma) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.TrueToken) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.FalseToken) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.NullToken) + _states[EXPRESSION].add_transition(YarnGlobals.TokenType.Identifier) + + _states[LINK] = LexerState.new(patterns) + _states[LINK].add_transition(YarnGlobals.TokenType.OptionEnd, BASE, true) + _states[LINK].add_transition(YarnGlobals.TokenType.OptionDelimit, link_destination, true) + _states[LINK].add_text_rule(YarnGlobals.TokenType.Text) + + _states[link_destination] = LexerState.new(patterns) + _states[link_destination].add_transition(YarnGlobals.TokenType.Identifier) + _states[link_destination].add_transition(YarnGlobals.TokenType.OptionEnd, BASE) + + _defaultState = _states[BASE] + + for stateKey in _states.keys(): + _states[stateKey].stateName = stateKey + + pass + +func tokenize(text:String)->Array: + + _indentStack.clear() + _indentStack.push_front(IntBoolPair.new(0,false)) + _shouldTrackIndent = false + + var tokens : Array = [] + + _currentState = _defaultState + + var lines : PoolStringArray = text.split(LINE_SEPARATOR) + lines.append("") + + var lineNumber : int = 1 + + for line in lines: + tokens+=tokenize_line(line,lineNumber) + lineNumber+=1 + + var endOfInput : Token = Token.new(YarnGlobals.TokenType.EndOfInput,_currentState,lineNumber,0) + tokens.append(endOfInput) + + # print(tokens) + + return tokens + +func tokenize_line(line:String, lineNumber : int)->Array: + var tokenStack : Array = [] + + var freshLine = line.replace("\t"," ").replace("\r","") + + #record indentation + var indentation = line_indentation(line) + var prevIndentation : IntBoolPair = _indentStack.front() + + if _shouldTrackIndent && indentation > prevIndentation.key: + #we add an indenation token to record indent level + _indentStack.push_front(IntBoolPair.new(indentation,true)) + + var indent : Token = Token.new(YarnGlobals.TokenType.Indent,_currentState,lineNumber,prevIndentation.key) + indent.value = "%*s" % [indentation - prevIndentation.key,""] + + _shouldTrackIndent = false + tokenStack.push_front(indent) + + elif indentation < prevIndentation.key: + #de-indent and then emit indentaiton token + + while indentation < _indentStack.front().key: + var top : IntBoolPair = _indentStack.pop_front() + if top.value: + var deIndent : Token = Token.new(YarnGlobals.TokenType.Dedent,_currentState,lineNumber,0) + tokenStack.push_front(deIndent) + + + var column : int = indentation + + var whitespace : RegEx = RegEx.new() + var error = whitespace.compile(WHITESPACE) + if error != OK: + printerr("unable to compile regex WHITESPACE") + return [] + + while column < freshLine.length(): + + if freshLine.substr(column).begins_with(LINE_COMENT): + break + + var matched : bool = false + + for rule in _currentState.rules: + var found : RegExMatch = rule.regex.search(freshLine, column) + + if !found: + continue + + var tokenText : String + + if rule.tokenType == YarnGlobals.TokenType.Text: + #if this is text then we back up to the most recent + #delimiting token and treat everything from there as text. + + var startIndex : int = indentation + + if tokenStack.size() > 0 : + while tokenStack.front().type == YarnGlobals.TokenType.Identifier: + tokenStack.pop_front() + + var startDelimitToken : Token = tokenStack.front() + startIndex = startDelimitToken.column + + if startDelimitToken.type == YarnGlobals.TokenType.Indent: + startIndex += startDelimitToken.value.length() + if startDelimitToken.type == YarnGlobals.TokenType.Dedent: + startIndex = indentation + # + + column = startIndex + var endIndex : int = found.get_start() + found.get_string().length() + + tokenText = freshLine.substr(startIndex,endIndex-startIndex) + + else: + tokenText = found.get_string() + + column += tokenText.length() + + #pre-proccess string + if rule.tokenType == YarnGlobals.TokenType.Str: + tokenText = tokenText.substr(1,tokenText.length() - 2) + tokenText = tokenText.replace("\\\\", "\\") + tokenText = tokenText.replace("\\\"","\"") + + var token : Token = Token.new(rule.tokenType,_currentState,lineNumber,column,tokenText) + token.delimitsText = rule.delimitsText + + tokenStack.push_front(token) + + if rule.enterState != null && rule.enterState.length() > 0: + + if !_states.has(rule.enterState): + printerr("State[%s] not known - line(%s) col(%s)"%[rule.enterState,lineNumber,column]) + return [] + + enter_state(_states[rule.enterState]) + + if _shouldTrackIndent: + if _indentStack.front().key < indentation: + _indentStack.append(IntBoolPair.new(indentation,false)) + + matched = true + break + + if !matched: + # TODO: Send out some helpful messages + printerr("expectedTokens [%s] - line(%s) col(%s)"%["refineErrors.Lexer.tokenize_line",lineNumber,column]) + return [] + + var lastWhiteSpace : RegExMatch = whitespace.search(line,column) + if lastWhiteSpace: + column += lastWhiteSpace.get_string().length() + + + tokenStack.invert() + + return tokenStack + +func line_indentation(line:String)->int: + var indentRegex : RegEx = RegEx.new() + indentRegex.compile("^(\\s*)") + + var found : RegExMatch = indentRegex.search(line) + + if !found || found.get_string().length() <= 0: + return 0 + + return found.get_string().length() + +func enter_state(state:LexerState): + _currentState = state; + if _currentState.track_indent: + _shouldTrackIndent = true + +class Token: + var type : int + var value : String + + var lineNumber : int + var column : int + var text : String + + var delimitsText : bool= false + var paramCount : int + var lexerState : String + + func _init(type:int,state: LexerState, lineNumber:int = -1,column:int = -1,value:String =""): + self.type = type + self.lexerState = state.stateName + self.lineNumber = lineNumber + self.column = column + self.value = value + + func _to_string(): + return "%s (%s) at %s:%s (state: %s)" % [YarnGlobals.token_type_name(type),value,lineNumber,column,lexerState] + + +class LexerState: + + var stateName : String + var patterns : Dictionary + var rules : Array = [] + var track_indent : bool = false + + func _init(patterns): + self.patterns = patterns + + func add_transition(type : int, state : String = "",delimitText : bool = false)->Rule: + var pattern = "\\G%s" % patterns[type] + # print("pattern = %s" % pattern) + var rule = Rule.new(type,pattern,state,delimitText) + rules.append(rule) + return rule + + func add_text_rule(type : int, state : String = "")->Rule: + if contains_text_rule() : + printerr("State already contains Text rule") + return null + + var delimiters:Array = [] + for rule in rules: + if rule.delimitsText: + delimiters.append("%s" % rule.regex.get_pattern().substr(2)) + + var pattern = "\\G((?!%s).)*" % [PoolStringArray(delimiters).join("|")] + var rule : Rule = add_transition(type,state) + rule.regex = RegEx.new() + rule.regex.compile(pattern) + rule.isTextRule = true + return rule + + func contains_text_rule()->bool: + for rule in rules: + if rule.isTextRule: + return true + return false + + +class Rule: + var regex : RegEx + + var enterState : String + var tokenType : int + var isTextRule : bool + var delimitsText : bool + + func _init(type : int , regex : String, enterState : String, delimitsText:bool): + self.tokenType = type + self.regex = RegEx.new() + self.regex.compile(regex) + self.enterState = enterState + self.delimitsText = delimitsText + + func _to_string(): + return "[Rule : %s - %s]" % [YarnGlobals.token_type_name(tokenType),regex] + +class IntBoolPair: + var key : int + var value : bool + + func _init(key:int,value:bool): + self.key = key + self.value = value + diff --git a/addons/Wol/core/compiler/parser.gd b/addons/Wol/core/compiler/parser.gd new file mode 100644 index 0000000..476bf45 --- /dev/null +++ b/addons/Wol/core/compiler/parser.gd @@ -0,0 +1,961 @@ +extends Object + +const YarnGlobals = preload("res://addons/Wol/autoloads/execution_states.gd") +const Lexer = preload("res://addons/Wol/core/compiler/lexer.gd") + + +var _tokens : Array = []#token + +func _init(tokens): + self._tokens = tokens + +#how to handle operations +enum Associativity { + Left,Right,None +} + +func parse_node()->YarnNode: + return YarnNode.new("Start",null,self) + +func next_symbol_is(validTypes:Array)->bool: + var type = self._tokens.front().type + for validType in validTypes: + if type == validType: + return true + return false + +#look ahead for `<<` and `else` +func next_symbols_are(validTypes:Array)->bool: + var temp = []+_tokens + for type in validTypes: + if temp.pop_front().type != type: + return false + return true + +func expect_symbol(tokenTypes:Array = [])->Lexer.Token: + var t = self._tokens.pop_front() as Lexer.Token + var size = tokenTypes.size() + + if size == 0: + if t.type == YarnGlobals.TokenType.EndOfInput: + printerr("unexpected end of input") + return null + return t + + for type in tokenTypes: + if t.type == type: + return t + + printerr("unexpexted token: expected[ %s ] but got [ %s ]"% (tokenTypes+[t.type])) + return null + +static func tab(indentLevel : int , input : String,newLine : bool = true)->String: + return ("%*s| %s%s"% [indentLevel*2,"",input,("" if !newLine else "\n")]) + +func tokens()->Array: + return _tokens + +class ParseNode: + var parent : ParseNode + var lineNumber : int + var tags : Array# + + func _init(parent:ParseNode,parser): + self.parent = parent + var tokens : Array = parser.tokens() as Array + if tokens.size() > 0: + lineNumber = tokens.front().lineNumber + else: + lineNumber = -1 + tags = [] + + func tree_string(indentLevel : int)->String: + return "NotImplemented" + + func tags_to_string(indentLevel : int)->String: + return "%s" % "TAGSNOTIMPLEMENTED" + + func get_node_parent()->YarnNode: + var node = self + while node != null: + if node.has_method("yarn_node"): + return node as YarnNode + node = node.parent + return null + + func tab(indentLevel : int , input : String,newLine : bool = true)->String: + return ("%*s| %s%s"% [indentLevel*2,"",input,("" if !newLine else "\n")]) + + + func set_parent(parent): + self.parent = parent + +#this is a Yarn Node - contains all the text +class YarnNode extends ParseNode: + + var name : String + var source : String + + var editorNodeTags : Array =[]#tags defined in node header + var statements : Array = []# Statement + + func _init(name:String,parent:ParseNode,parser).(parent,parser): + + self.name = name + while (parser.tokens().size() > 0 && + !parser.next_symbol_is([YarnGlobals.TokenType.Dedent,YarnGlobals.TokenType.EndOfInput])): + statements.append(Statement.new(self,parser)) + #print(statements.size()) + + func yarn_node(): + pass + + func tree_string(indentLevel : int)->String: + + var info : PoolStringArray = [] + + for statement in statements: + info.append(statement.tree_string(indentLevel +1)) + + #print("printing TREEEEEEEEEEEEE") + + return info.join("") + + +class Header extends ParseNode: + pass + + +class Statement extends ParseNode: + var Type = YarnGlobals.StatementTypes + + var type : int + var block : Block + var ifStatement : IfStatement + var optionStatement : OptionStatement + var assignment : Assignment + var shortcutOptionGroup : ShortcutOptionGroup + var customCommand : CustomCommand + var line : String + + func _init(parent:ParseNode,parser).(parent,parser): + + if Block.can_parse(parser): + block = Block.new(self,parser) + type = Type.Block + elif IfStatement.can_parse(parser): + ifStatement = IfStatement.new(self,parser) + type = Type.IfStatement + elif OptionStatement.can_parse(parser): + optionStatement = 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): + shortcutOptionGroup = ShortcutOptionGroup.new(self,parser) + type = Type.ShortcutOptionGroup + elif CustomCommand.can_parse(parser): + customCommand = CustomCommand.new(self,parser) + type = Type.CustomCommand + elif parser.next_symbol_is([YarnGlobals.TokenType.Text]): + line = parser.expect_symbol([YarnGlobals.TokenType.Text]).value + type = Type.Line + else: + printerr("expected a statement but got %s instead. (probably an inbalanced if statement)" % parser.tokens().front()._to_string()) + + + var tags : Array = [] + + while parser.next_symbol_is([YarnGlobals.TokenType.TagMarker]): + parser.expect_symbol([YarnGlobals.TokenType.TagMarker]) + var tag : String = parser.expect_symbol([YarnGlobals.TokenType.Identifier]).value + tags.append(tag) + + if(tags.size()>0): + self.tags = tags + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + + match type : + Type.Block: + info.append(block.tree_string(indentLevel)) + Type.IfStatement: + info.append(ifStatement.tree_string(indentLevel)) + Type.AssignmentStatement: + info.append(assignment.tree_string(indentLevel)) + Type.OptionStatement: + info.append(optionStatement.tree_string(indentLevel)) + Type.ShortcutOptionGroup: + info.append(shortcutOptionGroup.tree_string(indentLevel)) + Type.CustomCommand: + info.append(customCommand.tree_string(indentLevel)) + Type.Line: + info.append(tab(indentLevel,"Line: %s"%line)) + _: + printerr("cannot print statement") + + #print("statement --") + + return info.join("") + + + +class CustomCommand extends ParseNode: + + enum Type { + Expression,ClientCommand + } + + var type : int + var expression : ExpressionNode + var clientCommand : String + + func _init(parent:ParseNode,parser).(parent,parser): + parser.expect_symbol([YarnGlobals.TokenType.BeginCommand]) + + var commandTokens = [] + commandTokens.append(parser.expect_symbol()) + + while !parser.next_symbol_is([YarnGlobals.TokenType.EndCommand]): + commandTokens.append(parser.expect_symbol()) + + parser.expect_symbol([YarnGlobals.TokenType.EndCommand]) + + #if first token is identifier and second is leftt parenthesis + #evaluate as function + if (commandTokens.size() > 1 && commandTokens[0].type == YarnGlobals.TokenType.Identifier + && commandTokens[1].type == YarnGlobals.TokenType.LeftParen): + var p = get_script().new(commandTokens,parser.library) + var expression : ExpressionNode = ExpressionNode.parse(self,p) + type = Type.Expression + self.expression = expression + else: + #otherwise evaluuate command + type = Type.ClientCommand + self.clientCommand = commandTokens[0].value + + func tree_string(indentLevel : int)->String: + match type: + Type.Expression: + return tab(indentLevel,"Expression: %s"% expression.tree_string(indentLevel+1)) + Type.ClientCommand: + return tab(indentLevel,"Command: %s"%clientCommand) + return "" + + static func can_parse(parser)->bool: + return (parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand,YarnGlobals.TokenType.Text]) + || parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand,YarnGlobals.TokenType.Identifier])) + + + + + +class ShortcutOptionGroup extends ParseNode: + + var options : Array = []#ShortcutOptions + + func _init(parent:ParseNode,parser).(parent,parser): + + # parse options until there is no more + # expect one otherwise invalid + + var sIndex : int = 1 + options.append(ShortCutOption.new(sIndex, self, parser)) + sIndex+=1 + while parser.next_symbol_is([YarnGlobals.TokenType.ShortcutOption]): + options.append(ShortCutOption.new(sIndex, self, parser)) + sIndex+=1 + + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + + info.append(tab(indentLevel,"Shortcut Option Group{")) + + for option in options: + info.append(option.tree_string(indentLevel+1)) + + info.append(tab(indentLevel,"}")) + + return info.join("") + + static func can_parse(parser)->bool: + return parser.next_symbol_is([YarnGlobals.TokenType.ShortcutOption]) + pass + +class ShortCutOption extends ParseNode: + + var label : String + var condition : ExpressionNode + var node : YarnNode + + func _init(index:int, parent:ParseNode, parser).(parent,parser): + parser.expect_symbol([YarnGlobals.TokenType.ShortcutOption]) + label = parser.expect_symbol([YarnGlobals.TokenType.Text]).value + + # parse the conditional << if $x >> when it exists + + var tags : Array = []#string + while( parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand,YarnGlobals.TokenType.IfToken]) + || parser.next_symbol_is([YarnGlobals.TokenType.TagMarker])): + + if parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand, YarnGlobals.TokenType.IfToken]): + parser.expect_symbol([YarnGlobals.TokenType.BeginCommand]) + parser.expect_symbol([YarnGlobals.TokenType.IfToken]) + condition = ExpressionNode.parse(self,parser) + parser.expect_symbol([YarnGlobals.TokenType.EndCommand]) + elif parser.next_symbol_is([YarnGlobals.TokenType.TagMarker]): + parser.expect_symbol([YarnGlobals.TokenType.TagMarker]) + var tag : String = parser.expect_symbol([YarnGlobals.TokenType.Identifier]).value; + tags.append(tag) + + + self.tags = tags + # parse remaining statements + + if parser.next_symbol_is([YarnGlobals.TokenType.Indent]): + parser.expect_symbol([YarnGlobals.TokenType.Indent]) + node = YarnNode.new("%s.%s" %[self.get_node_parent().name ,index], self,parser) + parser.expect_symbol([YarnGlobals.TokenType.Dedent]) + + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + + info.append(tab(indentLevel,"Option \"%s\""%label)) + + if condition != null : + info.append(tab(indentLevel+1,"(when:")) + info.append(condition.tree_string(indentLevel+2)) + info.append(tab(indentLevel+1,"),")) + if node != null: + info.append(tab(indentLevel, "{")) + info.append(node.tree_string(indentLevel + 1)); + info.append(tab(indentLevel, "}")); + + return info.join("") + + + +#Blocks are groups of statements with the same indent level +class Block extends ParseNode: + + var statements : Array = [] + + func _init(parent:ParseNode, parser).(parent,parser): + #read indent + parser.expect_symbol([YarnGlobals.TokenType.Indent]) + + #keep reading statements until we hit a dedent + while !parser.next_symbol_is([YarnGlobals.TokenType.Dedent]): + #parse all statements including nested blocks + statements.append(Statement.new(self,parser)) + + #clean up dedent + parser.expect_symbol([YarnGlobals.TokenType.Dedent]) + + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + + info.append(tab(indentLevel,"Block {")) + + for statement in statements: + info.append(statement.tree_string(indentLevel+1)) + + info.append(tab(indentLevel,"}")) + + return info.join("") + + static func can_parse(parser)->bool: + return parser.next_symbol_is([YarnGlobals.TokenType.Indent]) + +#Option Statements are links to other nodes +class OptionStatement extends ParseNode: + + var destination : String + var label : String + + func _init(parent:ParseNode, parser).(parent,parser): + + var strings : Array = []#string + + #parse [[LABEL + parser.expect_symbol([YarnGlobals.TokenType.OptionStart]) + strings.append(parser.expect_symbol([YarnGlobals.TokenType.Text]).value) + + #if there is a | get the next string + if parser.next_symbol_is([YarnGlobals.TokenType.OptionDelimit]): + parser.expect_symbol([YarnGlobals.TokenType.OptionDelimit]) + var t = parser.expect_symbol([YarnGlobals.TokenType.Text,YarnGlobals.TokenType.Identifier]) + #print("Token %s"%t.value) + 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([YarnGlobals.TokenType.OptionEnd]) + + func tree_string(indentLevel : int)->String: + if label != null: + return tab(indentLevel,"Option: %s -> %s"%[label,destination]) + else: + return tab(indentLevel,"Option: -> %s"%destination) + + static func can_parse(parser)->bool: + return parser.next_symbol_is([YarnGlobals.TokenType.OptionStart]) + +class IfStatement extends ParseNode: + + var clauses : Array = []#Clauses + + func _init(parent:ParseNode, parser).(parent,parser): + + #<> + var prime : Clause = Clause.new() + + parser.expect_symbol([YarnGlobals.TokenType.BeginCommand]) + parser.expect_symbol([YarnGlobals.TokenType.IfToken]) + prime.expression = ExpressionNode.parse(self,parser) + parser.expect_symbol([YarnGlobals.TokenType.EndCommand]) + + #read statements until 'endif' or 'else' or 'else if' + var statements : Array = []#statement + while (!parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand, YarnGlobals.TokenType.EndIf]) + && !parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand, YarnGlobals.TokenType.ElseToken]) + && !parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand, YarnGlobals.TokenType.ElseIf])): + + statements.append(Statement.new(self,parser)) + + #ignore dedent + while parser.next_symbol_is([YarnGlobals.TokenType.Dedent]): + parser.expect_symbol([YarnGlobals.TokenType.Dedent]) + + + prime.statements = statements + clauses.append(prime) + + #handle all else if + while parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand,YarnGlobals.TokenType.ElseIf]): + var clauseElif : Clause = Clause.new() + + #parse condition syntax + parser.expect_symbol([YarnGlobals.TokenType.BeginCommand]) + parser.expect_symbol([YarnGlobals.TokenType.ElseIf]) + clauseElif.expression = ExpressionNode.parse(self,parser) + parser.expect_symbol([YarnGlobals.TokenType.EndCommand]) + + + var elifStatements : Array = []#statement + while (!parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand, YarnGlobals.TokenType.EndIf]) + && !parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand, YarnGlobals.TokenType.ElseToken]) + && !parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand, YarnGlobals.TokenType.ElseIf])): + + elifStatements.append(Statement.new(self,parser)) + + #ignore dedent + while parser.next_symbol_is([YarnGlobals.TokenType.Dedent]): + parser.expect_symbol([YarnGlobals.TokenType.Dedent]) + + + clauseElif.statements = statements + clauses.append(clauseElif) + + #handle else if exists + if (parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand, + YarnGlobals.TokenType.ElseToken,YarnGlobals.TokenType.EndCommand])): + + #expect no expression - just <> + parser.expect_symbol([YarnGlobals.TokenType.BeginCommand]) + parser.expect_symbol([YarnGlobals.TokenType.ElseToken]) + parser.expect_symbol([YarnGlobals.TokenType.EndCommand]) + + #parse until hit endif + var clauseElse : Clause = Clause.new() + var elStatements : Array = []#statement + while !parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand,YarnGlobals.TokenType.EndIf]): + elStatements.append(Statement.new(self,parser)) + + clauseElse.statements = elStatements + clauses.append(clauseElse) + + #ignore dedent + while parser.next_symbol_is([YarnGlobals.TokenType.Dedent]): + parser.expect_symbol([YarnGlobals.TokenType.Dedent]) + + + #finish + parser.expect_symbol([YarnGlobals.TokenType.BeginCommand]) + parser.expect_symbol([YarnGlobals.TokenType.EndIf]) + parser.expect_symbol([YarnGlobals.TokenType.EndCommand]) + + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + var first : bool = true + + for clause in clauses: + if first: + info.append(tab(indentLevel,"if:")) + elif clause.expression!=null: + info.append(tab(indentLevel,"Else If")) + else: + info.append(tab(indentLevel,"Else:")) + + info.append(clause.tree_string(indentLevel +1)) + + return info.join("") + + static func can_parse(parser)->bool: + return parser.next_symbols_are([YarnGlobals.TokenType.BeginCommand,YarnGlobals.TokenType.IfToken]) + pass + +class ValueNode extends ParseNode: + const Value = preload("res://addons/Wol/core/value.gd") + const Lexer = preload("res://addons/Wol/core/compiler/lexer.gd") + var value : Value + + func _init(parent:ParseNode, parser, token: Lexer.Token = null).(parent,parser): + + var t : Lexer.Token = token + if t == null : + parser.expect_symbol([YarnGlobals.TokenType.Number, + YarnGlobals.TokenType.Variable,YarnGlobals.TokenType.Str]) + use_token(t) + + #store value depending on type + func use_token(t:Lexer.Token): + match t.type: + YarnGlobals.TokenType.Number: + value = Value.new(float(t.value)) + YarnGlobals.TokenType.Str: + value = Value.new(t.value) + YarnGlobals.TokenType.FalseToken: + value = Value.new(false) + YarnGlobals.TokenType.TrueToken: + value = Value.new(true) + YarnGlobals.TokenType.Variable: + value = Value.new(null) + value.type = YarnGlobals.ValueType.Variable + value.variable = t.value + YarnGlobals.TokenType.NullToken: + value = Value.new(null) + _: + printerr("%s, Invalid token type" % t.name) + + func tree_string(indentLevel : int)->String: + return tab(indentLevel, "%s"%value.value()) + + +#Expressions encompass a wide range of things like: +# math (1 + 2 - 5 * 3 / 10 % 2) +# Identifiers +# Values +class ExpressionNode extends ParseNode: + + var type + var value : ValueNode + var function : String + var params : Array = []#ExpressionNode + + func _init(parent:ParseNode,parser,value:ValueNode,function:String="",params:Array=[]).(parent,parser): + + #no function - means value + if value!=null: + self.type = YarnGlobals.ExpressionType.Value + self.value = value + else:#function + + self.type = YarnGlobals.ExpressionType.FunctionCall + self.function = function + self.params = params + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + match type: + YarnGlobals.ExpressionType.Value: + return value.tree_string(indentLevel) + YarnGlobals.ExpressionType.FunctionCall: + info.append(tab(indentLevel,"Func[%s - params(%s)]:{"%[function,params.size()])) + for param in params: + #print("----> %s paramSize:%s"%[(function) , params.size()]) + info.append(param.tree_string(indentLevel+1)) + info.append(tab(indentLevel,"}")) + + return info.join("") + + #using Djikstra's shunting-yard algorithm to convert + #stream of expresions into postfix notaion, then + #build a tree of expressions + static func parse(parent:ParseNode,parser)->ExpressionNode: + + + var rpn : Array = []#token + var opStack : Array = []#token + + #track params + var funcStack : Array = []#token + + var validTypes : Array = [ + YarnGlobals.TokenType.Number, + YarnGlobals.TokenType.Variable, + YarnGlobals.TokenType.Str, + YarnGlobals.TokenType.LeftParen, + YarnGlobals.TokenType.RightParen, + YarnGlobals.TokenType.Identifier, + YarnGlobals.TokenType.Comma, + YarnGlobals.TokenType.TrueToken, + YarnGlobals.TokenType.FalseToken, + YarnGlobals.TokenType.NullToken + ] + validTypes+=Operator.op_types() + validTypes.invert() + + var last #Token + + #read expression content + while parser.tokens().size() > 0 && parser.next_symbol_is(validTypes): + var next = parser.expect_symbol(validTypes) #lexer.Token + + if( next.type == YarnGlobals.TokenType.Variable || + next.type == YarnGlobals.TokenType.Number || + next.type == YarnGlobals.TokenType.Str || + next.type == YarnGlobals.TokenType.TrueToken || + next.type == YarnGlobals.TokenType.FalseToken || + next.type == YarnGlobals.TokenType.NullToken ): + + #output primitives + rpn.append(next) + elif next.type == YarnGlobals.TokenType.Identifier: + opStack.push_back(next) + funcStack.push_back(next) + + #next token is parent - left + next = parser.expect_symbol([YarnGlobals.TokenType.LeftParen]) + opStack.push_back(next) + elif next.type == YarnGlobals.TokenType.Comma: + + #resolve sub expression before moving on + while opStack.back().type != YarnGlobals.TokenType.LeftParen: + var p = opStack.pop_back() + if p == null: + printerr("unbalanced parenthesis %s " % next.name) + break + rpn.append(p) + + + #next token in opStack left paren + # next parser token not allowed to be right paren or comma + if parser.next_symbol_is([YarnGlobals.TokenType.RightParen, + YarnGlobals.TokenType.Comma]): + printerr("Expected Expression : %s" % parser.tokens().front().name) + + #find the closest function on stack + #increment parameters + funcStack.back().paramCount+=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 == YarnGlobals.TokenType.Minus): + if (last == null || + last.type == YarnGlobals.TokenType.LeftParen || + Operator.is_op(last.type)): + #unary minus + next.type = YarnGlobals.TokenType.UnaryMinus + + #cannot assign inside expression + # x = a is the same as x == a + if next.type == YarnGlobals.TokenType.EqualToOrAssign: + next.type = YarnGlobals.TokenType.EqualTo + + + #operator precedence + while (ExpressionNode.is_apply_precedence(next.type,opStack)): + var op = opStack.pop_back() + rpn.append(op) + + opStack.push_back(next) + + elif next.type == YarnGlobals.TokenType.LeftParen: + #entered parenthesis sub expression + opStack.push_back(next) + elif next.type == YarnGlobals.TokenType.RightParen: + #leaving sub expression + # resolve order of operations + while opStack.back().type != YarnGlobals.TokenType.LeftParen: + rpn.append(opStack.pop_back()) + if opStack.back() == null: + printerr("Unbalanced parenthasis #RightParen. Parser.ExpressionNode") + + + opStack.pop_back() + if opStack.back().type == YarnGlobals.TokenType.Identifier: + #function call + #last token == left paren this == no params + #else + #we have more than 1 param + if last.type != YarnGlobals.TokenType.LeftParen: + funcStack.back().paramCount+=1 + + rpn.append(opStack.pop_back()) + funcStack.pop_back() + + #record last token used + last = next + + #no more tokens : pop operators to output + while opStack.size() > 0: + rpn.append(opStack.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 evalStack : Array = []#ExpressionNode + + while rpn.size() > 0: + + var next = rpn.pop_front() + if Operator.is_op(next.type): + #operation + var info : OperatorInfo = Operator.op_info(next.type) + + if evalStack.size() < info.arguments: + printerr("Error parsing : Not enough arguments for %s [ got %s expected - was %s]"%[YarnGlobals.token_type_name(next.type),evalStack.size(),info.arguments]) + + var params : Array = []#ExpressionNode + for i in range(info.arguments): + params.append(evalStack.pop_back()) + + params.invert() + + var function : String = get_func_name(next.type) + + var expression : ExpressionNode = ExpressionNode.new(parent,parser,null,function,params) + + evalStack.append(expression) + + elif next.type == YarnGlobals.TokenType.Identifier: + #function call + + var function : String = next.value + + var params : Array = []#ExpressionNode + for i in range(next.paramCount): + + params.append(evalStack.pop_back()) + + params.invert() + + var expression : ExpressionNode = ExpressionNode.new(parent,parser,null,function,params) + + evalStack.append(expression) + else: #raw value + var value : ValueNode = ValueNode.new(parent,parser,next) + var expression : ExpressionNode = ExpressionNode.new(parent,parser,value) + evalStack.append(expression) + + + #we should have a single root expression left + #if more then we failed ---- NANI + if evalStack.size() != 1: + printerr("[%s] Error parsing expression (stack did not reduce correctly )"%first.name) + + + + return evalStack.pop_back() + + # static func can_parse(parser)->bool: + # return false + + static func get_func_name(type)->String: + var string : String = "" + + for key in YarnGlobals.TokenType.keys(): + if YarnGlobals.TokenType[key] == type: + return key + return string + + static func is_apply_precedence(type,operatorStack:Array)->bool: + if operatorStack.size() == 0: + return false + + if !Operator.is_op(type): + printerr("Unable to parse expression!") + + var second = operatorStack.back().type + + if !Operator.is_op(second): + return false + + var firstInfo : OperatorInfo = Operator.op_info(type) + var secondInfo : OperatorInfo = Operator.op_info(second) + + if (firstInfo.associativity == Associativity.Left && + firstInfo.precedence <= secondInfo.precedence): + return true + + if (firstInfo.associativity == Associativity.Right && + firstInfo.precedence < secondInfo.precedence): + return true + + return false + +class Assignment extends ParseNode: + + var destination : String + var value : ExpressionNode + var operation + + func _init(parent:ParseNode,parser).(parent,parser): + parser.expect_symbol([YarnGlobals.TokenType.BeginCommand]) + parser.expect_symbol([YarnGlobals.TokenType.Set]) + destination = parser.expect_symbol([YarnGlobals.TokenType.Variable]).value + operation = parser.expect_symbol(Assignment.valid_ops()).type + value = ExpressionNode.parse(self,parser) + parser.expect_symbol([YarnGlobals.TokenType.EndCommand]) + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + info.append(tab(indentLevel,"set:")) + info.append(tab(indentLevel+1,destination)) + info.append(tab(indentLevel+1,YarnGlobals.token_type_name(operation))) + info.append(value.tree_string(indentLevel+1)) + return info.join("") + + + static func can_parse(parser)->bool: + return parser.next_symbols_are([ + YarnGlobals.TokenType.BeginCommand, + YarnGlobals.TokenType.Set + ]) + + static func valid_ops()->Array: + return [ + YarnGlobals.TokenType.EqualToOrAssign, + YarnGlobals.TokenType.AddAssign, + YarnGlobals.TokenType.MinusAssign, + YarnGlobals.TokenType.DivideAssign, + YarnGlobals.TokenType.MultiplyAssign + ] + +class Operator extends ParseNode: + + var opType + + func _init(parent:ParseNode,parser,opType=null).(parent,parser): + + if opType == null : + self.opType = parser.expect_symbol(Operator.op_types()).type + else: + self.opType = opType + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + info.append(tab(indentLevel,opType)) + return info.join("") + + static func op_info(op)->OperatorInfo: + if !Operator.is_op(op) : + printerr("%s is not a valid operator" % op.name) + + #determine associativity and operands + # each operand has + var TokenType = YarnGlobals.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)->bool: + return type in op_types() + + static func op_types()->Array: + return [ + YarnGlobals.TokenType.Not, + YarnGlobals.TokenType.UnaryMinus, + + YarnGlobals.TokenType.Add, + YarnGlobals.TokenType.Minus, + YarnGlobals.TokenType.Divide, + YarnGlobals.TokenType.Multiply, + YarnGlobals.TokenType.Modulo, + + YarnGlobals.TokenType.EqualToOrAssign, + YarnGlobals.TokenType.EqualTo, + YarnGlobals.TokenType.GreaterThan, + YarnGlobals.TokenType.GreaterThanOrEqualTo, + YarnGlobals.TokenType.LessThan, + YarnGlobals.TokenType.LessThanOrEqualTo, + YarnGlobals.TokenType.NotEqualTo, + + YarnGlobals.TokenType.And, + YarnGlobals.TokenType.Or, + + YarnGlobals.TokenType.Xor + ] + + +class OperatorInfo: + var associativity + var precedence : int + var arguments : int + + func _init(associativity,precedence:int,arguments:int): + self.associativity = associativity + self.precedence = precedence + self.arguments = arguments + + +class Clause: + var expression : ExpressionNode + var statements : Array = [] #Statement + + func _init(expression:ExpressionNode = null, statements : Array = []): + self.expression = expression + self.statements = statements + + func tree_string(indentLevel : int)->String: + var info : PoolStringArray = [] + if expression!=null: + info.append(expression.tree_string(indentLevel)) + info.append(tab(indentLevel,"{")) + for statement in statements: + info.append(statement.tree_string(indentLevel+1)) + + info.append(tab(indentLevel,"}")) + return info.join("") + + func tab(indentLevel : int , input : String,newLine : bool = true)->String: + return ("%*s| %s%s"% [indentLevel*2,"",input,("" if !newLine else "\n")]) diff --git a/addons/Wol/core/dialogue.gd b/addons/Wol/core/dialogue.gd new file mode 100644 index 0000000..ec4cca1 --- /dev/null +++ b/addons/Wol/core/dialogue.gd @@ -0,0 +1,136 @@ +extends Node + +const DEFAULT_START = "Start" +const FMF_PLACEHOLDE = "" + +const StandardLibrary = preload("res://addons/Wol/core/libraries/standard.gd") +const VirtualMachine = preload("res://addons/Wol/core/virtual_machine.gd") +const YarnLibrary = preload("res://addons/Wol/core/library.gd") +const Value = preload("res://addons/Wol/core/value.gd") +const YarnProgram = preload("res://addons/Wol/core/program/program.gd") + +var _variableStorage + +var _debugLog +var _errLog + +var _program +var library + +var _vm : VirtualMachine + +var _visitedNodeCount : Dictionary = {} + +var executionComplete : bool + +func _init(variableStorage): + _variableStorage = variableStorage + _vm = VirtualMachine.new(self) + library = YarnLibrary.new() + _debugLog = funcref(self, "dlog") + _errLog = funcref(self, "elog") + executionComplete = false + + # import the standard library + # this contains math constants, operations and checks + library.import_library(StandardLibrary.new())#FIX + + #add a function to lib that checks if node is visited + library.register_function("visited", -1, funcref(self, "is_node_visited"), true) + + #add function to lib that gets the node visit count + library.register_function("visit_count", -1, funcref(self, "node_visit_count"), true) + + +func dlog(message:String): + print("YARN_DEBUG : %s" % message) + +func elog(message:String): + print("YARN_ERROR : %s" % message) + +func is_active(): + return get_exec_state() != YarnGlobals.ExecutionState.Stopped + +#gets the current execution state of the virtual machine +func get_exec_state(): + return _vm.executionState + +func set_selected_option(option:int): + _vm.set_selected_option(option) + +func set_node(name:String = DEFAULT_START): + _vm.set_node(name) + +func resume(): + if _vm.executionState == YarnGlobals.ExecutionState.Running: + print('BLOCKED') + return + _vm.resume() + +func pause(): + _vm.pause() + +func stop(): + _vm.stop() + +func get_all_nodes(): + return _program.yarnNodes.keys() + +func current_node(): + return _vm.get_current() + +func get_node_id(name): + if _program.nodes.size() == 0: + _errLog.call_func("No nodes loaded") + return "" + if _program.nodes.has(name): + return "id:"+name + else: + _errLog.call_func("No node named [%s] exists" % name) + return "" + +func unloadAll(clear_visited:bool = true): + if clear_visited : + _visitedNodeCount.clear() + _program = null + +func dump()->String: + return _program.dump(library) + +func node_exists(name:String)->bool: + return _program.nodes.has(name) + +func set_program(program): + _program = program + _vm.set_program(_program) + _vm.reset() + +func get_program(): + return _program + +func get_vm(): + return _vm + +func is_node_visited(node = _vm.current_node_name()): + return node_visit_count(node) > 0 + +func node_visit_count(node = _vm.current_node_name()): + if node is Value: + node = _program.yarnStrings[node.value()].text + + var visitCount : int = 0 + if _visitedNodeCount.has(node): + visitCount = _visitedNodeCount[node] + + + print("visit count for %s is %d" % [node, visitCount]) + + return visitCount + +func get_visited_nodes(): + return _visitedNodeCount.keys() + +func set_visited_nodes(visitedList): + _visitedNodeCount.clear() + for string in visitedList: + _visitedNodeCount[string] = 1 diff --git a/addons/Wol/core/dialogue/command.gd b/addons/Wol/core/dialogue/command.gd new file mode 100644 index 0000000..2839281 --- /dev/null +++ b/addons/Wol/core/dialogue/command.gd @@ -0,0 +1,6 @@ +extends Object + +var command : String + +func _init(command : String): + self.command = command \ No newline at end of file diff --git a/addons/Wol/core/dialogue/format_function.gd b/addons/Wol/core/dialogue/format_function.gd new file mode 100644 index 0000000..1eccaec --- /dev/null +++ b/addons/Wol/core/dialogue/format_function.gd @@ -0,0 +1,16 @@ +extends Node + + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass diff --git a/addons/Wol/core/dialogue/line.gd b/addons/Wol/core/dialogue/line.gd new file mode 100644 index 0000000..388c8ec --- /dev/null +++ b/addons/Wol/core/dialogue/line.gd @@ -0,0 +1,11 @@ +extends Object + +const LineInfo = preload("res://addons/Wol/core/program/yarn_line.gd") + +var id : String +var substitutions : Array = []#String +var info : LineInfo + +func _init(id: String, info: LineInfo): + self.id = id + self.info = info diff --git a/addons/Wol/core/dialogue/option.gd b/addons/Wol/core/dialogue/option.gd new file mode 100644 index 0000000..86d3333 --- /dev/null +++ b/addons/Wol/core/dialogue/option.gd @@ -0,0 +1,13 @@ +extends Object + +const Line = preload("res://addons/Wol/core/dialogue/line.gd") + +var line : Line +var id : int +var destination : String + +func _init(line : Line,id : int, destination: String): + self.line = line + self.id = id + self.destination = destination + diff --git a/addons/Wol/core/function_info.gd b/addons/Wol/core/function_info.gd new file mode 100644 index 0000000..5805125 --- /dev/null +++ b/addons/Wol/core/function_info.gd @@ -0,0 +1,39 @@ +extends Object +var Value : GDScript = load("res://addons/Wol/core/value.gd") + +#name of the function +var name : String + +#param count of this function +# -1 means variable arguments +var paramCount : int = 0 +#function implementation +var function : FuncRef +var returnsValue : bool = false + +func _init(name: String, paramCount: int, function: FuncRef = null, returnsValue: bool = false): + self.name = name + self.paramCount = paramCount + self.function = function + self.returnsValue = returnsValue + +func invoke(params = []): + var length = 0 + if params != null: + length = params.size() + + if check_param_count(length): + if returnsValue: + if length > 0: + return Value.new(function.call_funcv(params)) + else: + return Value.new(function.call_func()) + else: + if length > 0: + function.call_funcv(params) + else : + function.call_func() + return null + +func check_param_count(paramCount: int): + return self.paramCount == paramCount || self.paramCount == -1 diff --git a/addons/Wol/core/libraries/standard.gd b/addons/Wol/core/libraries/standard.gd new file mode 100644 index 0000000..9f11f8b --- /dev/null +++ b/addons/Wol/core/libraries/standard.gd @@ -0,0 +1,69 @@ +extends "res://addons/Wol/core/library.gd" + +const Value = preload("res://addons/Wol/core/value.gd") + +func _init(): + register_function("Add",2,funcref(self,"add"),true) + register_function("Minus",2,funcref(self,"sub"),true) + register_function("UnaryMinus",1,funcref(self,"unary_minus"),true) + register_function("Divide",2,funcref(self,"div"),true) + register_function("Multiply",2,funcref(self,"mul"),true) + register_function("Modulo",2,funcref(self,"mod"),true) + register_function("EqualTo",2,funcref(self,"equal"),true) + register_function("NotEqualTo",2,funcref(self,"noteq"),true) + register_function("GreaterThan",2,funcref(self,"ge"),true) + register_function("GreaterThanOrEqualTo",2,funcref(self,"geq"),true) + register_function("LessThan",2,funcref(self,"le"),true) + register_function("LessThanOrEqualTo",2,funcref(self,"leq"),true) + register_function("And",2,funcref(self,"land"),true) + register_function("Or",2,funcref(self,"lor"),true) + register_function("Xor",2,funcref(self,"xor"),true) + register_function("Not",1,funcref(self,"lnot"),true) + +func add(param1:Value,param2:Value): + return param1.add(param2) + +func sub(param1:Value,param2:Value): + return param1.sub(param2) + +func unary_minus(param1:Value): + return param1.negative() + +func div(param1:Value,param2:Value): + return param1.div(param2) + +func mul(param1:Value,param2:Value): + return param1.mult(param2) + +func mod(param1:Value,param2:Value): + return param1.mod(param2) + +func equal(param1:Value,param2:Value): + return param1.equals(param2) + +func noteq(param1:Value,param2:Value): + return !param1.equals(param2) + +func ge(param1:Value,param2:Value): + return param1.greater(param2) + +func geq(param1:Value,param2:Value): + return param1.geq(param2) + +func le(param1:Value,param2:Value): + return param1.less(param2) + +func leq(param1:Value,param2:Value): + return param1.leq(param2) + +func land(param1:Value,param2:Value): + return param1.as_bool() && param2.as_bool() + +func lor(param1:Value,param2:Value): + return param1.as_bool() || param2.as_bool() + +func xor(param1:Value,param2:Value): + return param1.as_bool() != param2.as_bool() + +func lnot(param1:Value): + return !param1.as_bool() \ No newline at end of file diff --git a/addons/Wol/core/library.gd b/addons/Wol/core/library.gd new file mode 100644 index 0000000..8c16f07 --- /dev/null +++ b/addons/Wol/core/library.gd @@ -0,0 +1,27 @@ +extends Object + +const FunctionInfo = preload("res://addons/Wol/core/function_info.gd") + +var functions : Dictionary = {}# String , FunctionInfo + +func get_function(name:String)->FunctionInfo: + if functions.has(name): + return functions[name] + else : + printerr("Invalid Function: %s"% name) + return null + +func import_library(other)->void: + YarnGlobals.merge_dir(functions,other.functions) + +func register_function(name: String, paramCount: int, function: FuncRef, returnsValue: bool): + var functionInfo: FunctionInfo = FunctionInfo.new(name, paramCount, function, returnsValue) + functions[name] = functionInfo + +func deregister_function(name: String): + if !functions.erase(name): + pass + + + + diff --git a/addons/Wol/core/program/instruction.gd b/addons/Wol/core/program/instruction.gd new file mode 100644 index 0000000..08eed90 --- /dev/null +++ b/addons/Wol/core/program/instruction.gd @@ -0,0 +1,17 @@ +extends Object + +const Operand = preload("res://addons/Wol/core/program/operand.gd") + +var operation : int #bytcode +var operands : Array #Operands + +func _init(other=null): + if other != null && other.get_script() == self.get_script(): + self.operation = other.operation + self.operands += other.operands + +func dump(program,library)->String: + return "InstructionInformation:NotImplemented" + +func _to_string(): + return YarnGlobals.bytecode_name(operation) + ':' + operands as String diff --git a/addons/Wol/core/program/operand.gd b/addons/Wol/core/program/operand.gd new file mode 100644 index 0000000..93097fc --- /dev/null +++ b/addons/Wol/core/program/operand.gd @@ -0,0 +1,58 @@ +extends Object + +enum ValueType{ + None, + StringValue, + BooleanValue, + FloatValue +} + +var value + +var type + +func _init(value): + if typeof(value) == TYPE_OBJECT && value.get_script() == self.get_script(): + #operand + self.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) + _: + pass + +func set_boolean(value: bool): + _value(value) + type = ValueType.BooleanValue + return self + +func set_string(value:String): + _value(value) + type = ValueType.StringValue + return self + +func set_number(value:float): + _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] + +func _value(value): + self.value = value \ No newline at end of file diff --git a/addons/Wol/core/program/program.gd b/addons/Wol/core/program/program.gd new file mode 100644 index 0000000..017af15 --- /dev/null +++ b/addons/Wol/core/program/program.gd @@ -0,0 +1,30 @@ +extends Node + +var programName : String +var yarnStrings : Dictionary = {} +var yarnNodes : Dictionary = {} + +func get_node_tags(name:String)->Array: + return yarnNodes[name].tags + +func get_yarn_string(key:String)->String: + return yarnStrings[key] + +func get_node_text(name:String)->String: + var key = yarnNodes[name].sourceId + return get_yarn_string(key) + +#possible support for line tags +func get_untagged_strings()->Dictionary: + return {} + +func merge(other): + pass + +func include(other): + pass + +func dump(library): + print("not yet implemented") + pass + diff --git a/addons/Wol/core/program/yarn_line.gd b/addons/Wol/core/program/yarn_line.gd new file mode 100644 index 0000000..bbac596 --- /dev/null +++ b/addons/Wol/core/program/yarn_line.gd @@ -0,0 +1,16 @@ +extends Object + +var text : String +var nodeName : String +var lineNumber : int +var fileName : String +var implicit : bool +var meta : Array = [] + +func _init(text:String, nodeName:String, lineNumber:int, fileName:String, implicit:bool, meta:Array): + self.text = text + self.nodeName = nodeName + self.fileName = fileName + self.implicit = implicit + self.meta = meta + diff --git a/addons/Wol/core/program/yarn_node.gd b/addons/Wol/core/program/yarn_node.gd new file mode 100644 index 0000000..b4871a3 --- /dev/null +++ b/addons/Wol/core/program/yarn_node.gd @@ -0,0 +1,33 @@ +extends Object + +var nodeName : String +var instructions : Array = [] +var labels : Dictionary +var tags: Array +var sourceId : String + +func _init(other = null): + if other != null && other.get_script() == self.get_script(): + nodeName = other.nodeName + instructions+=other.instructions + for key in other.labels.keys(): + labels[key] = other.labels[key] + tags += other.tags + sourceId = other.sourceId + +func equals(other)->bool: + + if other.get_script() != self.get_script(): + return false + if other.name != self.name: + return false + if other.instructions != self.instructions: + return false + if other.label != self.label: + return false + if other.sourceId != self.sourceId: + return false + return true + +func _to_string(): + return "Node[%s:%s]" % [nodeName,sourceId] diff --git a/addons/Wol/core/value.gd b/addons/Wol/core/value.gd new file mode 100644 index 0000000..d362506 --- /dev/null +++ b/addons/Wol/core/value.gd @@ -0,0 +1,151 @@ +extends Object + +const YarnGlobals = preload("res://addons/Wol/autoloads/execution_states.gd") + +const NULL_STRING : String = "null" +const FALSE_STRING : String= "false" +const TRUE_STRING : String = "true" +const NANI : String = "NaN" + +var type : int = YarnGlobals.ValueType.Nullean +var number : float = 0 +var string : String = "" +var variable : String = "" +var boolean : bool = false + + +func _init(value = NANI): + if typeof(value) == TYPE_OBJECT && value.get_script() == self.get_script(): + if value.type == YarnGlobals.ValueType.Variable: + self.type = value.type + self.variable = value.variable + else: + set_value(value) + +func value(): + match type: + YarnGlobals.ValueType.Number: + return number + YarnGlobals.ValueType.Str: + return string + YarnGlobals.ValueType.Boolean: + return boolean + YarnGlobals.ValueType.Variable: + return variable + return null + +func as_bool(): + match type: + YarnGlobals.ValueType.Number: + return number != 0 + YarnGlobals.ValueType.Str: + return !string.empty() + YarnGlobals.ValueType.Boolean: + return boolean + return false + +func as_string(): + return "%s" % value() + +func as_number(): + match type: + YarnGlobals.ValueType.Number: + return number + YarnGlobals.ValueType.Str: + return float(string) + YarnGlobals.ValueType.Boolean: + return 0.0 if !boolean else 1.0 + return .0 + +func set_value(value): + if value == null || (typeof(value) == TYPE_STRING && value == NANI): + type = YarnGlobals.ValueType.Nullean + return + + match typeof(value): + TYPE_INT,TYPE_REAL: + type = YarnGlobals.ValueType.Number + number = value + TYPE_STRING: + type = YarnGlobals.ValueType.Str + string = value + TYPE_BOOL: + type = YarnGlobals.ValueType.Boolean + boolean = value + +#operations >> + +#addition +func add(other): + if self.type == YarnGlobals.ValueType.Str || other.type == YarnGlobals.ValueType.Str: + return get_script().new("%s%s"%[self.value(),other.value()]) + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return get_script().new(self.number + other.number) + return null + +func equals(other)->bool: + if other.get_script() != self.get_script(): + return false + if other.value() != self.value(): + return false + return true #refine this + +#subtract +func sub(other): + if self.type == YarnGlobals.ValueType.Str || other.type == YarnGlobals.ValueType.Str: + return get_script().new(str(value()).replace(str(other.value()),"")) + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return get_script().new(self.number - other.number) + return null + +#multiply +func mult(other): + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return get_script().new(self.number * other.number) + return null + +#division +func div(other): + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return get_script().new(self.number / other.number) + return null + +#modulus +func mod(other): + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return get_script().new(self.number % other.number) + return null + +func negative(): + if self.type == YarnGlobals.ValueType.Number: + return get_script().new(-self.number) + return null + +#greater than other +func greater(other)->bool: + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return self.number > other.number + return false + +#less than other +func less(other)->bool: + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return self.number < other.number + return false + +#greater than or equal to other +func geq(other)->bool: + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return self.number > other.number || self.equals(other) + return false + +#lesser than or equal to other +func leq(other)->bool: + if self.type == YarnGlobals.ValueType.Number && other.type == YarnGlobals.ValueType.Number: + return self.number < other.number || self.equals(other) + return false + +func _to_string(): + return "value(type[%s]: %s)" % [type,value()] + + diff --git a/addons/Wol/core/variable_storage.gd b/addons/Wol/core/variable_storage.gd new file mode 100644 index 0000000..8487614 --- /dev/null +++ b/addons/Wol/core/variable_storage.gd @@ -0,0 +1,23 @@ +extends Node + +signal values_changed + +const Value = preload("res://addons/Wol/core/value.gd") + +var variables = {} + +func set_value(name, value): + print('SETTING VALUES %s: %s' % [name, value]) + if !(value is Value): + variables[name] = Value.new(value) + else: + variables[name] = value + + emit_signal('values_changed') + +func get_value(name): + return variables.get(name) + +func clear_values(): + variables.clear() + emit_signal('values_changed') diff --git a/addons/Wol/core/virtual_machine.gd b/addons/Wol/core/virtual_machine.gd new file mode 100644 index 0000000..5390e95 --- /dev/null +++ b/addons/Wol/core/virtual_machine.gd @@ -0,0 +1,369 @@ +extends Node +var YarnGlobals = load("res://addons/Wol/autoloads/execution_states.gd") + +var FunctionInfo = load("res://addons/Wol/core/function_info.gd") +var Value = load("res://addons/Wol/core/value.gd") +var YarnProgram = load("res://addons/Wol/core/program/program.gd") +var YarnNode = load("res://addons/Wol/core/program/yarn_node.gd") +var Instruction = load("res://addons/Wol/core/program/instruction.gd") +var Line = load("res://addons/Wol/core/dialogue/line.gd") +var Command = load("res://addons/Wol/core/dialogue/command.gd") +var Option = load("res://addons/Wol/core/dialogue/option.gd") + +const EXECUTION_COMPLETE : String = "execution_complete_command" + +var NULL_VALUE = Value.new(null) + +# Function references to handlers +var lineHandler +var optionsHandler +var commandHandler +var nodeStartHandler +var nodeCompleteHandler +var dialogueCompleteHandler + +var _dialogue +var _program +var _state + +var _currentNode + +var executionState = YarnGlobals.ExecutionState.Stopped + +var string_table = {} + +func _init(dialogue): + self._dialogue = dialogue + _state = VmState.new() + + +func set_program(program): + _program = program + +#set the node to run +#return true if successeful false if no node +#of that name found +func set_node(name:String)->bool: + if _program == null || _program.yarnNodes.size() == 0: + printerr("Could not load %s : no nodes loaded" % name) + return false + + if !_program.yarnNodes.has(name): + executionState = YarnGlobals.ExecutionState.Stopped + reset() + printerr("No node named %s has been loaded" % name) + return false + + _dialogue.dlog("Running node %s" % name) + + _currentNode = _program.yarnNodes[name] + reset() + _state.currentNodeName = name + nodeStartHandler.call_func(name) + return true + + +func current_node_name()->String: + return _currentNode.nodeName + +func current_node(): + return _currentNode + +func pause(): + executionState = YarnGlobals.ExecutionState.Suspended + +#stop exectuion +func stop(): + executionState = YarnGlobals.ExecutionState.Stopped + reset() + _currentNode = null + +#set the currently selected option and +#resume execution if waiting for result +#return false if error +func set_selected_option(id): + if executionState != YarnGlobals.ExecutionState.WaitingForOption: + printerr("Unable to select option when dialogue not waiting for option") + return false + + if id < 0 || id >= _state.currentOptions.size(): + printerr("%d is not a valid option "%id) + return false + + var destination = _state.currentOptions[id].value + _state.push_value(destination) + _state.currentOptions.clear() + + #no longer waiting for option + executionState = YarnGlobals.ExecutionState.Suspended + + return true + +func has_options()->bool: + return _state.currentOptions.size() > 0 + +func reset(): + _state = VmState.new() + +#continue execution +func resume()->bool: + if _currentNode == null : + printerr("Cannot run dialogue with no node selected") + return false + if executionState == YarnGlobals.ExecutionState.WaitingForOption: + printerr("Cannot run while waiting for option") + return false + + if lineHandler == null : + printerr("Cannot run without a lineHandler") + return false + + if optionsHandler == null : + printerr("Cannot run without an optionsHandler") + return false + + if commandHandler == null : + printerr("Cannot run without an commandHandler") + return false + if nodeStartHandler == null : + printerr("Cannot run without a nodeStartHandler") + return false + if nodeCompleteHandler == null : + printerr("Cannot run without an nodeCompleteHandler") + return false + + + executionState = YarnGlobals.ExecutionState.Running + + #execute instruction until something cool happens + while executionState == YarnGlobals.ExecutionState.Running: + var currentInstruction = _currentNode.instructions[_state.programCounter] + + run_instruction(currentInstruction) + _state.programCounter+=1 + + if _state.programCounter >= _currentNode.instructions.size(): + nodeCompleteHandler.call_func(_currentNode.nodeName) + executionState = YarnGlobals.ExecutionState.Stopped + reset() + dialogueCompleteHandler.call_func() + _dialogue.dlog("Run Complete") + + return true + +func find_label_instruction(label:String)->int: + if !_currentNode.labels.has(label): + printerr("Unknown label:"+label) + return -1 + return _currentNode.labels[label] + +func run_instruction(instruction)->bool: + match instruction.operation: + YarnGlobals.ByteCode.Label: + #do nothing woooo! + pass + YarnGlobals.ByteCode.JumpTo: + #jump to named label + _state .programCounter = find_label_instruction(instruction.operands[0].value)-1 + YarnGlobals.ByteCode.RunLine: + #look up string from string table + #pass it to client as line + var key = instruction.operands[0].value + + var line = Line.new(key, _program.yarnStrings[key]) + + #the second operand is the expression count + # of format function + if instruction.operands.size() > 1: + pass#add format function support + + var pause : int = lineHandler.call_func(line) + + + if pause == YarnGlobals.HandlerState.PauseExecution: + executionState = YarnGlobals.ExecutionState.Suspended + + + YarnGlobals.ByteCode.RunCommand: + var commandText : String = instruction.operands[0].value + + if instruction.operands.size() > 1: + pass#add format function + + var command = Command.new(commandText) + + var pause = commandHandler.call_func(command) as int + if pause == YarnGlobals.HandlerState.PauseExecution: + executionState = YarnGlobals.ExecutionState.Suspended + + + YarnGlobals.ByteCode.PushString: + #push String var to stack + _state.push_value(instruction.operands[0].value) + YarnGlobals.ByteCode.PushNumber: + #push number to stack + _state.push_value(instruction.operands[0].value) + YarnGlobals.ByteCode.PushBool: + #push boolean to stack + _state.push_value(instruction.operands[0].value) + + YarnGlobals.ByteCode.PushNull: + #push null t + _state.push_value(NULL_VALUE) + + YarnGlobals.ByteCode.JumpIfFalse: + #jump to named label if value of stack top is false + if !_state.peek_value().as_bool(): + _state.programCounter = find_label_instruction(instruction.operands[0].value)-1 + + YarnGlobals.ByteCode.Jump: + #jump to label whose name is on the stack + var dest : String = _state.peek_value().as_string() + _state.programCounter = find_label_instruction(dest)-1 + YarnGlobals.ByteCode.Pop: + #pop value from stack + _state.pop_value() + YarnGlobals.ByteCode.CallFunc: + #call function with params on stack + #push any return value to stack + var functionName : String = instruction.operands[0].value + + var function = _dialogue.library.get_function(functionName) + + var expectedParamCount : int = function.paramCount + var actualParamCount : int = _state.pop_value().as_number() + + #if function takes in -1 params disregard + #expect the compiler to have placed the number of params + #at the top of the stack + if expectedParamCount == -1: + expectedParamCount = actualParamCount + + if expectedParamCount != actualParamCount: + printerr("Function %s expected %d parameters but got %d instead" %[functionName, + expectedParamCount,actualParamCount]) + return false + + var result + + if actualParamCount == 0: + result = function.invoke() + else: + var params : Array = []#value + for i in range(actualParamCount): + params.push_front(_state.pop_value()) + + result = function.invoke(params) + + if function.returnsValue: + _state.push_value(result) + pass + + YarnGlobals.ByteCode.PushVariable: + #get content of variable and push to stack + var name : String = instruction.operands[0].value + var loaded = _dialogue._variableStorage.get_value(name) + _state.push_value(loaded) + YarnGlobals.ByteCode.StoreVariable: + #store top stack value to variable + var top = _state.peek_value() + var destination : String = instruction.operands[0].value + _dialogue._variableStorage.set_value(destination,top) + + YarnGlobals.ByteCode.Stop: + #stop execution and repost it + nodeCompleteHandler.call_func(_currentNode.nodeName) + dialogueCompleteHandler.call_func() + executionState = YarnGlobals.ExecutionState.Stopped + reset() + + YarnGlobals.ByteCode.RunNode: + #run a node + var name : String + + if (instruction.operands.size() == 0 || instruction.operands[0].value.empty()): + #get string from stack and jump to node with that name + name = _state.peek_value().value() + else : + name = instruction.operands[0].value + + var pause = nodeCompleteHandler.call_func(_currentNode.nodeName) + set_node(name) + _state.programCounter-=1 + if pause == YarnGlobals.HandlerState.PauseExecution: + executionState = YarnGlobals.ExecutionState.Suspended + + YarnGlobals.ByteCode.AddOption: + # add an option to current state + var key = instruction.operands[0].value + + var line = Line.new(key, _program.yarnStrings[key]) + + if instruction.operands.size() > 2: + pass #formated text options + + # line to show and node name + _state.currentOptions.append(SimpleEntry.new(line,instruction.operands[1].value)) + YarnGlobals.ByteCode.ShowOptions: + #show options - stop if none + if _state.currentOptions.size() == 0: + executionState = YarnGlobals.ExecutionState.Stopped + reset() + dialogueCompleteHandler.call_func() + return false + + #present list of options + var choices : Array = []#Option + for optionIndex in range(_state.currentOptions.size()): + var option : SimpleEntry = _state.currentOptions[optionIndex] + choices.append(Option.new(option.key, optionIndex, option.value)) + + #we cant continue until option chosen + executionState = YarnGlobals.ExecutionState.WaitingForOption + + #pass the options to the client + #delegate for them to call + #when user makes selection + + optionsHandler.call_func(choices) + pass + _: + #bytecode messed up woopsise + executionState = YarnGlobals.ExecutionState.Stopped + reset() + printerr("Unknown Bytecode %s "%instruction.operation) + return false + + return true + +class VmState: + var Value = load("res://addons/Wol/core/value.gd") + + var currentNodeName : String + var programCounter : int = 0 + var currentOptions : Array = []#SimpleEntry + var stack : Array = [] #Value + + func push_value(value)->void: + if value is Value: + stack.push_back(value) + else: + stack.push_back(Value.new(value)) + + + func pop_value(): + return stack.pop_back() + + func peek_value(): + return stack.back() + + func clear_stack(): + stack.clear() + +class SimpleEntry: + var key + var value : String + + func _init(key,value:String): + self.key = key + self.value = value diff --git a/addons/Wol/examples/example1.yarn b/addons/Wol/examples/example1.yarn new file mode 100644 index 0000000..7c67745 --- /dev/null +++ b/addons/Wol/examples/example1.yarn @@ -0,0 +1,33 @@ +title: Start +tags: +colorID: 3 +position: 332,79 +--- +Oh well hello, what should we set number variable to? +<> +-> Set Number to 6 + <> +-> Set Number to 5 + <> +We will also set a cat boolean +-> to false + <> +-> to true + <> +[[SecondNode]] +=== +title: SecondNode +tags: +position: 678.16,263.66 +--- +sweet - Now we are in the Second node +->this one only shows when Visit count of first node is 1 <> +->This only options is showing because cat variable is true <> + ok then I guess cat is true +->Some other option with no requirement + this is some other option woooooohooo +->This shows when number greater than 5 < 5 >> + Coool, yes the number was greater than 5 <> + But not anymore! ha +Now we are finished, GoodBye! +=== diff --git a/addons/Wol/plugin.cfg b/addons/Wol/plugin.cfg new file mode 100644 index 0000000..8659bd8 --- /dev/null +++ b/addons/Wol/plugin.cfg @@ -0,0 +1,6 @@ +[plugin] +name="Wol" +description="A dialogue system for Godot based on YarnSpinner" +author="Bram Dingelstad (@bram_dingelstad)" +version="0.0.0" +script="plugin.gd" diff --git a/addons/Wol/plugin.gd b/addons/Wol/plugin.gd new file mode 100644 index 0000000..e31c2f7 --- /dev/null +++ b/addons/Wol/plugin.gd @@ -0,0 +1,20 @@ +tool +extends EditorPlugin + +func _enter_tree(): + add_autoload_singleton( + 'YarnGlobals', + 'res://addons/Wol/autoloads/execution_states.gd' + ) + + add_custom_type( + 'Wol', + 'Node', + load('res://addons/Wol/yarn_runner.gd'), + load('res://addons/Wol/assets/icon.png') + ) + + +func _exit_tree(): + remove_autoload_singleton('YarnGlobals') + remove_custom_type('Wol') diff --git a/addons/Wol/yarn_runner.gd b/addons/Wol/yarn_runner.gd new file mode 100644 index 0000000..67e58b6 --- /dev/null +++ b/addons/Wol/yarn_runner.gd @@ -0,0 +1,120 @@ +tool +extends Node + +signal node_started(node) +signal line(line) +signal options(options) +signal command(command) +signal node_completed(node) + +signal running +signal finished + +const YarnCompiler = preload("res://addons/Wol/core/compiler/compiler.gd") +const YarnDialogue = preload("res://addons/Wol/core/dialogue.gd") + +export(String, FILE, "*.yarn") var path setget set_path +export(String) var start_node = "Start" +export(bool) var auto_start = false +export(NodePath) var variable_storage_path + +onready var variable_storage = get_node(variable_storage_path) + +var program + +var dialogue +var running = false + +func _ready(): + if Engine.editor_hint: + return + + if not variable_storage: + variable_storage = Node.new() + variable_storage.name = 'VariableStorage' + variable_storage.set_script(load('res://addons/Wol/core/variable_storage.gd')) + add_child(variable_storage) + + if auto_start: + start() + +func init_dialogue(): + # FIXME: Move visited count to variable storage + var existing_state + if dialogue != null: + existing_state = dialogue._visitedNodeCount + + dialogue = YarnDialogue.new(variable_storage) + + # FIXME: Remove these lines + if existing_state: + dialogue._visitedNodeCount = existing_state + + dialogue.get_vm().lineHandler = funcref(self, "_handle_line") + dialogue.get_vm().optionsHandler = funcref(self, "_handle_options") + dialogue.get_vm().commandHandler = funcref(self, "_handle_command") + dialogue.get_vm().nodeCompleteHandler = funcref(self, "_handle_node_complete") + dialogue.get_vm().dialogueCompleteHandler = funcref(self, "_handle_dialogue_complete") + dialogue.get_vm().nodeStartHandler = funcref(self, "_handle_node_start") + + dialogue.set_program(program) + +func set_path(_path): + var file = File.new() + file.open(_path, File.READ) + var source = file.get_as_text() + file.close() + program = YarnCompiler.compile_string(source, _path) + path = _path + +func _handle_line(line): + call_deferred('emit_signal', 'line', line) + return YarnGlobals.HandlerState.PauseExecution + +func _handle_command(command): + call_deferred('emit_signal', 'command', command) + return YarnGlobals.HandlerState.PauseExecution + +func _handle_options(options): + emit_signal('options', options) + return YarnGlobals.HandlerState.PauseExecution + +func _handle_dialogue_complete(): + emit_signal('finished') + running = false + +func _handle_node_start(node): + emit_signal('node_started', node) + print('node started') + dialogue.resume() + + if !dialogue._visitedNodeCount.has(node): + dialogue._visitedNodeCount[node] = 1 + else: + dialogue._visitedNodeCount[node] += 1 + + print(dialogue._visitedNodeCount) + +func _handle_node_complete(node): + emit_signal('node_completed', node) + running = false + return YarnGlobals.HandlerState.ContinueExecution + +func select_option(id): + dialogue.get_vm().set_selected_option(id) + +func pause(): + dialogue.call_deferred('pause') + +func start(node = start_node): + if running: + return + + init_dialogue() + emit_signal('running') + + running = true + dialogue.set_node(node) + +func resume(): + dialogue.call_deferred('resume') diff --git a/default_env.tres b/default_env.tres new file mode 100644 index 0000000..20207a4 --- /dev/null +++ b/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="ProceduralSky" id=1] + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/dialogue.yarn b/dialogue.yarn new file mode 100644 index 0000000..954c4a5 --- /dev/null +++ b/dialogue.yarn @@ -0,0 +1,319 @@ +title: Waking up +tags: +colorID: 0 +position: -1544,-923 +--- +<> +<> +<> +<> + +>.< +>…< +>…< +>I woke up to a pulsating pain in my head.< +>It hurts.< +>It hurts a lot.< +>It was like someone was hammering an ice pick right into my head and chiselling my brain into multiple chunks at once.< +>I tried to reach my head in an attempt to cradle it, hoping I could just slump into a fetal position and wait for the pain to go away.< +>But I couldn’t move my arms.< +>Something was keeping them in place.< +>Something cold and really sturdy.< +>Then the implications of that set in.< +>And I shot my eyes wide open.< +>But all I could see was [p;pink].< +>So much [p;pink].< +>And a figure sitting in front of me.< + +//<> +<> +<> +<> +<> +<> +<> +<> +<> + +>Ah… that’s just me. Dang, I almost got startled by my own reflection.< +>Why is there a mirror in front of me anyway?< +>I looked down at my reflection’s hands.< +>Yeah, I was definitely tied up alright. Fun.< +>I tried to yank my arm out of the handcuffs in hopes of a poor job done by whoever had gotten me in this situation.< +>After a few pulling attempts all I got was a bruised wrist. It was a long shot anyways.< +>Okay… What can I do in this situation?< +>I looked around me.< +>This looks like a welcome party or something… Except for all those chairs straight from an old timey asylum.< +>The chairs looked pretty much like mine, meaning-< +???: Ugh… +>The sound came from behind me.< +>I tried to crane my neck to look at the source of the voice but a dull thud accompanied by the feeling of my helmet bouncing against something stopped me.< +???: Ow! +>A long string of surprisingly mild but creative curses echoed in the room.< +???: Was that really necessary? +>I couldn’t see much from the angle I was in but from the voice I figured a man around my age was sitting with his back against mine.< +>The mirror wasn’t much help either. All I could see was a little bit of dark auburn hair.< +>It seems like I didn’t learn from the first attempt and tried to turn my head again to see the source of the voice.< +???: OW! Christ! Please stop hitting your head against mine! +Kiwa: Oh, my bad? +???: Who wears a helmet inside? +Kiwa: Precautions? +Kiwa: I mean, my head doesn’t have a bump right now, so I guess that’s a testimony to it. +???: And mine was hurting enough as it was. +Kiwa: Uh? + +<> +<> + +???: What’s happening? I can’t see from this position. +Kiwa: A… a countdown? +Kiwa: What does this mean? +>I had a bad feeling. A feeling that solidified into a webbed dark lump inside my chest.< +>My mind was screaming at me to run away as the countdown neared zero.< +-PAIR 8, STARTING PREPARATIONS- +Kiwa: Starting... Wha- +-Click- +???: The restraints! +>We scrambled off the chairs as quickly as humanly possible.< + +<> + +???: ... +Kiwa: Wait, I’d be amazed if that door is open by any chance. +-Click Click- +???: It really is locked. +???: I figured I had nothing to lose by trying. +Kiwa: We’ll just have to wait then. What is this room anyway? +???: All I can say is this looks rather dubious. +Kiwa: Hmm. +-Welcome esteemed guests! The game you have been dying to participate in has begun!- +-But first, let us get you familiar with the control scheme- +-I know this can get quite tedious to a brilliant mind such as yours truly, but some of the controls and mechanics may vary from the original games.- +-By pressing [p;Shift], you can see circles around objects or characters that indicate there’s dialogue to be explored. [b;Blue circles] are optional dialogue while [y; yellow circles] will progress the plot.- +-If a scene has multiple [y;yellow circles], you need to exhaust all yellow dialogue options before you can progress with the plot.- +-[p;Ctrl] opens and closes the transcript in case you need to review previous dialogue.- +-Now then, explore your first area to your heart’s content, participant.- + +<> +<> +=== +title: The Revolver +tags: +colorID: 0 +position: -1892,-1013 +--- +<> +<> + +Masami: Gina, this way! +Gina: Patience! I’m coming. +>Still, she didn’t pick up her pace and kept hesitantly looking around.< +Masami: Everything should be fine, let’s just go. We won. +Masami: Unless you are planning on backstabbing me at the finish line, that is. +Gina: How funny. You are not that special. +Masami: Har har. Now start walking or I’ll carry you out the front door. +Masami: I don’t want to give Monokuma the time to come up with a new death game for two while we are trying to get through the exit. +Gina: … + +<> + +Masami: Wait, the vault door… It’s not open. +Gina: So it seems. +Masami: But that doesn’t make sense! So far we have opened one lock per two deceased. All seven locks should be open by now. +>In desperation I tried the vault handle. I wasn’t exceptionally strong, but I doubted even a weightlifter could open this door whether it was held shut by one or dozen locks.< +Gina: Rather than using your energy by tearing at the 700kg door- +Masami: I wasn’t trying to brute force it. +Gina: Anyway, I would suggest you spare a glance at the reception desk. It appears the staff has given us one last gift. +Masami: The reception desk? + +<> + +Masami: A paper and… +Masami: … That’s a gun. +Gina: A revolver, in fact. Such an old fashioned choice of firearm. Though, I appreciate the aesthetic. How pretty. +Masami: I don’t like this. Why is it here? +>I took the paper from the table and briskly unfolded it.< +- Congratulations on reaching the final part of the game!- +- Unfortunately, no one remembers a silver medalist.- +- Let’s round up the game with a match of our favorite luck game! Russian roulette should be simple enough.- +- Now then, ladies first!- +-Staff- +Masami: Russian roulette… Are you kidding me? +Masami: This can’t be part of the game. This wasn’t in the rules! +Masami: I know the winning condition hasn’t been met but- +Gina: It’s rigged. +Masami: What? +>Gina had picked up the revolver from the table and without better judgement, opened the cylinder for inspection.< +Gina: It’s rigged. +Masami: ... “Ladies first.” +Masami: The cylinder is full, isn't it? +>Gina let out a tired sigh. Her shoulders slumping heavily from disappointment.< +Gina: I got so far. +Gina: I don’t know what led me to believe the outcome would be any different. +Gina: This is such a waste of resources as well. +Masami: Gina, put the revolver down. If the game is rigged, you don’t have to play. The rules won’t matter then. +Gina: Let it rest. +Gina: Like any of it matters. +Masami: What do you mean? +>She didn’t answer.< +>Instead, she absentmindedly fiddled with the cylinder before locking it in place.< +>Then she spun the cylinder for the hell of it before raising the revolver’s barrel against her very own temple.< +>She looked resigned. Though I couldn’t help but notice the slight glint of fear in her eyes.< +>None of us wanted to die.< +Masami: Gina, please. +Gina: Congratulations. +Masami: Gina! +<> +<> + +[[Waking up]] +=== +title: Screen +tags: +colorID: 0 +position: -1170,-560 +--- +<> +???: Pair 8? +Kiwa: Us, probably. +???: What are they preparing us for exactly? +Kiwa: I don’t think I want to find out. +Kiwa: The countdown already made me nervous. +=== +title: Chairs +tags: +colorID: 0 +position: -874,-551 +--- +<> +Kiwa: That’s a lot of chairs. +???: 16 of them… +Kiwa: Were we the last to wake up? +???: My question is where are the other 14 people? +???: What happened to them? +=== +title: Decorations +tags: +colorID: 0 +position: -593,-554 +--- +<> +Kiwa: That’s… a lot of hearts. +???: My eyes hurt just from looking at all of this. It feels like a crime to have this many shades of pink together at once. +Kiwa: It’s definitely ugly. +=== +title: Vent +tags: +colorID: 0 +position: -338,-536 +--- +<> +Kiwa: Do you think we could get out through that vent? +???: I don’t even know how we could reach it. Let alone open it. +Kiwa: Yeah, the chairs look like they have been bolted to the floor. +Kiwa: Also, what’s with this smell? It’s really sweet. Like someone dumped a bag of sugar into an air filter. +???: You are right. It’s quite overpowering. I hope it won’t make me sick. +=== +title: Mirror +tags: +colorID: 0 +position: -1438,-573 +--- +<> +>I look like I always look. A disheveled hobo skater who got severely dunked on recently.< +???: Admiring yourself in the mirror? +Kiwa: Not really. +???: I wonder why there are so many mirrors in this room. It feels like an interactive exhibit of a contemporary art gallery or something along those lines. +Kiwa: Or a fun house without those wobbly mirrors. +???: Or that... +=== +title: Ending +tags: +colorID: 0 +position: -139,-184 +--- +<> + <> + -You are a hopeless cause.- + <> + ???: ... + Kiwa: He's looking around the room. Maybe I should do the same. Access my surroundings and all that... + - ...Hmm. Like I previously established, please take a look at your surroundings.- + -By pressing [p;Shift], you can see circles around objects or characters that indicate there’s dialogue to be explored. [b;Blue circles] are optional dialogue while [y; yellow circles] will progress the plot.- + -If a scene has multiple [y;yellow circles], you need to exhaust all yellow dialogue options before you can progress with the plot.- + -[p;Ctrl] opens and closes the transcript in case you need to review previous dialogue.- + -You can move around the room with the [p;A] and [p;D] keys.- + -Do not make me repeat myself again.- + <> +<> + >Well, there isn’t much else to look at. Maybe I should try to talk to him more. He might know something.< + >I guess I’ll try to make light of the situation. That works with lowering stressful atmosphere, right?< + Kiwa: Uh, so… Do you come around here often? + ???: Seriously? In this situation? + Kiwa: It was just a joke? I think? + ???: You are taking this very nonchalantly. + ???: And by this, I mean the obvious kidnapping for… for… + ???: I don’t know. Hopefully just hostage money. + Kiwa: Maybe we’ll get sold for the black market. + ???: Oh god. Hopefully not. + Kiwa: Anyway, what sort of torture chamber is covered in pink hearts? + ???: I don’t even want to entertain the implications. + ???: Let’s just say the uncomfortable kind. + >He suddenly perked up and started to pat down his pant pockets before his stature deflated again.< + ???: Of course they would empty our pockets. What’s left of my belongings is some spare change and pocket lint. + Kiwa: Oh. + >I quickly checked my own pockets.< + Kiwa: You are right. No phone, or keys... or wallet. + Kiwa: They only left me with my lighter but no cigarettes. Why would they take those away but not the lighter? + ???: So arson is fine but they draw the line at lung abuse? + Kiwa: This is going to be a problem… mainly for me. + Kiwa: Well, if it comes to it, I can at least set this room on fire. + ???: Please don’t. I don’t want to get grouped in with your lawsuit. + >That’s the part you are worried about?< + ???: Are you some sort of criminal? + Kiwa: Something along those lines, sure. + ???: I’m assuming a petty one at that then. + Kiwa: Yeah. + Kiwa: Sorry, I guess introductions weren’t the first thing on my mind. + Kiwa: I’m Kiwa Fukuda, the former ultimate scapegoat. + ???: What a title. And you were an ultimate to boot. + ???: How come I have never heard of you? + Kiwa: I try to keep a low profile. That works best with what I do. + ???: What exactly does a scapegoat do for a living? + Kiwa: I’m really good at pointing fingers at people. + ???: Meaning? + Kiwa: If you have a problem and you don’t want to deal with it like a respectable member of society, you come to me. I will either make it look like I fucked up the whole situation or I make someone else who is more suited for the situation look like they were at fault. + ???: How do you find out you have such a talent in the first place? + Kiwa: With a lot of stupid actions, a lot of accidents, and a lot of dumb luck. + Kiwa: It can get very messy sometimes but it pays my rent three times over. + Kiwa: There are a lot of people who would like to have my head for less than one yen, so I wouldn’t be surprised if this whole kidnapping thing was my fault. + Kiwa: Though I don’t get why you would be in the same situation then? I have never seen you in my life. + Kiwa: Who are you? + ???: …Right. + Masami: My name is Masami Kiyokane, the former ultimate croupier. + Kiwa: A croupier? + Masami: Basically, I am a card dealer at a casino. The Valentine’s Hand Casino to be exact. You may have heard of it. + Kiwa: Ah, yeah. That one is really highly praised. One of the best in the country, right? Never been there myself. I don’t travel much. + Masami: Well, I work there nowadays. + Masami: My work includes taking bets, distributing cards to the players, and making sure those players don’t cheat among other things. + Kiwa: Cool. So, you are a former ultimate yourself? + Masami: That’s what that would imply, yes. + Kiwa: I really don’t remember you. What class were you in? I didn’t interact much with upper or underclassmen during my time in Hope’s Peak. + Masami: 72nd, Class A. + Kiwa: Ah, I’m from 71st B. I guess we both didn’t lose our titles that long ago. + Masami: Still, like you said. If we don’t even know each other, how are we in this situation? + Masami: To be honest, I thought the kidnapping was my fault at the start. But now that I have someone here who runs into legal problems constantly… + Kiwa: Why would you think this is your fault? + Masami: Well- + ???: Good afternoon. + Masami: What... is that? + Kiwa: A bird? + ???: You have successfully woken up. Good. + ???: You are expected at the trial grounds. Follow me. + >The strange bird turned it’s back to us and started to leave through the open door.< + >Figuring out I didn’t exactly have anywhere else to go I started following it. Or tried to, at least.< + >A hand grabbed my wrist and forcefully yanked me to run into another direction.< + + <> +<> +=== diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..c98fbb6 Binary files /dev/null and b/icon.png differ diff --git a/icon.png.import b/icon.png.import new file mode 100644 index 0000000..a4c02e6 --- /dev/null +++ b/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.png" +dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..0179fa5 --- /dev/null +++ b/project.godot @@ -0,0 +1,33 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +[application] + +config/name="YarnSpinner" +config/icon="res://icon.png" + +[autoload] + +YarnGlobals="*res://addons/Wol/autoloads/execution_states.gd" + +[editor_plugins] + +enabled=PoolStringArray( "res://addons/Wol/plugin.cfg" ) + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +quality/driver/driver_name="GLES2" +vram_compression/import_etc=true +vram_compression/import_etc2=false +environment/default_environment="res://default_env.tres"