├── .gitignore ├── CHANGELOG.md ├── Extras └── supertalk-vscode │ ├── .vscode │ └── launch.json │ ├── .vscodeignore │ ├── README.md │ ├── language-configuration.json │ ├── package.json │ ├── syntaxes │ └── supertalk.tmLanguage.json │ └── vsc-extension-quickstart.md ├── LICENSE ├── README.md ├── Resources └── Icon128.png ├── Source ├── Supertalk │ ├── Supertalk.Build.cs │ ├── Supertalk.cpp │ ├── Supertalk.h │ ├── SupertalkExpression.cpp │ ├── SupertalkExpression.h │ ├── SupertalkLine.cpp │ ├── SupertalkLine.h │ ├── SupertalkPlayer.cpp │ ├── SupertalkPlayer.h │ ├── SupertalkUtilities.cpp │ ├── SupertalkUtilities.h │ ├── SupertalkValue.cpp │ └── SupertalkValue.h └── SupertalkEditor │ ├── SSupertalkScriptAssetEditor.cpp │ ├── SSupertalkScriptAssetEditor.h │ ├── SupertalkEditor.build.cs │ ├── SupertalkEditor.cpp │ ├── SupertalkEditor.h │ ├── SupertalkEditorSettings.cpp │ ├── SupertalkEditorSettings.h │ ├── SupertalkEditorStyle.cpp │ ├── SupertalkEditorStyle.h │ ├── SupertalkParser.cpp │ ├── SupertalkParser.h │ ├── SupertalkRichTextSyntaxHighlighterTextLayoutMarshaller.cpp │ ├── SupertalkRichTextSyntaxHighlighterTextLayoutMarshaller.h │ ├── SupertalkScriptAssetFactory.cpp │ ├── SupertalkScriptAssetFactory.h │ ├── SupertalkScriptCompiler.cpp │ ├── SupertalkScriptCompiler.h │ ├── SupertalkScriptEditorCommands.cpp │ ├── SupertalkScriptEditorCommands.h │ ├── SupertalkScriptEditorToolkit.cpp │ └── SupertalkScriptEditorToolkit.h └── Supertalk.uplugin /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | 22 | # Compiled Static libraries 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | 28 | # Executables 29 | *.exe 30 | *.out 31 | *.app 32 | *.ipa 33 | 34 | # These project files can be generated by the engine 35 | *.xcodeproj 36 | *.xcworkspace 37 | *.sln 38 | *.suo 39 | *.opensdf 40 | *.sdf 41 | *.VC.db 42 | *.VC.opendb 43 | 44 | # Precompiled Assets 45 | SourceArt/**/*.png 46 | SourceArt/**/*.tga 47 | 48 | # Binary Files 49 | Binaries/* 50 | Plugins/*/Binaries/* 51 | 52 | # Builds 53 | Build/* 54 | 55 | # Whitelist PakBlacklist-.txt files 56 | !Build/*/ 57 | Build/*/** 58 | !Build/*/PakBlacklist*.txt 59 | 60 | # Don't ignore icon files in Build 61 | !Build/**/*.ico 62 | 63 | # Built data for maps 64 | *_BuiltData.uasset 65 | 66 | # Configuration files generated by the Editor 67 | Saved/* 68 | 69 | # Compiled source files for the engine to use 70 | Intermediate/* 71 | Plugins/*/Intermediate/* 72 | 73 | # Cache files for the editor to use 74 | DerivedDataCache/* 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | This file contains information about major changes and features. 2 | 3 | # Upcoming (0.7) 4 | 5 | * Better (experimental) editor experience 6 | * Line numbers and compiler output within the script editor. 7 | * Option to write out raw sts script files when saving a script asset in the script editor. 8 | 9 | # 0.6 10 | 11 | * Supertalk source data is now stored inside assets (editor-only) 12 | * Supertalk assets now show the original file content. 13 | * Assets from previous versions must be reimported for this to work (this only affects the ability to view source data, old script assets will still work fine) 14 | * An experimental editor for Supertalk assets has been implemented. 15 | * This must be enabled in Project Settings > Editor > Supertalk Editor. Restart the editor after changing this setting. 16 | * This also lets you create Supertalk scripts within the editor without having to import from an external file. 17 | * The editor is somewhat untested and due to how it works it is possible to lose script data. Be careful when enabling this option! 18 | * Old script assets must be reimported before they can be edited from within the editor. 19 | * Script assets can now be exported to text files if they were imported after this version. 20 | * Script assets are now compiled upon saving (incl. when packaging). 21 | * Old assets that don't have source data available are an exception to this. 22 | * This means that referencing a script that fails to compile will fail packaging. 23 | 24 | # 0.5 25 | 26 | * Property-based variable providers 27 | * Supports passing any UObject to the Supertalk player and accessing properties on it from scripts. 28 | * `USupertalkObjectValue` now lets you access exposed properties. 29 | * Initial `TMap` support (only for maps with `FString`/`FName`/`FText` keys) 30 | * Added an editor-only "notes" field to `FSupertalkTableRow` 31 | 32 | # 0.4 33 | 34 | * Function calls can now have variables passed to them as arguments. 35 | * This is somewhat hacky at the moment, still requiring arguments to be parsed out from a string. This functionality will eventually be rewritten. 36 | * Function-based variable providers 37 | * When a variable can't be found by the supertalk player, it runs these functions to allow them to provide it instead. 38 | 39 | # 0.3 40 | 41 | * Choices can now have localization keys applied. 42 | * This has been implemented in a somewhat hacky way to get around limitations of the lexer. 43 | * Default localization namespace changed to `Supertalk.Script.Default` from `Script.Default`, to match documentation. 44 | 45 | # 0.2 46 | 47 | * Parallel actions now use `[` and `]` 48 | * Queued actions now use `{` and `}` (this used to be used for parallels) 49 | * `(` and `)` are now for grouping expressions. 50 | * "Names" (unquoted strings/bare words) can no longer contain whitespace. If you need whitespace, use a quoted 51 | string. 52 | * Jumps and sections are unaffected - these can still contain spaces. 53 | * Basic expressions are now supported (`==`, `~` for not as `!` is already taken, `~=`) 54 | * Further logical operators will be added in the future. Math operators will come later. 55 | * Supported in if statements and assignment. 56 | * Not supported in formatting markup inside text. 57 | * This is unlikely to supported any time soon (if ever) as it would require either running the expression parser at 58 | runtime or pre-parsing text formatting and then modifying it. 59 | * Long-form `if ... then ... else ...` actions have been added. 60 | * Shorthand `value? true, false` conditionals have been removed entirely due to complications with expression parsing. -------------------------------------------------------------------------------- /Extras/supertalk-vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Extras/supertalk-vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | vsc-extension-quickstart.md 5 | -------------------------------------------------------------------------------- /Extras/supertalk-vscode/README.md: -------------------------------------------------------------------------------- 1 | # supertalk README 2 | 3 | This is the README for your extension "supertalk". After writing up a brief description, we recommend including the following sections. 4 | 5 | ## Features 6 | 7 | Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. 8 | 9 | For example if there is an image subfolder under your extension project workspace: 10 | 11 | \!\[feature X\]\(images/feature-x.png\) 12 | 13 | > Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. 14 | 15 | ## Requirements 16 | 17 | If you have any requirements or dependencies, add a section describing those and how to install and configure them. 18 | 19 | ## Extension Settings 20 | 21 | Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. 22 | 23 | For example: 24 | 25 | This extension contributes the following settings: 26 | 27 | * `myExtension.enable`: enable/disable this extension 28 | * `myExtension.thing`: set to `blah` to do something 29 | 30 | ## Known Issues 31 | 32 | Calling out known issues can help limit users opening duplicate issues against your extension. 33 | 34 | ## Release Notes 35 | 36 | Users appreciate release notes as you update your extension. 37 | 38 | ### 1.0.0 39 | 40 | Initial release of ... 41 | 42 | ### 1.0.1 43 | 44 | Fixed issue #. 45 | 46 | ### 1.1.0 47 | 48 | Added features X, Y, and Z. 49 | 50 | ----------------------------------------------------------------------------------------------------------- 51 | 52 | ## Working with Markdown 53 | 54 | **Note:** You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: 55 | 56 | * Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux) 57 | * Toggle preview (`Shift+CMD+V` on macOS or `Shift+Ctrl+V` on Windows and Linux) 58 | * Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (macOS) to see a list of Markdown snippets 59 | 60 | ### For more information 61 | 62 | * [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) 63 | * [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) 64 | 65 | **Enjoy!** 66 | -------------------------------------------------------------------------------- /Extras/supertalk-vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "--", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": [] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"], 12 | ["(", ")"] 13 | ], 14 | // symbols that are auto closed when typing 15 | "autoClosingPairs": [ 16 | ["{", "}"], 17 | ["[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["'", "'"] 21 | ], 22 | // symbols that can be used to surround a selection 23 | "surroundingPairs": [ 24 | ["{", "}"], 25 | ["[", "]"], 26 | ["(", ")"], 27 | ["\"", "\""], 28 | ["'", "'"] 29 | ] 30 | } -------------------------------------------------------------------------------- /Extras/supertalk-vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supertalk-vscode", 3 | "displayName": "Supertalk", 4 | "description": "Supertalk language support", 5 | "version": "0.0.1", 6 | "engines": { 7 | "vscode": "^1.56.0" 8 | }, 9 | "categories": [ 10 | "Programming Languages" 11 | ], 12 | "contributes": { 13 | "languages": [{ 14 | "id": "supertalk", 15 | "aliases": ["Supertalk", "supertalk"], 16 | "extensions": [".sts"], 17 | "configuration": "./language-configuration.json" 18 | }], 19 | "grammars": [{ 20 | "language": "supertalk", 21 | "scopeName": "source.supertalk", 22 | "path": "./syntaxes/supertalk.tmLanguage.json" 23 | }] 24 | } 25 | } -------------------------------------------------------------------------------- /Extras/supertalk-vscode/syntaxes/supertalk.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Supertalk", 4 | "patterns": [ 5 | { 6 | "include": "#comments" 7 | }, 8 | { 9 | "include": "#text" 10 | }, 11 | { 12 | "include": "#string-single" 13 | }, 14 | { 15 | "include": "#string-double" 16 | }, 17 | { 18 | "include": "#section" 19 | }, 20 | { 21 | "include": "#jump" 22 | }, 23 | { 24 | "include": "#directive" 25 | }, 26 | { 27 | "include": "#command" 28 | }, 29 | { 30 | "include": "#assign" 31 | }, 32 | { 33 | "include": "#conditional" 34 | }, 35 | { 36 | "include": "#localization-tag" 37 | }, 38 | { 39 | "include": "#asset" 40 | }, 41 | { 42 | "include": "#keywords" 43 | } 44 | ], 45 | "repository": { 46 | "text": { 47 | "name": "meta.supertalk.textline", 48 | "begin": "^([ \\t]*)(.*?)(:)", 49 | "beginCaptures": { 50 | "2": { 51 | "patterns": [ 52 | { 53 | "include": "#attributes" 54 | }, 55 | { 56 | "include": "$self" 57 | } 58 | ] 59 | }, 60 | "3": { 61 | "name": "keyword.operator.supertalk.text" 62 | } 63 | }, 64 | "end": "^((?!\\1[ \\t]+)|(?=[ \\t]*\\*))", 65 | "contentName": "string.unquoted.supertalk.text", 66 | "patterns": [ 67 | { 68 | "include": "#text-variable" 69 | }, 70 | { 71 | "include": "#markup-tag" 72 | } 73 | ] 74 | }, 75 | 76 | "comments": { 77 | "name": "comment.line.double-dash.supertalk", 78 | "match": "--.*$" 79 | }, 80 | 81 | "section": { 82 | "name": "entity.name.supertalk.section", 83 | "match": "\\s*(#)(.*)$", 84 | "captures": { 85 | "1": { 86 | "name": "keyword.control.supertalk.section" 87 | }, 88 | "2": { 89 | "name": "entity.name.type.supertalk.sectionname" 90 | } 91 | } 92 | }, 93 | 94 | "jump": { 95 | "name": "supertalk.jump", 96 | "match": "(->)(.*)$", 97 | "captures": { 98 | "1": { 99 | "name": "keyword.control.supertalk.jump" 100 | }, 101 | "2": { 102 | "name": "entity.name.type.supertalk.sectionname" 103 | } 104 | } 105 | }, 106 | 107 | "assign": { 108 | "name": "keyword.operator.supertalk.assign", 109 | "match": "=" 110 | }, 111 | 112 | "conditional": { 113 | "name": "keyword.operator.supertalk.conditional", 114 | "match": "\\?" 115 | }, 116 | 117 | "asset": { 118 | "name": "variable.name.supertalk.asset", 119 | "match": "/[^\\.>\\[\\]=:,\\n\\r]+" 120 | }, 121 | 122 | "localization-tag": { 123 | "name": "variable.name.supertalk.localization-tag", 124 | "match": "@[^\\s:'\"]+" 125 | }, 126 | 127 | "directive": { 128 | "name": "supertalk.directive", 129 | "match": "(![^\\s]+)[^\\n\\r]*", 130 | "captures": { 131 | "1": { 132 | "name": "keyword.control.supertalk.directive" 133 | }, 134 | "2": { 135 | "name": "meta.parameter" 136 | } 137 | } 138 | }, 139 | 140 | "string-single": { 141 | "name": "string.quoted.single", 142 | "match": "'[^\\n\\r']*?'" 143 | }, 144 | "string-double": { 145 | "name": "string.quoted.double", 146 | "match": "\"[^\\n\\r\"]*?\"" 147 | }, 148 | 149 | "command": { 150 | "name": "supertalk.command", 151 | "match": "^\\s*(>)\\s*([^\\s]*)(.*)$", 152 | "captures": { 153 | "1": { 154 | "name": "keyword.operator.supertalk.command" 155 | }, 156 | "2": { 157 | "name": "entity.name.function.supertalk.command" 158 | }, 159 | "3": { 160 | "name": "meta.parameter" 161 | } 162 | } 163 | }, 164 | 165 | "attributes": { 166 | "name": "supertalk.attributes", 167 | "begin": "\\[", 168 | "end": "\\]", 169 | "patterns": [ 170 | { 171 | "match": "[^,\\]]+", 172 | "name": "variable.name" 173 | } 174 | ], 175 | "captures": { 176 | "2": { 177 | "name": "string.other.supertalk.attribute" 178 | }, 179 | "4": { 180 | "name": "string.other.supertalk.attribute" 181 | } 182 | } 183 | }, 184 | 185 | "text-variable": { 186 | "name": "supertalk.text.variable", 187 | "match": "{(.*?)}", 188 | "captures": { 189 | "1": { 190 | "name": "variable.other.supertalk.text" 191 | } 192 | } 193 | }, 194 | 195 | "markup-tag": { 196 | "name": "entity.name.tag", 197 | "match": "<([^\\n/>]*)/?>" 198 | }, 199 | 200 | "keywords": { 201 | "name": "keyword.other", 202 | "match": "(?i)\\b(true|false|none|if|then|else)\\b" 203 | } 204 | }, 205 | "scopeName": "source.supertalk" 206 | } -------------------------------------------------------------------------------- /Extras/supertalk-vscode/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your language support and define the location of the grammar file that has been copied into your extension. 7 | * `syntaxes/supertalk.tmLanguage.json` - this is the Text mate grammar file that is used for tokenization. 8 | * `language-configuration.json` - this is the language configuration, defining the tokens that are used for comments and brackets. 9 | 10 | ## Get up and running straight away 11 | 12 | * Make sure the language configuration settings in `language-configuration.json` are accurate. 13 | * Press `F5` to open a new window with your extension loaded. 14 | * Create a new file with a file name suffix matching your language. 15 | * Verify that syntax highlighting works and that the language configuration settings are working. 16 | 17 | ## Make changes 18 | 19 | * You can relaunch the extension from the debug toolbar after making changes to the files listed above. 20 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 21 | 22 | ## Add more language features 23 | 24 | * To add features such as intellisense, hovers and validators check out the VS Code extenders documentation at https://code.visualstudio.com/docs 25 | 26 | ## Install your extension 27 | 28 | * To start using your extension with Visual Studio Code copy it into the `/.vscode/extensions` folder and restart Code. 29 | * To share your extension with the world, read on https://code.visualstudio.com/docs about publishing an extension. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sam Bloomberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Supertalk 2 | 3 | **[Sample Project](https://github.com/redxdev/SupertalkSample)** 4 | 5 | Welcome to Supertalk! This is a simple dialogue scripting language for Unreal Engine created due to frustration with 6 | visual dialogue tree workflows. 7 | 8 | The plugin is made up of two parts: an editor module and a runtime module. The editor module handles importing supertalk 9 | scripts (.sts files) into a form that the engine can use. The runtime module includes a supertalk "player" which handles 10 | playback for imported scripts. 11 | 12 | Supertalk is currently configured for Unreal Engine 5.1, but may work with earlier versions with some minor modifications. 13 | 14 | **Supertalk is not a ready-to-use solution for dialogue systems!** It requires a good amount of integration work and comes 15 | with no UI. While it does integrate well with blueprint, initial integration with a game requires knowledge of C++ and no 16 | support will be offered beyond this documentation. 17 | 18 | ## Warning 19 | 20 | Supertalk is still under development. While it works, syntax is subject to change. 21 | 22 | ## Known Issues / TODOs 23 | 24 | * Issue: Recursive calls into `USupertalkPlayer::PlayScript` are not currently allowed, though they should technically be possible. This 25 | somewhat limits how dynamic scripts can be (you can't change what section is playing based on external data) but this will be 26 | fixed in the future. 27 | * Issue: The way the supertalk parser/importer emits errors is a bit odd, haven't quite figured out how to deal with the message log the 28 | right way. It generally works fine but might not be implemented the "right" way. 29 | * Issue: Localization support hasn't been very well tested. 30 | * TODO: Tool to automatically insert localization markup in script files. 31 | * TODO: Support for escape sequences in strings. 32 | * TODO: More expression operators (basic math, less/greater than, etc) 33 | * Math operators will require explicit number type support 34 | * TODO: Replace variable values with an expression type. 35 | * Requires expression support in more places before this change can be made. 36 | * Also requires replacing "member" values with an expression. Unsure how this will be implemented, will likely need to break backwards compatibility. 37 | * TODO: Rework `USupertalkValue` to not require an entirely new object for every value being passed around. 38 | * Potentially support raw UObjects without a wrapper object. 39 | * Refactor things to support passing around basic types without a UObject allocation. 40 | * TODO: track source file context at least in non-shipping builds to allow emitting better error messages at runtime. 41 | * TODO: True function call parsing 42 | * Current method of calling functions relies on FText and CallFunctionByNameWithArguments, both of which are bad since we can't pass complex types around. 43 | * TODO: Support for additional types 44 | * Integers, floats, etc. 45 | * Should be done as part of the new function call parsing system 46 | * TODO: Editor improvements 47 | * General stability improvements for the script editor 48 | 49 | ## Scripting Syntax + Features 50 | 51 | Scripts have the extension `.sts` and can be imported once this plugin is enabled simply by dragging one into the content browser. 52 | Alternatively, you may turn on auto-import which is the more ideal workflow as it will automatically re-import changes made to the 53 | script files. 54 | 55 | ``` 56 | -- Comments are preceded by two dashes 57 | 58 | -- Very simple variables can be used. You can reference assets within a script which can be used 59 | -- to define who is speaking a line of dialogue, or if they implement the ISupertalkDisplayInterface 60 | -- they can be embedded within lines of dialogue themselves. 61 | -- Supertalk will attempt to resolve the variable to an asset as long as the value starts with a slash. 62 | -- For example, you could have a "character" asset that would be used here that contains references to 63 | -- portrait images, the character's name, etc. 64 | Person1 = /Game/MyGame/Characters/Person1 65 | Person2 = /Game/MyGame/Characters/Person2 66 | 67 | -- This will play a line of dialogue. 68 | Person1: Hello, world! 69 | 70 | -- You can use FString::Format syntax to reference variables. 71 | Person2: Hello, {Person2}! 72 | 73 | -- Data tables with a row type of FSupertalkTableRow can be used to share textual data across scripts. 74 | -- Rows can be accessed via a "." - types other than data tables may also support this syntax, see the 75 | -- integration guide for details. 76 | MyDataTable = /Game/MyGame/Data/SomeDataTable 77 | 78 | Person2: My datatable says {MyDataTable.Key1} and {MyDataTable.Key2}! 79 | 80 | -- You can pass just a string as a speaker's name 81 | "Some third person": I'm not predefined in a variable! 82 | 83 | -- If you don't have any special characters, you don't need to quote the name (though you may still want to 84 | -- in order to differentiate it from an actual variable). 85 | Person3: I will appear as "Person3". 86 | 87 | -- You can pass an alternate name for a predefined speaker 88 | Person1,"An alternate name": I'm still {Person1} but with a different name. 89 | 90 | -- You can pass along attributes with a line - each attribute, separated by a comma, is passed as an FName 91 | -- as part of the line of dialogue. This can be used, for example, to define a position for character portraits to appear 92 | -- or what emotion a character should be showing. 93 | Person2 [Left, Sad]: I'm sad now :( 94 | 95 | -- You can add linebreaks freely as long as they are indented. Line breaks will only be added to the final output if 96 | -- you have an empty line (similar to markdown). 97 | Person1: This sentence will span 98 | multiple lines in the script 99 | but will appear as a single 100 | one when played. 101 | 102 | This will appear as a second 103 | line to go along with the first. 104 | 105 | -- Supertalk supports calling events on blueprints and UFUNCTIONs on C++ UObjects. See the integration guide for an 106 | -- example of how to do this. Supertalk even supports latent actions (events that take time) and will wait on them 107 | -- if they have been configured appropriately. 108 | -- You can use {Formatting} syntax to pass variables to functions (with some limitations). 109 | > Wait 10 110 | > DoSomethingCool 111 | > DoSomethingElse {Variable1} 112 | 113 | -- In some cases you may want to do multiple things at a time. For example, display a line of dialogue at the same time 114 | -- that you have a character walk to a new position. You can do this with a "parallel" block. Note that this is not truly 115 | -- parallel in the sense of threading - this simply runs all statements inside it in order *and then* waits for all of them 116 | -- to complete if any are latent. 117 | [ 118 | > WalkToNewPosition 119 | Person2: I need to say something while someone walks to a new position. 120 | ] 121 | 122 | -- Supertalk supports passing along a list of choices with a dialogue line. You can then define what happens with each choice. 123 | -- By default you may only pass along a single statement to execute with a choice - if you want to pass multiple you can use 124 | -- curly braces (*not* square brackets - that would be a parallel block instead) to group multiple statements together. 125 | Person1: Do you like cake or pie? 126 | * Cake 127 | Person1: You chose cake! 128 | * Pie 129 | { 130 | Person1: You chose pie! 131 | > DoSomethingCool 132 | } 133 | 134 | -- Depending on the game you might find it useful for emit "blank" lines to your integration. This syntax will cause 135 | -- FSupertalkLine::bIsBlankLine to be set to true. The primary use-case is, for example, to setup multiple character portraits 136 | -- without requiring them to actually speak. 137 | Person1 [Sad]; 138 | Person2 [Happy]; 139 | 140 | -- Supertalk supports having multiple "sections" in a single script. Only the first section will execute by default, but you can 141 | -- either include a jump (using an arrow ->) or from C++ tell the supertalk player to run an alternate section. Jumps are normal 142 | -- statements like commands or dialogue lines and as such can be used pretty much anywhere. 143 | -- Note that there is *not* any fall-through from one section to another. If you reach the end of a section and there is no jump then 144 | -- the script will simply stop instead of falling through to the next section. 145 | -- 146 | -- The first section - whether named or not - will play when "None" is passed as the initial section to play. 147 | 148 | -> Section2 149 | 150 | # Section2 151 | 152 | Person2: Which section do you want to go to? 153 | * 3 154 | -> Section3 155 | * 4 156 | -> Section4 157 | 158 | # Section3 159 | 160 | Person2: I'm in section 3! 161 | -> Section5 162 | 163 | # Section4 164 | 165 | Person2: I'm in section 4! 166 | -> Conditionals 167 | 168 | # Conditionals 169 | -- Supertalk has very basic support for control flow/conditional execution. You can set a variable to true or false either in a script or from C++: 170 | MyTrueValue = true 171 | MyFalseValue = false 172 | 173 | -- And then test the value using an if statement. 174 | if MyTrueValue then Person1: I'll be executed because this conditional is true! 175 | 176 | -- You can add an else clause as well: 177 | if MyFalseValue then Person1: I won't be executed. 178 | else Person1: I will be executed! 179 | 180 | -- You can chain if/else statements together: 181 | if MyFalseValue then Person1: I won't be executed. 182 | else if MyTrueValue then Person1: I will be executed! 183 | else Person1: I won't be executed. 184 | 185 | -- You can run multiple statements at a time using the usual block syntax with curly braces. 186 | -- Parallel blocks are also supported. 187 | if MyTrueValue then 188 | { 189 | Person1: I can do one thing. 190 | Person2: I can do another! 191 | } 192 | else 193 | [ 194 | Person1: If this were executed, it would be in parallel with the next command. 195 | > SomeCommand 196 | ] 197 | 198 | -- Basic operators are supported, including tests for equality and unary not operations. 199 | -- Note that the "not" syntax is similar to lua using ~ instead of !. 200 | if ~FalseValue then Person1: I will be executed! 201 | if FalseValue == FalseValue then Person1: I will also be executed! 202 | if FalseValue ~= FalseValue then Person1: I won't be executed. 203 | 204 | -> Localization 205 | 206 | # Localization 207 | 208 | -- Adding '@' after the list of attributes (if they exist) will be used as the key for the dialogue line, for use in i18n. 209 | -- Keys are any arbitrary value that are used to lookup alternative translations - see 210 | -- https://docs.unrealengine.com/4.26/en-US/ProductionPipelines/Localization/Formatting/ for more information. 211 | -- If you don't specify a key then one will be automatically assigned. Unfortunately if the line changes so will the 212 | -- automatically assigned id. As such you should always assign keys manually to any line that can be localized. 213 | -- At some point a tool to auto-generate keys and insert them into supertalk scripts would be nice but it doesn't currently exist. 214 | Person1 [Happy] @L01A: This line could be translated! 215 | 216 | -- Choices are localized separately from their owning lines. 217 | Person1 @L01B: Here are some choices that could be translated. 218 | * @L01B_C1 I'm choice 1! 219 | * @L01B_C2 I'm choice 2! 220 | 221 | -- Namespaces can be specified by separating them from the key with '/' 222 | -- If a namespace isn't specified, the default namespace "Supertalk.Script.Default" will be used. 223 | Person1 [Sad] @Dialogue.Example/L01C: This line could also be translated! 224 | 225 | -- String literals (which are stored internally as FText) can be given localization keys as well, with the same syntax. 226 | MyLocalizedString = @Dialogue.Example/L01D "I'm a string literal that can be localized!" 227 | 228 | -- You can apply a single namespace to everything below it with a "namespace directive". 229 | -- This applies regardless of section due to namespace directives being resolved at the time of importing the script rather than 230 | -- at runtime. Specifying a namespace directly (such as in the previous two examples) will override the namespace directive where 231 | -- it is used. 232 | !namespace Dialogue.Example 233 | 234 | -- That's it for localization! 235 | -> TheEnd 236 | 237 | # TheEnd 238 | 239 | Person1: That's the end of the supertalk script overview. Goodbye for now! 240 | 241 | -- You can end a script's execution either by having no more statements in a section or by jumping to 'None' 242 | -> None 243 | ``` 244 | 245 | ## VSCode Syntax Highlighting 246 | 247 | A basic syntax highlighting extension for Visual Studio Code can be found in the `Extras` directory. 248 | 249 | ![image](https://user-images.githubusercontent.com/472625/121821540-88d35a00-cc4e-11eb-9910-64d69edc6012.png) 250 | 251 | ## Experimental Editor 252 | 253 | An experimental editor can be enabled by going to Project Settings > Editor > Supertalk Editor. Restart the engine after changing this option. 254 | This also lets you create Supertalk scripts within the editor without having to import from an external file. 255 | 256 | This editor is *super* experimental and you may lose data when using it! 257 | 258 | ![image](https://user-images.githubusercontent.com/472625/212571344-f7f56e0e-3f20-4758-ae14-95bf3ae393a8.png) 259 | 260 | ## Integration Guide 261 | 262 | This is an overview of the work necessary to integrate Supertalk into a project. 263 | 264 | ### Major Types 265 | 266 | All of these types are in the `Supertalk` module - generally you will not have to use anything in `SupertalkEditor` as it exists purely 267 | to provide the editor with an importer for supertalk script files. 268 | 269 | #### `USupertalkPlayer` 270 | 271 | This is your primary interaction point with Supertalk and implements almost all of the major functionality. It is a very simple "VM" that 272 | executes supertalk scripts. 273 | 274 | Each Supertalk player contains its own state of execution and list of variables. You can get and set variables on it manually as well. 275 | 276 | #### `USupertalkScript` 277 | 278 | This is the main asset type for Supertalk. 279 | 280 | #### `FSupertalkLine` 281 | 282 | Dialogue lines are emitted via this struct. It contains a value representing who is speaking the line, a value for a name override for the 283 | speaker, a list of attributes applied to the line, and the text itself. 284 | 285 | Generally you'll want to call `FSupertalkLine::FormatText` to get the text for display, as this will replace any variable placeholders in the 286 | line with data from the Supertalk player. 287 | 288 | #### `USupertalkValue` 289 | 290 | Base class for a "value" which can be anything - an object, text, datatable, etc. 291 | 292 | #### `USupertalkExpression` 293 | 294 | Base class for an evaluatable expression that results in a value. 295 | 296 | #### `ISupertalkDisplayInterface` 297 | 298 | Implement on custom UObjects to provide a way to override how Supertalk will display those objects in dialogue lines. You can also override 299 | `ISupertalkDisplayInterface::GetSupertalkMember` in order to provide a way to get sub-values of an object. 300 | 301 | #### `FSupertalkTableRow` 302 | 303 | Any data table used within Supertalk should use this row type, which allows storing text that can be reused in multiple scripts. 304 | 305 | #### `FSupertalkLatentFunctionFinalizer` 306 | 307 | Supertalk uses this object as a way to tell when latent functions are being called. Calling `FSupertalkLatentFunctionFinalizer::Complete()` will tell 308 | the Supertalk player that the action has completed and it may continue. These are also usable in blueprint (to allow for calling latent actions in 309 | events) - a blueprint event being called from Supertalk should call `USupertalkPlayer::MakeLatentFunction` to receive a finalizer and then call 310 | `USupertalkPlayer::CompleteFunction` when it has finished. 311 | 312 | If an event is not latent (i.e. it doesn't take up any time) then it should not call `MakeLatentFunction` at all. 313 | 314 | ### Integrating the Supertalk Player 315 | 316 | The Supertalk player is your primary entrypoint into Supertalk - it represents the "state" of a script. You can have any number of them at a time 317 | and they can share scripts, but each one maintains its own state completely separate from the others. 318 | 319 | You can create a Supertalk player with `NewObject()`. Once created, you can bind events to handle when dialogue lines (or set of choices) 320 | are played, and you can register handlers to receive 321 | 322 | ```cpp 323 | // In some function somewhere - maybe the initialization for a cutscene class. 324 | USupertalkPlayer* STPlayer = NewObject(this); 325 | STPlayer->OnPlayLineEvent.BindUObject(this, &ThisClass::OnPlayLine); 326 | STPlayer->OnPlayChoiceEvent.BindUObject(this, &ThisClass::OnPlayChoice); 327 | 328 | // If you want to be able to call functions on the current object (or some other object) from within a script, you need to add a function call receiver. 329 | // Any public UFUNCTION (or blueprint event or function) will be callable, and the syntax for calling a command is the same as the "ke" console command. 330 | STPlayer->AddFunctionCallReceiver(this); 331 | 332 | // If you want to expose global variables to a script without having to set them manually, you can add a "variable provider" which is a function that is 333 | // called when a variable isn't found in the player itself. For example, if a variable named `Actor_Something` is found you could parse out the "Something" 334 | // and return a value pointing to the actor named "Something" in the current level. 335 | // Providers are executed in order until one returns something other than null. If they all return null then the variable doesn't exist. 336 | STPlayer->AddVariableProvider(FSupertalkProvideVariableDelegate::CreateUObject(this, &ThisClass::VarProvider_Actors)); 337 | 338 | // You can also expose all properties of an object to a script. The second argument is a "filter" - properties will only be exposed 339 | // if they exist on child classes (*not including the class itself*) 340 | STPlayer->AddVariableProvider(this, &ThisClass::StaticClass()); 341 | ``` 342 | 343 | `OnPlayLineEvent` and `OnPlayChoiceEvent` are important to implement or else you will not find out when a dialogue line/choice has been played. When you are 344 | done playing the dialogue line/choice (which generally entails showing it on-screen and then waiting for user input to continue) you should call the `Completed` 345 | delegate. 346 | 347 | For `OnPlayChoiceEvent` you must also pass an integer representing the index of the selected choice. If an invalid choice (or no choice, if that's allowed) is 348 | selected then you should pass `INDEX_NONE` as the index. Note that this will result in no choice statements being executed - the Supertalk player will simply 349 | continue to the next statement after the current set of choices. 350 | 351 | After you've created the Supertalk player, you can run scripts with `USupertalkPlayer::RunScript(USupertalkScript* Script)`. 352 | 353 | ## VM Internals 354 | 355 | The Supertalk parser is *only* used for asset importing and as such is editor-only. It is a very simple handwritten lexer and parser combo - The primary entrypoint 356 | is `USupertalkParser::Parse` which calls into both the lexer (`USupertalkParser::RunLexer`) and the parser (`USupertalkParser::RunParser`). Lexer functions are prefixed 357 | by `Lx` while parser functions are prefixed by `Pa`. The result of the parser is a `USupertalkScript` object which can be saved to disk as an asset. 358 | 359 | The Supertalk VM/Player is incredibly simple - a script asset is effectively just the AST of the original supertalk script with a little bit of extra processing 360 | done on it. The list of possible action types can be found in `ESupertalkOperation`, while actions themselves are implemented in `USupertalkPlayer`. Each action can 361 | also have additional parameters, implemented by subclassing `USupertalkOperationParams`. Actions themselves are represented by `FSupertalkAction`. 362 | 363 | The VM contains a list of stacks (`FSupertalkStack`). Each stack contains a list of actions to be executed and a single active action. Each stack is assigned an id, 364 | as is each action. Ids are used throughout the VM to retrieve stacks and to validate that an operation is happening on the correct stack/action. When a script is 365 | played the VM will start with a single stack and it will queue all the actions for the initial section of that script. 366 | 367 | When a latent action is executed (either due to a dialogue line/choice executing or due to `MakeLatentFunction` being called) the current stack is paused. When the action is 368 | completed (via a `Completed` delegate call or via `FSupertalkLatentFunctionFinalizer::Complete()`) the stack is notified and continues. 369 | 370 | When a parallel block is encountered a set of new stacks is created - one for each action (or group of actions) inside the block. These stacks are assigned unique ids and the 371 | original stack stores these ids in a waiting list (`FSupertalkStack::WaitingOn`) and pauses itself. When a stack completes (usually due to running out of actions to execute) it 372 | removes itself from the original stack's waiting list. Once that waiting list is empty the original stack will resume execution. 373 | 374 | Execution of queue blocks (lists of actions to be executed sequentially inside parenthesis) is not given any special handling - actions are simply added to the current stack 375 | in the appropriate order. 376 | 377 | Section jumps cause the current stack to be emptied and then refilled from the given section. The one case where this doesn't happen is upon a section jump to `None`, in which 378 | case the stack is simply emptied - thus ending that stack's execution. 379 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redxdev/Supertalk/98bc97ef7a5909d4c4bd8eec4599b9dd05989525/Resources/Icon128.png -------------------------------------------------------------------------------- /Source/Supertalk/Supertalk.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | using UnrealBuildTool; 4 | 5 | public class Supertalk : ModuleRules 6 | { 7 | public Supertalk(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicDependencyModuleNames.AddRange( 12 | new string[] 13 | { 14 | "Core", 15 | "CoreUObject", 16 | "Engine", 17 | }); 18 | 19 | if (Target.Configuration != UnrealTargetConfiguration.Shipping) 20 | { 21 | PrivateDependencyModuleNames.AddRange( 22 | new string[] 23 | { 24 | "MessageLog", 25 | }); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Source/Supertalk/Supertalk.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "Supertalk.h" 4 | 5 | #if !UE_BUILD_SHIPPING 6 | #include "MessageLogModule.h" 7 | #endif 8 | 9 | DEFINE_LOG_CATEGORY(LogSupertalk) 10 | 11 | const FName SupertalkMessageLogName = FName(TEXT("SupertalkMessageLog")); 12 | 13 | IMPLEMENT_MODULE(FSupertalkModule, Supertalk); 14 | 15 | void FSupertalkModule::StartupModule() 16 | { 17 | UE_LOG(LogSupertalk, Log, TEXT("Supertalk startup")); 18 | 19 | #if !UE_BUILD_SHIPPING 20 | if (FModuleManager::Get().IsModuleLoaded("MessageLog")) 21 | { 22 | FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); 23 | 24 | FMessageLogInitializationOptions Options; 25 | Options.bAllowClear = true; 26 | Options.bShowInLogWindow = true; 27 | MessageLogModule.RegisterLogListing(SupertalkMessageLogName, NSLOCTEXT("Supertalk", "SupertalkMessageLog", "Supertalk"), Options); 28 | } 29 | #endif 30 | } 31 | 32 | void FSupertalkModule::ShutdownModule() 33 | { 34 | UE_LOG(LogSupertalk, Log, TEXT("Supertalk shutdown")); 35 | 36 | #if !UE_BUILD_SHIPPING 37 | if (FModuleManager::Get().IsModuleLoaded("MessageLog")) 38 | { 39 | FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); 40 | MessageLogModule.UnregisterLogListing(SupertalkMessageLogName); 41 | } 42 | #endif 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Source/Supertalk/Supertalk.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | SUPERTALK_API DECLARE_LOG_CATEGORY_EXTERN(LogSupertalk, Log, All); 8 | 9 | extern SUPERTALK_API const FName SupertalkMessageLogName; 10 | 11 | SUPERTALK_API class FSupertalkModule : public IModuleInterface 12 | { 13 | public: 14 | virtual void StartupModule() override; 15 | virtual void ShutdownModule() override; 16 | }; -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkExpression.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkExpression.h" 4 | #include "SupertalkValue.h" 5 | #include "SupertalkPlayer.h" 6 | 7 | const USupertalkValue* ResolveExpression(USupertalkPlayer* Player, USupertalkExpression* Expr) 8 | { 9 | if (IsValid(Expr)) 10 | { 11 | const USupertalkValue* Value = Expr->Evaluate(Player); 12 | if (IsValid(Value)) 13 | { 14 | return Value->GetResolvedValue(Player); 15 | } 16 | } 17 | 18 | return nullptr; 19 | } 20 | 21 | const USupertalkValue* USupertalkExpression_Value::Evaluate(USupertalkPlayer* Player) 22 | { 23 | return Value; 24 | } 25 | 26 | const USupertalkValue* USupertalkExpression_Equality::Evaluate(USupertalkPlayer* Player) 27 | { 28 | if (SubExpressions.Num() == 0) 29 | { 30 | return nullptr; 31 | } 32 | 33 | check(SubExpressions.Num() == Operations.Num() + 1); 34 | const USupertalkValue* Result = ResolveExpression(Player, SubExpressions[0]); 35 | for (int32 Idx = 1; Idx < SubExpressions.Num(); ++Idx) 36 | { 37 | USupertalkBooleanValue* BoolResult = NewObject(Player); 38 | if (IsValid(Result)) 39 | { 40 | BoolResult->bValue = Result->IsValueEqualTo(ResolveExpression(Player, SubExpressions[Idx])); 41 | } 42 | else 43 | { 44 | BoolResult->bValue = !ResolveExpression(Player, SubExpressions[Idx]); 45 | } 46 | 47 | if (Operations[Idx - 1] == ESupertalkExpression_Equality_Operation::NotEqual) 48 | { 49 | BoolResult->bValue = !BoolResult->bValue; 50 | } 51 | 52 | Result = BoolResult; 53 | } 54 | 55 | return Result; 56 | } 57 | 58 | const USupertalkValue* USupertalkExpression_Not::Evaluate(USupertalkPlayer* Player) 59 | { 60 | const USupertalkValue* EvaluatedValue = IsValid(Value) ? Value->Evaluate(Player) : nullptr; 61 | EvaluatedValue = EvaluatedValue ? EvaluatedValue->GetResolvedValue(Player) : nullptr; 62 | 63 | bool Result; 64 | if (EvaluatedValue) 65 | { 66 | if (const USupertalkBooleanValue* BoolValue = Cast(EvaluatedValue)) 67 | { 68 | Result = BoolValue->bValue; 69 | } 70 | else 71 | { 72 | Result = true; 73 | } 74 | } 75 | else 76 | { 77 | Result = false; 78 | } 79 | 80 | Result = !Result; 81 | 82 | USupertalkBooleanValue* ResultValue = NewObject(Player); 83 | ResultValue->bValue = Result; 84 | return ResultValue; 85 | } 86 | -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkExpression.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "SupertalkExpression.generated.h" 7 | 8 | UCLASS(Abstract) 9 | class SUPERTALK_API USupertalkExpression : public UObject 10 | { 11 | GENERATED_BODY() 12 | 13 | public: 14 | virtual const class USupertalkValue* Evaluate(class USupertalkPlayer* Player) PURE_VIRTUAL(USupertalkExpression::Evaluate,return nullptr;) 15 | }; 16 | 17 | UCLASS() 18 | class SUPERTALK_API USupertalkExpression_Value : public USupertalkExpression 19 | { 20 | GENERATED_BODY() 21 | 22 | public: 23 | UPROPERTY() 24 | TObjectPtr Value; 25 | 26 | virtual const USupertalkValue* Evaluate(class USupertalkPlayer* Player) override; 27 | }; 28 | 29 | UENUM() 30 | enum class ESupertalkExpression_Equality_Operation 31 | { 32 | Equal, 33 | NotEqual 34 | }; 35 | 36 | UCLASS() 37 | class SUPERTALK_API USupertalkExpression_Equality : public USupertalkExpression 38 | { 39 | GENERATED_BODY() 40 | 41 | public: 42 | UPROPERTY() 43 | TArray> SubExpressions; 44 | 45 | UPROPERTY() 46 | TArray Operations; 47 | 48 | virtual const class USupertalkValue* Evaluate(class USupertalkPlayer* Player) override; 49 | }; 50 | 51 | UCLASS() 52 | class SUPERTALK_API USupertalkExpression_Not : public USupertalkExpression 53 | { 54 | GENERATED_BODY() 55 | 56 | public: 57 | UPROPERTY() 58 | TObjectPtr Value; 59 | 60 | virtual const class USupertalkValue* Evaluate(USupertalkPlayer* Player) override; 61 | }; -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkLine.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkLine.h" 4 | #include "SupertalkPlayer.h" 5 | #include "SupertalkUtilities.h" 6 | #include "SupertalkValue.h" 7 | #include "Internationalization/TextFormatter.h" 8 | 9 | FText FSupertalkLine::GetSpeakerName(const USupertalkPlayer* Player) const 10 | { 11 | check(Player); 12 | 13 | if (IsValid(SpeakerNameOverride)) 14 | { 15 | return SpeakerNameOverride->ToResolvedDisplayText(Player); 16 | } 17 | 18 | if (IsValid(Speaker)) 19 | { 20 | return Speaker->ToResolvedDisplayText(Player); 21 | } 22 | 23 | return FText(); 24 | } 25 | 26 | FText FSupertalkLine::FormatText(const USupertalkPlayer* Player) const 27 | { 28 | return FSupertalkUtilities::FormatText(Text, Player, true); 29 | } 30 | -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkLine.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "SupertalkLine.generated.h" 7 | 8 | USTRUCT(BlueprintType) 9 | struct SUPERTALK_API FSupertalkAttribute 10 | { 11 | GENERATED_BODY() 12 | 13 | UPROPERTY() 14 | FName Name; 15 | }; 16 | 17 | USTRUCT(BlueprintType) 18 | struct SUPERTALK_API FSupertalkLine 19 | { 20 | GENERATED_BODY() 21 | 22 | FSupertalkLine() 23 | { 24 | Speaker = nullptr; 25 | SpeakerNameOverride = nullptr; 26 | bIsBlankLine = false; 27 | } 28 | 29 | UPROPERTY() 30 | TObjectPtr Speaker; 31 | 32 | UPROPERTY() 33 | TObjectPtr SpeakerNameOverride; 34 | 35 | UPROPERTY() 36 | TArray Attributes; 37 | 38 | UPROPERTY() 39 | FText Text; 40 | 41 | // "Blank" doesn't mean that Text is blank, but that the line should not cause a full line to appear. 42 | // This can be useful for applying attributes without actually playing a line. 43 | UPROPERTY() 44 | uint32 bIsBlankLine : 1; 45 | 46 | FText GetSpeakerName(const class USupertalkPlayer* Player) const; 47 | FText FormatText(const class USupertalkPlayer* Player) const; 48 | }; -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkPlayer.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkPlayer.h" 4 | #include "Supertalk.h" 5 | #include "SupertalkExpression.h" 6 | #include "SupertalkUtilities.h" 7 | #include "SupertalkValue.h" 8 | #include "EditorFramework/AssetImportData.h" 9 | #include "Logging/MessageLog.h" 10 | #include "UObject/ObjectSaveContext.h" 11 | 12 | #define LOCTEXT_NAMESPACE "Supertalk" 13 | 14 | const FGuid FSupertalkScriptCustomVersion::GUID(0xB47DCFA0, 0x0EE34B12, 0x11FFE8DC, 0xCF812143); 15 | static FDevVersionRegistration GRegisterSupertalkScriptCustomVersion(FSupertalkScriptCustomVersion::GUID, FSupertalkScriptCustomVersion::LatestVersion, TEXT("SupertalkScript")); 16 | 17 | USupertalkScript::USupertalkScript() 18 | { 19 | #if WITH_EDITOR 20 | bCanCompileFromSource = true; 21 | #endif 22 | } 23 | 24 | #if WITH_EDITOR 25 | 26 | FOnSupertalkScriptPreSave USupertalkScript::OnScriptPreSave; 27 | 28 | void USupertalkScript::PreSave(FObjectPreSaveContext SaveContext) 29 | { 30 | UObject::PreSave(SaveContext); 31 | 32 | OnScriptPreSave.ExecuteIfBound(this); 33 | } 34 | 35 | void USupertalkScript::PostInitProperties() 36 | { 37 | if (!HasAnyFlags(RF_ClassDefaultObject)) 38 | { 39 | AssetImportData = NewObject(this, TEXT("AssetImportData")); 40 | } 41 | 42 | Super::PostInitProperties(); 43 | } 44 | 45 | void USupertalkScript::GetAssetRegistryTags(TArray& OutTags) const 46 | { 47 | if (AssetImportData) 48 | { 49 | OutTags.Add(FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden)); 50 | } 51 | 52 | Super::GetAssetRegistryTags(OutTags); 53 | } 54 | 55 | void USupertalkScript::OpenSourceFileInExternalProgram() 56 | { 57 | if (AssetImportData) 58 | { 59 | const FString Filename = AssetImportData->GetFirstFilename(); 60 | if (FPaths::FileExists(Filename)) 61 | { 62 | FPlatformProcess::LaunchFileInDefaultExternalApplication(*Filename); 63 | } 64 | } 65 | } 66 | #endif 67 | 68 | #if WITH_EDITORONLY_DATA 69 | void USupertalkScript::Serialize(FArchive& Ar) 70 | { 71 | Super::Serialize(Ar); 72 | 73 | Ar.UsingCustomVersion(FSupertalkScriptCustomVersion::GUID); 74 | if (Ar.IsLoading()) 75 | { 76 | // There are a few cases to handle with regards to whether we enable compilation or not: 77 | // Super old (pre-AssetImportData) script -> disable compilation as we don't have any import data 78 | // Super old script that has AssetImportData but is missing SourceData -> disable compilation, assume this is a script that was resaved at some point without being reimported 79 | // Old script that has AssetImportData and SourceData -> enable compilation, we have SourceData. 80 | // New script (post-PreSaveCompilation) -> this check is skipped entirely, compilation will be enabled. 81 | // 82 | // Invalid AssetImportData but valid SourceData isn't possible until post-PreSaveCompilation so it's not a case we have to handle. 83 | // 84 | // The reason for disabling compilation when we don't have valid SourceData is that old versions of Supertalk didn't save the raw script text into the asset and only compiled it 85 | // at import-time. We don't want to attempt to recompile those old scripts as that would blank out the compiled data despite the scripts otherwise working fine. Instead, we just emit a 86 | // warning during packaging because the scripts should probably be reimported at some point. 87 | if (Ar.CustomVer(FSupertalkScriptCustomVersion::GUID) < FSupertalkScriptCustomVersion::PreSaveCompilation && (!AssetImportData || SourceData.IsEmpty())) 88 | { 89 | SourceData = TEXT("!error This script was created with an old version of supertalk. To view, edit, or compile its contents please re-import it."); 90 | bCanCompileFromSource = false; 91 | } 92 | 93 | if (Ar.UEVer() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !AssetImportData) 94 | { 95 | // AssetImportData should always be valid 96 | AssetImportData = NewObject(this, TEXT("AssetImportData")); 97 | } 98 | } 99 | } 100 | #endif 101 | 102 | void USupertalkAssignParams::PostLoad() 103 | { 104 | Super::PostLoad(); 105 | 106 | if (IsValid(Value_DEPRECATED)) 107 | { 108 | if (!IsValid(Expression)) 109 | { 110 | USupertalkExpression_Value* ValueExpr = NewObject(Value_DEPRECATED->GetOuter()); 111 | ValueExpr->Value = Value_DEPRECATED; 112 | Expression = ValueExpr; 113 | } 114 | else 115 | { 116 | UE_LOG(LogSupertalk, Warning, TEXT("USupertalkAssignParams::PostLoad() - Both Value_DEPRECATED and Expression are set, removing Value_DEPRECATED.")); 117 | } 118 | 119 | Value_DEPRECATED = nullptr; 120 | } 121 | } 122 | 123 | void USupertalkConditionalParams::PostLoad() 124 | { 125 | Super::PostLoad(); 126 | 127 | if (IsValid(Value_DEPRECATED)) 128 | { 129 | if (!IsValid(Expression)) 130 | { 131 | USupertalkExpression_Value* ValueExpr = NewObject(Value_DEPRECATED->GetOuter()); 132 | ValueExpr->Value = Value_DEPRECATED; 133 | Expression = ValueExpr; 134 | } 135 | else 136 | { 137 | UE_LOG(LogSupertalk, Warning, TEXT("USupertalkConditionalParams::PostLoad() - Both Value_DEPRECATED and Expression are set, removing Value_DEPRECATED.")); 138 | } 139 | 140 | Value_DEPRECATED = nullptr; 141 | } 142 | } 143 | 144 | USupertalkPlayer::USupertalkPlayer() 145 | { 146 | NextActionId = 1; 147 | NextStackId = 1; 148 | } 149 | 150 | void USupertalkPlayer::SetVariable(FName Name, const USupertalkValue* Value) 151 | { 152 | if (!IsValid(Value)) 153 | { 154 | Variables.Remove(Name); 155 | } 156 | else 157 | { 158 | // We don't allow aliasing (pointers/references), so always resolve values. 159 | // TODO: can we store const TObjectPtrs? Is that a thing? 160 | Variables.Add(Name, const_cast(Value->GetResolvedValue(this))); 161 | } 162 | } 163 | 164 | void USupertalkPlayer::SetVariable(FName Name, bool Value) 165 | { 166 | USupertalkBooleanValue* BoolValue = NewObject(this); 167 | BoolValue->bValue = Value; 168 | SetVariable(Name, BoolValue); 169 | } 170 | 171 | void USupertalkPlayer::SetVariable(FName Name, FText Value) 172 | { 173 | USupertalkTextValue* TextValue = NewObject(this); 174 | TextValue->Text = Value; 175 | SetVariable(Name, TextValue); 176 | } 177 | 178 | const USupertalkValue* USupertalkPlayer::GetVariable(FName Name) const 179 | { 180 | const TObjectPtr* VarValue = Variables.Find(Name); 181 | if (VarValue != nullptr) 182 | { 183 | return *VarValue; 184 | } 185 | 186 | for (const FSupertalkVariableProviderObject& Provider : VariableProviderObjects) 187 | { 188 | if (!Provider.Object) 189 | { 190 | continue; 191 | } 192 | 193 | UClass* Class = Provider.Object->GetClass(); 194 | if (FProperty* Property = Class->FindPropertyByName(Name)) 195 | { 196 | UClass* OwnerClass = Property->GetOwnerClass(); 197 | if (Provider.ClassFilter && (!OwnerClass || OwnerClass == Provider.ClassFilter || !OwnerClass->IsChildOf(Provider.ClassFilter))) 198 | { 199 | continue; 200 | } 201 | 202 | USupertalkValue* Value = nullptr; 203 | if (USupertalkValue::PropertyToValue(const_cast(this), Provider.Object, Provider.Object, Property, true, Value)) 204 | { 205 | return Value; 206 | } 207 | } 208 | } 209 | 210 | for (const FSupertalkProvideVariableDelegate& Provider : VariableProviderDelegates) 211 | { 212 | if (Provider.IsBound()) 213 | { 214 | const USupertalkValue* Result = Provider.Execute(this, Name); 215 | if (Result != nullptr) 216 | { 217 | return Result; 218 | } 219 | } 220 | } 221 | 222 | return nullptr; 223 | } 224 | 225 | void USupertalkPlayer::ClearVariables() 226 | { 227 | Variables.Empty(); 228 | } 229 | 230 | void USupertalkPlayer::AddFunctionCallReceiver(UObject* Obj) 231 | { 232 | check(Obj); 233 | FunctionCallReceivers.AddUnique(Obj); 234 | } 235 | 236 | void USupertalkPlayer::AddVariableProvider(FSupertalkProvideVariableDelegate Provider) 237 | { 238 | check(Provider.IsBound()); 239 | VariableProviderDelegates.Add(Provider); 240 | } 241 | 242 | void USupertalkPlayer::AddVariableProvider(UObject* Object, UClass* ClassFilter) 243 | { 244 | if (ensure(Object)) 245 | { 246 | check(!ClassFilter || Object->IsA(ClassFilter)); 247 | VariableProviderObjects.Add({ Object, ClassFilter }); 248 | } 249 | } 250 | 251 | void USupertalkPlayer::RunScript(const USupertalkScript* Script, FName InitialSection) 252 | { 253 | FMessageLog MessageLog(SupertalkMessageLogName); 254 | 255 | if (ensureAlwaysMsgf(!IsRunningScript(), TEXT("Cannot run a script on a USupertalkPlayer when a script is already running"))) 256 | { 257 | if (!IsValid(Script)) 258 | { 259 | UE_LOG(LogSupertalk, Error, TEXT("Cannot run invalid script")); 260 | return; 261 | } 262 | 263 | if (InitialSection == NAME_None) 264 | { 265 | InitialSection = Script->DefaultSection; 266 | } 267 | 268 | const FSupertalkSection* Section = Script->Sections.Find(InitialSection); 269 | if (Section == nullptr) 270 | { 271 | MessageLog.Error(FText::Format(LOCTEXT("UnknownSectionError", "Unable to find section '{0}' in script '{1}"), FText::FromName(InitialSection), FText::FromString(Script->GetName()))); 272 | return; 273 | } 274 | 275 | if (Section->Actions.Num() == 0) 276 | { 277 | MessageLog.Warning(FText::Format(LOCTEXT("NoActionsWarning", "Section '{0}' in script '{1}' has no actions"), FText::FromName(Section->Name), FText::FromString(Script->GetName()))); 278 | return; 279 | } 280 | 281 | FSupertalkStack& Stack = CreateNewStack(); 282 | 283 | PushActions(Stack.StackId, Script, Section->Actions); 284 | TickStack(Stack.StackId); 285 | } 286 | else 287 | { 288 | MessageLog.Error(LOCTEXT("ScriptAlreadyRunningError", "Cannot run a script on a USupertalkPlayer when a script is already running")); 289 | } 290 | } 291 | 292 | void USupertalkPlayer::Stop() 293 | { 294 | Stacks.Empty(); 295 | } 296 | 297 | FSupertalkLatentFunctionFinalizer USupertalkPlayer::MakeLatentFunction() 298 | { 299 | if (CurrentFunctionFinalizer && !bIsFunctionCallLatent) 300 | { 301 | bIsFunctionCallLatent = true; 302 | return *CurrentFunctionFinalizer; 303 | } 304 | 305 | return FSupertalkLatentFunctionFinalizer(); 306 | } 307 | 308 | void USupertalkPlayer::CompleteFunction(FSupertalkLatentFunctionFinalizer Finalizer) 309 | { 310 | Finalizer.Complete(); 311 | } 312 | 313 | void USupertalkPlayer::OnPlayLine(const FSupertalkLine& Line, FSupertalkEventCompletedDelegate Completed) 314 | { 315 | FMessageLog MessageLog(SupertalkMessageLogName); 316 | 317 | if (OnPlayLineEvent.IsBound()) 318 | { 319 | OnPlayLineEvent.Execute(Line, Completed); 320 | } 321 | else 322 | { 323 | MessageLog.Warning(LOCTEXT("OnPlayLineUnimplemented", "OnPlayLine has not been implemented")); 324 | Completed.ExecuteIfBound(); 325 | } 326 | } 327 | 328 | void USupertalkPlayer::OnPlayChoice(const FSupertalkLine& Line, const TArray& Choices, FSupertalkChoiceCompletedDelegate Completed) 329 | { 330 | FMessageLog MessageLog(SupertalkMessageLogName); 331 | 332 | if (OnPlayChoiceEvent.IsBound()) 333 | { 334 | OnPlayChoiceEvent.Execute(Line, Choices, Completed); 335 | } 336 | else 337 | { 338 | MessageLog.Warning(LOCTEXT("OnPlayChoiceUnimplemented", "OnPlayChoice has not been implemented")); 339 | Completed.ExecuteIfBound(INDEX_NONE); 340 | } 341 | } 342 | 343 | FSupertalkStack& USupertalkPlayer::CreateNewStack() 344 | { 345 | FSupertalkStack Stack; 346 | Stack.StackId = GetNewStackId(); 347 | return Stacks.Add(Stack.StackId, Stack); 348 | } 349 | 350 | uint32 USupertalkPlayer::GetNewActionId() 351 | { 352 | const uint32 NewId = NextActionId++; 353 | if (NextActionId == 0) 354 | { 355 | NextActionId = 1; 356 | } 357 | 358 | return NewId; 359 | } 360 | 361 | uint32 USupertalkPlayer::GetNewStackId() 362 | { 363 | const uint32 NewId = NextStackId++; 364 | if (NextStackId == 0) 365 | { 366 | NextStackId = 1; 367 | } 368 | 369 | return NewId; 370 | } 371 | 372 | void USupertalkPlayer::PushActions(uint32 StackId, const USupertalkScript* Script, const TArray& Actions) 373 | { 374 | check(Script); 375 | check(Actions.Num() > 0); 376 | 377 | FSupertalkStack& Stack = Stacks.FindChecked(StackId); 378 | Stack.QueuedActions.Reserve(Stack.QueuedActions.Num() + Actions.Num()); 379 | for (int32 Idx = Actions.Num() - 1; Idx >= 0; --Idx) 380 | { 381 | const FSupertalkAction& Action = Actions[Idx]; 382 | PushAction(Stack, Script, Action); 383 | } 384 | } 385 | 386 | void USupertalkPlayer::PushAction(FSupertalkStack& Stack, const USupertalkScript* Script, const FSupertalkAction& Action) 387 | { 388 | check(Script); 389 | 390 | FSupertalkActionWithContext Context; 391 | Context.Source = Script; 392 | Context.Action = Action; 393 | Context.Key.StackId = Stack.StackId; 394 | Context.Key.ActionId = GetNewActionId(); 395 | Stack.QueuedActions.Add(Context); 396 | } 397 | 398 | void USupertalkPlayer::PushAction(uint32 StackId, const USupertalkScript* Script, const FSupertalkAction& Action) 399 | { 400 | check(Script); 401 | 402 | FSupertalkStack& Stack = Stacks.FindChecked(StackId); 403 | PushAction(Stack, Script, Action); 404 | } 405 | 406 | void USupertalkPlayer::CompleteActionAndTick(FSupertalkActionKey Key) 407 | { 408 | CompleteAction(Key); 409 | TickStack(Key.StackId); 410 | } 411 | 412 | void USupertalkPlayer::CompleteAction(FSupertalkActionKey Key) 413 | { 414 | FSupertalkStack* Stack = Stacks.Find(Key.StackId); 415 | if (Stack == nullptr) 416 | { 417 | UE_LOG(LogSupertalk, Warning, TEXT("CompleteAction called with unknown stack id %u"), Key.StackId); 418 | return; 419 | } 420 | 421 | if (!Key.IsValid() || Stack->ActiveAction.Key != Key) 422 | { 423 | UE_LOG(LogSupertalk, Warning, TEXT("CompleteAction called with unknown action id %u (stack %u expects %u)"), Key.ActionId, Key.StackId, Stack->ActiveAction.Key.ActionId); 424 | return; 425 | } 426 | 427 | Stack->ActiveAction = FSupertalkActionWithContext(); 428 | } 429 | 430 | void USupertalkPlayer::TickStack(uint32 StackId) 431 | { 432 | FSupertalkStack* Stack = Stacks.Find(StackId); 433 | if (Stack == nullptr) 434 | { 435 | UE_LOG(LogSupertalk, Error, TEXT("TickStack called with unknown stack id %u"), StackId); 436 | return; 437 | } 438 | 439 | // Prevent recursive ticking 440 | if (Stack->bIsTicking) 441 | { 442 | return; 443 | } 444 | 445 | Stack->bIsTicking = true; 446 | 447 | while (Stack != nullptr && !Stack->ActiveAction.Key.IsValid() && Stack->WaitingOn.Num() == 0) 448 | { 449 | if (Stack->QueuedActions.Num() > 0) 450 | { 451 | Stack->ActiveAction = Stack->QueuedActions.Last(); 452 | Stack->QueuedActions.RemoveAt(Stack->QueuedActions.Num() - 1); 453 | 454 | ExecuteAction(Stack->ActiveAction); 455 | } 456 | else 457 | { 458 | uint32 ExitingId = Stack->StackId; 459 | uint32 WaitingId = Stack->SourceId; 460 | 461 | Stacks.Remove(StackId); 462 | Stack = nullptr; 463 | 464 | if (WaitingId != 0) 465 | { 466 | FinishWaitingOnStack(ExitingId, WaitingId); 467 | } 468 | 469 | break; 470 | } 471 | 472 | // Need to re-find just in case Stacks was modified during execution. 473 | Stack = Stacks.Find(StackId); 474 | } 475 | 476 | if (Stack != nullptr) 477 | { 478 | Stack->bIsTicking = false; 479 | } 480 | } 481 | 482 | void USupertalkPlayer::ExecuteAction(const FSupertalkActionWithContext& Context) 483 | { 484 | check(Context.Source); 485 | check(Stacks.Contains(Context.Key.StackId)); 486 | check(Stacks[Context.Key.StackId].ActiveAction.Key == Context.Key); 487 | 488 | switch (Context.Action.Operation) 489 | { 490 | default: 491 | checkNoEntry(); 492 | // fall-through on purpose, count this as a no-op if checks are disabled. 493 | 494 | case ESupertalkOperation::Noop: 495 | CompleteAction(Context.Key); 496 | break; 497 | 498 | case ESupertalkOperation::Line: 499 | HandlePlayLine(Context); 500 | break; 501 | 502 | case ESupertalkOperation::Choice: 503 | HandlePlayChoice(Context); 504 | break; 505 | 506 | case ESupertalkOperation::Assign: 507 | HandleAssign(Context); 508 | break; 509 | 510 | case ESupertalkOperation::Call: 511 | HandleCall(Context); 512 | break; 513 | 514 | case ESupertalkOperation::Jump: 515 | HandleJump(Context); 516 | break; 517 | 518 | case ESupertalkOperation::Parallel: 519 | HandleParallel(Context); 520 | break; 521 | 522 | case ESupertalkOperation::Queue: 523 | HandleQueue(Context); 524 | break; 525 | 526 | case ESupertalkOperation::Conditional: 527 | HandleConditional(Context); 528 | break; 529 | } 530 | } 531 | 532 | void USupertalkPlayer::HandlePlayLine(const FSupertalkActionWithContext& Context) 533 | { 534 | USupertalkPlayLineParams* Params = CastChecked(Context.Action.Params); 535 | 536 | FSupertalkEventCompletedDelegate Completed; 537 | Completed.BindUObject(this, &ThisClass::CompleteActionAndTick, Context.Key); 538 | 539 | OnPlayLine(Params->Line, Completed); 540 | } 541 | 542 | void USupertalkPlayer::HandlePlayChoice(const FSupertalkActionWithContext& Context) 543 | { 544 | USupertalkPlayChoiceParams* Params = CastChecked(Context.Action.Params); 545 | check(Params->Choices.Num() > 0); 546 | 547 | FSupertalkChoiceCompletedDelegate Completed; 548 | Completed.BindUObject(this, &ThisClass::ReceiveChoice, Context.Key); 549 | 550 | TArray Choices; 551 | Choices.Reserve(Params->Choices.Num()); 552 | 553 | for (const FSupertalkChoice& Choice : Params->Choices) 554 | { 555 | Choices.Add(Choice.Text); 556 | } 557 | 558 | OnPlayChoice(Params->Line, Choices, Completed); 559 | } 560 | 561 | void USupertalkPlayer::ReceiveChoice(int32 ChoiceIndex, FSupertalkActionKey Key) 562 | { 563 | FSupertalkStack* Stack = Stacks.Find(Key.StackId); 564 | if (Stack == nullptr) 565 | { 566 | UE_LOG(LogSupertalk, Warning, TEXT("ReceiveChoice called with unknown stack id %u"), Key.StackId); 567 | return; 568 | } 569 | 570 | if (!Key.IsValid() || Stack->ActiveAction.Key != Key) 571 | { 572 | UE_LOG(LogSupertalk, Warning, TEXT("ReceiveChoice called with unknown action id %u (stack %u expects %u)"), Key.ActionId, Key.StackId, Stack->ActiveAction.Key.ActionId); 573 | return; 574 | } 575 | 576 | USupertalkPlayChoiceParams* Params = CastChecked(Stack->ActiveAction.Action.Params); 577 | if (ChoiceIndex < 0) 578 | { 579 | CompleteActionAndTick(Key); 580 | return; 581 | } 582 | 583 | if (ChoiceIndex >= Params->Choices.Num()) 584 | { 585 | UE_LOG(LogSupertalk, Error, TEXT("ReceiveChoice called with invalid choice index %d (expected < %d)"), ChoiceIndex, Params->Choices.Num()); 586 | } 587 | 588 | const FSupertalkChoice& Choice = Params->Choices[ChoiceIndex]; 589 | PushAction(*Stack, Stack->ActiveAction.Source, Choice.SubAction); 590 | 591 | CompleteActionAndTick(Key); 592 | } 593 | 594 | void USupertalkPlayer::HandleAssign(const FSupertalkActionWithContext& Context) 595 | { 596 | USupertalkAssignParams* Params = CastChecked(Context.Action.Params); 597 | 598 | const USupertalkValue* Value = nullptr; 599 | if (IsValid(Params->Expression)) 600 | { 601 | Value = Params->Expression->Evaluate(this); 602 | } 603 | else 604 | { 605 | Value = Params->Value_DEPRECATED; 606 | } 607 | 608 | Value = Value ? Value->GetResolvedValue(this) : nullptr; 609 | 610 | SetVariable(Params->Variable, Value); 611 | 612 | // Not necessary to tick, this can only happen as the result of an ongoing tick. 613 | CompleteAction(Context.Key); 614 | } 615 | 616 | void USupertalkPlayer::HandleCall(const FSupertalkActionWithContext& Context) 617 | { 618 | FMessageLog MessageLog(SupertalkMessageLogName); 619 | 620 | USupertalkCallParams* Params = CastChecked(Context.Action.Params); 621 | 622 | if (!ensure(!CurrentFunctionFinalizer)) 623 | { 624 | MessageLog.Error(FText::Format(LOCTEXT("EventCallFinalizerExists", "Finalizer already set, was there a recursive event call? Skipping function call: {0}"), FText::FromString(Params->Arguments))); 625 | CompleteAction(Context.Key); 626 | return; 627 | } 628 | 629 | FSupertalkLatentFunctionFinalizer Finalizer; 630 | Finalizer.Completed.BindUObject(this, &ThisClass::CompleteActionAndTick, Context.Key); 631 | CurrentFunctionFinalizer = &Finalizer; 632 | bIsFunctionCallLatent = false; 633 | 634 | // TODO: shouldn't be using FText for this. It's slow, it's converting back and forth between FText/FString. 635 | // Function calls need to be rewritten to support actual objects at some point and not strings, so this will go away whenever that happens. 636 | const FString FormattedArgs = FSupertalkUtilities::FormatText(FText::FromString(Params->Arguments), this, false).ToString(); 637 | 638 | bool bCalledFunction = false; 639 | for (UObject* Receiver : FunctionCallReceivers) 640 | { 641 | if (Receiver->CallFunctionByNameWithArguments(*FormattedArgs, *GLog, nullptr, true)) 642 | { 643 | bCalledFunction = true; 644 | break; 645 | } 646 | } 647 | 648 | CurrentFunctionFinalizer = nullptr; 649 | 650 | if (!bCalledFunction) 651 | { 652 | MessageLog.Error(FText::Format(LOCTEXT("FunctionCallFail", "Failed to call function from script: {0}"), FText::FromString(Params->Arguments))); 653 | CompleteAction(Context.Key); 654 | return; 655 | } 656 | 657 | if (!bIsFunctionCallLatent) 658 | { 659 | // MakeLatentFunction was not called, complete this event immediately 660 | CompleteAction(Context.Key); 661 | } 662 | } 663 | 664 | void USupertalkPlayer::HandleJump(const FSupertalkActionWithContext& Context) 665 | { 666 | check(Context.Source); 667 | 668 | FMessageLog MessageLog(SupertalkMessageLogName); 669 | 670 | USupertalkJumpParams* Params = CastChecked(Context.Action.Params); 671 | 672 | FSupertalkStack* Stack = Stacks.Find(Context.Key.StackId); 673 | if (Stack == nullptr) 674 | { 675 | UE_LOG(LogSupertalk, Error, TEXT("Cannot jump using unknown stack %u"), Context.Key.StackId); 676 | CompleteAction(Context.Key); 677 | return; 678 | } 679 | 680 | if (Params->JumpTarget == NAME_None) 681 | { 682 | // Forcefully end this stack 683 | Stack->QueuedActions.Empty(); 684 | CompleteAction(Context.Key); 685 | return; 686 | } 687 | 688 | const FSupertalkSection* NewSection = Context.Source->Sections.Find(Params->JumpTarget); 689 | if (NewSection == nullptr) 690 | { 691 | MessageLog.Error(FText::Format(LOCTEXT("JumpSectionError", "Cannot jump to unknown section '{0}'"), FText::FromName(Params->JumpTarget))); 692 | CompleteAction(Context.Key); 693 | return; 694 | } 695 | 696 | Stack->QueuedActions.Empty(); 697 | PushActions(Stack->StackId, Context.Source, NewSection->Actions); 698 | CompleteAction(Context.Key); 699 | } 700 | 701 | void USupertalkPlayer::HandleParallel(const FSupertalkActionWithContext& Context) 702 | { 703 | USupertalkParallelParams* Params = CastChecked(Context.Action.Params); 704 | if (Params->SubActions.Num() == 0) 705 | { 706 | UE_LOG(LogSupertalk, Warning, TEXT("Parallel action with no subactions, skipped")); 707 | CompleteAction(Context.Key); 708 | return; 709 | } 710 | 711 | FSupertalkStack* SourceStack = Stacks.Find(Context.Key.StackId); 712 | if (SourceStack == nullptr) 713 | { 714 | UE_LOG(LogSupertalk, Error, TEXT("Unknown stack %u when setting up parallel execution"), Context.Key.StackId); 715 | CompleteAction(Context.Key); 716 | return; 717 | } 718 | 719 | for (const FSupertalkAction& Action : Params->SubActions) 720 | { 721 | FSupertalkStack& Stack = CreateNewStack(); 722 | Stack.SourceId = SourceStack->StackId; 723 | SourceStack->WaitingOn.Add(Stack.StackId); 724 | 725 | PushAction(Stack, Context.Source, Action); 726 | TickStack(Stack.StackId); 727 | } 728 | 729 | CompleteAction(Context.Key); 730 | } 731 | 732 | void USupertalkPlayer::FinishWaitingOnStack(uint32 ExitingId, uint32 WaitingId) 733 | { 734 | FSupertalkStack* Stack = Stacks.Find(WaitingId); 735 | if (Stack == nullptr) 736 | { 737 | UE_LOG(LogSupertalk, Error, TEXT("FinishWaitingOnStack was given an invalid WaitingId of %u"), WaitingId); 738 | return; 739 | } 740 | 741 | if (!Stack->WaitingOn.Remove(ExitingId)) 742 | { 743 | UE_LOG(LogSupertalk, Error, TEXT("FinishWaitingOnStack was given an invalid ExitingId of %u for stack %u"), ExitingId, WaitingId); 744 | return; 745 | } 746 | 747 | TickStack(WaitingId); 748 | } 749 | 750 | void USupertalkPlayer::HandleQueue(const FSupertalkActionWithContext& Context) 751 | { 752 | USupertalkQueueParams* Params = CastChecked(Context.Action.Params); 753 | if (Params->SubActions.Num() == 0) 754 | { 755 | CompleteAction(Context.Key); 756 | return; 757 | } 758 | 759 | PushActions(Context.Key.StackId, Context.Source, Params->SubActions); 760 | CompleteAction(Context.Key); 761 | } 762 | 763 | void USupertalkPlayer::HandleConditional(const FSupertalkActionWithContext& Context) 764 | { 765 | USupertalkConditionalParams* Params = CastChecked(Context.Action.Params); 766 | 767 | const USupertalkValue* Value = nullptr; 768 | if (IsValid(Params->Expression)) 769 | { 770 | Value = Params->Expression->Evaluate(this); 771 | } 772 | else 773 | { 774 | Value = Params->Value_DEPRECATED; 775 | } 776 | 777 | Value = Value ? Value->GetResolvedValue(this) : nullptr; 778 | 779 | bool bConditionalValue; 780 | if (Value) 781 | { 782 | const USupertalkBooleanValue* BoolValue = Cast(Value); 783 | if (!BoolValue) 784 | { 785 | UE_LOG(LogSupertalk, Warning, TEXT("Conditional action received non-boolean value '%s', skipping (non-boolean values are not supported at this time)"), *Value->ToDisplayText().ToString()); 786 | CompleteAction(Context.Key); 787 | return; 788 | } 789 | 790 | bConditionalValue = BoolValue->bValue; 791 | } 792 | else 793 | { 794 | bConditionalValue = false; 795 | } 796 | 797 | PushAction(Context.Key.StackId, Context.Source, bConditionalValue ? Params->TrueAction : Params->FalseAction); 798 | CompleteAction(Context.Key); 799 | } 800 | 801 | #undef LOCTEXT_NAMESPACE 802 | -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkPlayer.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "SupertalkLine.h" 7 | #include "SupertalkPlayer.generated.h" 8 | 9 | class USupertalkValue; 10 | class USupertalkExpression; 11 | class USupertalkPlayer; 12 | 13 | UENUM() 14 | enum class ESupertalkOperation : uint8 15 | { 16 | Noop, 17 | Line, 18 | Choice, 19 | Assign, 20 | Call, 21 | Jump, 22 | Parallel, 23 | Queue, 24 | Conditional 25 | }; 26 | 27 | UCLASS(MinimalAPI) 28 | class USupertalkOperationParams : public UObject 29 | { 30 | GENERATED_BODY() 31 | }; 32 | 33 | USTRUCT() 34 | struct SUPERTALK_API FSupertalkAction 35 | { 36 | GENERATED_BODY() 37 | 38 | FSupertalkAction() 39 | { 40 | Operation = ESupertalkOperation::Noop; 41 | Params = nullptr; 42 | } 43 | 44 | UPROPERTY(VisibleAnywhere) 45 | ESupertalkOperation Operation; 46 | 47 | UPROPERTY(VisibleAnywhere) 48 | TObjectPtr Params; 49 | }; 50 | 51 | USTRUCT() 52 | struct SUPERTALK_API FSupertalkSection 53 | { 54 | GENERATED_BODY() 55 | 56 | UPROPERTY(VisibleAnywhere) 57 | FName Name; 58 | 59 | UPROPERTY(VisibleAnywhere) 60 | TArray Actions; 61 | }; 62 | 63 | struct FSupertalkScriptCustomVersion 64 | { 65 | enum Type 66 | { 67 | // Before versioning was implemented 68 | BeforeCustomVersionWasAdded, 69 | 70 | // Added PreSave compilation support 71 | PreSaveCompilation, 72 | 73 | VersionPlusOne, 74 | LatestVersion = VersionPlusOne - 1 75 | }; 76 | 77 | const static FGuid GUID; 78 | 79 | private: 80 | FSupertalkScriptCustomVersion() {} 81 | }; 82 | 83 | #if WITH_EDITOR 84 | DECLARE_DELEGATE_OneParam(FOnSupertalkScriptPreSave, class USupertalkScript*); 85 | #endif 86 | 87 | UCLASS(BlueprintType, HideCategories=(Object)) 88 | class SUPERTALK_API USupertalkScript : public UObject 89 | { 90 | GENERATED_BODY() 91 | 92 | public: 93 | USupertalkScript(); 94 | 95 | UPROPERTY(VisibleAnywhere, Category = Script) 96 | FName DefaultSection; 97 | 98 | UPROPERTY(VisibleAnywhere, Category = Script) 99 | TMap Sections; 100 | 101 | #if WITH_EDITORONLY_DATA 102 | UPROPERTY(VisibleAnywhere, Category = Script) 103 | FString SourceData; 104 | 105 | UPROPERTY() 106 | uint8 bCanCompileFromSource : 1; 107 | 108 | UPROPERTY(VisibleAnywhere, Instanced, Category=ImportSettings) 109 | TObjectPtr AssetImportData; 110 | #endif 111 | 112 | #if WITH_EDITOR 113 | // Used for the compiler to hook into saving and packaging operations. 114 | static FOnSupertalkScriptPreSave OnScriptPreSave; 115 | 116 | virtual void PreSave(FObjectPreSaveContext SaveContext) override; 117 | virtual void PostInitProperties() override; 118 | virtual void GetAssetRegistryTags(TArray& OutTags) const override; 119 | 120 | UFUNCTION(CallInEditor, Category = Commands) 121 | void OpenSourceFileInExternalProgram(); 122 | #endif 123 | 124 | #if WITH_EDITORONLY_DATA 125 | virtual void Serialize(FArchive& Ar) override; 126 | #endif 127 | }; 128 | 129 | UCLASS() 130 | class SUPERTALK_API USupertalkPlayLineParams : public USupertalkOperationParams 131 | { 132 | GENERATED_BODY() 133 | 134 | public: 135 | 136 | UPROPERTY() 137 | FSupertalkLine Line; 138 | }; 139 | 140 | USTRUCT() 141 | struct SUPERTALK_API FSupertalkChoice 142 | { 143 | GENERATED_BODY() 144 | 145 | UPROPERTY() 146 | FText Text; 147 | 148 | UPROPERTY() 149 | FSupertalkAction SubAction; 150 | }; 151 | 152 | UCLASS() 153 | class SUPERTALK_API USupertalkPlayChoiceParams : public USupertalkPlayLineParams 154 | { 155 | GENERATED_BODY() 156 | 157 | public: 158 | 159 | UPROPERTY() 160 | TArray Choices; 161 | }; 162 | 163 | UCLASS() 164 | class SUPERTALK_API USupertalkAssignParams : public USupertalkOperationParams 165 | { 166 | GENERATED_BODY() 167 | 168 | public: 169 | UPROPERTY() 170 | FName Variable; 171 | 172 | UPROPERTY() 173 | TObjectPtr Value_DEPRECATED; 174 | 175 | UPROPERTY() 176 | TObjectPtr Expression; 177 | 178 | virtual void PostLoad() override; 179 | }; 180 | 181 | UCLASS() 182 | class SUPERTALK_API USupertalkCallParams : public USupertalkOperationParams 183 | { 184 | GENERATED_BODY() 185 | 186 | public: 187 | UPROPERTY() 188 | FString Arguments; 189 | }; 190 | 191 | UCLASS() 192 | class SUPERTALK_API USupertalkJumpParams : public USupertalkOperationParams 193 | { 194 | GENERATED_BODY() 195 | 196 | public: 197 | UPROPERTY() 198 | FName JumpTarget; 199 | }; 200 | 201 | UCLASS() 202 | class SUPERTALK_API USupertalkParallelParams : public USupertalkOperationParams 203 | { 204 | GENERATED_BODY() 205 | 206 | public: 207 | UPROPERTY() 208 | TArray SubActions; 209 | }; 210 | 211 | UCLASS() 212 | class SUPERTALK_API USupertalkQueueParams : public USupertalkOperationParams 213 | { 214 | GENERATED_BODY() 215 | 216 | public: 217 | UPROPERTY() 218 | TArray SubActions; 219 | }; 220 | 221 | UCLASS() 222 | class SUPERTALK_API USupertalkConditionalParams : public USupertalkOperationParams 223 | { 224 | GENERATED_BODY() 225 | 226 | public: 227 | UPROPERTY() 228 | TObjectPtr Value_DEPRECATED; 229 | 230 | UPROPERTY() 231 | TObjectPtr Expression; 232 | 233 | UPROPERTY() 234 | FSupertalkAction TrueAction; 235 | 236 | UPROPERTY() 237 | FSupertalkAction FalseAction; 238 | 239 | virtual void PostLoad() override; 240 | }; 241 | 242 | struct FSupertalkActionKey 243 | { 244 | friend class USupertalkPlayer; 245 | 246 | FSupertalkActionKey() 247 | { 248 | StackId = 0; 249 | ActionId = 0; 250 | } 251 | 252 | FORCEINLINE bool IsValid() const 253 | { 254 | return StackId > 0 && ActionId > 0; 255 | } 256 | 257 | friend bool operator==(const FSupertalkActionKey& Lhs, const FSupertalkActionKey& Rhs) 258 | { 259 | return Lhs.StackId == Rhs.StackId && Lhs.ActionId == Rhs.ActionId; 260 | } 261 | 262 | friend bool operator!=(const FSupertalkActionKey& Lhs, const FSupertalkActionKey& Rhs) 263 | { 264 | return !(Lhs == Rhs); 265 | } 266 | 267 | private: 268 | uint32 StackId; 269 | uint32 ActionId; 270 | }; 271 | 272 | USTRUCT() 273 | struct FSupertalkActionWithContext 274 | { 275 | GENERATED_BODY() 276 | 277 | FSupertalkActionWithContext() 278 | { 279 | Key = FSupertalkActionKey(); 280 | Source = nullptr; 281 | Action = FSupertalkAction(); 282 | } 283 | 284 | UPROPERTY() 285 | TObjectPtr Source; 286 | 287 | UPROPERTY() 288 | FSupertalkAction Action; 289 | 290 | FSupertalkActionKey Key; 291 | }; 292 | 293 | USTRUCT() 294 | struct FSupertalkStack 295 | { 296 | GENERATED_BODY() 297 | 298 | FSupertalkStack() 299 | { 300 | StackId = 0; 301 | SourceId = 0; 302 | bIsTicking = false; 303 | } 304 | 305 | uint32 StackId; 306 | 307 | // Id of the stack that created this one. 308 | uint32 SourceId; 309 | 310 | // Ids that this stack is waiting on. 311 | TSet WaitingOn; 312 | 313 | uint32 bIsTicking : 1; 314 | 315 | UPROPERTY() 316 | FSupertalkActionWithContext ActiveAction; 317 | 318 | UPROPERTY() 319 | TArray QueuedActions; 320 | }; 321 | 322 | DECLARE_DELEGATE(FSupertalkEventCompletedDelegate); 323 | 324 | DECLARE_DELEGATE_OneParam(FSupertalkChoiceCompletedDelegate, int32); 325 | DECLARE_DELEGATE_TwoParams(FSupertalkPlayLineDelegate, const FSupertalkLine&, FSupertalkEventCompletedDelegate Completed); 326 | DECLARE_DELEGATE_ThreeParams(FSupertalkPlayChoiceDelegate, const FSupertalkLine&, const TArray& Choices, FSupertalkChoiceCompletedDelegate Completed); 327 | DECLARE_DELEGATE_RetVal_TwoParams(const USupertalkValue*, FSupertalkProvideVariableDelegate, const USupertalkPlayer* Player, FName Name); 328 | 329 | // Used to let a script know that a latent function has completed. 330 | USTRUCT(BlueprintType) 331 | struct SUPERTALK_API FSupertalkLatentFunctionFinalizer 332 | { 333 | GENERATED_BODY() 334 | 335 | friend class USupertalkPlayer; 336 | 337 | FORCEINLINE void Complete() { Completed.ExecuteIfBound(); } 338 | 339 | private: 340 | FSupertalkEventCompletedDelegate Completed; 341 | }; 342 | 343 | USTRUCT() 344 | struct FSupertalkVariableProviderObject 345 | { 346 | GENERATED_BODY() 347 | 348 | UPROPERTY() 349 | TObjectPtr Object = nullptr; 350 | 351 | UPROPERTY() 352 | TObjectPtr ClassFilter = nullptr; 353 | }; 354 | 355 | UCLASS() 356 | class SUPERTALK_API USupertalkPlayer : public UObject 357 | { 358 | GENERATED_BODY() 359 | 360 | public: 361 | USupertalkPlayer(); 362 | 363 | void SetVariable(FName Name, const USupertalkValue* Value); 364 | void SetVariable(FName Name, bool Value); 365 | void SetVariable(FName Name, FText Value); 366 | 367 | const USupertalkValue* GetVariable(FName Name) const; 368 | void ClearVariables(); 369 | 370 | void AddFunctionCallReceiver(UObject* Obj); 371 | void AddVariableProvider(FSupertalkProvideVariableDelegate Provider); 372 | void AddVariableProvider(UObject* Object, UClass* ClassFilter = nullptr); 373 | 374 | void RunScript(const class USupertalkScript* Script, FName InitialSection = NAME_None); 375 | void Stop(); 376 | 377 | FORCEINLINE bool IsRunningScript() const { return Stacks.Num() > 0; } 378 | 379 | FSupertalkPlayLineDelegate OnPlayLineEvent; 380 | FSupertalkPlayChoiceDelegate OnPlayChoiceEvent; 381 | 382 | // When called from a function that was executed by a running script, this will let the function 383 | // become latent. Use the returned finalizer 384 | UFUNCTION(BlueprintCallable) 385 | FSupertalkLatentFunctionFinalizer MakeLatentFunction(); 386 | 387 | UFUNCTION(BlueprintCallable) 388 | static void CompleteFunction(FSupertalkLatentFunctionFinalizer Finalizer); 389 | 390 | protected: 391 | 392 | virtual void OnPlayLine(const FSupertalkLine& Line, FSupertalkEventCompletedDelegate Completed); 393 | virtual void OnPlayChoice(const FSupertalkLine& Line, const TArray& Choices, FSupertalkChoiceCompletedDelegate Completed); 394 | 395 | private: 396 | uint32 NextActionId; 397 | uint32 NextStackId; 398 | 399 | UPROPERTY() 400 | TMap Stacks; 401 | 402 | UPROPERTY() 403 | TMap> Variables; 404 | 405 | UPROPERTY() 406 | TArray> FunctionCallReceivers; 407 | 408 | UPROPERTY() 409 | TArray VariableProviderObjects; 410 | TArray VariableProviderDelegates; 411 | 412 | bool bIsFunctionCallLatent; 413 | FSupertalkLatentFunctionFinalizer* CurrentFunctionFinalizer = nullptr; 414 | 415 | FSupertalkStack& CreateNewStack(); 416 | 417 | uint32 GetNewActionId(); 418 | uint32 GetNewStackId(); 419 | 420 | // Pushes actions onto the top of the stack. They will execute next, in the order given in the array. 421 | void PushActions(uint32 StackId, const USupertalkScript* Script, const TArray& Actions); 422 | 423 | // Pushes a single action onto the top of the stack. It will execute next. 424 | void PushAction(FSupertalkStack& Stack, const USupertalkScript* Script, const FSupertalkAction& Action); 425 | void PushAction(uint32 StackId, const USupertalkScript* Script, const FSupertalkAction& Action); 426 | 427 | void CompleteActionAndTick(FSupertalkActionKey Key); 428 | void CompleteAction(FSupertalkActionKey Key); 429 | void TickStack(uint32 StackId); 430 | 431 | void ExecuteAction(const FSupertalkActionWithContext& Context); 432 | 433 | void HandlePlayLine(const FSupertalkActionWithContext& Context); 434 | 435 | void HandlePlayChoice(const FSupertalkActionWithContext& Context); 436 | void ReceiveChoice(int32 ChoiceIndex, FSupertalkActionKey Key); 437 | 438 | void HandleAssign(const FSupertalkActionWithContext& Context); 439 | 440 | void HandleCall(const FSupertalkActionWithContext& Context); 441 | 442 | void HandleJump(const FSupertalkActionWithContext& Context); 443 | 444 | void HandleParallel(const FSupertalkActionWithContext& Context); 445 | void FinishWaitingOnStack(uint32 ExitingId, uint32 WaitingId); 446 | 447 | void HandleQueue(const FSupertalkActionWithContext& Context); 448 | 449 | void HandleConditional(const FSupertalkActionWithContext& Context); 450 | }; 451 | -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkUtilities.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkUtilities.h" 4 | #include "SupertalkPlayer.h" 5 | #include "SupertalkValue.h" 6 | 7 | namespace FSupertalkUtilities 8 | { 9 | bool IsMemberExpression(const FString& Input) 10 | { 11 | return Input.Contains(TEXT(".")); 12 | } 13 | 14 | FText FormatText(const FText& Format, const USupertalkPlayer* Player, bool bIsDisplayText) 15 | { 16 | check(Player); 17 | 18 | TArray ParameterNames; 19 | FText::GetFormatPatternParameters(Format, ParameterNames); 20 | 21 | FFormatNamedArguments FormatArgs; 22 | for (const FString& Param : ParameterNames) 23 | { 24 | if (IsMemberExpression(Param)) 25 | { 26 | TArray MemberStrings; 27 | Param.ParseIntoArrayWS(MemberStrings, TEXT(".")); 28 | check(MemberStrings.Num() > 0); 29 | 30 | FName VarName = FName(MemberStrings[0]); 31 | MemberStrings.RemoveAt(0); 32 | 33 | USupertalkMemberValue* MemberValue = NewObject(); 34 | MemberValue->Variable = VarName; 35 | for (const FString& Member : MemberStrings) 36 | { 37 | MemberValue->Members.Add(FName(Member)); 38 | } 39 | 40 | if (bIsDisplayText) 41 | { 42 | FormatArgs.Add(Param, MemberValue->ToResolvedDisplayText(Player)); 43 | } 44 | else 45 | { 46 | FormatArgs.Add(Param, FText::FromString(MemberValue->ToResolvedInternalString(Player))); 47 | } 48 | } 49 | else 50 | { 51 | FName VarName(Param); 52 | const USupertalkValue* Value = Player->GetVariable(VarName); 53 | if (Value) 54 | { 55 | if (bIsDisplayText) 56 | { 57 | FormatArgs.Add(Param, Value->ToResolvedDisplayText(Player)); 58 | } 59 | else 60 | { 61 | FormatArgs.Add(Param, FText::FromString(Value->ToResolvedInternalString(Player))); 62 | } 63 | } 64 | else 65 | { 66 | FormatArgs.Add(Param, FText::FromName(VarName)); 67 | } 68 | } 69 | } 70 | 71 | return FText::Format(Format, FormatArgs); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkUtilities.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | class USupertalkPlayer; 8 | 9 | namespace FSupertalkUtilities 10 | { 11 | SUPERTALK_API bool IsMemberExpression(const FString& Input); 12 | SUPERTALK_API FText FormatText(const FText& Format, const USupertalkPlayer* Player, bool bIsDisplayText = true); 13 | } 14 | -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkValue.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkValue.h" 4 | 5 | #include "Supertalk.h" 6 | #include "SupertalkPlayer.h" 7 | 8 | #define LOCTEXT_NAMESPACE "SupertalkValue" 9 | 10 | const USupertalkValue* USupertalkValue::GetResolvedValue(const USupertalkPlayer* Player) const 11 | { 12 | check(Player); 13 | 14 | const USupertalkValue* GoodValue = this; 15 | const USupertalkValue* Value = this; 16 | while (IsValid(Value)) 17 | { 18 | GoodValue = Value; 19 | Value = Value->ResolveValue(Player); 20 | } 21 | 22 | return GoodValue; 23 | } 24 | 25 | FText USupertalkValue::ToResolvedDisplayText(const USupertalkPlayer* Player) const 26 | { 27 | const USupertalkValue* Value = GetResolvedValue(Player); 28 | if (IsValid(Value)) 29 | { 30 | return Value->ToDisplayText(); 31 | } 32 | 33 | return ToDisplayText(); 34 | } 35 | 36 | FString USupertalkValue::ToResolvedInternalString(const USupertalkPlayer* Player) const 37 | { 38 | const USupertalkValue* Value = GetResolvedValue(Player); 39 | if (IsValid(Value)) 40 | { 41 | return Value->ToInternalString(); 42 | } 43 | 44 | return ToInternalString(); 45 | } 46 | 47 | bool USupertalkValue::PropertyToValue(USupertalkPlayer* Player, void* ValuePtr, UObject* Owner, FProperty* Property, bool bValueIsContainer, USupertalkValue*& OutResult) 48 | { 49 | check(ValuePtr); 50 | check(Property); 51 | 52 | UObject* Outer = IsValid(Player) ? static_cast(Player) : static_cast(GetTransientPackage()); 53 | 54 | if (FObjectPropertyBase* ObjProp = CastField(Property)) 55 | { 56 | USupertalkObjectValue* Value = NewObject(Outer); 57 | Value->Object = bValueIsContainer ? ObjProp->GetObjectPropertyValue_InContainer(ValuePtr) : ObjProp->GetObjectPropertyValue(ValuePtr); 58 | OutResult = Value; 59 | return true; 60 | } 61 | else if (FBoolProperty* BoolProp = CastField(Property)) 62 | { 63 | USupertalkBooleanValue* Value = NewObject(Outer); 64 | Value->bValue = bValueIsContainer ? BoolProp->GetPropertyValue_InContainer(ValuePtr) : BoolProp->GetPropertyValue(ValuePtr); 65 | OutResult = Value; 66 | return true; 67 | } 68 | else if (FTextProperty* TextProp = CastField(Property)) 69 | { 70 | USupertalkTextValue* Value = NewObject(Outer); 71 | Value->Text = bValueIsContainer ? TextProp->GetPropertyValue_InContainer(ValuePtr) : TextProp->GetPropertyValue(ValuePtr); 72 | OutResult = Value; 73 | return true; 74 | } 75 | else if (FMapProperty* MapProp = CastField(Property)) 76 | { 77 | USupertalkMapPropertyValue* Value = NewObject(Outer); 78 | Value->Owner = Owner; 79 | Value->TargetProperty = MapProp; 80 | OutResult = Value; 81 | return true; 82 | } 83 | else 84 | { 85 | FString Str; 86 | if (bValueIsContainer) 87 | { 88 | Property->ExportText_InContainer(0, Str, ValuePtr, ValuePtr, nullptr, PPF_None); 89 | } 90 | else 91 | { 92 | Property->ExportText_Direct(Str, ValuePtr, ValuePtr, nullptr, PPF_None); 93 | } 94 | 95 | USupertalkTextValue* Value = NewObject(Outer); 96 | Value->Text = FText::FromString(Str); 97 | OutResult = Value; 98 | return true; 99 | } 100 | 101 | return false; 102 | } 103 | 104 | const USupertalkValue* USupertalkValue::ResolveValue(const USupertalkPlayer* Player) const 105 | { 106 | return nullptr; 107 | } 108 | 109 | FText USupertalkBooleanValue::ToDisplayText() const 110 | { 111 | return bValue ? LOCTEXT("True", "true") : LOCTEXT("False", "false"); 112 | } 113 | 114 | FString USupertalkBooleanValue::ToInternalString() const 115 | { 116 | return bValue ? TEXT("1") : TEXT("0"); 117 | } 118 | 119 | const USupertalkValue* USupertalkBooleanValue::GetMember(FName MemberName) const 120 | { 121 | return nullptr; 122 | } 123 | 124 | bool USupertalkBooleanValue::IsValueEqualTo(const USupertalkValue* Other) const 125 | { 126 | if (!IsValid(Other)) 127 | { 128 | return false; 129 | } 130 | 131 | if (const USupertalkBooleanValue* OtherBool = Cast(Other)) 132 | { 133 | return bValue == OtherBool->bValue; 134 | } 135 | 136 | return true; 137 | } 138 | 139 | FText USupertalkTextValue::ToDisplayText() const 140 | { 141 | return Text; 142 | } 143 | 144 | FString USupertalkTextValue::ToInternalString() const 145 | { 146 | FString Result = Text.ToString(); 147 | Result.ReplaceCharWithEscapedCharInline(); 148 | return Result; 149 | } 150 | 151 | const USupertalkValue* USupertalkTextValue::GetMember(FName MemberName) const 152 | { 153 | return nullptr; 154 | } 155 | 156 | bool USupertalkTextValue::IsValueEqualTo(const USupertalkValue* Other) const 157 | { 158 | if (!IsValid(Other)) 159 | { 160 | return false; 161 | } 162 | 163 | if (const USupertalkTextValue* OtherText = Cast(Other)) 164 | { 165 | return Text.EqualTo(OtherText->Text); 166 | } 167 | 168 | return true; 169 | } 170 | 171 | FText USupertalkVariableValue::ToDisplayText() const 172 | { 173 | return FText::FromName(Variable); 174 | } 175 | 176 | FString USupertalkVariableValue::ToInternalString() const 177 | { 178 | checkNoEntry(); 179 | return ToDisplayText().ToString(); 180 | } 181 | 182 | const USupertalkValue* USupertalkVariableValue::GetMember(FName MemberName) const 183 | { 184 | return nullptr; 185 | } 186 | 187 | const USupertalkValue* USupertalkVariableValue::ResolveValue(const USupertalkPlayer* Player) const 188 | { 189 | return Player->GetVariable(Variable); 190 | } 191 | 192 | FText USupertalkMemberValue::ToDisplayText() const 193 | { 194 | FString Str = Super::ToDisplayText().ToString(); 195 | for (FName Member : Members) 196 | { 197 | Str += TEXT(".") + Member.ToString(); 198 | } 199 | 200 | return FText::FromString(Str); 201 | } 202 | 203 | FString USupertalkMemberValue::ToInternalString() const 204 | { 205 | checkNoEntry(); 206 | return ToDisplayText().ToString(); 207 | } 208 | 209 | const USupertalkValue* USupertalkMemberValue::GetMember(FName MemberName) const 210 | { 211 | USupertalkMemberValue* NewMember = DuplicateObject(this, GetOuter()); 212 | NewMember->Members.Add(MemberName); 213 | return NewMember; 214 | } 215 | 216 | const USupertalkValue* USupertalkMemberValue::ResolveValue(const USupertalkPlayer* Player) const 217 | { 218 | const USupertalkValue* CurrentValue = Super::ResolveValue(Player); 219 | if (!IsValid(CurrentValue)) 220 | { 221 | return nullptr; 222 | } 223 | 224 | for (FName Member : Members) 225 | { 226 | CurrentValue = CurrentValue->GetMember(Member); 227 | if (!IsValid(CurrentValue)) 228 | { 229 | return nullptr; 230 | } 231 | 232 | CurrentValue = CurrentValue->GetResolvedValue(Player); 233 | if (!IsValid(CurrentValue)) 234 | { 235 | return nullptr; 236 | } 237 | } 238 | 239 | return CurrentValue; 240 | } 241 | 242 | FText ISupertalkDisplayInterface::GetSupertalkDisplayText() const 243 | { 244 | return FText(); 245 | } 246 | 247 | const USupertalkValue* ISupertalkDisplayInterface::GetSupertalkMember(FName MemberName) const 248 | { 249 | return nullptr; 250 | } 251 | 252 | FText USupertalkObjectValue::ToDisplayText() const 253 | { 254 | if (IsValid(Object)) 255 | { 256 | if (ISupertalkDisplayInterface* DisplayInterface = Cast(Object)) 257 | { 258 | return DisplayInterface->GetSupertalkDisplayText(); 259 | } 260 | 261 | return FText::FromString(Object->GetName()); 262 | } 263 | 264 | return FText(); 265 | } 266 | 267 | FString USupertalkObjectValue::ToInternalString() const 268 | { 269 | return IsValid(Object) ? Object.GetPath() : TEXT("None"); 270 | } 271 | 272 | const USupertalkValue* USupertalkObjectValue::GetMember(FName MemberName) const 273 | { 274 | if (IsValid(Object)) 275 | { 276 | if (ISupertalkDisplayInterface* DisplayInterface = Cast(Object)) 277 | { 278 | return DisplayInterface->GetSupertalkMember(MemberName); 279 | } 280 | else if (UDataTable* DataTable = Cast(Object)) 281 | { 282 | // TODO: Can't subclass UDataTable for now, so we have to handle it separately :( 283 | FSupertalkTableRow* Row = DataTable->FindRow(MemberName, TEXT("SupertalkObjectValue::GetMember")); 284 | if (Row != nullptr) 285 | { 286 | USupertalkTextValue* TextValue = NewObject(); 287 | TextValue->Text = Row->Value; 288 | return TextValue; 289 | } 290 | } 291 | else if (FProperty* Prop = Object->GetClass()->FindPropertyByName(MemberName)) 292 | { 293 | USupertalkValue* Value; 294 | if (PropertyToValue(Cast(GetOuter()), Object, Object, Prop, true, Value)) 295 | { 296 | return Value; 297 | } 298 | } 299 | } 300 | 301 | return nullptr; 302 | } 303 | 304 | bool USupertalkObjectValue::IsValueEqualTo(const USupertalkValue* Other) const 305 | { 306 | if (!IsValid(Other)) 307 | { 308 | return false; 309 | } 310 | 311 | if (const USupertalkObjectValue* OtherObj = Cast(Other)) 312 | { 313 | return Object == OtherObj->Object; 314 | } 315 | 316 | return true; 317 | } 318 | 319 | FText USupertalkMapPropertyValue::ToDisplayText() const 320 | { 321 | return FText::FromString(ToInternalString()); 322 | } 323 | 324 | FString USupertalkMapPropertyValue::ToInternalString() const 325 | { 326 | // Is there a string representation for maps? 327 | return IsValid(Owner) ? 328 | FString::Printf(TEXT("%s.%s"), *Owner->GetName(), TargetProperty ? *TargetProperty->GetName() : TEXT("None")) 329 | : TEXT("None"); 330 | } 331 | 332 | const USupertalkValue* USupertalkMapPropertyValue::GetMember(FName MemberName) const 333 | { 334 | if (!IsValid(Owner) || !TargetProperty) 335 | { 336 | return nullptr; 337 | } 338 | 339 | FScriptMapHelper Helper(TargetProperty, TargetProperty->ContainerPtrToValuePtr(Owner)); 340 | if (FNameProperty* NameProp = CastField(TargetProperty->KeyProp)) 341 | { 342 | if (uint8* ValuePtr = Helper.FindValueFromHash(&MemberName)) 343 | { 344 | USupertalkValue* Result = nullptr; 345 | if (PropertyToValue(Cast(GetOuter()), ValuePtr, Owner, TargetProperty->ValueProp, false, Result)) 346 | { 347 | return Result; 348 | } 349 | } 350 | 351 | return nullptr; 352 | } 353 | else if (FStrProperty* StrProp = CastField(TargetProperty->KeyProp)) 354 | { 355 | FString Key = MemberName.ToString(); 356 | if (uint8* ValuePtr = Helper.FindValueFromHash(&Key)) 357 | { 358 | USupertalkValue* Result = nullptr; 359 | if (PropertyToValue(Cast(GetOuter()), ValuePtr, Owner, TargetProperty->ValueProp, false, Result)) 360 | { 361 | return Result; 362 | } 363 | } 364 | } 365 | else if (FTextProperty* TextProp = CastField(TargetProperty->KeyProp)) 366 | { 367 | FText Key = FText::FromString(MemberName.ToString()); 368 | if (uint8* ValuePtr = Helper.FindValueFromHash(&Key)) 369 | { 370 | USupertalkValue* Result = nullptr; 371 | if (PropertyToValue(Cast(GetOuter()), ValuePtr, Owner, TargetProperty->ValueProp, false, Result)) 372 | { 373 | return Result; 374 | } 375 | } 376 | } 377 | 378 | UE_LOG(LogSupertalk, Warning, TEXT("Unable to access member '%s' of '%s.%s': key type of map is not a name, string, or text."), *MemberName.ToString(), *Owner->GetName(), *TargetProperty->GetName()); 379 | return nullptr; 380 | } 381 | 382 | bool USupertalkMapPropertyValue::IsValueEqualTo(const USupertalkValue* Other) const 383 | { 384 | if (!IsValid(Other)) 385 | { 386 | return false; 387 | } 388 | 389 | if (const USupertalkMapPropertyValue* OtherObj = Cast(Other)) 390 | { 391 | return Owner == OtherObj->Owner && TargetProperty == OtherObj->TargetProperty; 392 | } 393 | 394 | return false; 395 | } 396 | 397 | #undef LOCTEXT_NAMESPACE 398 | -------------------------------------------------------------------------------- /Source/Supertalk/SupertalkValue.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "SupertalkPlayer.h" 7 | #include "Engine/DataTable.h" 8 | #include "SupertalkValue.generated.h" 9 | 10 | class USupertalkPlayer; 11 | 12 | UCLASS(Abstract) 13 | class SUPERTALK_API USupertalkValue : public UObject 14 | { 15 | GENERATED_BODY() 16 | 17 | public: 18 | const USupertalkValue* GetResolvedValue(const USupertalkPlayer* Player) const; 19 | FText ToResolvedDisplayText(const USupertalkPlayer* Player) const; 20 | FString ToResolvedInternalString(const USupertalkPlayer* Player) const; 21 | 22 | // Text meant for display to the user. 23 | virtual FText ToDisplayText() const PURE_VIRTUAL(USupertalkValue::ToDisplayText,return FText();) 24 | 25 | // Text meant for passing data around - i.e. object paths. This will eventually be removed 26 | // once we don't require string handling to call functions. 27 | virtual FString ToInternalString() const PURE_VIRTUAL(USupertalkValue::ToInternalText,return FString();) 28 | 29 | virtual const USupertalkValue* GetMember(FName MemberName) const PURE_VIRTUAL(USupertalkValue::GetMember,return nullptr;) 30 | 31 | virtual bool IsValueEqualTo(const USupertalkValue* Other) const { return this == Other; } 32 | 33 | static bool PropertyToValue(USupertalkPlayer* Player, void* ValuePtr, UObject* Owner, FProperty* Property, bool bValueIsContainer, USupertalkValue*& OutResult); 34 | 35 | protected: 36 | virtual const USupertalkValue* ResolveValue(const USupertalkPlayer* Player) const; 37 | }; 38 | 39 | UCLASS() 40 | class SUPERTALK_API USupertalkBooleanValue : public USupertalkValue 41 | { 42 | GENERATED_BODY() 43 | 44 | public: 45 | UPROPERTY(VisibleAnywhere) 46 | uint8 bValue : 1; 47 | 48 | virtual FText ToDisplayText() const override; 49 | virtual FString ToInternalString() const override; 50 | virtual const USupertalkValue* GetMember(FName MemberName) const override; 51 | 52 | virtual bool IsValueEqualTo(const USupertalkValue* Other) const override; 53 | }; 54 | 55 | UCLASS() 56 | class SUPERTALK_API USupertalkTextValue : public USupertalkValue 57 | { 58 | GENERATED_BODY() 59 | 60 | public: 61 | UPROPERTY(VisibleAnywhere) 62 | FText Text; 63 | 64 | virtual FText ToDisplayText() const override; 65 | virtual FString ToInternalString() const override; 66 | virtual const USupertalkValue* GetMember(FName MemberName) const override; 67 | 68 | virtual bool IsValueEqualTo(const USupertalkValue* Other) const override; 69 | }; 70 | 71 | UCLASS() 72 | class SUPERTALK_API USupertalkVariableValue : public USupertalkValue 73 | { 74 | GENERATED_BODY() 75 | 76 | public: 77 | UPROPERTY(VisibleAnywhere) 78 | FName Variable; 79 | 80 | virtual FText ToDisplayText() const override; 81 | virtual FString ToInternalString() const override; 82 | virtual const USupertalkValue* GetMember(FName MemberName) const override; 83 | 84 | virtual bool IsValueEqualTo(const USupertalkValue* Other) const override { checkNoEntry(); return false; } 85 | 86 | protected: 87 | virtual const USupertalkValue* ResolveValue(const USupertalkPlayer* Player) const override; 88 | }; 89 | 90 | // This should be replaced with an expression at some point. 91 | UCLASS() 92 | class SUPERTALK_API USupertalkMemberValue : public USupertalkVariableValue 93 | { 94 | GENERATED_BODY() 95 | 96 | public: 97 | UPROPERTY(VisibleAnywhere) 98 | TArray Members; 99 | 100 | virtual FText ToDisplayText() const override; 101 | virtual FString ToInternalString() const override; 102 | virtual const USupertalkValue* GetMember(FName MemberName) const override; 103 | 104 | virtual bool IsValueEqualTo(const USupertalkValue* Other) const override { checkNoEntry(); return false; } 105 | 106 | protected: 107 | virtual const USupertalkValue* ResolveValue(const USupertalkPlayer* Player) const override; 108 | }; 109 | 110 | UINTERFACE() 111 | class USupertalkDisplayInterface : public UInterface 112 | { 113 | GENERATED_BODY() 114 | }; 115 | 116 | class SUPERTALK_API ISupertalkDisplayInterface 117 | { 118 | GENERATED_BODY() 119 | 120 | public: 121 | // Get the text to be displayed for this object for a supertalk line. 122 | virtual FText GetSupertalkDisplayText() const; 123 | 124 | // Get a sub-member of this object. 125 | virtual const USupertalkValue* GetSupertalkMember(FName MemberName) const; 126 | }; 127 | 128 | UCLASS() 129 | class SUPERTALK_API USupertalkObjectValue : public USupertalkValue 130 | { 131 | GENERATED_BODY() 132 | 133 | public: 134 | UPROPERTY(VisibleAnywhere) 135 | TObjectPtr Object; 136 | 137 | virtual FText ToDisplayText() const override; 138 | virtual FString ToInternalString() const override; 139 | virtual const USupertalkValue* GetMember(FName MemberName) const override; 140 | 141 | virtual bool IsValueEqualTo(const USupertalkValue* Other) const override; 142 | }; 143 | 144 | UCLASS() 145 | class SUPERTALK_API USupertalkMapPropertyValue : public USupertalkValue 146 | { 147 | GENERATED_BODY() 148 | 149 | public: 150 | UPROPERTY(VisibleAnywhere) 151 | TObjectPtr Owner; 152 | 153 | FMapProperty* TargetProperty; 154 | 155 | virtual FText ToDisplayText() const override; 156 | virtual FString ToInternalString() const override; 157 | virtual const USupertalkValue* GetMember(FName MemberName) const override; 158 | 159 | virtual bool IsValueEqualTo(const USupertalkValue* Other) const override; 160 | }; 161 | 162 | USTRUCT(BlueprintType) 163 | struct SUPERTALK_API FSupertalkTableRow : public FTableRowBase 164 | { 165 | GENERATED_BODY() 166 | 167 | public: 168 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 169 | FText Value; 170 | 171 | #if WITH_EDITORONLY_DATA 172 | 173 | // Notes for developers, stripped out of non-editor builds. 174 | UPROPERTY(EditAnywhere, meta = (MultiLine = true)) 175 | FString Notes; 176 | 177 | #endif 178 | }; -------------------------------------------------------------------------------- /Source/SupertalkEditor/SSupertalkScriptAssetEditor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | 4 | #include "SSupertalkScriptAssetEditor.h" 5 | #include "SupertalkEditorStyle.h" 6 | #include "SupertalkParser.h" 7 | #include "SupertalkRichTextSyntaxHighlighterTextLayoutMarshaller.h" 8 | #include "Supertalk/SupertalkPlayer.h" 9 | #include "Widgets/Layout/SGridPanel.h" 10 | #include "Widgets/Layout/SScrollBarTrack.h" 11 | #include "Widgets/Layout/SScrollBox.h" 12 | #include "Widgets/Text/SlateEditableTextLayout.h" 13 | 14 | class SMultiLineEditableTextWithScrollExposed : public SMultiLineEditableText 15 | { 16 | public: 17 | float GetVerticalScroll() 18 | { 19 | return EditableTextLayout->GetScrollOffset().Y; 20 | } 21 | }; 22 | 23 | SSupertalkScriptAssetEditor::SSupertalkScriptAssetEditor() 24 | : MessageLog(TEXT("Supertalk Editor")) 25 | { 26 | } 27 | 28 | SSupertalkScriptAssetEditor::~SSupertalkScriptAssetEditor() 29 | { 30 | FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this); 31 | } 32 | 33 | void SSupertalkScriptAssetEditor::Construct(const FArguments& InArgs, class USupertalkScript* InScriptAsset) 34 | { 35 | check(InScriptAsset); 36 | ScriptAsset = InScriptAsset; 37 | 38 | TSharedRef Parser = FSupertalkParser::Create(nullptr); 39 | TSharedRef RichTextMarshaller = FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::Create( 40 | Parser, 41 | FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::FSyntaxTextStyle()); 42 | 43 | TSharedRef HorizontalScrollBar = 44 | SNew(SScrollBar) 45 | .Orientation(Orient_Horizontal) 46 | .Thickness(FVector2D(7.0, 7.0)); 47 | 48 | TSharedRef VerticalScrollBar = 49 | SNew(SScrollBar) 50 | .Orientation(Orient_Vertical) 51 | .Thickness(FVector2D(7.0, 7.0)); 52 | 53 | ChildSlot 54 | [ 55 | SNew(SGridPanel) 56 | .FillColumn(1, 1.0f) 57 | .FillRow(0, 1.0f) 58 | + SGridPanel::Slot(0, 0) 59 | [ 60 | SAssignNew(LineNumbersScroll, SScrollBox) 61 | .Orientation(Orient_Vertical) 62 | .ScrollBarVisibility(EVisibility::Collapsed) 63 | + SScrollBox::Slot() 64 | [ 65 | SNew(SBorder) 66 | .BorderBackgroundColor(FLinearColor(FColor(0xffffffff))) 67 | .Padding(0.5f) 68 | [ 69 | SAssignNew(LineNumbers, STextBlock) 70 | .TextStyle(FSupertalkEditorStyle::Get(), "TextEditor.LineNumberText") 71 | .Justification(ETextJustify::Right) 72 | .Margin(FMargin(5.f, 0.f)) 73 | ] 74 | ] 75 | ] 76 | + SGridPanel::Slot(1, 0) 77 | [ 78 | SAssignNew(EditableText, SMultiLineEditableTextWithScrollExposed) 79 | .IsReadOnly(InArgs._IsReadOnly) 80 | .Text(FText::FromString(ScriptAsset->SourceData)) 81 | .Font(FSupertalkEditorStyle::Get().GetWidgetStyle("TextEditor.NormalText").Font) 82 | .TextStyle(FSupertalkEditorStyle::Get(), "TextEditor.EditableTextBox") 83 | .Marshaller(RichTextMarshaller) 84 | .HScrollBar(HorizontalScrollBar) 85 | .VScrollBar(VerticalScrollBar) 86 | .AutoWrapText(false) 87 | .Margin(FMargin(2.f, 0.f)) 88 | .OnTextChanged(this, &SSupertalkScriptAssetEditor::OnTextChanged) 89 | .OnIsTypedCharValid_Lambda([](TCHAR Character) { return true; }) 90 | ] 91 | + SGridPanel::Slot(1, 1) 92 | [ 93 | HorizontalScrollBar 94 | ] 95 | + SGridPanel::Slot(2, 0) 96 | [ 97 | VerticalScrollBar 98 | ] 99 | ]; 100 | 101 | RefreshLineNumbers(); 102 | 103 | FCoreUObjectDelegates::OnObjectPropertyChanged.AddSP(this, &SSupertalkScriptAssetEditor::HandleScriptAssetPropertyChanged); 104 | } 105 | 106 | void SSupertalkScriptAssetEditor::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) 107 | { 108 | LineNumbersScroll->SetScrollOffset(EditableText->GetVerticalScroll()); 109 | 110 | SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); 111 | } 112 | 113 | void SSupertalkScriptAssetEditor::HandleScriptAssetPropertyChanged(UObject* Object, FPropertyChangedEvent& PropertyChangedEvent) 114 | { 115 | if (Object == ScriptAsset) 116 | { 117 | EditableText->SetText(FText::FromString(ScriptAsset->SourceData)); 118 | RefreshLineNumbers(); 119 | } 120 | } 121 | 122 | void SSupertalkScriptAssetEditor::OnTextChanged(const FText& NewText) 123 | { 124 | if (IsValid(ScriptAsset)) 125 | { 126 | ScriptAsset->SourceData = NewText.ToString(); 127 | ScriptAsset->MarkPackageDirty(); 128 | RefreshLineNumbers(); 129 | } 130 | } 131 | 132 | static int32 CountLineBreaks(const FString& Str) 133 | { 134 | int32 Result = 0; 135 | for (int32 Idx = 0; Idx < Str.Len(); ++Idx) 136 | { 137 | TCHAR Char = Str[Idx]; 138 | if (Char == TEXT('\n')) 139 | { 140 | ++Result; 141 | } 142 | } 143 | 144 | return Result; 145 | } 146 | 147 | void SSupertalkScriptAssetEditor::RefreshLineNumbers() 148 | { 149 | // TODO: text box doesn't expose the number of lines, but it's really where we should be getting this information from. 150 | // Could use a custom subclass to access that info. 151 | int32 WantedNumbers = IsValid(ScriptAsset) ? (CountLineBreaks(ScriptAsset->SourceData) + 1) : 1; 152 | if (WantedNumbers <= PreviousNumbers) 153 | { 154 | return; 155 | } 156 | 157 | while (WantedNumbers > CacheLen) 158 | { 159 | ++CacheLen; 160 | NumberCache += FString::FromInt(CacheLen) + '\n'; 161 | } 162 | 163 | LineNumbers->SetText(FText::FromString(NumberCache)); 164 | } 165 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SSupertalkScriptAssetEditor.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Widgets/SCompoundWidget.h" 7 | #include "Widgets/Input/SMultiLineEditableTextBox.h" 8 | #include "Widgets/Layout/SScrollBox.h" 9 | 10 | class SSupertalkScriptAssetEditor : public SCompoundWidget 11 | { 12 | public: 13 | SLATE_BEGIN_ARGS(SSupertalkScriptAssetEditor) 14 | : 15 | _IsReadOnly(false) 16 | {} 17 | SLATE_ARGUMENT(bool, IsReadOnly) 18 | SLATE_END_ARGS() 19 | 20 | SSupertalkScriptAssetEditor(); 21 | virtual ~SSupertalkScriptAssetEditor(); 22 | 23 | void Construct(const FArguments& InArgs, class USupertalkScript* InScriptAsset); 24 | 25 | virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; 26 | 27 | private: 28 | void HandleScriptAssetPropertyChanged(UObject* Object, FPropertyChangedEvent& PropertyChangedEvent); 29 | void OnTextChanged(const FText& NewText); 30 | 31 | void RefreshLineNumbers(); 32 | 33 | USupertalkScript* ScriptAsset = nullptr; 34 | 35 | 36 | FMessageLog MessageLog; 37 | TSharedPtr EditableText; 38 | 39 | // This method of implementing line numbers is horrible and will hopefully be replaced one day... maybe. 40 | TSharedPtr LineNumbers; 41 | TSharedPtr LineNumbersScroll; 42 | FString NumberCache; 43 | int32 CacheLen = 0; 44 | int32 PreviousNumbers = 0; 45 | }; 46 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkEditor.build.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | using UnrealBuildTool; 4 | 5 | public class SupertalkEditor : ModuleRules 6 | { 7 | public SupertalkEditor(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicDependencyModuleNames.AddRange( 12 | new string[] 13 | { 14 | "Core", 15 | "CoreUObject", 16 | "SlateCore", 17 | "Slate", 18 | "Supertalk", 19 | "DeveloperSettings" 20 | }); 21 | 22 | PrivateDependencyModuleNames.AddRange( 23 | new string[] 24 | { 25 | "UnrealEd", 26 | "AssetTools", 27 | "Engine", 28 | "MessageLog", 29 | "EditorStyle", 30 | "SourceControl", 31 | }); 32 | } 33 | } -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkEditor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkEditor.h" 4 | 5 | #include "AssetToolsModule.h" 6 | #include "IAssetTools.h" 7 | #include "SupertalkEditorStyle.h" 8 | #include "SupertalkScriptAssetFactory.h" 9 | #include "SupertalkScriptCompiler.h" 10 | 11 | IMPLEMENT_MODULE(FSupertalkEditorModule, SupertalkEditor); 12 | 13 | void FSupertalkEditorModule::StartupModule() 14 | { 15 | FSupertalkEditorStyle::Initialize(); 16 | 17 | IAssetTools& AssetTools = FModuleManager::GetModuleChecked("AssetTools").Get(); 18 | AssetTools.RegisterAssetTypeActions(MakeShareable(new FAssetTypeActions_SupertalkScript())); 19 | 20 | FSupertalkScriptCompiler::Initialize(); 21 | } 22 | 23 | void FSupertalkEditorModule::ShutdownModule() 24 | { 25 | FSupertalkScriptCompiler::Shutdown(); 26 | 27 | FSupertalkEditorStyle::Shutdown(); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkEditor.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | SUPERTALK_API class FSupertalkEditorModule : public IModuleInterface 8 | { 9 | public: 10 | virtual void StartupModule() override; 11 | virtual void ShutdownModule() override; 12 | }; -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkEditorSettings.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkEditorSettings.h" 4 | 5 | USupertalkEditorSettings::USupertalkEditorSettings() 6 | { 7 | bEnableScriptEditor = false; 8 | bSaveSourceFilesInScriptEditor = false; 9 | } 10 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkEditorSettings.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "SupertalkEditorSettings.generated.h" 7 | 8 | UCLASS(Config=Editor, DefaultConfig, meta = (DisplayName = "Supertalk Editor")) 9 | class SUPERTALKEDITOR_API USupertalkEditorSettings : public UDeveloperSettings 10 | { 11 | GENERATED_BODY() 12 | 13 | public: 14 | USupertalkEditorSettings(); 15 | 16 | // If enabled, supertalk scripts can be created and modified within the editor. 17 | // Changing this option may require a restart. 18 | // This is incredibly experimental - the text editor has not been well tested and is liable to delete text at random! 19 | UPROPERTY(EditAnywhere, Config, Category = Experimental) 20 | uint8 bEnableScriptEditor : 1; 21 | 22 | // If enabled, saving a script in the script editor will also save the script asset's source file. 23 | // This will attempt to checkout the source file in source control if necessary. 24 | UPROPERTY(EditAnywhere, Config, Category = Experimental, meta = (EditCondition = "bEnableScriptEditor")) 25 | uint8 bSaveSourceFilesInScriptEditor : 1; 26 | }; 27 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkEditorStyle.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkEditorStyle.h" 4 | 5 | #include "Styling/SlateStyleRegistry.h" 6 | 7 | TSharedPtr FSupertalkEditorStyle::StyleSet = nullptr; 8 | 9 | #define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) 10 | #define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) 11 | #define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) 12 | #define DEFAULT_FONT(...) FCoreStyle::GetDefaultFontStyle(__VA_ARGS__) 13 | 14 | void FSupertalkEditorStyle::Initialize() 15 | { 16 | if (StyleSet.IsValid()) 17 | { 18 | return; 19 | } 20 | 21 | StyleSet = MakeShared("SupertalkEditor"); 22 | StyleSet->SetContentRoot( FPaths::EngineContentDir() / TEXT("Editor/Slate") ); 23 | StyleSet->SetCoreContentRoot(FPaths::EngineContentDir() / TEXT("Slate")); 24 | 25 | const FSlateFontInfo EditorFont = DEFAULT_FONT("Mono", 11); 26 | 27 | const FTextBlockStyle NormalText = FTextBlockStyle() 28 | .SetFont(EditorFont) 29 | .SetColorAndOpacity(FLinearColor::White) 30 | .SetShadowOffset(FVector2D::ZeroVector) 31 | .SetShadowColorAndOpacity(FLinearColor::Black) 32 | .SetHighlightColor(FLinearColor(0.02f, 0.3f, 0.0f)) 33 | .SetHighlightShape(BOX_BRUSH("Common/TextBlockHighlightShape", FMargin(3.f/8.f))); 34 | 35 | { 36 | StyleSet->Set("TextEditor.NormalText", NormalText); 37 | 38 | StyleSet->Set("SyntaxHighlight.STS.Normal", NormalText); 39 | StyleSet->Set("SyntaxHighlight.STS.Comment", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xff009615)))); 40 | StyleSet->Set("SyntaxHighlight.STS.Keyword", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xff67b1f5)))); 41 | StyleSet->Set("SyntaxHighlight.STS.Operator", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xffc07cf7)))); 42 | StyleSet->Set("SyntaxHighlight.STS.Value", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xffaad1fa)))); 43 | StyleSet->Set("SyntaxHighlight.STS.String", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xfffc9d53)))); 44 | StyleSet->Set("SyntaxHighlight.STS.Section", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xffda8bfc)))); 45 | StyleSet->Set("SyntaxHighlight.STS.Command", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xfff7d67c)))); 46 | StyleSet->Set("SyntaxHighlight.STS.Jump", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xff7dfa9e)))); 47 | StyleSet->Set("SyntaxHighlight.STS.Error", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xffff7083)))); 48 | 49 | const FEditableTextBoxStyle EditableTextBoxStyle = FEditableTextBoxStyle() 50 | .SetTextStyle(NormalText) 51 | .SetBackgroundColor(FLinearColor::Black) 52 | .SetBackgroundImageNormal( FSlateNoResource() ) 53 | .SetBackgroundImageHovered( FSlateNoResource() ) 54 | .SetBackgroundImageFocused( FSlateNoResource() ) 55 | .SetBackgroundImageReadOnly( FSlateNoResource() ); 56 | 57 | StyleSet->Set("TextEditor.EditableTextBox", EditableTextBoxStyle); 58 | 59 | StyleSet->Set("TextEditor.LineNumberText", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(FColor(0xffbbbbbb))))); 60 | } 61 | 62 | FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get()); 63 | } 64 | 65 | #undef IMAGE_BRUSH 66 | #undef BOX_BRUSH 67 | #undef BORDER_BRUSH 68 | #undef DEFAULT_FONT 69 | 70 | void FSupertalkEditorStyle::Shutdown() 71 | { 72 | if (StyleSet.IsValid()) 73 | { 74 | FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get()); 75 | ensure(StyleSet.IsUnique()); 76 | StyleSet.Reset(); 77 | } 78 | } 79 | 80 | const ISlateStyle& FSupertalkEditorStyle::Get() 81 | { 82 | return *StyleSet.Get(); 83 | } 84 | 85 | const FName& FSupertalkEditorStyle::GetStyleSetName() 86 | { 87 | return StyleSet->GetStyleSetName(); 88 | } 89 | 90 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkEditorStyle.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Styling/SlateStyle.h" 7 | 8 | class FSupertalkEditorStyle 9 | { 10 | public: 11 | static void Initialize(); 12 | static void Shutdown(); 13 | 14 | static const ISlateStyle& Get(); 15 | 16 | static const FName& GetStyleSetName(); 17 | 18 | private: 19 | static TSharedPtr StyleSet; 20 | }; -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkParser.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkParser.h" 4 | #include "Misc/FeedbackContext.h" 5 | #include "Supertalk/Supertalk.h" 6 | #include "Supertalk/SupertalkPlayer.h" 7 | #include "Supertalk/SupertalkValue.h" 8 | #include "Supertalk/SupertalkLine.h" 9 | 10 | #define LOCTEXT_NAMESPACE "SupertalkParser" 11 | 12 | #define STP_LOG(Verbosity, Format, ...) if (Ar) { Ar->CategorizedLogf(LogSupertalk.GetCategoryName(), ELogVerbosity::Verbosity, Format, ##__VA_ARGS__); } 13 | 14 | DEFINE_ENUM_TO_STRING(ESupertalkTokenType, "/Script/SupertalkEditor") 15 | 16 | namespace Symbols 17 | { 18 | static const TCHAR Separator = TEXT(','); 19 | static const TCHAR Member = TEXT('.'); 20 | static const TCHAR Equals = TEXT('='); 21 | 22 | static const TCHAR TextStart = TEXT(':'); 23 | 24 | static const TCHAR AssetStart1 = TEXT('/'); 25 | static const TCHAR AssetStart2 = TEXT('\\'); 26 | 27 | static const TCHAR ChoiceStart = TEXT('*'); 28 | 29 | static const TCHAR SectionStart = TEXT('#'); 30 | 31 | static const TCHAR Comment = TEXT('-'); 32 | 33 | static const TCHAR Jump = TEXT('>'); 34 | 35 | static const TCHAR CommandStart = TEXT('>'); 36 | 37 | static const TCHAR ParallelStart = TEXT('['); 38 | static const TCHAR ParallelEnd = TEXT(']'); 39 | 40 | static const TCHAR QueueStart = TEXT('{'); 41 | static const TCHAR QueueEnd = TEXT('}'); 42 | 43 | static const TCHAR StatementEnd = TEXT(';'); 44 | 45 | static const TCHAR SingleQuote = TEXT('\''); 46 | static const TCHAR DoubleQuote = TEXT('"'); 47 | 48 | static const TCHAR LocalizationKeyStart = TEXT('@'); 49 | 50 | static const TCHAR DirectiveStart = TEXT('!'); 51 | 52 | static const TCHAR GroupStart = TEXT('('); 53 | static const TCHAR GroupEnd = TEXT(')'); 54 | 55 | static const TCHAR Not = TEXT('~'); 56 | 57 | // TODO: implement escape sequences 58 | // This isn't used yet, sadly. 59 | static const TCHAR Escape = TEXT('\\'); 60 | } 61 | 62 | bool IsTokenSymbol(TCHAR Char) 63 | { 64 | switch (Char) 65 | { 66 | default: 67 | return false; 68 | 69 | case Symbols::Separator: 70 | case Symbols::Member: 71 | case Symbols::Equals: 72 | case Symbols::TextStart: 73 | case Symbols::AssetStart1: 74 | case Symbols::AssetStart2: 75 | case Symbols::ChoiceStart: 76 | case Symbols::SectionStart: 77 | case Symbols::Comment: 78 | case Symbols::CommandStart: 79 | case Symbols::ParallelStart: 80 | case Symbols::ParallelEnd: 81 | case Symbols::QueueStart: 82 | case Symbols::QueueEnd: 83 | case Symbols::StatementEnd: 84 | case Symbols::SingleQuote: 85 | case Symbols::DoubleQuote: 86 | case Symbols::LocalizationKeyStart: 87 | case Symbols::DirectiveStart: 88 | case Symbols::GroupStart: 89 | case Symbols::GroupEnd: 90 | case Symbols::Not: 91 | return true; 92 | } 93 | } 94 | 95 | static TMap NameToTokenOverrideMap = { 96 | { TEXT("if"), ESupertalkTokenType::If }, 97 | { TEXT("then"), ESupertalkTokenType::Then }, 98 | { TEXT("else"), ESupertalkTokenType::Else } 99 | }; 100 | 101 | TSharedRef FSupertalkParser::Create(FOutputDevice* Ar) 102 | { 103 | TSharedRef Parser = MakeShareable(new FSupertalkParser()); 104 | Parser->Ar = Ar; 105 | Parser->Initialize(); 106 | return Parser; 107 | } 108 | 109 | bool FSupertalkParser::ParseIntoScript(FString File, FString Input, USupertalkScript* Script, FOutputDevice* Ar) 110 | { 111 | check(Script); 112 | 113 | TSharedRef Parser = Create(Ar); 114 | return Parser->Parse(File, Input, Script); 115 | } 116 | 117 | bool FSupertalkParser::IsReservedName(FName Input) 118 | { 119 | static TSet ReservedNames = { 120 | NAME_None, 121 | NAME_TRUE, 122 | NAME_FALSE 123 | }; 124 | 125 | return ReservedNames.Contains(Input); 126 | } 127 | 128 | FString FSupertalkParser::FTokenContext::ToString() const 129 | { 130 | return FString::Format(TEXT("{0}:{1}:{2}"), { File, Line, Col }); 131 | } 132 | 133 | FString FSupertalkParser::FToken::GetDisplayName() const 134 | { 135 | return EnumToString(Type); 136 | } 137 | 138 | bool FSupertalkParser::FToken::IsIgnorable() const 139 | { 140 | switch (Type) 141 | { 142 | default: 143 | return false; 144 | 145 | case ESupertalkTokenType::Eof: 146 | case ESupertalkTokenType::Ignore: 147 | case ESupertalkTokenType::Comment: 148 | return true; 149 | } 150 | } 151 | 152 | FString FSupertalkParser::FToken::GetGeneratedLocalizationKey() const 153 | { 154 | FString GameContentDir = FPaths::ProjectContentDir(); 155 | FString FileLocation = Context.File; 156 | FPaths::MakePathRelativeTo(FileLocation, *GameContentDir); 157 | return FString::Format(TEXT("{0}_L{1}C{2}"), { FileLocation, Context.Line, Context.Col }); 158 | } 159 | 160 | TCHAR FSupertalkParser::FLxStream::ReadChar() 161 | { 162 | if (IsEOF()) 163 | { 164 | ++CurrentChar; // so that GoBack works correctly on Eof 165 | return TEXT('\0'); 166 | } 167 | 168 | if (!Lines[CurrentLine].IsValidIndex(CurrentChar)) 169 | { 170 | CurrentChar = 0; 171 | ++CurrentLine; 172 | return TEXT('\n'); 173 | } 174 | 175 | return Lines[CurrentLine][CurrentChar++]; 176 | } 177 | 178 | FString FSupertalkParser::FLxStream::ReadToEndOfLine() 179 | { 180 | if (IsEOF()) 181 | { 182 | return FString(); 183 | } 184 | 185 | if (!Lines[CurrentLine].IsValidIndex(CurrentChar)) 186 | { 187 | ReadChar(); 188 | return FString(); 189 | } 190 | 191 | FString Result = Lines[CurrentLine].Mid(CurrentChar); 192 | 193 | CurrentChar = 0; 194 | ++CurrentLine; 195 | 196 | return Result; 197 | } 198 | 199 | FString FSupertalkParser::FLxStream::ReadToEndOfFile() 200 | { 201 | FString Str = ReadToEndOfLine(); 202 | while (Lines.IsValidIndex(CurrentLine)) 203 | { 204 | Str += TEXT("\n") + ReadToEndOfLine(); 205 | } 206 | 207 | return Str; 208 | } 209 | 210 | FString FSupertalkParser::FLxStream::ReadBetweenContexts(const FTokenContext& Left, const FTokenContext& Right) 211 | { 212 | FString Str; 213 | for (int32 LineIdx = Left.Line; LineIdx <= Right.Line; ++LineIdx) 214 | { 215 | if (!Lines.IsValidIndex(LineIdx - 1)) 216 | { 217 | break; 218 | } 219 | 220 | FString& Line = Lines[LineIdx - 1]; 221 | int32 ColStart = LineIdx == Left.Line ? Left.Col : 1; 222 | int32 ColEnd = LineIdx == Right.Line ? Right.Col : INT32_MAX; 223 | for (int32 ColIdx = ColStart; ColIdx < ColEnd; ++ColIdx) 224 | { 225 | if (!Line.IsValidIndex(ColIdx - 1)) 226 | { 227 | break; 228 | } 229 | 230 | Str += Line[ColIdx - 1]; 231 | } 232 | 233 | if (LineIdx != Right.Line) 234 | { 235 | Str += "\n"; 236 | } 237 | } 238 | 239 | return Str; 240 | } 241 | 242 | void FSupertalkParser::FLxStream::GoBack(int32 Count) 243 | { 244 | check(Count >= 0); 245 | 246 | for (int32 Idx = 0; Idx < Count; ++Idx) 247 | { 248 | --CurrentChar; 249 | if (CurrentChar < 0) 250 | { 251 | --CurrentLine; 252 | if (CurrentLine < 0) 253 | { 254 | CurrentLine = 0; 255 | } 256 | 257 | if (Lines.IsValidIndex(CurrentLine) && !Lines[CurrentLine].IsEmpty()) 258 | { 259 | CurrentChar = Lines[CurrentLine].Len() - 1; 260 | } 261 | else 262 | { 263 | CurrentChar = 0; 264 | } 265 | } 266 | } 267 | } 268 | 269 | bool FSupertalkParser::FLxStream::IsOnEmptyLine() 270 | { 271 | if (IsEOF() || Lines[CurrentLine].TrimStartAndEnd().IsEmpty()) 272 | { 273 | return true; 274 | } 275 | 276 | return false; 277 | } 278 | 279 | FSupertalkParser::FToken FSupertalkParser::FPaStream::ReadToken() 280 | { 281 | if (IsEOF()) 282 | { 283 | FToken Token; 284 | Token.Type = ESupertalkTokenType::Eof; 285 | ++CurrentToken; // so that GoBack works correctly at Eof 286 | return Token; 287 | } 288 | 289 | return Tokens[CurrentToken++]; 290 | } 291 | 292 | FSupertalkParser::FToken FSupertalkParser::FPaStream::PeekToken() const 293 | { 294 | if (IsEOF()) 295 | { 296 | FToken Token; 297 | Token.Type = ESupertalkTokenType::Eof; 298 | return Token; 299 | } 300 | 301 | return Tokens[CurrentToken]; 302 | } 303 | 304 | void FSupertalkParser::FPaStream::GoBack(int32 Count) 305 | { 306 | check(Count >= 0); 307 | 308 | CurrentToken -= Count; 309 | if (CurrentToken < 0) 310 | { 311 | CurrentToken = 0; 312 | } 313 | } 314 | 315 | FSupertalkParser::~FSupertalkParser() 316 | { 317 | } 318 | 319 | bool FSupertalkParser::Parse(const FString& File, const FString& Input, USupertalkScript* Script) 320 | { 321 | if (!ensure(Script)) 322 | { 323 | return false; 324 | } 325 | 326 | TArray Tokens; 327 | if (!RunLexer(File, Input, Tokens)) 328 | { 329 | STP_LOG(Error, TEXT("Compilation of '%s' failed to a lexer error."), *File); 330 | return false; 331 | } 332 | 333 | if (!RunParser(Script, Tokens)) 334 | { 335 | STP_LOG(Error, TEXT("Compilation of '%s' failed to a parser error."), *File); 336 | return false; 337 | } 338 | 339 | STP_LOG(Log, TEXT("Compilation of '%s' succeeded."), *File); 340 | return true; 341 | } 342 | 343 | bool FSupertalkParser::TokenizeSyntax(const FString& File, const FString& Input, TArray& OutTokens) 344 | { 345 | return RunLexer(File, Input, OutTokens, true); 346 | } 347 | 348 | /* 349 | void FSupertalkParser::RunTest() 350 | { 351 | FString File = TEXT("RunTest.sts"); 352 | FString Content = TEXT("Foo: bar\n baz blah\n\n \n bing\n * abcd -- test\nBar = /abc/def/ghi -- test\n> Wait abcd"); 353 | TArray Tokens; 354 | 355 | if (RunLexer(File, Content, Tokens)) 356 | { 357 | for (const FToken& Token : Tokens) 358 | { 359 | UE_LOG(LogSupertalk, Log, TEXT("Token at %d:%d %s with content %s"), Token.Context.Line, Token.Context.Col, *Token.GetDisplayName().ToString(), *Token.Content); 360 | } 361 | } 362 | else 363 | { 364 | UE_LOG(LogSupertalk, Warning, TEXT("Failed to run lexer")); 365 | return; 366 | } 367 | 368 | USupertalkScript* Script = NewObject(); 369 | if (RunParser(Script, Tokens)) 370 | { 371 | UE_LOG(LogSupertalk, Log, TEXT("Parser success")); 372 | } 373 | else 374 | { 375 | UE_LOG(LogSupertalk, Warning, TEXT("Failed to run parser")); 376 | } 377 | } 378 | */ 379 | 380 | FSupertalkParser::FSupertalkParser() 381 | { 382 | } 383 | 384 | void FSupertalkParser::Initialize() 385 | { 386 | } 387 | 388 | bool FSupertalkParser::RunLexer(const FString& File, const FString& Input, TArray& OutTokens, bool bIgnoreErrors) 389 | { 390 | FLxContext Ctx; 391 | Ctx.File = File; 392 | Ctx.Stream.CurrentChar = 0; 393 | Ctx.Stream.CurrentLine = 0; 394 | 395 | Input.ParseIntoArrayLines(Ctx.Stream.Lines, false); 396 | 397 | while (!Ctx.Stream.IsEOF()) 398 | { 399 | FTokenContext CurrentContext = Ctx.CreateContext(); 400 | 401 | FToken Token; 402 | bool bLexResult = LxToken(Ctx, Token); 403 | FTokenContext NewContext = Ctx.CreateContext(); 404 | Token.Source = Ctx.Stream.ReadBetweenContexts(CurrentContext, NewContext); 405 | if (!bLexResult) 406 | { 407 | if (bIgnoreErrors) 408 | { 409 | if (CurrentContext == NewContext) 410 | { 411 | // Continuing would result in an infinite loop, we can't continue ignoring errors. 412 | Token.Type = ESupertalkTokenType::Unknown; 413 | Token.Source += Ctx.Stream.ReadToEndOfFile(); 414 | OutTokens.Add(Token); 415 | return false; 416 | } 417 | 418 | OutTokens.Add(Token); 419 | continue; 420 | } 421 | 422 | STP_LOG(Error, TEXT("%s - lexer: unexpected content '%s'"), *Token.Context.ToString(), *Token.Content); 423 | return false; 424 | } 425 | 426 | if (!OutTokens.IsEmpty() && FToken::IsPotentialLoop(Token, OutTokens.Last())) 427 | { 428 | if (bIgnoreErrors) 429 | { 430 | // Continuing would result in an infinite loop, we can't continue ignoring errors. 431 | Token = FToken(); 432 | Token.Context = CurrentContext; 433 | Token.Type = ESupertalkTokenType::Unknown; 434 | Token.Source += Ctx.Stream.ReadToEndOfFile(); 435 | OutTokens.Add(Token); 436 | } 437 | else 438 | { 439 | STP_LOG(Error, TEXT("%s - lexer: repeated token, potentially caught in infinite loop. Talk to a developer!"), *Token.Context.ToString()); 440 | } 441 | 442 | return false; 443 | } 444 | 445 | OutTokens.Add(Token); 446 | } 447 | 448 | return true; 449 | } 450 | 451 | bool FSupertalkParser::RunParser(USupertalkScript* Script, const TArray& InTokens) 452 | { 453 | check(Script); 454 | 455 | FPaContext Ctx; 456 | Ctx.Script = Script; 457 | Ctx.DefaultNamespace = TEXT("Supertalk.Script.Default"); 458 | Ctx.Stream.Tokens = InTokens; 459 | 460 | // Reset the state of the script 461 | Ctx.Script->Sections.Empty(); 462 | Ctx.Script->DefaultSection = NAME_None; 463 | 464 | // Remove any ignorable tokens (for example, comments) from the stream. 465 | Ctx.Stream.Tokens.RemoveAll([](const FToken& Token) { return Token.IsIgnorable(); }); 466 | 467 | FName DefaultSection = NAME_None; 468 | TMap Sections; 469 | 470 | while (!Ctx.Stream.IsEOF()) 471 | { 472 | // All statements must be inside a section, though the first section of the file is implicit. 473 | // The first section, if it has any statements, will be used as the default section. 474 | bool bIsUnnamedSection = false; 475 | FName SectionName; 476 | if (!PaTrySectionName(Ctx, SectionName)) 477 | { 478 | if (DefaultSection == NAME_None) 479 | { 480 | bIsUnnamedSection = true; 481 | SectionName = NAME_Default; 482 | } 483 | else 484 | { 485 | ParseTokenError(Ctx, Ctx.Stream.PeekToken(), ESupertalkTokenType::Section); 486 | return false; 487 | } 488 | } 489 | 490 | if (SectionName == NAME_None) 491 | { 492 | ParseError(Ctx, TEXT("Section name cannot be 'None'")); 493 | return false; 494 | } 495 | 496 | if (!PaSection(Ctx, SectionName)) 497 | { 498 | return false; 499 | } 500 | 501 | // Certain actions (specifically, directives) don't actually compile to anything and as such 502 | // shouldn't cause an unnamed default section to be created. 503 | if (bIsUnnamedSection && Ctx.Script->Sections[SectionName].Actions.Num() == 0) 504 | { 505 | Ctx.Script->Sections.Remove(SectionName); 506 | continue; 507 | } 508 | 509 | if (Ctx.Script->DefaultSection == NAME_None) 510 | { 511 | Ctx.Script->DefaultSection = SectionName; 512 | } 513 | } 514 | 515 | // Make sure jumps all happened with valid section names 516 | bool bAllValidJumps = true; 517 | for (const auto& Jump : Ctx.Jumps) 518 | { 519 | // Always allow None as a jump destination, as it is used to signal the script to end. 520 | if (Jump.Value != NAME_None && !Ctx.Script->Sections.Contains(Jump.Value)) 521 | { 522 | bAllValidJumps = false; 523 | ParseError(Ctx, Jump.Key, FString::Format(TEXT("jump to unknown section '{0}'"), { Jump.Value.ToString() })); 524 | } 525 | } 526 | 527 | if (!bAllValidJumps) 528 | { 529 | return false; 530 | } 531 | 532 | return true; 533 | } 534 | 535 | void FSupertalkParser::ConsumeEmptyLines(FLxContext& InCtx) 536 | { 537 | int32 InitialLine = InCtx.Stream.CurrentLine; 538 | int32 InitialChar = InCtx.Stream.CurrentChar; 539 | while (!InCtx.Stream.IsEOF()) 540 | { 541 | switch (InCtx.Stream.ReadChar()) 542 | { 543 | default: 544 | if (InitialLine != InCtx.Stream.CurrentLine) 545 | { 546 | InCtx.Stream.CurrentChar = 0; 547 | } 548 | else 549 | { 550 | InCtx.Stream.CurrentChar = InitialChar; 551 | } 552 | return; 553 | 554 | case TEXT('\0'): 555 | case TEXT('\n'): 556 | case TEXT('\r'): 557 | case TEXT(' '): 558 | case TEXT('\t'): 559 | break; 560 | } 561 | } 562 | 563 | // HACK 564 | if (InitialLine != InCtx.Stream.CurrentLine) 565 | { 566 | InCtx.bIsChoiceLine = false; 567 | } 568 | } 569 | 570 | int32 FSupertalkParser::ConsumeWhitespaceUpdateIndentation(FLxContext& InCtx) 571 | { 572 | if (InCtx.Stream.CurrentChar == 0) 573 | { 574 | InCtx.CurrentIndentation = ConsumeWhitespace(InCtx); 575 | return InCtx.CurrentIndentation; 576 | } 577 | 578 | return ConsumeWhitespace(InCtx); 579 | } 580 | 581 | int32 FSupertalkParser::ConsumeWhitespace(FLxContext& InCtx) 582 | { 583 | if (InCtx.Stream.IsEOF()) 584 | { 585 | return 0; 586 | } 587 | 588 | int32 Consumed = 0; 589 | while (!InCtx.Stream.IsEOF()) 590 | { 591 | switch (InCtx.Stream.ReadChar()) 592 | { 593 | default: 594 | InCtx.Stream.GoBack(1); 595 | return Consumed; 596 | 597 | case ' ': 598 | case '\t': 599 | ++Consumed; 600 | break; 601 | } 602 | } 603 | 604 | return Consumed; 605 | } 606 | 607 | bool FSupertalkParser::LxToken(FLxContext& InCtx, FToken& OutToken) 608 | { 609 | if (InCtx.Stream.IsEOF()) 610 | { 611 | return false; 612 | } 613 | 614 | ConsumeEmptyLines(InCtx); 615 | ConsumeWhitespaceUpdateIndentation(InCtx); 616 | 617 | OutToken = FToken(); 618 | OutToken.Type = ESupertalkTokenType::Unknown; 619 | OutToken.Context = InCtx.CreateContext(); 620 | OutToken.Indentation = InCtx.CurrentIndentation; 621 | 622 | if (InCtx.Stream.IsEOF()) 623 | { 624 | OutToken.Type = ESupertalkTokenType::Eof; 625 | return true; 626 | } 627 | 628 | TCHAR Char = InCtx.Stream.ReadChar(); 629 | OutToken.Content = FString() + Char; 630 | 631 | // HACK: choices need to have special handling to consume all the text left on the line, except if we find a 632 | // localization key. Eventually this should be rewritten because not only is it hacky but it also means each choice 633 | // can only take place on a single line. 634 | if (InCtx.bIsChoiceLine) 635 | { 636 | if (Char == Symbols::LocalizationKeyStart) 637 | { 638 | OutToken.Type = ESupertalkTokenType::LocalizationKey; 639 | return LxTokenLocalizationKey(InCtx, OutToken); 640 | } 641 | else 642 | { 643 | InCtx.Stream.GoBack(1); 644 | OutToken.Type = ESupertalkTokenType::Text; 645 | if (!LxTokenText(InCtx, OutToken, ETextParseMode::SingleLine)) 646 | { 647 | OutToken.Content = FString(); 648 | } 649 | 650 | InCtx.bIsChoiceLine = false; 651 | return true; 652 | } 653 | } 654 | 655 | switch (Char) 656 | { 657 | default: 658 | InCtx.Stream.GoBack(1); 659 | 660 | OutToken.Type = ESupertalkTokenType::Name; 661 | if (!LxTokenName(InCtx, OutToken, false)) 662 | { 663 | return false; 664 | } 665 | 666 | if (ESupertalkTokenType* Override = NameToTokenOverrideMap.Find(*OutToken.Content)) 667 | { 668 | OutToken.Type = *Override; 669 | } 670 | 671 | return true; 672 | 673 | case ' ': 674 | case '\t': 675 | OutToken.Type = ESupertalkTokenType::Ignore; 676 | return true; 677 | 678 | case Symbols::Separator: 679 | OutToken.Type = ESupertalkTokenType::Separator; 680 | return true; 681 | 682 | case Symbols::Member: 683 | OutToken.Type = ESupertalkTokenType::Member; 684 | return true; 685 | 686 | case Symbols::Equals: 687 | if (InCtx.Stream.ReadChar() == Symbols::Equals) 688 | { 689 | OutToken.Content = TEXT("=="); 690 | OutToken.Type = ESupertalkTokenType::Equal; 691 | } 692 | else 693 | { 694 | InCtx.Stream.GoBack(1); 695 | OutToken.Type = ESupertalkTokenType::Assign; 696 | } 697 | 698 | return true; 699 | 700 | case Symbols::TextStart: 701 | OutToken.Type = ESupertalkTokenType::Text; 702 | if (!LxTokenText(InCtx, OutToken, ETextParseMode::MultiLine)) 703 | { 704 | OutToken.Content = FString(); 705 | } 706 | 707 | return true; 708 | 709 | case Symbols::SingleQuote: 710 | case Symbols::DoubleQuote: 711 | OutToken.Type = ESupertalkTokenType::Text; 712 | InCtx.Stream.GoBack(1); 713 | return LxTokenText(InCtx, OutToken, ETextParseMode::Quoted); 714 | 715 | case Symbols::AssetStart1: 716 | case Symbols::AssetStart2: 717 | OutToken.Type = ESupertalkTokenType::Asset; 718 | InCtx.Stream.GoBack(1); 719 | return LxTokenAsset(InCtx, OutToken); 720 | 721 | case Symbols::ChoiceStart: 722 | OutToken.Type = ESupertalkTokenType::Choice; 723 | InCtx.bIsChoiceLine = true; // HACK 724 | return true; 725 | 726 | case Symbols::SectionStart: 727 | OutToken.Type = ESupertalkTokenType::Section; 728 | return LxTokenName(InCtx, OutToken, true); 729 | 730 | case Symbols::Comment: 731 | Char = InCtx.Stream.ReadChar(); 732 | OutToken.Content = OutToken.Content + Char; 733 | switch (Char) 734 | { 735 | default: 736 | InCtx.Stream.GoBack(2); 737 | return false; 738 | 739 | case Symbols::Comment: 740 | OutToken.Type = ESupertalkTokenType::Comment; 741 | return LxTokenComment(InCtx, OutToken); 742 | 743 | case Symbols::Jump: 744 | OutToken.Type = ESupertalkTokenType::Jump; 745 | return LxTokenName(InCtx, OutToken, true); 746 | } 747 | 748 | case Symbols::CommandStart: 749 | OutToken.Type = ESupertalkTokenType::Command; 750 | return LxTokenText(InCtx, OutToken, ETextParseMode::SingleLine); 751 | 752 | case Symbols::ParallelStart: 753 | OutToken.Type = ESupertalkTokenType::ParallelStart; 754 | return true; 755 | 756 | case Symbols::ParallelEnd: 757 | OutToken.Type = ESupertalkTokenType::ParallelEnd; 758 | return true; 759 | 760 | case Symbols::QueueStart: 761 | OutToken.Type = ESupertalkTokenType::QueueStart; 762 | return true; 763 | 764 | case Symbols::QueueEnd: 765 | OutToken.Type = ESupertalkTokenType::QueueEnd; 766 | return true; 767 | 768 | case Symbols::StatementEnd: 769 | OutToken.Type = ESupertalkTokenType::StatementEnd; 770 | return true; 771 | 772 | case Symbols::DirectiveStart: 773 | OutToken.Type = ESupertalkTokenType::Directive; 774 | return LxTokenDirective(InCtx, OutToken); 775 | 776 | case Symbols::LocalizationKeyStart: 777 | OutToken.Type = ESupertalkTokenType::LocalizationKey; 778 | return LxTokenLocalizationKey(InCtx, OutToken); 779 | 780 | case Symbols::GroupStart: 781 | OutToken.Type = ESupertalkTokenType::GroupStart; 782 | return true; 783 | 784 | case Symbols::GroupEnd: 785 | OutToken.Type = ESupertalkTokenType::GroupEnd; 786 | return true; 787 | 788 | case Symbols::Not: 789 | OutToken.Type = ESupertalkTokenType::Not; 790 | Char = InCtx.Stream.ReadChar(); 791 | switch (Char) 792 | { 793 | default: 794 | InCtx.Stream.GoBack(1); 795 | break; 796 | 797 | case Symbols::Equals: 798 | OutToken.Content = TEXT("~="); 799 | OutToken.Type = ESupertalkTokenType::NotEqual; 800 | break; 801 | } 802 | 803 | return true; 804 | } 805 | } 806 | 807 | bool FSupertalkParser::LxTokenName(FLxContext& InCtx, FToken& OutName, bool AllowWhitespace) 808 | { 809 | FString Content; 810 | bool bHasStarted = false; 811 | while (!InCtx.Stream.IsEOF()) 812 | { 813 | TCHAR Char = InCtx.Stream.ReadChar(); 814 | if (!bHasStarted && FChar::IsWhitespace(Char)) 815 | { 816 | continue; 817 | } 818 | 819 | if (IsTokenSymbol(Char) || Char == TEXT('\n') || (!AllowWhitespace && FChar::IsWhitespace(Char))) 820 | { 821 | if (Char != TEXT('\n')) 822 | { 823 | InCtx.Stream.GoBack(1); 824 | } 825 | 826 | break; 827 | } 828 | else if (NameToTokenOverrideMap.Contains(*Content) && FChar::IsWhitespace(Char)) 829 | { 830 | InCtx.Stream.GoBack(1); 831 | break; 832 | } 833 | else 834 | { 835 | bHasStarted = true; 836 | Content += Char; 837 | } 838 | } 839 | 840 | Content.TrimStartAndEndInline(); 841 | if (Content.IsEmpty()) 842 | { 843 | return false; 844 | } 845 | 846 | OutName.Content = Content; 847 | return true; 848 | } 849 | 850 | bool FSupertalkParser::LxTokenText(FLxContext& InCtx, FToken& OutText, ETextParseMode Mode) 851 | { 852 | FString Content; 853 | int32 InitialIndentation = InCtx.CurrentIndentation; 854 | bool bOnNewLine = false; 855 | 856 | TCHAR QuoteChar = Symbols::DoubleQuote; 857 | if (Mode == ETextParseMode::Quoted) 858 | { 859 | QuoteChar = InCtx.Stream.ReadChar(); 860 | } 861 | 862 | while (!InCtx.Stream.IsEOF()) 863 | { 864 | TCHAR Char = InCtx.Stream.ReadChar(); 865 | 866 | switch (Char) 867 | { 868 | case Symbols::ChoiceStart: 869 | if (bOnNewLine) 870 | { 871 | InCtx.Stream.CurrentChar = 0; 872 | goto complete; 873 | } 874 | 875 | goto textChar; 876 | 877 | case Symbols::DoubleQuote: 878 | case Symbols::SingleQuote: 879 | if (Mode == ETextParseMode::Quoted && QuoteChar == Char) 880 | { 881 | goto complete; 882 | } 883 | 884 | goto textChar; 885 | 886 | textChar: 887 | default: 888 | bOnNewLine = false; 889 | Content += Char; 890 | break; 891 | 892 | case TEXT('\n'): 893 | if (Mode == ETextParseMode::MultiLine) 894 | { 895 | if (bOnNewLine) 896 | { 897 | Content += TEXT('\n'); 898 | } 899 | 900 | if (InCtx.Stream.IsOnEmptyLine()) 901 | { 902 | ConsumeWhitespace(InCtx); 903 | } 904 | else 905 | { 906 | if (!bOnNewLine) 907 | { 908 | Content += TEXT(' '); 909 | } 910 | 911 | if (ConsumeWhitespace(InCtx) <= InitialIndentation) 912 | { 913 | InCtx.Stream.CurrentChar = 0; 914 | goto complete; 915 | } 916 | } 917 | 918 | bOnNewLine = true; 919 | break; 920 | } 921 | else if (Mode == ETextParseMode::Quoted) 922 | { 923 | return false; 924 | } 925 | else 926 | { 927 | goto complete; 928 | } 929 | } 930 | } 931 | 932 | complete: 933 | Content.TrimStartAndEndInline(); 934 | if (Content.IsEmpty()) 935 | { 936 | return false; 937 | } 938 | 939 | OutText.Content = Content; 940 | return true; 941 | } 942 | 943 | bool FSupertalkParser::LxTokenAsset(FLxContext& InCtx, FToken& OutAsset) 944 | { 945 | FString Content = FString(); 946 | while (!InCtx.Stream.IsEOF()) 947 | { 948 | TCHAR Char = InCtx.Stream.ReadChar(); 949 | if (FChar::IsIdentifier(Char) || Char == TEXT('/') || Char == TEXT('\\') || Char == TEXT(' ') || Char == TEXT('.')) 950 | { 951 | Content += Char; 952 | } 953 | else 954 | { 955 | if (Char != TEXT('\n')) 956 | { 957 | InCtx.Stream.GoBack(1); 958 | } 959 | break; 960 | } 961 | } 962 | 963 | Content.TrimStartAndEndInline(); 964 | if (Content.IsEmpty()) 965 | { 966 | return false; 967 | } 968 | 969 | OutAsset.Content = Content; 970 | return true; 971 | } 972 | 973 | bool FSupertalkParser::LxTokenComment(FLxContext& InCtx, FToken& OutComment) 974 | { 975 | OutComment.Content = InCtx.Stream.ReadToEndOfLine(); 976 | return true; 977 | } 978 | 979 | bool FSupertalkParser::LxTokenDirective(FLxContext& InCtx, FToken& OutDirective) 980 | { 981 | FString Content = FString(); 982 | while (!InCtx.Stream.IsEOF()) 983 | { 984 | TCHAR Char = InCtx.Stream.ReadChar(); 985 | if (Char == '\n') 986 | { 987 | break; 988 | } 989 | 990 | Content += Char; 991 | } 992 | 993 | Content.TrimStartAndEndInline(); 994 | if (Content.IsEmpty()) 995 | { 996 | return false; 997 | } 998 | 999 | OutDirective.Content = Content; 1000 | return true; 1001 | } 1002 | 1003 | bool FSupertalkParser::LxTokenLocalizationKey(FLxContext& InCtx, FToken& OutLocalizationKey) 1004 | { 1005 | FString Content = FString(); 1006 | FString Namespace; 1007 | while (!InCtx.Stream.IsEOF()) 1008 | { 1009 | TCHAR Char = InCtx.Stream.ReadChar(); 1010 | if (Char == TEXT('/')) 1011 | { 1012 | if (!Namespace.IsEmpty()) 1013 | { 1014 | return false; 1015 | } 1016 | 1017 | Namespace = Content; 1018 | Content = FString(); 1019 | } 1020 | else if (Char == Symbols::SingleQuote 1021 | || Char == Symbols::DoubleQuote 1022 | || Char == Symbols::TextStart 1023 | || FChar::IsWhitespace(Char)) 1024 | { 1025 | InCtx.Stream.GoBack(1); 1026 | break; 1027 | } 1028 | else 1029 | { 1030 | Content += Char; 1031 | } 1032 | } 1033 | 1034 | Content.TrimStartAndEndInline(); 1035 | if (Content.IsEmpty()) 1036 | { 1037 | return false; 1038 | } 1039 | 1040 | Namespace.TrimStartAndEndInline(); 1041 | OutLocalizationKey.Content = Content; 1042 | OutLocalizationKey.Namespace = Namespace; 1043 | return true; 1044 | } 1045 | 1046 | void FSupertalkParser::ParseTokenError(const FPaContext& InCtx, const FToken& Token, ESupertalkTokenType ExpectedTokenType) const 1047 | { 1048 | FToken ExpectedToken; 1049 | ExpectedToken.Type = ExpectedTokenType; 1050 | 1051 | ParseTokenError(InCtx, Token, ExpectedToken.GetDisplayName()); 1052 | } 1053 | 1054 | void FSupertalkParser::ParseTokenError(const FPaContext& InCtx, const FToken& Token, const FString& Expected) const 1055 | { 1056 | ParseError(InCtx, Token, FString::Format(TEXT("expected token {0} but found {1} near '{2}'"), { Expected, Token.GetDisplayName(), Token.Content })); 1057 | } 1058 | 1059 | void FSupertalkParser::ParseError(const FPaContext& InCtx, const FToken& Token, const FString& Message) const 1060 | { 1061 | STP_LOG(Error, TEXT("%s - parser: %s"), *Token.Context.ToString(), *Message); 1062 | } 1063 | 1064 | void FSupertalkParser::ParseError(const FPaContext& InCtx, const FString& Message) const 1065 | { 1066 | ParseError(InCtx, InCtx.Stream.PeekToken(), Message); 1067 | } 1068 | 1069 | void FSupertalkParser::ParseWarning(const FPaContext& InCtx, const FToken& Token, const FString& Message) const 1070 | { 1071 | STP_LOG(Warning, TEXT("%s - parser: %s"), *Token.Context.ToString(), *Message); 1072 | } 1073 | 1074 | void FSupertalkParser::ParseWarning(const FPaContext& InCtx, const FString& Message) const 1075 | { 1076 | ParseWarning(InCtx, InCtx.Stream.PeekToken(), Message); 1077 | } 1078 | 1079 | bool FSupertalkParser::PaTrySectionName(FPaContext& InCtx, FName& OutName) 1080 | { 1081 | FToken Token = InCtx.Stream.ReadToken(); 1082 | if (Token.Type != ESupertalkTokenType::Section) 1083 | { 1084 | InCtx.Stream.GoBack(1); 1085 | return false; 1086 | } 1087 | 1088 | OutName = FName(Token.Content); 1089 | return true; 1090 | } 1091 | 1092 | bool FSupertalkParser::PaSection(FPaContext& InCtx, FName Name) 1093 | { 1094 | FSupertalkSection Section; 1095 | Section.Name = Name; 1096 | 1097 | while (!InCtx.Stream.IsEOF() && InCtx.Stream.PeekToken().Type != ESupertalkTokenType::Section) 1098 | { 1099 | FSupertalkAction Action; 1100 | if (!PaAction(InCtx, Action)) 1101 | { 1102 | return false; 1103 | } 1104 | 1105 | if (Action.Operation != ESupertalkOperation::Noop) 1106 | { 1107 | Section.Actions.Add(Action); 1108 | } 1109 | } 1110 | 1111 | InCtx.Script->Sections.Add(Section.Name, Section); 1112 | 1113 | return true; 1114 | } 1115 | 1116 | bool FSupertalkParser::PaAction(FPaContext& InCtx, FSupertalkAction& OutAction) 1117 | { 1118 | FToken Token = InCtx.Stream.ReadToken(); 1119 | switch (Token.Type) 1120 | { 1121 | default: 1122 | ParseTokenError(InCtx, Token, TEXT("Name, Command, Jump, ParallelStart, QueueStart")); 1123 | return false; 1124 | 1125 | case ESupertalkTokenType::Directive: 1126 | InCtx.Stream.GoBack(1); 1127 | return PaDirective(InCtx); 1128 | 1129 | case ESupertalkTokenType::Name: 1130 | { 1131 | FToken Token2 = InCtx.Stream.ReadToken(); 1132 | switch (Token2.Type) 1133 | { 1134 | default: 1135 | ParseTokenError(InCtx, Token, TEXT("Assign, AttrStart, Text")); 1136 | return false; 1137 | 1138 | case ESupertalkTokenType::Assign: 1139 | InCtx.Stream.GoBack(2); 1140 | return PaAssign(InCtx, OutAction); 1141 | 1142 | case ESupertalkTokenType::Separator: 1143 | case ESupertalkTokenType::AttrStart: 1144 | case ESupertalkTokenType::LocalizationKey: 1145 | case ESupertalkTokenType::Text: 1146 | InCtx.Stream.GoBack(2); 1147 | return PaLine(InCtx, OutAction); 1148 | } 1149 | } 1150 | 1151 | case ESupertalkTokenType::If: 1152 | InCtx.Stream.GoBack(1); 1153 | return PaConditional(InCtx, OutAction); 1154 | 1155 | case ESupertalkTokenType::Command: 1156 | InCtx.Stream.GoBack(1); 1157 | return PaCommand(InCtx, OutAction); 1158 | 1159 | case ESupertalkTokenType::Jump: 1160 | InCtx.Stream.GoBack(1); 1161 | return PaJump(InCtx, OutAction); 1162 | 1163 | case ESupertalkTokenType::ParallelStart: 1164 | InCtx.Stream.GoBack(1); 1165 | return PaParallel(InCtx, OutAction); 1166 | 1167 | case ESupertalkTokenType::QueueStart: 1168 | InCtx.Stream.GoBack(1); 1169 | return PaQueue(InCtx, OutAction); 1170 | } 1171 | } 1172 | 1173 | bool FSupertalkParser::PaDirective(FPaContext& InCtx) 1174 | { 1175 | FToken Token = InCtx.Stream.ReadToken(); 1176 | check(Token.Type == ESupertalkTokenType::Directive); 1177 | 1178 | FString Directive; 1179 | FString Content; 1180 | if (!Token.Content.Split(TEXT(" "), &Directive, &Content)) 1181 | { 1182 | Directive = Token.Content; 1183 | } 1184 | 1185 | if (Directive.Compare(TEXT("namespace"), ESearchCase::IgnoreCase) == 0) 1186 | { 1187 | Content.TrimStartAndEndInline(); 1188 | if (Content.IsEmpty()) 1189 | { 1190 | ParseError(InCtx, Token, TEXT("namespace directive cannot be passed empty text")); 1191 | return false; 1192 | } 1193 | 1194 | InCtx.DefaultNamespace = Content; 1195 | return true; 1196 | } 1197 | else if (Directive.Compare(TEXT("error"), ESearchCase::IgnoreCase) == 0) 1198 | { 1199 | Content.TrimStartAndEndInline(); 1200 | ParseError(InCtx, Token, FString::Format(TEXT("error directive: {0}"), { Content })); 1201 | return false; 1202 | } 1203 | 1204 | ParseError(InCtx, Token, FString::Format(TEXT("unknown directive '{0}'"), { Directive })); 1205 | return false; 1206 | } 1207 | 1208 | bool FSupertalkParser::PaAssign(FPaContext& InCtx, FSupertalkAction& OutAction) 1209 | { 1210 | FToken VarToken = InCtx.Stream.ReadToken(); 1211 | check(VarToken.Type == ESupertalkTokenType::Name); 1212 | 1213 | FName VarName = FName(VarToken.Content); 1214 | if (IsReservedName(VarName)) 1215 | { 1216 | ParseError(InCtx, VarToken, TEXT("invalid variable name")); 1217 | return false; 1218 | } 1219 | 1220 | FToken AssignToken = InCtx.Stream.ReadToken(); 1221 | if (AssignToken.Type != ESupertalkTokenType::Assign) 1222 | { 1223 | ParseTokenError(InCtx, AssignToken, ESupertalkTokenType::Assign); 1224 | return false; 1225 | } 1226 | 1227 | TObjectPtr Expr; 1228 | if (!PaExpression(InCtx, Expr)) 1229 | { 1230 | return false; 1231 | } 1232 | 1233 | OutAction.Operation = ESupertalkOperation::Assign; 1234 | 1235 | USupertalkAssignParams* Params = NewObject(InCtx.Script); 1236 | Params->Variable = FName(VarToken.Content); 1237 | Params->Expression = Expr; 1238 | OutAction.Params = Params; 1239 | 1240 | return true; 1241 | } 1242 | 1243 | bool FSupertalkParser::PaLine(FPaContext& InCtx, FSupertalkAction& OutAction) 1244 | { 1245 | FSupertalkLine Line; 1246 | if (!PaValue(InCtx, Line.Speaker)) 1247 | { 1248 | return false; 1249 | } 1250 | 1251 | FToken Token = InCtx.Stream.ReadToken(); 1252 | if (Token.Type == ESupertalkTokenType::Separator) 1253 | { 1254 | if (!PaValue(InCtx, Line.SpeakerNameOverride)) 1255 | { 1256 | return false; 1257 | } 1258 | 1259 | Token = InCtx.Stream.ReadToken(); 1260 | } 1261 | 1262 | if (Token.Type == ESupertalkTokenType::AttrStart) 1263 | { 1264 | InCtx.Stream.GoBack(1); 1265 | if (!PaAttributeList(InCtx, Line.Attributes)) 1266 | { 1267 | return false; 1268 | } 1269 | 1270 | Token = InCtx.Stream.ReadToken(); 1271 | } 1272 | 1273 | if (Token.Type == ESupertalkTokenType::StatementEnd) 1274 | { 1275 | Line.bIsBlankLine = true; 1276 | 1277 | OutAction.Operation = ESupertalkOperation::Line; 1278 | 1279 | USupertalkPlayLineParams* Params = NewObject(InCtx.Script); 1280 | Params->Line = Line; 1281 | OutAction.Params = Params; 1282 | 1283 | return true; 1284 | } 1285 | 1286 | FString Namespace = InCtx.DefaultNamespace; 1287 | FString Key; 1288 | if (Token.Type == ESupertalkTokenType::LocalizationKey) 1289 | { 1290 | if (!Token.Namespace.IsEmpty()) 1291 | { 1292 | Namespace = Token.Namespace; 1293 | } 1294 | 1295 | Key = Token.Content; 1296 | 1297 | Token = InCtx.Stream.ReadToken(); 1298 | } 1299 | 1300 | if (Token.Type != ESupertalkTokenType::Text) 1301 | { 1302 | ParseTokenError(InCtx, Token, ESupertalkTokenType::Text); 1303 | return false; 1304 | } 1305 | 1306 | // Is there a better way to initialize an FText with a variable namespace/key? The FText constructor we want isn't 1307 | // accessible sadly, and double-constructing an FText (due to the FText::FromString call) seems inefficient. 1308 | Line.Text = FText::ChangeKey(FTextKey(Namespace), FTextKey(Key.IsEmpty() ? Token.GetGeneratedLocalizationKey() : Key), FText::FromString(Token.Content)); 1309 | 1310 | FToken NextToken = InCtx.Stream.PeekToken(); 1311 | if (NextToken.Type == ESupertalkTokenType::Choice && NextToken.Indentation >= Token.Indentation) 1312 | { 1313 | // Choice uses a different operation + params, so pass our line off to it. 1314 | return PaChoice(InCtx, OutAction, Line); 1315 | } 1316 | 1317 | OutAction.Operation = ESupertalkOperation::Line; 1318 | 1319 | USupertalkPlayLineParams* Params = NewObject(InCtx.Script); 1320 | Params->Line = Line; 1321 | OutAction.Params = Params; 1322 | 1323 | return true; 1324 | } 1325 | 1326 | bool FSupertalkParser::PaChoice(FPaContext& InCtx, FSupertalkAction& OutAction, FSupertalkLine& Line) 1327 | { 1328 | USupertalkPlayChoiceParams* Params = NewObject(InCtx.Script); 1329 | Params->Line = Line; 1330 | 1331 | FToken Token = InCtx.Stream.ReadToken(); 1332 | check(Token.Type == ESupertalkTokenType::Choice); 1333 | while (Token.Type == ESupertalkTokenType::Choice) 1334 | { 1335 | FSupertalkChoice Choice; 1336 | Token = InCtx.Stream.ReadToken(); 1337 | FString Namespace = InCtx.DefaultNamespace; 1338 | FString Key; 1339 | if (Token.Type == ESupertalkTokenType::LocalizationKey) 1340 | { 1341 | if (!Token.Namespace.IsEmpty()) 1342 | { 1343 | Namespace = Token.Namespace; 1344 | } 1345 | 1346 | Key = Token.Content; 1347 | Token = InCtx.Stream.ReadToken(); 1348 | } 1349 | 1350 | if (Token.Type != ESupertalkTokenType::Text) 1351 | { 1352 | ParseTokenError(InCtx, Token, ESupertalkTokenType::Text); 1353 | return false; 1354 | } 1355 | 1356 | Choice.Text = FText::ChangeKey(FTextKey(Namespace), FTextKey(Key.IsEmpty() ? Token.GetGeneratedLocalizationKey() : Key), FText::FromString(Token.Content)); 1357 | 1358 | if (InCtx.Stream.PeekToken().Indentation >= Token.Indentation) 1359 | { 1360 | if (!PaAction(InCtx, Choice.SubAction)) 1361 | { 1362 | return false; 1363 | } 1364 | } 1365 | else 1366 | { 1367 | Choice.SubAction.Operation = ESupertalkOperation::Noop; 1368 | } 1369 | 1370 | Params->Choices.Add(Choice); 1371 | 1372 | Token = InCtx.Stream.ReadToken(); 1373 | } 1374 | 1375 | InCtx.Stream.GoBack(1); 1376 | 1377 | OutAction.Operation = ESupertalkOperation::Choice; 1378 | OutAction.Params = Params; 1379 | 1380 | return true; 1381 | } 1382 | 1383 | bool FSupertalkParser::PaCommand(FPaContext& InCtx, FSupertalkAction& OutAction) 1384 | { 1385 | FToken CommandToken = InCtx.Stream.ReadToken(); 1386 | check(CommandToken.Type == ESupertalkTokenType::Command); 1387 | 1388 | OutAction.Operation = ESupertalkOperation::Call; 1389 | USupertalkCallParams* Params = NewObject(InCtx.Script); 1390 | Params->Arguments = CommandToken.Content; 1391 | OutAction.Params = Params; 1392 | 1393 | return true; 1394 | } 1395 | 1396 | bool FSupertalkParser::PaJump(FPaContext& InCtx, FSupertalkAction& OutAction) 1397 | { 1398 | FToken Token = InCtx.Stream.ReadToken(); 1399 | check(Token.Type == ESupertalkTokenType::Jump); 1400 | 1401 | OutAction.Operation = ESupertalkOperation::Jump; 1402 | USupertalkJumpParams* Params = NewObject(InCtx.Script); 1403 | Params->JumpTarget = FName(Token.Content); 1404 | OutAction.Params = Params; 1405 | 1406 | InCtx.Jumps.Add(TTuple(Token, Params->JumpTarget)); 1407 | 1408 | return true; 1409 | } 1410 | 1411 | bool FSupertalkParser::PaParallel(FPaContext& InCtx, FSupertalkAction& OutAction) 1412 | { 1413 | FToken Token = InCtx.Stream.ReadToken(); 1414 | check(Token.Type == ESupertalkTokenType::ParallelStart); 1415 | 1416 | OutAction.Operation = ESupertalkOperation::Parallel; 1417 | USupertalkParallelParams* Params = NewObject(InCtx.Script); 1418 | OutAction.Params = Params; 1419 | 1420 | while (!InCtx.Stream.IsEOF()) 1421 | { 1422 | Token = InCtx.Stream.ReadToken(); 1423 | if (Token.Type == ESupertalkTokenType::ParallelEnd) 1424 | { 1425 | break; 1426 | } 1427 | 1428 | InCtx.Stream.GoBack(1); 1429 | 1430 | FSupertalkAction SubAction; 1431 | if (!PaAction(InCtx, SubAction)) 1432 | { 1433 | return false; 1434 | } 1435 | 1436 | Params->SubActions.Add(SubAction); 1437 | } 1438 | 1439 | if (Token.Type != ESupertalkTokenType::ParallelEnd) 1440 | { 1441 | ParseTokenError(InCtx, Token, ESupertalkTokenType::ParallelEnd); 1442 | return false; 1443 | } 1444 | 1445 | return true; 1446 | } 1447 | 1448 | bool FSupertalkParser::PaQueue(FPaContext& InCtx, FSupertalkAction& OutAction) 1449 | { 1450 | FToken Token = InCtx.Stream.ReadToken(); 1451 | check(Token.Type == ESupertalkTokenType::QueueStart); 1452 | 1453 | OutAction.Operation = ESupertalkOperation::Queue; 1454 | USupertalkQueueParams* Params = NewObject(InCtx.Script); 1455 | OutAction.Params = Params; 1456 | 1457 | while (!InCtx.Stream.IsEOF()) 1458 | { 1459 | Token = InCtx.Stream.ReadToken(); 1460 | if (Token.Type == ESupertalkTokenType::QueueEnd) 1461 | { 1462 | break; 1463 | } 1464 | 1465 | InCtx.Stream.GoBack(1); 1466 | 1467 | FSupertalkAction SubAction; 1468 | if (!PaAction(InCtx, SubAction)) 1469 | { 1470 | return false; 1471 | } 1472 | 1473 | Params->SubActions.Add(SubAction); 1474 | } 1475 | 1476 | if (Token.Type != ESupertalkTokenType::QueueEnd) 1477 | { 1478 | ParseTokenError(InCtx, Token, ESupertalkTokenType::QueueEnd); 1479 | return false; 1480 | } 1481 | 1482 | return true; 1483 | } 1484 | 1485 | bool FSupertalkParser::PaConditional(FPaContext& InCtx, FSupertalkAction& OutAction) 1486 | { 1487 | FToken Token = InCtx.Stream.ReadToken(); 1488 | check(Token.Type == ESupertalkTokenType::If); 1489 | 1490 | OutAction.Operation = ESupertalkOperation::Conditional; 1491 | USupertalkConditionalParams* Params = NewObject(InCtx.Script); 1492 | OutAction.Params = Params; 1493 | 1494 | if (!PaExpression(InCtx, Params->Expression)) 1495 | { 1496 | return false; 1497 | } 1498 | 1499 | Token = InCtx.Stream.ReadToken(); 1500 | if (Token.Type != ESupertalkTokenType::Then) 1501 | { 1502 | ParseTokenError(InCtx, Token, ESupertalkTokenType::Then); 1503 | return false; 1504 | } 1505 | 1506 | if (!PaAction(InCtx, Params->TrueAction)) 1507 | { 1508 | return false; 1509 | } 1510 | 1511 | Token = InCtx.Stream.PeekToken(); 1512 | if (Token.Type == ESupertalkTokenType::Else) 1513 | { 1514 | InCtx.Stream.ReadToken(); 1515 | return PaAction(InCtx, Params->FalseAction); 1516 | } 1517 | 1518 | return true; 1519 | } 1520 | 1521 | bool FSupertalkParser::PaExpression(FPaContext& InCtx, TObjectPtr& OutExpression) 1522 | { 1523 | return PaEqualityExpression(InCtx, OutExpression); 1524 | } 1525 | 1526 | bool FSupertalkParser::PaEqualityExpression(FPaContext& InCtx, TObjectPtr& OutExpression) 1527 | { 1528 | TArray> Expressions; 1529 | TArray Operations; 1530 | TObjectPtr Expr; 1531 | FToken Token; 1532 | do 1533 | { 1534 | if (!PaUnaryExpression(InCtx, Expr)) 1535 | { 1536 | return false; 1537 | } 1538 | 1539 | Expressions.Add(Expr); 1540 | Token = InCtx.Stream.ReadToken(); 1541 | Operations.Add(Token.Type == ESupertalkTokenType::Equal ? ESupertalkExpression_Equality_Operation::Equal : ESupertalkExpression_Equality_Operation::NotEqual); 1542 | } 1543 | while (Token.Type == ESupertalkTokenType::Equal || Token.Type == ESupertalkTokenType::NotEqual); 1544 | InCtx.Stream.GoBack(1); 1545 | Operations.RemoveAt(Operations.Num() - 1); 1546 | 1547 | switch (Expressions.Num()) 1548 | { 1549 | case 0: 1550 | checkNoEntry(); 1551 | return false; 1552 | 1553 | case 1: 1554 | OutExpression = Expressions[0]; 1555 | return true; 1556 | } 1557 | 1558 | USupertalkExpression_Equality* Equality = NewObject(InCtx.Script); 1559 | Equality->SubExpressions = Expressions; 1560 | Equality->Operations = Operations; 1561 | OutExpression = Equality; 1562 | 1563 | return true; 1564 | } 1565 | 1566 | bool FSupertalkParser::PaUnaryExpression(FPaContext& InCtx, TObjectPtr& OutExpression) 1567 | { 1568 | FToken Token = InCtx.Stream.PeekToken(); 1569 | switch (Token.Type) 1570 | { 1571 | default: 1572 | return PaGroupExpression(InCtx, OutExpression); 1573 | 1574 | case ESupertalkTokenType::Not: 1575 | InCtx.Stream.ReadToken(); 1576 | USupertalkExpression_Not* Unary = NewObject(InCtx.Script); 1577 | OutExpression = Unary; 1578 | return PaUnaryExpression(InCtx, Unary->Value); 1579 | } 1580 | } 1581 | 1582 | bool FSupertalkParser::PaGroupExpression(FPaContext& InCtx, TObjectPtr& OutExpression) 1583 | { 1584 | FToken Token = InCtx.Stream.PeekToken(); 1585 | if (Token.Type == ESupertalkTokenType::GroupStart) 1586 | { 1587 | InCtx.Stream.ReadToken(); 1588 | if (!PaExpression(InCtx, OutExpression)) 1589 | { 1590 | return false; 1591 | } 1592 | 1593 | Token = InCtx.Stream.ReadToken(); 1594 | if (Token.Type != ESupertalkTokenType::GroupEnd) 1595 | { 1596 | ParseTokenError(InCtx, Token, ESupertalkTokenType::GroupEnd); 1597 | return false; 1598 | } 1599 | } 1600 | 1601 | return PaValueExpression(InCtx, OutExpression); 1602 | } 1603 | 1604 | bool FSupertalkParser::PaValueExpression(FPaContext& InCtx, TObjectPtr& OutExpression) 1605 | { 1606 | TObjectPtr Value; 1607 | if (!PaValue(InCtx, Value)) 1608 | { 1609 | return false; 1610 | } 1611 | 1612 | USupertalkExpression_Value* Expr = NewObject(InCtx.Script); 1613 | Expr->Value = Value; 1614 | 1615 | OutExpression = Expr; 1616 | return true; 1617 | } 1618 | 1619 | bool FSupertalkParser::PaValue(FPaContext& InCtx, TObjectPtr& OutValue) 1620 | { 1621 | FToken Token = InCtx.Stream.PeekToken(); 1622 | switch (Token.Type) 1623 | { 1624 | default: 1625 | ParseTokenError(InCtx, Token, TEXT("Name, Text, Asset")); 1626 | return false; 1627 | 1628 | case ESupertalkTokenType::Name: 1629 | return PaVariableValue(InCtx, OutValue); 1630 | 1631 | case ESupertalkTokenType::LocalizationKey: 1632 | case ESupertalkTokenType::Text: 1633 | return PaTextValue(InCtx, OutValue); 1634 | 1635 | case ESupertalkTokenType::Asset: 1636 | return PaAssetValue(InCtx, OutValue); 1637 | } 1638 | } 1639 | 1640 | bool FSupertalkParser::PaVariableValue(FPaContext& InCtx, TObjectPtr& OutValue) 1641 | { 1642 | FToken Token = InCtx.Stream.ReadToken(); 1643 | check(Token.Type == ESupertalkTokenType::Name); 1644 | 1645 | FName VarName = FName(Token.Content); 1646 | if (IsReservedName(VarName)) 1647 | { 1648 | if (VarName == NAME_None) 1649 | { 1650 | OutValue = nullptr; 1651 | return true; 1652 | } 1653 | 1654 | if (VarName == NAME_TRUE) 1655 | { 1656 | USupertalkBooleanValue* BoolValue = NewObject(InCtx.Script); 1657 | BoolValue->bValue = true; 1658 | OutValue = BoolValue; 1659 | return true; 1660 | } 1661 | 1662 | if (VarName == NAME_FALSE) 1663 | { 1664 | USupertalkBooleanValue* BoolValue = NewObject(InCtx.Script); 1665 | BoolValue->bValue = false; 1666 | OutValue = BoolValue; 1667 | return true; 1668 | } 1669 | 1670 | checkNoEntry(); 1671 | ParseTokenError(InCtx, Token, TEXT("Unknown (reserved name found but not implemented)")); 1672 | return false; 1673 | } 1674 | else if (InCtx.Stream.PeekToken().Type == ESupertalkTokenType::Member) 1675 | { 1676 | USupertalkMemberValue* Member = NewObject(InCtx.Script); 1677 | Member->Variable = VarName; 1678 | 1679 | while (!InCtx.Stream.IsEOF() && InCtx.Stream.PeekToken().Type == ESupertalkTokenType::Member) 1680 | { 1681 | Token = InCtx.Stream.ReadToken(); 1682 | check(Token.Type == ESupertalkTokenType::Member); 1683 | 1684 | Token = InCtx.Stream.ReadToken(); 1685 | if (Token.Type != ESupertalkTokenType::Name) 1686 | { 1687 | ParseTokenError(InCtx, Token, ESupertalkTokenType::Name); 1688 | return false; 1689 | } 1690 | 1691 | Member->Members.Add(FName(Token.Content)); 1692 | } 1693 | 1694 | OutValue = Member; 1695 | return true; 1696 | } 1697 | else 1698 | { 1699 | USupertalkVariableValue* Variable = NewObject(InCtx.Script); 1700 | Variable->Variable = VarName; 1701 | OutValue = Variable; 1702 | return true; 1703 | } 1704 | } 1705 | 1706 | bool FSupertalkParser::PaTextValue(FPaContext& InCtx, TObjectPtr& OutValue) 1707 | { 1708 | FToken Token = InCtx.Stream.ReadToken(); 1709 | FString Namespace = InCtx.DefaultNamespace; 1710 | FString Key; 1711 | if (Token.Type == ESupertalkTokenType::LocalizationKey) 1712 | { 1713 | if (!Token.Namespace.IsEmpty()) 1714 | { 1715 | Namespace = Token.Namespace; 1716 | } 1717 | 1718 | Key = Token.Content; 1719 | 1720 | Token = InCtx.Stream.ReadToken(); 1721 | } 1722 | 1723 | check(Token.Type == ESupertalkTokenType::Text); 1724 | 1725 | USupertalkTextValue* Text = NewObject(InCtx.Script); 1726 | Text->Text = FText::ChangeKey(FTextKey(Namespace), FTextKey(Key.IsEmpty() ? Token.GetGeneratedLocalizationKey() : Key), FText::FromString(Token.Content)); 1727 | OutValue = Text; 1728 | return true; 1729 | } 1730 | 1731 | bool FSupertalkParser::PaAssetValue(FPaContext& InCtx, TObjectPtr& OutValue) 1732 | { 1733 | FToken Token = InCtx.Stream.ReadToken(); 1734 | check(Token.Type == ESupertalkTokenType::Asset); 1735 | 1736 | UObject* Asset = LoadObject(nullptr, *Token.Content); 1737 | //UObject* Asset = FindObject(nullptr, *Token.Content, false); 1738 | if (Asset == nullptr) 1739 | { 1740 | ParseWarning(InCtx, Token, FString::Format(TEXT("Failed to find asset '{0}'"), { Token.Content })); 1741 | return true; 1742 | } 1743 | 1744 | USupertalkObjectValue* Value = NewObject(InCtx.Script); 1745 | Value->Object = Asset; 1746 | OutValue = Value; 1747 | return true; 1748 | } 1749 | 1750 | bool FSupertalkParser::PaAttributeList(FPaContext& InCtx, TArray& OutAttributes) 1751 | { 1752 | FToken Token = InCtx.Stream.ReadToken(); 1753 | check(Token.Type == ESupertalkTokenType::AttrStart); 1754 | 1755 | while (!InCtx.Stream.IsEOF()) 1756 | { 1757 | Token = InCtx.Stream.ReadToken(); 1758 | if (Token.Type != ESupertalkTokenType::Name) 1759 | { 1760 | ParseTokenError(InCtx, Token, ESupertalkTokenType::Name); 1761 | return false; 1762 | } 1763 | 1764 | FSupertalkAttribute Attr; 1765 | Attr.Name = FName(Token.Content); 1766 | 1767 | OutAttributes.Add(Attr); 1768 | 1769 | Token = InCtx.Stream.ReadToken(); 1770 | switch (Token.Type) 1771 | { 1772 | default: 1773 | InCtx.Stream.GoBack(1); 1774 | break; 1775 | 1776 | case ESupertalkTokenType::Separator: 1777 | break; 1778 | 1779 | case ESupertalkTokenType::AttrEnd: 1780 | goto complete; 1781 | } 1782 | } 1783 | 1784 | complete: 1785 | if (Token.Type != ESupertalkTokenType::AttrEnd) 1786 | { 1787 | ParseTokenError(InCtx, Token, ESupertalkTokenType::AttrEnd); 1788 | return false; 1789 | } 1790 | 1791 | return true; 1792 | } 1793 | 1794 | #undef STP_LOG 1795 | #undef LOCTEXT_NAMESPACE 1796 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkParser.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Supertalk/SupertalkExpression.h" 7 | #include "Supertalk/SupertalkLine.h" 8 | #include "SupertalkParser.generated.h" 9 | 10 | class USupertalkScript; 11 | class USupertalkValue; 12 | struct FSupertalkAction; 13 | 14 | UENUM() 15 | enum class ESupertalkTokenType : uint8 16 | { 17 | Unknown, 18 | Ignore, 19 | Eof, 20 | Name, 21 | Separator, 22 | Member, 23 | Assign, 24 | Text, 25 | Asset, 26 | Choice, 27 | Section, 28 | Comment, 29 | Jump, 30 | Command, 31 | ParallelStart, 32 | ParallelEnd, 33 | QueueStart, 34 | QueueEnd, 35 | StatementEnd, 36 | LocalizationKey, 37 | Directive, 38 | GroupStart, 39 | GroupEnd, 40 | If, 41 | Then, 42 | Else, 43 | Equal, 44 | NotEqual, 45 | Not, 46 | 47 | AttrStart = ParallelStart, 48 | AttrEnd = ParallelEnd 49 | }; 50 | DECLARE_ENUM_TO_STRING(ESupertalkTokenType); 51 | 52 | /** 53 | * A parser for Supertalk Script (*.sts) files 54 | */ 55 | class FSupertalkParser : public TSharedFromThis 56 | { 57 | public: 58 | static TSharedRef Create(FOutputDevice* Ar); 59 | static bool ParseIntoScript(FString File, FString Input, class USupertalkScript* Script, FOutputDevice* Ar); 60 | 61 | static bool IsReservedName(FName Input); 62 | 63 | public: 64 | enum class ETextParseMode : uint8 65 | { 66 | SingleLine, 67 | MultiLine, 68 | Quoted, 69 | }; 70 | 71 | struct FTokenContext 72 | { 73 | FString File; 74 | int32 Line = -1; 75 | int32 Col = -1; 76 | 77 | friend bool operator==(const FTokenContext& Lhs, const FTokenContext& Rhs) 78 | { 79 | return Lhs.File == Rhs.File && Lhs.Line == Rhs.Line && Lhs.Col == Rhs.Col; 80 | } 81 | 82 | FString ToString() const; 83 | }; 84 | 85 | struct FToken 86 | { 87 | FTokenContext Context; 88 | ESupertalkTokenType Type = ESupertalkTokenType::Unknown; 89 | FString Content; 90 | FString Source; 91 | FString Namespace; 92 | int32 Indentation; 93 | 94 | FString GetDisplayName() const; 95 | 96 | bool IsIgnorable() const; 97 | 98 | FString GetGeneratedLocalizationKey() const; 99 | 100 | static bool IsPotentialLoop(const FToken& Lhs, const FToken& Rhs) 101 | { 102 | // Used to test if the same token is being emitted over and over again. 103 | return Lhs.Context == Rhs.Context && Lhs.Type == Rhs.Type && Lhs.Content == Rhs.Content; 104 | } 105 | }; 106 | 107 | struct FLxStream 108 | { 109 | FLxStream() 110 | { 111 | CurrentLine = 0; 112 | CurrentChar = 0; 113 | } 114 | 115 | TArray Lines; 116 | int32 CurrentLine; 117 | int32 CurrentChar; 118 | 119 | FORCEINLINE bool IsEOF() const 120 | { 121 | return !Lines.IsValidIndex(CurrentLine); 122 | } 123 | 124 | TCHAR ReadChar(); 125 | FString ReadToEndOfLine(); 126 | FString ReadToEndOfFile(); 127 | FString ReadBetweenContexts(const FTokenContext& Left, const FTokenContext& Right); 128 | 129 | void GoBack(int32 Count); 130 | 131 | bool IsOnEmptyLine(); 132 | }; 133 | 134 | struct FLxContext 135 | { 136 | FString File; 137 | FLxStream Stream; 138 | int32 CurrentIndentation; 139 | 140 | // HACK: emitting a choice token results in some special behavior around consuming the rest of the line. 141 | uint32 bIsChoiceLine : 1; 142 | 143 | FLxContext() 144 | { 145 | bIsChoiceLine = false; 146 | } 147 | 148 | FORCEINLINE FTokenContext CreateContext() const 149 | { 150 | FTokenContext Ctx; 151 | Ctx.File = File; 152 | Ctx.Line = Stream.CurrentLine + 1; 153 | Ctx.Col = Stream.CurrentChar + 1; 154 | return Ctx; 155 | } 156 | }; 157 | 158 | struct FPaStream 159 | { 160 | FPaStream() 161 | { 162 | CurrentToken = 0; 163 | } 164 | 165 | TArray Tokens; 166 | int32 CurrentToken; 167 | 168 | FORCEINLINE bool IsEOF() const 169 | { 170 | return !Tokens.IsValidIndex(CurrentToken); 171 | } 172 | 173 | FToken ReadToken(); 174 | FToken PeekToken() const; 175 | void GoBack(int32 Count); 176 | }; 177 | 178 | struct FPaContext 179 | { 180 | FPaStream Stream; 181 | 182 | USupertalkScript* Script; 183 | 184 | FString DefaultNamespace; 185 | 186 | // Used for checking that all jumps are valid later. 187 | TArray> Jumps; 188 | }; 189 | 190 | public: 191 | ~FSupertalkParser(); 192 | 193 | bool Parse(const FString& File, const FString& Input, USupertalkScript* Script); 194 | bool TokenizeSyntax(const FString& File, const FString& Input, TArray& OutTokens); 195 | 196 | //void RunTest(); 197 | 198 | private: 199 | FSupertalkParser(); 200 | void Initialize(); 201 | 202 | // If bIgnoreErrors is true, attempts to ignore any problems with lexing. 203 | // If an error can't be ignored, the rest of the input will be appended as a final "unknown" token. 204 | bool RunLexer(const FString& File, const FString& Input, TArray& OutTokens, bool bIgnoreErrors = false); 205 | bool RunParser(USupertalkScript* Script, const TArray& InTokens); 206 | 207 | void ConsumeEmptyLines(FLxContext& InCtx); 208 | int32 ConsumeWhitespaceUpdateIndentation(FLxContext& InCtx); 209 | int32 ConsumeWhitespace(FLxContext& InCtx); 210 | 211 | bool LxToken(FLxContext& InCtx, FToken& OutToken); 212 | 213 | bool LxTokenName(FLxContext& InCtx, FToken& OutName, bool AllowWhitespace); 214 | bool LxTokenText(FLxContext& InCtx, FToken& OutText, ETextParseMode Mode); 215 | bool LxTokenAsset(FLxContext& InCtx, FToken& OutAsset); 216 | bool LxTokenComment(FLxContext& InCtx, FToken& OutComment); 217 | bool LxTokenDirective(FLxContext& InCtx, FToken& OutDirective); 218 | bool LxTokenLocalizationKey(FLxContext& InCtx, FToken& OutLocalizationKey); 219 | 220 | void ParseTokenError(const FPaContext& InCtx, const FToken& Token, ESupertalkTokenType ExpectedTokenType) const; 221 | void ParseTokenError(const FPaContext& InCtx, const FToken& Token, const FString& Expected) const; 222 | void ParseError(const FPaContext& InCtx, const FToken& Token, const FString& Message) const; 223 | void ParseError(const FPaContext& InCtx, const FString& Message) const; 224 | void ParseWarning(const FPaContext& InCtx, const FToken& Token, const FString& Message) const; 225 | void ParseWarning(const FPaContext& InCtx, const FString& Message) const; 226 | 227 | bool PaTrySectionName(FPaContext& InCtx, FName& OutName); 228 | bool PaSection(FPaContext& InCtx, FName Name); 229 | 230 | bool PaAction(FPaContext& InCtx, FSupertalkAction& OutAction); 231 | 232 | bool PaDirective(FPaContext& InCtx); 233 | 234 | bool PaAssign(FPaContext& InCtx, FSupertalkAction& OutAction); 235 | bool PaLine(FPaContext& InCtx, FSupertalkAction& OutAction); 236 | bool PaChoice(FPaContext& InCtx, FSupertalkAction& OutAction, struct FSupertalkLine& Line); 237 | bool PaCommand(FPaContext& InCtx, FSupertalkAction& OutAction); 238 | bool PaJump(FPaContext& InCtx, FSupertalkAction& OutAction); 239 | bool PaParallel(FPaContext& InCtx, FSupertalkAction& OutAction); 240 | bool PaQueue(FPaContext& InCtx, FSupertalkAction& OutAction); 241 | bool PaConditional(FPaContext& InCtx, FSupertalkAction& OutAction); 242 | 243 | bool PaExpression(FPaContext& InCtx, TObjectPtr& OutExpression); 244 | bool PaEqualityExpression(FPaContext& InCtx, TObjectPtr& OutExpression); 245 | bool PaUnaryExpression(FPaContext& InCtx, TObjectPtr& OutExpression); 246 | bool PaGroupExpression(FPaContext& InCtx, TObjectPtr& OutExpression); 247 | bool PaValueExpression(FPaContext& InCtx, TObjectPtr& OutExpression); 248 | 249 | bool PaValue(FPaContext& InCtx, TObjectPtr& OutValue); 250 | bool PaVariableValue(FPaContext& InCtx, TObjectPtr& OutValue); 251 | bool PaTextValue(FPaContext& InCtx, TObjectPtr& OutValue); 252 | bool PaAssetValue(FPaContext& InCtx, TObjectPtr& OutValue); 253 | 254 | bool PaAttributeList(FPaContext& InCtx, TArray& OutAttributes); 255 | 256 | FOutputDevice* Ar = nullptr; 257 | }; -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkRichTextSyntaxHighlighterTextLayoutMarshaller.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkRichTextSyntaxHighlighterTextLayoutMarshaller.h" 4 | #include "SupertalkEditorStyle.h" 5 | #include "SupertalkParser.h" 6 | #include "Framework/Text/ISlateRun.h" 7 | #include "Framework/Text/SlateTextRun.h" 8 | 9 | #define GET_TEXT_STYLE(Name) FSupertalkEditorStyle::Get().GetWidgetStyle(Name) 10 | FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::FSyntaxTextStyle::FSyntaxTextStyle() 11 | : NormalTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Normal")) 12 | , CommentTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Comment")) 13 | , KeywordTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Keyword")) 14 | , OperatorTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Operator")) 15 | , ValueTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Value")) 16 | , StringTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.String")) 17 | , CommandTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Command")) 18 | , SectionTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Section")) 19 | , JumpTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Jump")) 20 | , ErrorTextStyle(GET_TEXT_STYLE("SyntaxHighlight.STS.Error")) 21 | { 22 | } 23 | #undef GET_TEXT_STYLE 24 | 25 | TSharedRef FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::Create(TSharedRef InParser, const FSyntaxTextStyle& InSyntaxTextStyle) 26 | { 27 | return MakeShareable(new FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller(InParser, InSyntaxTextStyle)); 28 | } 29 | 30 | FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::~FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller() 31 | { 32 | } 33 | 34 | void FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::SetText(const FString& SourceString, FTextLayout& TargetTextLayout) 35 | { 36 | ParseTokens(SourceString, TargetTextLayout); 37 | } 38 | 39 | bool FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::RequiresLiveUpdate() const 40 | { 41 | return true; 42 | } 43 | 44 | FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller(TSharedRef InParser, const FSyntaxTextStyle& InSyntaxTextStyle) 45 | : SyntaxTextStyle(InSyntaxTextStyle), Parser(InParser) 46 | { 47 | } 48 | 49 | void FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller::ParseTokens(const FString& SourceString, FTextLayout& TargetTextLayout) 50 | { 51 | // This entire function is *beyond* hacky, as the supertalk parser tokenizes things into a different format than what FTextLayout works with. 52 | // It might be worth refactoring FSupertalkParser to use ranges or something at some point, but for now this is "good enough". 53 | 54 | TArray Tokens; 55 | Parser->TokenizeSyntax(FString("Input"), SourceString, Tokens); 56 | 57 | TArray LinesToAdd; 58 | TSharedRef ModelString = MakeShared(); 59 | TArray> Runs; 60 | for (const FSupertalkParser::FToken& Token : Tokens) 61 | { 62 | TArray Lines; 63 | Token.Source.ParseIntoArrayLines(Lines, false); 64 | 65 | for (int32 LineIdx = 0; LineIdx < Lines.Num(); ++LineIdx) 66 | { 67 | if (LineIdx != 0) 68 | { 69 | LinesToAdd.Emplace(ModelString, Runs); 70 | ModelString = MakeShared(); 71 | Runs = TArray>(); 72 | } 73 | 74 | FRunInfo RunInfo; 75 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Normal"); 76 | 77 | FTextBlockStyle TextBlockStyle = SyntaxTextStyle.NormalTextStyle; 78 | 79 | switch (Token.Type) 80 | { 81 | case ESupertalkTokenType::Comment: 82 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Comment"); 83 | TextBlockStyle = SyntaxTextStyle.CommentTextStyle; 84 | break; 85 | 86 | case ESupertalkTokenType::If: 87 | case ESupertalkTokenType::Then: 88 | case ESupertalkTokenType::Else: 89 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Keyword"); 90 | TextBlockStyle = SyntaxTextStyle.KeywordTextStyle; 91 | break; 92 | 93 | 94 | case ESupertalkTokenType::Directive: 95 | if (Token.Content.StartsWith("error", ESearchCase::IgnoreCase)) 96 | { 97 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Error"); 98 | TextBlockStyle = SyntaxTextStyle.ErrorTextStyle; 99 | } 100 | else 101 | { 102 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Keyword"); 103 | TextBlockStyle = SyntaxTextStyle.KeywordTextStyle; 104 | } 105 | 106 | break; 107 | 108 | case ESupertalkTokenType::Name: 109 | if (FSupertalkParser::IsReservedName(FName(Token.Content))) 110 | { 111 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Keyword"); 112 | TextBlockStyle = SyntaxTextStyle.KeywordTextStyle; 113 | } 114 | 115 | break; 116 | 117 | case ESupertalkTokenType::Separator: 118 | case ESupertalkTokenType::Assign: 119 | case ESupertalkTokenType::ParallelStart: 120 | case ESupertalkTokenType::ParallelEnd: 121 | case ESupertalkTokenType::QueueStart: 122 | case ESupertalkTokenType::QueueEnd: 123 | case ESupertalkTokenType::StatementEnd: 124 | case ESupertalkTokenType::GroupStart: 125 | case ESupertalkTokenType::GroupEnd: 126 | case ESupertalkTokenType::Equal: 127 | case ESupertalkTokenType::NotEqual: 128 | case ESupertalkTokenType::Not: 129 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Operator"); 130 | TextBlockStyle = SyntaxTextStyle.OperatorTextStyle; 131 | break; 132 | 133 | case ESupertalkTokenType::Asset: 134 | case ESupertalkTokenType::LocalizationKey: 135 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Value"); 136 | TextBlockStyle = SyntaxTextStyle.ValueTextStyle; 137 | break; 138 | 139 | case ESupertalkTokenType::Text: 140 | RunInfo.Name = TEXT("SyntaxHighlight.STS.String"); 141 | TextBlockStyle = SyntaxTextStyle.StringTextStyle; 142 | break; 143 | 144 | case ESupertalkTokenType::Command: 145 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Command"); 146 | TextBlockStyle = SyntaxTextStyle.CommandTextStyle; 147 | break; 148 | 149 | case ESupertalkTokenType::Section: 150 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Section"); 151 | TextBlockStyle = SyntaxTextStyle.SectionTextStyle; 152 | break; 153 | 154 | case ESupertalkTokenType::Jump: 155 | RunInfo.Name = TEXT("SyntaxHighlight.STS.Jump"); 156 | TextBlockStyle = SyntaxTextStyle.JumpTextStyle; 157 | break; 158 | } 159 | 160 | const FString& Line = Lines[LineIdx]; 161 | const FTextRange ModelRange(ModelString->Len(), ModelString->Len() + Line.Len()); 162 | ModelString->Append(*Line); 163 | 164 | TSharedRef Run = FSlateTextRun::Create(RunInfo, ModelString, TextBlockStyle, ModelRange); 165 | Runs.Add(Run); 166 | } 167 | } 168 | 169 | if (!ModelString->IsEmpty()) 170 | { 171 | LinesToAdd.Emplace(MoveTemp(ModelString), MoveTemp(Runs)); 172 | } 173 | 174 | TargetTextLayout.AddLines(LinesToAdd); 175 | } 176 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkRichTextSyntaxHighlighterTextLayoutMarshaller.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Framework/Text/PlainTextLayoutMarshaller.h" 7 | 8 | class FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller : public FPlainTextLayoutMarshaller 9 | { 10 | public: 11 | struct FSyntaxTextStyle 12 | { 13 | FSyntaxTextStyle(); 14 | FSyntaxTextStyle( 15 | const FTextBlockStyle& InNormalTextStyle, 16 | const FTextBlockStyle& InCommentTextStyle, 17 | const FTextBlockStyle& InKeywordTextStyle, 18 | const FTextBlockStyle& InOperatorTextStyle, 19 | const FTextBlockStyle& InValueTextStyle, 20 | const FTextBlockStyle& InStringTextStyle, 21 | const FTextBlockStyle& InCommandTextStyle, 22 | const FTextBlockStyle& InSectionTextStyle, 23 | const FTextBlockStyle& InJumpTextStyle, 24 | const FTextBlockStyle& InErrorTextStyle) 25 | : NormalTextStyle(InNormalTextStyle) 26 | , CommentTextStyle(InCommentTextStyle) 27 | , KeywordTextStyle(InKeywordTextStyle) 28 | , OperatorTextStyle(InOperatorTextStyle) 29 | , ValueTextStyle(InValueTextStyle) 30 | , StringTextStyle(InStringTextStyle) 31 | , CommandTextStyle(InCommandTextStyle) 32 | , SectionTextStyle(InSectionTextStyle) 33 | , JumpTextStyle(InJumpTextStyle) 34 | , ErrorTextStyle(InErrorTextStyle) 35 | { 36 | } 37 | 38 | FTextBlockStyle NormalTextStyle; 39 | FTextBlockStyle CommentTextStyle; 40 | FTextBlockStyle KeywordTextStyle; 41 | FTextBlockStyle OperatorTextStyle; 42 | FTextBlockStyle ValueTextStyle; 43 | FTextBlockStyle StringTextStyle; 44 | FTextBlockStyle CommandTextStyle; 45 | FTextBlockStyle SectionTextStyle; 46 | FTextBlockStyle JumpTextStyle; 47 | FTextBlockStyle ErrorTextStyle; 48 | }; 49 | 50 | static TSharedRef Create(TSharedRef InParser, const FSyntaxTextStyle& InSyntaxTextStyle); 51 | 52 | virtual ~FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller(); 53 | 54 | virtual void SetText(const FString& SourceString, FTextLayout& TargetTextLayout) override; 55 | 56 | virtual bool RequiresLiveUpdate() const override; 57 | 58 | protected: 59 | FSupertalkRichTextSyntaxHighlighterTextLayoutMarshaller(TSharedRef InParser, const FSyntaxTextStyle& InSyntaxTextStyle); 60 | 61 | virtual void ParseTokens(const FString& SourceString, FTextLayout& TargetTextLayout); 62 | 63 | FSyntaxTextStyle SyntaxTextStyle; 64 | TSharedRef Parser; 65 | }; 66 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkScriptAssetFactory.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkScriptAssetFactory.h" 4 | #include "IMessageLogListing.h" 5 | #include "MessageLogModule.h" 6 | #include "SupertalkEditorSettings.h" 7 | #include "SupertalkParser.h" 8 | #include "SupertalkScriptEditorToolkit.h" 9 | #include "EditorFramework/AssetImportData.h" 10 | #include "Supertalk/SupertalkPlayer.h" 11 | #include "Supertalk/Supertalk.h" 12 | 13 | #define LOCTEXT_NAMESPACE "SupertalkScriptAssetFactory" 14 | 15 | USupertalkScriptAssetFactory::USupertalkScriptAssetFactory() 16 | { 17 | SupportedClass = USupertalkScript::StaticClass(); 18 | bEditAfterNew = true; 19 | bEditorImport = true; 20 | bText = true; 21 | 22 | bCreateNew = GetDefault()->bEnableScriptEditor; 23 | 24 | Formats.Add(TEXT("sts;Supertalk Script")); 25 | } 26 | 27 | FText USupertalkScriptAssetFactory::GetDisplayName() const 28 | { 29 | return LOCTEXT("FactoryDescription", "Supertalk Script"); 30 | } 31 | 32 | bool USupertalkScriptAssetFactory::FactoryCanImport(const FString& Filename) 33 | { 34 | const FString Extension = FPaths::GetExtension(Filename); 35 | 36 | return Extension == TEXT("sts"); 37 | } 38 | 39 | UObject* USupertalkScriptAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) 40 | { 41 | return NewObject(InParent, SupportedClass, InName, Flags | RF_Transactional); 42 | } 43 | 44 | UObject* USupertalkScriptAssetFactory::FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, 45 | const TCHAR* BufferEnd, FFeedbackContext* Warn) 46 | { 47 | GEditor->GetEditorSubsystem()->BroadcastAssetPreImport(this, InClass, InParent, InName, Type); 48 | 49 | const FString FileContent(BufferEnd - Buffer, Buffer); 50 | 51 | USupertalkScript* Script = NewObject(InParent, InName, Flags | RF_Transactional); 52 | 53 | FBufferedOutputDevice Output; 54 | bool bResult = FSupertalkParser::ParseIntoScript(GetCurrentFilename(), FileContent, Script, IsRunningCommandlet() ? static_cast(GLog) : &Output); 55 | 56 | if (!IsRunningCommandlet()) 57 | { 58 | FModuleManager::GetModuleChecked("MessageLog") 59 | .GetLogListing(SupertalkMessageLogName)->NewPage( 60 | FText::Format( 61 | LOCTEXT("MessageLogPageName", "Compile of {0} at {1}"), 62 | FText::FromString(GetCurrentFilename()), 63 | FText::AsDateTime(FDateTime::Now()))); 64 | 65 | FMessageLog MessageLog(SupertalkMessageLogName); 66 | TArray Lines; 67 | Output.GetContents(Lines); 68 | for (const FBufferedLine& Line : Lines) 69 | { 70 | FText Message = FText::FromString(Line.Data); 71 | switch (Line.Verbosity) 72 | { 73 | default: 74 | MessageLog.Info(Message); 75 | break; 76 | 77 | case ELogVerbosity::Fatal: 78 | case ELogVerbosity::Error: 79 | MessageLog.Error(Message); 80 | break; 81 | 82 | case ELogVerbosity::Warning: 83 | MessageLog.Warning(Message); 84 | break; 85 | } 86 | } 87 | 88 | MessageLog.Notify(LOCTEXT("SupertalkCompilerErrorsReported", "Errors were reported by the Supertalk compiler")); 89 | } 90 | 91 | if (!bResult) 92 | { 93 | return nullptr; 94 | } 95 | 96 | Script->SourceData = FileContent; 97 | Script->bCanCompileFromSource = true; 98 | Script->AssetImportData->Update(GetCurrentFilename()); 99 | 100 | GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, Script); 101 | 102 | return Script; 103 | } 104 | 105 | bool USupertalkScriptAssetFactory::CanReimport(UObject* Obj, TArray& OutFilenames) 106 | { 107 | if (USupertalkScript* Script = Cast(Obj)) 108 | { 109 | if (Script && Script->AssetImportData) 110 | { 111 | Script->AssetImportData->ExtractFilenames(OutFilenames); 112 | return true; 113 | } 114 | } 115 | 116 | return false; 117 | } 118 | 119 | void USupertalkScriptAssetFactory::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) 120 | { 121 | if (USupertalkScript* Script = Cast(Obj)) 122 | { 123 | if (ensure(NewReimportPaths.Num() == 1)) 124 | { 125 | Script->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]); 126 | } 127 | } 128 | } 129 | 130 | EReimportResult::Type USupertalkScriptAssetFactory::Reimport(UObject* Obj) 131 | { 132 | USupertalkScript* Script = Cast(Obj); 133 | if (!Script) 134 | { 135 | return EReimportResult::Failed; 136 | } 137 | 138 | const FString Filename = Script->AssetImportData->GetFirstFilename(); 139 | if (Filename.IsEmpty() || !FPaths::FileExists(*Filename)) 140 | { 141 | return EReimportResult::Failed; 142 | } 143 | 144 | bool OutCancelled = false; 145 | if (FactoryCreateFile(Script->GetClass(), Script->GetOuter(), Script->GetFName(), Script->GetFlags(), Filename, nullptr, GWarn, OutCancelled) != nullptr) 146 | { 147 | UE_LOG(LogSupertalk, Log, TEXT("Imported successfully")); 148 | 149 | Script->AssetImportData->Update(Filename); 150 | 151 | // Try to find the outer package so we can dirty it up 152 | if (Script->GetOuter()) 153 | { 154 | Script->GetOuter()->MarkPackageDirty(); 155 | } 156 | 157 | Script->MarkPackageDirty(); 158 | 159 | return EReimportResult::Succeeded; 160 | } 161 | else 162 | { 163 | if (OutCancelled) 164 | { 165 | UE_LOG(LogSupertalk, Warning, TEXT("-- import canceled")); 166 | return EReimportResult::Cancelled; 167 | } 168 | else 169 | { 170 | UE_LOG(LogSupertalk, Warning, TEXT("-- import failed")); 171 | return EReimportResult::Failed; 172 | } 173 | } 174 | } 175 | 176 | FText FAssetTypeActions_SupertalkScript::GetName() const 177 | { 178 | return LOCTEXT("SupertalkScriptAssetName", "Supertalk Script"); 179 | } 180 | 181 | UClass* FAssetTypeActions_SupertalkScript::GetSupportedClass() const 182 | { 183 | return USupertalkScript::StaticClass(); 184 | } 185 | 186 | FColor FAssetTypeActions_SupertalkScript::GetTypeColor() const 187 | { 188 | return FColor::Cyan; 189 | } 190 | 191 | uint32 FAssetTypeActions_SupertalkScript::GetCategories() 192 | { 193 | return EAssetTypeCategories::Misc; 194 | } 195 | 196 | bool FAssetTypeActions_SupertalkScript::IsImportedAsset() const 197 | { 198 | return true; 199 | } 200 | 201 | void FAssetTypeActions_SupertalkScript::GetResolvedSourceFilePaths(const TArray& TypeAssets, TArray& OutSourceFilePaths) const 202 | { 203 | for (const auto& Asset : TypeAssets) 204 | { 205 | const auto Script = CastChecked(Asset); 206 | if (Script->AssetImportData) 207 | { 208 | Script->AssetImportData->ExtractFilenames(OutSourceFilePaths); 209 | } 210 | } 211 | } 212 | 213 | void FAssetTypeActions_SupertalkScript::OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) 214 | { 215 | EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() 216 | ? EToolkitMode::WorldCentric 217 | : EToolkitMode::Standalone; 218 | 219 | for (UObject* Obj : InObjects) 220 | { 221 | USupertalkScript* ScriptAsset = Cast(Obj); 222 | if (ScriptAsset) 223 | { 224 | TSharedRef EditorToolkit = MakeShareable(new FSupertalkScriptEditorToolkit()); 225 | EditorToolkit->Initialize(ScriptAsset, Mode, EditWithinLevelEditor); 226 | } 227 | } 228 | } 229 | 230 | USupertalkScriptExporter::USupertalkScriptExporter() 231 | { 232 | SupportedClass = USupertalkScript::StaticClass(); 233 | bText = true; 234 | FormatExtension.Add(TEXT("sts")); 235 | FormatDescription.Add(TEXT("Supertalk Script")); 236 | PreferredFormatIndex = 0; 237 | } 238 | 239 | bool USupertalkScriptExporter::ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags) 240 | { 241 | USupertalkScript* Script = CastChecked(Object); 242 | Ar.Log(Script->SourceData); 243 | return true; 244 | } 245 | 246 | #undef LOCTEXT_NAMESPACE 247 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkScriptAssetFactory.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "AssetTypeActions_Base.h" 7 | #include "EditorReimportHandler.h" 8 | #include "Exporters/Exporter.h" 9 | #include "SupertalkScriptAssetFactory.generated.h" 10 | 11 | UCLASS(HideCategories=(Object)) 12 | class USupertalkScriptAssetFactory : public UFactory, public FReimportHandler 13 | { 14 | GENERATED_BODY() 15 | 16 | public: 17 | USupertalkScriptAssetFactory(); 18 | 19 | virtual FText GetDisplayName() const override; 20 | virtual bool FactoryCanImport(const FString& Filename) override; 21 | 22 | virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) override; 23 | virtual UObject* FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn) override; 24 | 25 | virtual bool CanReimport(UObject* Obj, TArray& OutFilenames) override; 26 | virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override; 27 | virtual EReimportResult::Type Reimport(UObject* Obj) override; 28 | }; 29 | 30 | class FAssetTypeActions_SupertalkScript : public FAssetTypeActions_Base 31 | { 32 | public: 33 | virtual FText GetName() const override; 34 | virtual UClass* GetSupportedClass() const override; 35 | virtual FColor GetTypeColor() const override; 36 | virtual uint32 GetCategories() override; 37 | virtual bool IsImportedAsset() const override; 38 | virtual void GetResolvedSourceFilePaths(const TArray& TypeAssets, TArray& OutSourceFilePaths) const override; 39 | virtual void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) override; 40 | }; 41 | 42 | UCLASS() 43 | class USupertalkScriptExporter : public UExporter 44 | { 45 | GENERATED_BODY() 46 | 47 | public: 48 | USupertalkScriptExporter(); 49 | 50 | virtual bool ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags) override; 51 | }; -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkScriptCompiler.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #include "SupertalkScriptCompiler.h" 4 | 5 | #include "SupertalkParser.h" 6 | #include "Supertalk/Supertalk.h" 7 | #include "Supertalk/SupertalkPlayer.h" 8 | 9 | FOnSupertalkScriptCompiled FSupertalkScriptCompiler::OnScriptCompiled; 10 | 11 | void FSupertalkScriptCompiler::Initialize() 12 | { 13 | USupertalkScript::OnScriptPreSave.BindStatic(&FSupertalkScriptCompiler::OnScriptPreSave); 14 | } 15 | 16 | void FSupertalkScriptCompiler::Shutdown() 17 | { 18 | USupertalkScript::OnScriptPreSave.Unbind(); 19 | } 20 | 21 | bool FSupertalkScriptCompiler::CompileScript(USupertalkScript* Script) 22 | { 23 | if (!IsValid(Script)) 24 | { 25 | UE_LOG(LogSupertalk, Error, TEXT("Cannot compile invalid script")); 26 | return false; 27 | } 28 | 29 | if (!Script->bCanCompileFromSource) 30 | { 31 | UE_LOG(LogSupertalk, Warning, TEXT("Script '%s' cannot be compiled - please reimport or recreate to enable compilation. This will not affect the ability to run the script."), *Script->GetName()); 32 | return false; 33 | } 34 | 35 | FBufferedOutputDevice Buffer; 36 | 37 | FOutputDeviceRedirector Output; 38 | Output.AddOutputDevice(GLog); 39 | Output.AddOutputDevice(&Buffer); 40 | 41 | bool bResult = FSupertalkParser::ParseIntoScript(Script->GetName(), Script->SourceData, Script, &Output); 42 | 43 | TArray CompilerOutput; 44 | Buffer.GetContents(CompilerOutput); 45 | 46 | if (bResult) 47 | { 48 | UE_LOG(LogSupertalk, Log, TEXT("Successfully compiled script '%s' (stats: %d sections)"), *Script->GetName(), Script->Sections.Num()); 49 | OnScriptCompiled.Broadcast(Script, true, CompilerOutput); 50 | } 51 | else 52 | { 53 | UE_LOG(LogSupertalk, Error, TEXT("Failed to compile script '%s', check log for details"), *Script->GetName()); 54 | OnScriptCompiled.Broadcast(Script, false, CompilerOutput); 55 | } 56 | 57 | return bResult; 58 | } 59 | 60 | void FSupertalkScriptCompiler::OnScriptPreSave(USupertalkScript* Script) 61 | { 62 | check(Script); 63 | CompileScript(Script); 64 | } 65 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkScriptCompiler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | // OnScriptCompiled(Script, bSuccess, CompilerOutput) 8 | DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnSupertalkScriptCompiled, class USupertalkScript*, bool, const TArray&); 9 | 10 | class FSupertalkScriptCompiler 11 | { 12 | public: 13 | static void Initialize(); 14 | static void Shutdown(); 15 | 16 | static bool CompileScript(class USupertalkScript* Script); 17 | 18 | static FOnSupertalkScriptCompiled OnScriptCompiled; 19 | 20 | private: 21 | FSupertalkScriptCompiler() {} 22 | 23 | static void OnScriptPreSave(class USupertalkScript* Script); 24 | }; -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkScriptEditorCommands.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | 4 | #include "SupertalkScriptEditorCommands.h" 5 | 6 | #define LOCTEXT_NAMESPACE "SupertalkScriptEditorCommands" 7 | 8 | void FSupertalkScriptEditorCommands::RegisterCommands() 9 | { 10 | UI_COMMAND(OpenSourceFile, "Open Source File", "Opens the source file in an external editor", EUserInterfaceActionType::Button, FInputChord()); 11 | UI_COMMAND(CompileScript, "Compile Script", "Compiles the supertalk script", EUserInterfaceActionType::Button, FInputChord()); 12 | } 13 | 14 | #undef LOCTEXT_NAMESPACE -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkScriptEditorCommands.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "EditorStyleSet.h" 7 | 8 | class FSupertalkScriptEditorCommands : public TCommands 9 | { 10 | public: 11 | FSupertalkScriptEditorCommands() 12 | : TCommands( 13 | TEXT("SupertalkScriptEditor"), 14 | NSLOCTEXT("Contexts", "SupertalkScriptEditor", "Supertalk Script Editor"), 15 | NAME_None, 16 | FAppStyle::GetAppStyleSetName()) 17 | { 18 | } 19 | 20 | virtual void RegisterCommands() override; 21 | 22 | public: 23 | TSharedPtr OpenSourceFile; 24 | TSharedPtr CompileScript; 25 | }; 26 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkScriptEditorToolkit.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | 4 | #include "SupertalkScriptEditorToolkit.h" 5 | #include "Supertalk/SupertalkPlayer.h" 6 | #include "EditorReimportHandler.h" 7 | #include "IMessageLogListing.h" 8 | #include "ISourceControlModule.h" 9 | #include "ISourceControlProvider.h" 10 | #include "MessageLogModule.h" 11 | #include "SourceControlOperations.h" 12 | #include "SSupertalkScriptAssetEditor.h" 13 | #include "SupertalkEditorSettings.h" 14 | #include "SupertalkScriptCompiler.h" 15 | #include "SupertalkScriptEditorCommands.h" 16 | #include "UnrealEdGlobals.h" 17 | #include "AutoReimport/AutoReimportManager.h" 18 | #include "Editor/UnrealEdEngine.h" 19 | #include "EditorFramework/AssetImportData.h" 20 | #include "Framework/Notifications/NotificationManager.h" 21 | #include "Misc/FileHelper.h" 22 | #include "Widgets/Notifications/SNotificationList.h" 23 | 24 | #define LOCTEXT_NAMESPACE "FSupertalkScriptEditorToolkit" 25 | 26 | static const FName SupertalkAssetEditorAppIdentifier("SupertalkAssetEditorApp"); 27 | static const FName SupertalkScriptEditorTabId("SupertalkScriptEditor"); 28 | static const FName SupertalkCompilerOutputTabId("SupertalkCompilerOutput"); 29 | 30 | FSupertalkScriptEditorToolkit::~FSupertalkScriptEditorToolkit() 31 | { 32 | FSupertalkScriptCompiler::OnScriptCompiled.RemoveAll(this); 33 | 34 | FReimportManager::Instance()->OnPreReimport().RemoveAll(this); 35 | FReimportManager::Instance()->OnPostReimport().RemoveAll(this); 36 | } 37 | 38 | void FSupertalkScriptEditorToolkit::Initialize(USupertalkScript* InScriptAsset, const EToolkitMode::Type InMode, const TSharedPtr& InToolkitHost) 39 | { 40 | ScriptAsset = InScriptAsset; 41 | 42 | { 43 | FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); 44 | FMessageLogInitializationOptions CompilerLogOptions; 45 | CompilerLogOptions.bAllowClear = true; 46 | CompilerLogOptions.bDiscardDuplicates = false; 47 | CompilerLogOptions.bShowFilters = false; 48 | CompilerLogOptions.bShowPages = false; 49 | CompilerLogOptions.bScrollToBottom = false; 50 | CompilerLogOptions.bShowInLogWindow = false; 51 | CompilerLogListing = MessageLogModule.CreateLogListing("SupertalkCompiler", CompilerLogOptions); 52 | } 53 | 54 | FSupertalkScriptEditorCommands::Register(); 55 | BindCommands(); 56 | 57 | const TSharedRef Layout = FTabManager::NewLayout("Standalone_SupertalkScriptAssetEditor_v3") 58 | ->AddArea 59 | ( 60 | FTabManager::NewPrimaryArea() 61 | ->SetOrientation(Orient_Vertical) 62 | ->Split 63 | ( 64 | FTabManager::NewStack() 65 | ->SetSizeCoefficient(0.8) 66 | ->AddTab(SupertalkScriptEditorTabId, ETabState::OpenedTab) 67 | ->SetHideTabWell(true) 68 | ) 69 | ->Split 70 | ( 71 | FTabManager::NewStack() 72 | ->SetSizeCoefficient(0.2f) 73 | ->AddTab(SupertalkCompilerOutputTabId, ETabState::OpenedTab) 74 | ) 75 | ); 76 | 77 | FAssetEditorToolkit::InitAssetEditor( 78 | InMode, 79 | InToolkitHost, 80 | SupertalkAssetEditorAppIdentifier, 81 | Layout, 82 | true, 83 | true, 84 | InScriptAsset); 85 | 86 | struct Local 87 | { 88 | static void FillToolbar(FToolBarBuilder& ToolbarBuilder) 89 | { 90 | ToolbarBuilder.BeginSection("Command"); 91 | { 92 | ToolbarBuilder.AddToolBarButton(FSupertalkScriptEditorCommands::Get().CompileScript); 93 | ToolbarBuilder.AddSeparator(); 94 | ToolbarBuilder.AddToolBarButton(FSupertalkScriptEditorCommands::Get().OpenSourceFile); 95 | } 96 | ToolbarBuilder.EndSection(); 97 | } 98 | }; 99 | 100 | TSharedPtr ToolbarExtender = MakeShareable(new FExtender); 101 | ToolbarExtender->AddToolBarExtension( 102 | "Asset", 103 | EExtensionHook::After, 104 | GetToolkitCommands(), 105 | FToolBarExtensionDelegate::CreateStatic(&Local::FillToolbar)); 106 | 107 | AddToolbarExtender(ToolbarExtender); 108 | 109 | RegenerateMenusAndToolbars(); 110 | 111 | FSupertalkScriptCompiler::OnScriptCompiled.AddRaw(this, &FSupertalkScriptEditorToolkit::OnScriptCompiled); 112 | } 113 | 114 | FString FSupertalkScriptEditorToolkit::GetDocumentationLink() const 115 | { 116 | return TEXT("https://github.com/redxdev/Supertalk"); 117 | } 118 | 119 | void FSupertalkScriptEditorToolkit::RegisterTabSpawners(const TSharedRef& InTabManager) 120 | { 121 | WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_SupertalkScriptAssetEditor", "Supertalk Script Asset Editor")); 122 | auto WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef(); 123 | 124 | FAssetEditorToolkit::RegisterTabSpawners(InTabManager); 125 | 126 | InTabManager->RegisterTabSpawner(SupertalkScriptEditorTabId, FOnSpawnTab::CreateSP(this, &FSupertalkScriptEditorToolkit::HandleTabManagerSpawnTab, SupertalkScriptEditorTabId)) 127 | .SetDisplayName(LOCTEXT("SupertalkScriptEditorTabName", "Supertalk Script Editor")) 128 | .SetGroup(WorkspaceMenuCategoryRef) 129 | .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Viewports")); 130 | 131 | InTabManager->RegisterTabSpawner(SupertalkCompilerOutputTabId, FOnSpawnTab::CreateSP(this, &FSupertalkScriptEditorToolkit::HandleTabManagerSpawnTab, SupertalkCompilerOutputTabId)) 132 | .SetDisplayName(LOCTEXT("SupertalkCompilerOutputTabName", "Compiler Output")) 133 | .SetGroup(WorkspaceMenuCategoryRef); 134 | } 135 | 136 | void FSupertalkScriptEditorToolkit::UnregisterTabSpawners(const TSharedRef& InTabManager) 137 | { 138 | FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); 139 | 140 | InTabManager->UnregisterTabSpawner(SupertalkScriptEditorTabId); 141 | InTabManager->UnregisterTabSpawner(SupertalkCompilerOutputTabId); 142 | } 143 | 144 | FText FSupertalkScriptEditorToolkit::GetBaseToolkitName() const 145 | { 146 | return LOCTEXT("AppLabel", "Supertalk Script Editor"); 147 | } 148 | 149 | FName FSupertalkScriptEditorToolkit::GetToolkitFName() const 150 | { 151 | return FName("SupertalkScriptAssetEditor"); 152 | } 153 | 154 | FLinearColor FSupertalkScriptEditorToolkit::GetWorldCentricTabColorScale() const 155 | { 156 | return FLinearColor(0.0f, 0.0f, 0.5f, 0.5f); 157 | } 158 | 159 | FString FSupertalkScriptEditorToolkit::GetWorldCentricTabPrefix() const 160 | { 161 | return LOCTEXT("WorldCentricTabPrefix", "SupertalkScriptAsset ").ToString(); 162 | } 163 | 164 | void FSupertalkScriptEditorToolkit::AddReferencedObjects(FReferenceCollector& Collector) 165 | { 166 | Collector.AddReferencedObject(ScriptAsset); 167 | } 168 | 169 | FString FSupertalkScriptEditorToolkit::GetReferencerName() const 170 | { 171 | return TEXT("FSupertalkScriptEditorToolkit"); 172 | } 173 | 174 | void FSupertalkScriptEditorToolkit::SaveAsset_Execute() 175 | { 176 | // TODO: only do this if we receive confirmation 177 | const USupertalkEditorSettings* Settings = GetDefault(); 178 | if (Settings->bEnableScriptEditor && Settings->bSaveSourceFilesInScriptEditor) 179 | { 180 | if (IsValid(ScriptAsset) && ScriptAsset->bCanCompileFromSource) 181 | { 182 | bool bIsNewFile = false; 183 | FString RelativeFilename; 184 | if (!IsValid(ScriptAsset->AssetImportData) || ScriptAsset->AssetImportData->SourceData.SourceFiles.IsEmpty()) 185 | { 186 | RelativeFilename = ScriptAsset->GetName(); 187 | RelativeFilename.RemoveFromStart("ST_"); 188 | RelativeFilename.Append(".sts"); 189 | bIsNewFile = true; 190 | } 191 | else 192 | { 193 | RelativeFilename = ScriptAsset->AssetImportData->SourceData.SourceFiles[0].RelativeFilename; 194 | } 195 | 196 | const FString PackagePath = FPackageName::GetLongPackagePath(ScriptAsset->GetPathName()) / TEXT(""); 197 | const FString AbsoluteSrcPath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(PackagePath)); 198 | const FString SrcFile = AbsoluteSrcPath / RelativeFilename; 199 | if (SaveToSourceFile(SrcFile)) 200 | { 201 | if (bIsNewFile) 202 | { 203 | if (!IsValid(ScriptAsset->AssetImportData)) 204 | { 205 | ScriptAsset->AssetImportData = NewObject(ScriptAsset, "AssetImportData"); 206 | } 207 | 208 | ScriptAsset->AssetImportData->SourceData.Insert({ RelativeFilename }); 209 | } 210 | 211 | FAssetImportInfo::FSourceFile& SourceFile = ScriptAsset->AssetImportData->SourceData.SourceFiles[0]; 212 | SourceFile.FileHash = FMD5Hash::HashFile(*SrcFile); 213 | SourceFile.Timestamp = IFileManager::Get().GetTimeStamp(*SrcFile); 214 | 215 | if (bIsNewFile) 216 | { 217 | GUnrealEd->AutoReimportManager->IgnoreNewFile(SrcFile); 218 | } 219 | else 220 | { 221 | GUnrealEd->AutoReimportManager->IgnoreFileModification(SrcFile); 222 | } 223 | } 224 | } 225 | } 226 | 227 | FAssetEditorToolkit::SaveAsset_Execute(); 228 | } 229 | 230 | bool FSupertalkScriptEditorToolkit::SaveToSourceFile(const FString& Path) 231 | { 232 | ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); 233 | ISourceControlProvider* SourceControlProvider = SourceControlModule.IsEnabled() ? &SourceControlModule.GetProvider() : nullptr; 234 | 235 | TArray FilesToCheckout { Path }; 236 | bool bNeedsMarkForAdd = false; 237 | 238 | if (SourceControlProvider) 239 | { 240 | if (FPaths::FileExists(Path)) 241 | { 242 | SourceControlProvider->Execute(ISourceControlOperation::Create(), FilesToCheckout); 243 | } 244 | else 245 | { 246 | bNeedsMarkForAdd = true; 247 | } 248 | } 249 | 250 | bool bResult = FFileHelper::SaveStringToFile(ScriptAsset->SourceData, *Path); 251 | 252 | if (SourceControlProvider && bNeedsMarkForAdd) 253 | { 254 | SourceControlProvider->Execute(ISourceControlOperation::Create(), FilesToCheckout); 255 | } 256 | 257 | return bResult; 258 | } 259 | 260 | void FSupertalkScriptEditorToolkit::BindCommands() 261 | { 262 | const FSupertalkScriptEditorCommands& Commands = FSupertalkScriptEditorCommands::Get(); 263 | const TSharedRef& UICommandList = GetToolkitCommands(); 264 | 265 | UICommandList->MapAction( 266 | Commands.OpenSourceFile, 267 | FExecuteAction::CreateUObject(ScriptAsset, &USupertalkScript::OpenSourceFileInExternalProgram), 268 | FCanExecuteAction::CreateWeakLambda(ScriptAsset, [Script=ScriptAsset]() 269 | { 270 | const FString Filename = Script->AssetImportData->GetFirstFilename(); 271 | return !Filename.IsEmpty() && FPaths::FileExists(*Filename); 272 | })); 273 | 274 | UICommandList->MapAction( 275 | Commands.CompileScript, 276 | FExecuteAction::CreateRaw(this, &FSupertalkScriptEditorToolkit::CompileScript), 277 | FCanExecuteAction::CreateWeakLambda(ScriptAsset, [Script=ScriptAsset]() 278 | { 279 | return GetDefault()->bEnableScriptEditor && Script->bCanCompileFromSource; 280 | })); 281 | } 282 | 283 | TSharedRef FSupertalkScriptEditorToolkit::HandleTabManagerSpawnTab(const FSpawnTabArgs& Args, FName TabIdentifier) 284 | { 285 | if (TabIdentifier == SupertalkScriptEditorTabId) 286 | { 287 | return SNew(SDockTab) 288 | .TabRole(ETabRole::PanelTab) 289 | [ 290 | SNew(SSupertalkScriptAssetEditor, ScriptAsset) 291 | .IsReadOnly(!GetDefault()->bEnableScriptEditor || !ScriptAsset->bCanCompileFromSource) 292 | ]; 293 | } 294 | else if (TabIdentifier == SupertalkCompilerOutputTabId) 295 | { 296 | FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); 297 | return SNew(SDockTab) 298 | .TabRole(ETabRole::NomadTab) 299 | [ 300 | MessageLogModule.CreateLogListingWidget(CompilerLogListing.ToSharedRef()) 301 | ]; 302 | } 303 | 304 | return SNew(SDockTab); 305 | } 306 | 307 | void FSupertalkScriptEditorToolkit::CompileScript() 308 | { 309 | if (GetDefault()->bEnableScriptEditor && IsValid(ScriptAsset)) 310 | { 311 | FSupertalkScriptCompiler::CompileScript(ScriptAsset); 312 | ScriptAsset->MarkPackageDirty(); 313 | } 314 | } 315 | 316 | void FSupertalkScriptEditorToolkit::OnScriptCompiled(USupertalkScript* InScript, bool bResult, const TArray& CompilerOutput) 317 | { 318 | if (ScriptAsset != InScript || !IsValid(InScript)) 319 | { 320 | return; 321 | } 322 | 323 | TArray> Messages; 324 | Messages.Reserve(CompilerOutput.Num()); 325 | for (const FBufferedLine& Line : CompilerOutput) 326 | { 327 | EMessageSeverity::Type MessageSeverity; 328 | switch (Line.Verbosity) 329 | { 330 | default: 331 | MessageSeverity = EMessageSeverity::Info; 332 | break; 333 | 334 | case ELogVerbosity::Fatal: 335 | case ELogVerbosity::Error: 336 | MessageSeverity = EMessageSeverity::Error; 337 | break; 338 | 339 | case ELogVerbosity::Warning: 340 | MessageSeverity = EMessageSeverity::Warning; 341 | break; 342 | } 343 | 344 | Messages.Add(FTokenizedMessage::Create(MessageSeverity, FText::FromString(Line.Data))); 345 | } 346 | 347 | CompilerLogListing->ClearMessages(); 348 | CompilerLogListing->AddMessages(Messages, false); 349 | } 350 | 351 | 352 | #undef LOCTEXT_NAMESPACE 353 | -------------------------------------------------------------------------------- /Source/SupertalkEditor/SupertalkScriptEditorToolkit.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) MissiveArts LLC 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | class FSupertalkScriptEditorToolkit : public FAssetEditorToolkit, public FGCObject 8 | { 9 | public: 10 | virtual ~FSupertalkScriptEditorToolkit(); 11 | 12 | void Initialize(class USupertalkScript* InScriptAsset, const EToolkitMode::Type InMode, const TSharedPtr& InToolkitHost); 13 | 14 | virtual FString GetDocumentationLink() const override; 15 | virtual void RegisterTabSpawners(const TSharedRef& InTabManager) override; 16 | virtual void UnregisterTabSpawners(const TSharedRef& InTabManager) override; 17 | 18 | virtual FText GetBaseToolkitName() const override; 19 | virtual FName GetToolkitFName() const override; 20 | virtual FLinearColor GetWorldCentricTabColorScale() const override; 21 | virtual FString GetWorldCentricTabPrefix() const override; 22 | 23 | virtual void AddReferencedObjects(FReferenceCollector& Collector) override; 24 | virtual FString GetReferencerName() const override; 25 | 26 | protected: 27 | virtual void SaveAsset_Execute() override; 28 | 29 | bool SaveToSourceFile(const FString& Path); 30 | 31 | private: 32 | void BindCommands(); 33 | 34 | TSharedRef HandleTabManagerSpawnTab(const FSpawnTabArgs& Args, FName TabIdentifier); 35 | 36 | void CompileScript(); 37 | void OnScriptCompiled(class USupertalkScript* InScript, bool bResult, const TArray& CompilerOutput); 38 | 39 | USupertalkScript* ScriptAsset = nullptr; 40 | 41 | TSharedPtr CompilerLogListing; 42 | }; -------------------------------------------------------------------------------- /Supertalk.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 7, 4 | "VersionName": "0.7.0", 5 | "FriendlyName": "Supertalk", 6 | "Description": "Supertalk script importer and runtime", 7 | "Category": "Other", 8 | "CreatedBy": "MissiveArts LLC", 9 | "CreatedByURL": "https://xbloom.io", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": false, 14 | "IsBetaVersion": true, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "Supertalk", 20 | "Type": "Runtime", 21 | "LoadingPhase": "Default", 22 | "AdditionalDependencies": [ 23 | "MessageLog" 24 | ] 25 | }, 26 | { 27 | "Name": "SupertalkEditor", 28 | "Type": "UncookedOnly", 29 | "LoadingPhase": "Default", 30 | "AdditionalDependencies": [ 31 | "UnrealEd", 32 | "AssetTools", 33 | "MessageLog", 34 | "Supertalk" 35 | ] 36 | } 37 | ] 38 | } --------------------------------------------------------------------------------