├── .gitignore ├── LICENSE ├── README.md └── addons └── godot-fsharp-tools ├── create_fsharp_script_dialog.gd ├── create_fsharp_script_dialog.tscn ├── fsharp_setup_dialog.gd ├── fsharp_setup_dialog.tscn ├── icons ├── icon_folder.svg └── icon_folder.svg.import ├── plugin.cfg └── plugin.gd /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Godot-specific ignores 3 | .import/ 4 | export.cfg 5 | export_presets.cfg 6 | 7 | # Mono-specific ignores 8 | .mono/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Will Nations 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 | # Godot F# Tools 2 | 3 | A Godot Engine plugin to simplify using F# through the C# Mono language. 4 | 5 | ## Features 6 | 7 | - Generating an F# project file and adding it to your Godot C# project/solution 8 | - via Tools menu shortcut. 9 | - Generating an F# script from a selected C# script. 10 | - via Tools menu shortcut. 11 | - Automatically Generating F# scripts from all C# scripts. 12 | - via configuration in ProjectSettings under Mono > F# Tools 13 | - Note: All F# classes must have the same name as their C# counterpart with an "Fs" on the end, e.g. `MyClass.cs -> MyClassFs.fs`. 14 | 15 | ## How to install 16 | 17 | 1. Download the .zip from GitHub or clone the repository. 18 | 2. Copy/paste the `addons` directory into your project or create a symlink between the `addons/godot-fsharp-tools` directory and a similar one in your project. 19 | 3. Open the ProjectSettings, go to the Plugins tab, find "Godot F# Tools" and switch it from "Inactive" to "Active" on the right-hand side. 20 | 4. Make sure that you've installed the Mono version of Godot and the [dotnet](https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x) command line tool of which this plugin makes heavy use. 21 | 22 | ## How to use 23 | 24 | These instructions assume that you... 25 | 26 | 1. Have already created a C# project/solution by first creating at least one C# script in your Godot project. 27 | 1. Have installed and activated the plugin. 28 | 29 | ### Generate F# Project 30 | 31 | 1. Go to `Project > Tools > Setup F# project...`. A dialog will open. 32 | 1. Fill in the necessary fields. Unnecessary fields will tell you what default value they become if left empty. 33 | 1. Once confirmed, the dialog will generate the F# project and connect it to your C# project/solution for you. This may take a short while. 34 | 35 | ### Generate single F# script from a C# script 36 | 37 | 1. Go to `Project > Tools > Generate F# script from C# script...`. A dialog will open. 38 | 1. Fill in the necessary fields. Unnecessary fields will tell you what default value they become if left empty. 39 | - The namespace must match that of the F# library project to which you plan to add it. 40 | 1. Once confirmed, the dialog will generate the F# script and update the C# script to inherit from your F# class and include its namespace. 41 | 1. You will need to add the new F# script file to your F# library project manually.\* 42 | 43 | ### Generate F# scripts from all created C# scripts 44 | 45 | 1. Go to `Project > ProjectSettings`. Go to the `General` tab. Scroll all the way to the bottom and find the `Mono > F# Tools` category. 46 | 1. Fill in information for all fields in this section. 47 | - The namespace must match that of the F# library project to which you plan to add it. 48 | - For better organization, we recommend using the F# library project directory for the output directory. 49 | 1. Create a C# script. The editor will generate a corresponding F# script in the output directory and update the C# script to inherit from your F# class and include its namespace. 50 | 1. You will need to add the new F# script file to your F# library project manually.\* 51 | 52 | --- 53 | 54 | \* The reason you must do this manually is because... 55 | 56 | 1. the `dotnet` tool from Microsoft does not support adding items to projects ("Really? Seriously? Professional stuff here guys"). 57 | 1. Godot's `XmlParser` class only allows you to read XML nodes, but not insert them into an .xml file ("Really? I mean, that could be useful guys..."). 58 | 1. If you want to write your own XML parsing code to inject the file reference into the `` tag hierarchy, it would be appreciated. 59 | 60 | For the uninformed, you add an existing item to an F# project in the following way: 61 | 62 | 1. Have Visual Studio installed with F# support. 63 | 1. Open the Godot .sln file in Visual Studio. 64 | 1. Right click on the F# library project in the Solution Explorer dock. 65 | 1. Go to `Add > Add Existing Item...`. 66 | 1. Choose the `Fs.fs` file you generated. Hit "OK". 67 | 68 | OR 69 | 70 | 1. Have Visual Studio Code installed with the `Ionide-fsharp` extension. 71 | 1. Open the Godot directory in your workspace. Ionide's F# solution tab should 72 | automatically detect and add the F# project. 73 | 1. In the F# tab on the left, you should see your project's .sln and under it the .csproj / .fsproj directories. 74 | 1. Right-click the F# project directory. Choose `Add file`. 75 | 1. Within the command pallete line edit, type out the name of the F# source file in that directory you want to add. Hit `Enter`. 76 | 77 | The F# source file is now added to the F# project! 78 | 79 | --- 80 | 81 | If you like the project, please give it a star and consider donating to my [Kofi](https://ko-fi.com/willnationsdev). If you have any problems whatsoever, do not hesitate to open an Issue. 82 | -------------------------------------------------------------------------------- /addons/godot-fsharp-tools/create_fsharp_script_dialog.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ConfirmationDialog 3 | 4 | onready var grid := $VBoxContainer/GridContainer 5 | onready var cs_path_edit: LineEdit = grid.get_node("CSPathEdit") 6 | onready var cs_path_button: ToolButton = grid.get_node("CSPathButton") 7 | onready var fs_proj_path_edit: LineEdit = grid.get_node("FSProjPathEdit") 8 | onready var fs_proj_path_button: ToolButton = grid.get_node("FSProjPathButton") 9 | # warning-ignore:unused_class_variable 10 | onready var namespace_edit: LineEdit = grid.get_node("NamespaceEdit") 11 | # warning-ignore:unused_class_variable 12 | onready var name_edit: LineEdit = grid.get_node("NameEdit") 13 | onready var final_path: Label = $VBoxContainer/FinalPathLabel 14 | onready var file_dialog_cs: FileDialog = $VBoxContainer/FileDialogCS 15 | onready var file_dialog_fs: FileDialog = $VBoxContainer/FileDialogLib 16 | 17 | var plugin: EditorPlugin = null 18 | var initialized := false 19 | 20 | func init(p_plugin: EditorPlugin) -> void: 21 | if not p_plugin: 22 | push_error(tr("%s/fsharp_setup_dialog.gd, line 9: Invalid EditorPlugin reference passed to FSharpSetupDialog. Please open an issue at https://github.com/willnationsdev/%s." % [plugin.PLUGIN_DIR, plugin.REPO_NAME])) 23 | return 24 | 25 | if initialized: 26 | return 27 | 28 | initialized = true 29 | 30 | plugin = p_plugin 31 | theme = plugin.get_editor_theme() 32 | 33 | # warning-ignore:return_value_discarded 34 | cs_path_button.connect("pressed", file_dialog_cs, "popup_centered_ratio", [0.75]) 35 | # warning-ignore:return_value_discarded 36 | fs_proj_path_button.connect("pressed", file_dialog_fs, "popup_centered_ratio", [0.75]) 37 | # warning-ignore:return_value_discarded 38 | cs_path_edit.connect("text_changed", self, "_reset_final_path") 39 | # warning-ignore:return_value_discarded 40 | fs_proj_path_edit.connect("text_changed", self, "_reset_final_path") 41 | # warning-ignore:return_value_discarded 42 | file_dialog_cs.connect("confirmed", self, "_on_cs_path_confirmed") 43 | # warning-ignore:return_value_discarded 44 | file_dialog_cs.connect("file_selected", self, "_on_cs_path_confirmed") 45 | # warning-ignore:return_value_discarded 46 | file_dialog_fs.connect("confirmed", self, "_on_fs_proj_path_confirmed") 47 | # warning-ignore:return_value_discarded 48 | file_dialog_fs.connect("file_selected", self, "_on_fs_proj_path_confirmed") 49 | # warning-ignore:return_value_discarded 50 | connect("confirmed", self, "_on_confirmed") 51 | 52 | func _on_confirmed() -> void: 53 | plugin.create_fsharp_script_from_csharp(get_final_path(), cs_path_edit.text, name_edit.text, namespace_edit.text) 54 | 55 | func _on_cs_path_confirmed(_p_path = "") -> void: 56 | cs_path_edit.text = file_dialog_cs.current_path 57 | _reset_final_path() 58 | 59 | func _on_fs_proj_path_confirmed(_p_path = "") -> void: 60 | fs_proj_path_edit.text = file_dialog_fs.current_path 61 | _reset_final_path() 62 | 63 | func _reset_final_path(_p_text = null) -> void: 64 | final_path.text = fs_proj_path_edit.text.get_base_dir().plus_file(cs_path_edit.text.get_file().get_basename() + "Fs.fs") 65 | 66 | func get_final_path() -> String: 67 | return final_path.text -------------------------------------------------------------------------------- /addons/godot-fsharp-tools/create_fsharp_script_dialog.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://addons/godot-fsharp-tools/create_fsharp_script_dialog.gd" type="Script" id=1] 4 | [ext_resource path="res://addons/godot-fsharp-tools/icons/icon_folder.svg" type="Texture" id=2] 5 | 6 | [node name="ConfirmationDialog" type="ConfirmationDialog"] 7 | visible = true 8 | margin_top = 1.0 9 | margin_right = 348.0 10 | margin_bottom = 175.0 11 | rect_min_size = Vector2( 500, 200 ) 12 | window_title = "Create an F# Script from C#" 13 | script = ExtResource( 1 ) 14 | 15 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 16 | anchor_right = 1.0 17 | anchor_bottom = 1.0 18 | margin_left = 8.0 19 | margin_top = 8.0 20 | margin_right = -8.0 21 | margin_bottom = -36.0 22 | size_flags_horizontal = 3 23 | size_flags_vertical = 3 24 | 25 | [node name="GridContainer" type="GridContainer" parent="VBoxContainer"] 26 | margin_right = 484.0 27 | margin_bottom = 108.0 28 | columns = 3 29 | 30 | [node name="CSPathLabel" type="Label" parent="VBoxContainer/GridContainer"] 31 | margin_top = 5.0 32 | margin_right = 101.0 33 | margin_bottom = 19.0 34 | text = "C# Script Path:" 35 | 36 | [node name="CSPathEdit" type="LineEdit" parent="VBoxContainer/GridContainer"] 37 | margin_left = 105.0 38 | margin_right = 452.0 39 | margin_bottom = 24.0 40 | size_flags_horizontal = 3 41 | placeholder_text = "res://MyClass.cs -> MyClassFs" 42 | 43 | [node name="CSPathButton" type="ToolButton" parent="VBoxContainer/GridContainer"] 44 | margin_left = 456.0 45 | margin_right = 484.0 46 | margin_bottom = 24.0 47 | icon = ExtResource( 2 ) 48 | 49 | [node name="FSProjPathLabel" type="Label" parent="VBoxContainer/GridContainer"] 50 | margin_top = 33.0 51 | margin_right = 101.0 52 | margin_bottom = 47.0 53 | text = "F# Project Path:" 54 | 55 | [node name="FSProjPathEdit" type="LineEdit" parent="VBoxContainer/GridContainer"] 56 | margin_left = 105.0 57 | margin_top = 28.0 58 | margin_right = 452.0 59 | margin_bottom = 52.0 60 | size_flags_horizontal = 3 61 | text = "res://" 62 | placeholder_text = "\"MyLib\" -> res;//MyLib.fsproj" 63 | 64 | [node name="FSProjPathButton" type="ToolButton" parent="VBoxContainer/GridContainer"] 65 | margin_left = 456.0 66 | margin_top = 28.0 67 | margin_right = 484.0 68 | margin_bottom = 52.0 69 | icon = ExtResource( 2 ) 70 | 71 | [node name="NameLabel" type="Label" parent="VBoxContainer/GridContainer"] 72 | margin_top = 61.0 73 | margin_right = 101.0 74 | margin_bottom = 75.0 75 | text = "F# Class Name:" 76 | 77 | [node name="NameEdit" type="LineEdit" parent="VBoxContainer/GridContainer"] 78 | margin_left = 105.0 79 | margin_top = 56.0 80 | margin_right = 452.0 81 | margin_bottom = 80.0 82 | size_flags_horizontal = 3 83 | placeholder_text = "Defaults to + \"Fs\"" 84 | 85 | [node name="Control" type="Control" parent="VBoxContainer/GridContainer"] 86 | margin_left = 456.0 87 | margin_top = 56.0 88 | margin_right = 484.0 89 | margin_bottom = 80.0 90 | 91 | [node name="NamespaceLabel" type="Label" parent="VBoxContainer/GridContainer"] 92 | margin_top = 89.0 93 | margin_right = 101.0 94 | margin_bottom = 103.0 95 | text = "Namespace:" 96 | 97 | [node name="NamespaceEdit" type="LineEdit" parent="VBoxContainer/GridContainer"] 98 | margin_left = 105.0 99 | margin_top = 84.0 100 | margin_right = 452.0 101 | margin_bottom = 108.0 102 | size_flags_horizontal = 3 103 | placeholder_text = "MyNamespace; Defaults to " 104 | 105 | [node name="Control2" type="Control" parent="VBoxContainer/GridContainer"] 106 | margin_left = 456.0 107 | margin_top = 84.0 108 | margin_right = 484.0 109 | margin_bottom = 108.0 110 | 111 | [node name="FileDialogCS" type="FileDialog" parent="VBoxContainer"] 112 | margin_left = 8.0 113 | margin_top = 8.0 114 | margin_right = 340.0 115 | margin_bottom = 138.0 116 | window_title = "Open a File" 117 | resizable = true 118 | mode = 0 119 | filters = PoolStringArray( "*.cs ; C# Script" ) 120 | show_hidden_files = true 121 | 122 | [node name="FileDialogLib" type="FileDialog" parent="VBoxContainer"] 123 | margin_left = 8.0 124 | margin_top = 8.0 125 | margin_right = 340.0 126 | margin_bottom = 138.0 127 | window_title = "Open a File" 128 | resizable = true 129 | mode = 0 130 | filters = PoolStringArray( "*.fsproj ; F# Project" ) 131 | show_hidden_files = true 132 | 133 | [node name="FinalPathLabel" type="Label" parent="VBoxContainer"] 134 | margin_top = 127.0 135 | margin_right = 484.0 136 | margin_bottom = 141.0 137 | size_flags_horizontal = 3 138 | size_flags_vertical = 6 139 | valign = 1 140 | -------------------------------------------------------------------------------- /addons/godot-fsharp-tools/fsharp_setup_dialog.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ConfirmationDialog 3 | 4 | onready var grid := $VBoxContainer/GridContainer 5 | onready var name_edit: LineEdit = grid.get_node("NameEdit") 6 | onready var path_edit: LineEdit = grid.get_node("PathEdit") 7 | onready var path_button: ToolButton = grid.get_node("PathButton") 8 | onready var create_dir_check: CheckBox = grid.get_node("CreateDirCheck") 9 | onready var final_path: Label = $VBoxContainer/FinalPathLabel 10 | onready var file_dialog: FileDialog = $VBoxContainer/FileDialog 11 | 12 | var plugin: EditorPlugin = null 13 | var initialized := false 14 | 15 | func init(p_plugin: EditorPlugin) -> void: 16 | if not p_plugin: 17 | push_error(tr("res://addons/godot-fsharp-tools/fsharp_setup_dialog.gd, line 9: Invalid EditorPlugin reference passed to FSharpSetupDialog. Please open an issue at https://github.com/willnationsdev/godot-fsharp-tools.")) 18 | return 19 | 20 | if initialized: 21 | return 22 | 23 | initialized = true 24 | 25 | plugin = p_plugin 26 | theme = plugin.get_editor_theme() 27 | 28 | # warning-ignore:return_value_discarded 29 | path_button.connect("pressed", file_dialog, "popup_centered_ratio", [0.75]) 30 | # warning-ignore:return_value_discarded 31 | create_dir_check.connect("toggled", self, "_reset_final_path") 32 | # warning-ignore:return_value_discarded 33 | name_edit.connect("text_changed", self, "_reset_final_path") 34 | # warning-ignore:return_value_discarded 35 | path_edit.connect("text_changed", self, "_reset_final_path") 36 | # warning-ignore:return_value_discarded 37 | connect("confirmed", self, "_on_confirmed") 38 | 39 | func _on_confirmed(): 40 | plugin.setup_fsharp_project(get_final_path()) 41 | 42 | func _reset_final_path(_p_text) -> void: 43 | final_path.text = path_edit.text.plus_file((name_edit.text + "/" if create_dir_check.pressed else "") + name_edit.text + ".fsproj") 44 | 45 | func get_final_path() -> String: 46 | return final_path.text -------------------------------------------------------------------------------- /addons/godot-fsharp-tools/fsharp_setup_dialog.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://addons/godot-fsharp-tools/fsharp_setup_dialog.gd" type="Script" id=1] 4 | [ext_resource path="res://addons/godot-fsharp-tools/icons/icon_folder.svg" type="Texture" id=2] 5 | 6 | [node name="ConfirmationDialog" type="ConfirmationDialog"] 7 | margin_top = 1.0 8 | margin_right = 348.0 9 | margin_bottom = 175.0 10 | rect_min_size = Vector2( 250, 125 ) 11 | window_title = "F# Project Setup" 12 | script = ExtResource( 1 ) 13 | 14 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 15 | anchor_right = 1.0 16 | anchor_bottom = 1.0 17 | margin_left = 8.0 18 | margin_top = 8.0 19 | margin_right = -8.0 20 | margin_bottom = -36.0 21 | size_flags_horizontal = 3 22 | size_flags_vertical = 3 23 | 24 | [node name="GridContainer" type="GridContainer" parent="VBoxContainer"] 25 | margin_right = 332.0 26 | margin_bottom = 80.0 27 | columns = 3 28 | 29 | [node name="NameLabel" type="Label" parent="VBoxContainer/GridContainer"] 30 | margin_top = 5.0 31 | margin_right = 117.0 32 | margin_bottom = 19.0 33 | text = "Library Name:" 34 | 35 | [node name="NameEdit" type="LineEdit" parent="VBoxContainer/GridContainer"] 36 | margin_left = 121.0 37 | margin_right = 300.0 38 | margin_bottom = 24.0 39 | size_flags_horizontal = 3 40 | placeholder_text = "\"MyLib\" -> MyLib.fsproj" 41 | 42 | [node name="Control" type="Control" parent="VBoxContainer/GridContainer"] 43 | margin_left = 304.0 44 | margin_right = 332.0 45 | margin_bottom = 24.0 46 | 47 | [node name="PathLabel" type="Label" parent="VBoxContainer/GridContainer"] 48 | margin_top = 33.0 49 | margin_right = 117.0 50 | margin_bottom = 47.0 51 | text = "Library Path:" 52 | 53 | [node name="PathEdit" type="LineEdit" parent="VBoxContainer/GridContainer"] 54 | margin_left = 121.0 55 | margin_top = 28.0 56 | margin_right = 300.0 57 | margin_bottom = 52.0 58 | size_flags_horizontal = 3 59 | text = "res://" 60 | placeholder_text = "\"MyLib\" -> res;//MyLib.fsproj" 61 | 62 | [node name="PathButton" type="ToolButton" parent="VBoxContainer/GridContainer"] 63 | margin_left = 304.0 64 | margin_top = 28.0 65 | margin_right = 332.0 66 | margin_bottom = 52.0 67 | icon = ExtResource( 2 ) 68 | 69 | [node name="CreateDirLabel" type="Label" parent="VBoxContainer/GridContainer"] 70 | margin_top = 61.0 71 | margin_right = 117.0 72 | margin_bottom = 75.0 73 | text = "Create Project Dir:" 74 | 75 | [node name="CreateDirCheck" type="CheckBox" parent="VBoxContainer/GridContainer"] 76 | margin_left = 121.0 77 | margin_top = 56.0 78 | margin_right = 300.0 79 | margin_bottom = 80.0 80 | pressed = true 81 | 82 | [node name="Control2" type="Control" parent="VBoxContainer/GridContainer"] 83 | margin_left = 304.0 84 | margin_top = 56.0 85 | margin_right = 332.0 86 | margin_bottom = 80.0 87 | 88 | [node name="FileDialog" type="FileDialog" parent="VBoxContainer"] 89 | margin_left = 8.0 90 | margin_top = 8.0 91 | margin_right = 340.0 92 | margin_bottom = 138.0 93 | window_title = "Open a Directory" 94 | resizable = true 95 | mode = 2 96 | show_hidden_files = true 97 | 98 | [node name="FinalPathLabel" type="Label" parent="VBoxContainer"] 99 | margin_top = 87.0 100 | margin_right = 332.0 101 | margin_bottom = 101.0 102 | size_flags_horizontal = 3 103 | size_flags_vertical = 6 104 | valign = 1 105 | 106 | [node name="Disclaimer" type="Label" parent="VBoxContainer"] 107 | margin_top = 112.0 108 | margin_right = 332.0 109 | margin_bottom = 126.0 110 | size_flags_horizontal = 3 111 | size_flags_vertical = 6 112 | text = "(This will take a while)" 113 | align = 1 114 | valign = 1 115 | -------------------------------------------------------------------------------- /addons/godot-fsharp-tools/icons/icon_folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/godot-fsharp-tools/icons/icon_folder.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon_folder.svg-88c0e41dec71aa577ae0b180e586abdc.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/godot-fsharp-tools/icons/icon_folder.svg" 13 | dest_files=[ "res://.import/icon_folder.svg-88c0e41dec71aa577ae0b180e586abdc.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/godot-fsharp-tools/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="FSharpTools" 4 | description="A set of tools to facilitate easier F# Mono development in Godot Engine." 5 | author="willnationsdev" 6 | version="0.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/godot-fsharp-tools/plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | ##### CLASSES ##### 5 | 6 | ##### SIGNALS ##### 7 | 8 | ##### CONSTANTS ##### 9 | 10 | const MENU_FSHARP_SETUP := "Setup F# project..." 11 | const MENU_FSHARP_GENERATE_SCRIPT := "Generate F# script from C# script..." 12 | const SETTINGS_FSHARP_AUTOGEN := "mono/f#_tools/auto_generate_f#_scripts" 13 | const SETTINGS_FSHARP_DEFAULT_OUTPUT_DIR := "mono/f#_tools/default_output_dir" 14 | const SETTINGS_FSHARP_DEFAULT_NAMESPACE := "mono/f#_tools/default_namespace" 15 | # The version of Mono C# that Godot Engine supports 16 | 17 | const REPO_NAME = "godot-fsharp-tools" 18 | const PLUGIN_DIR = "res://addons/" + REPO_NAME 19 | 20 | ##### PROPERTIES ##### 21 | 22 | # .NET 4.5 (net45) is the currently supported C# Mono version in Godot Engine. 23 | # GodotSharp.dll is a dependency required for an F# library to access Godot-related classes. 24 | # Library.fs is the default name given to the source file made for a classlib. 25 | var default_fsharp_project_text :=( 26 | """ 27 | 28 | 29 | net45 30 | 31 | 32 | 33 | 34 | %s 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | """ 44 | ) 45 | 46 | # 47 | var default_fsharp_file_text :=( 48 | """namespace %s 49 | 50 | open Godot 51 | 52 | type %s() = 53 | inherit %s() 54 | 55 | [] 56 | member val Text = \"Hello World!\" with get, set 57 | 58 | override this._Ready() = 59 | GD.Print(this.Text) 60 | """ 61 | ) 62 | 63 | var default_csharp_file_text :=( 64 | """using Godot; 65 | using System; 66 | 67 | using %s; 68 | 69 | public class %s : %s 70 | { 71 | } 72 | 73 | """ 74 | ) 75 | 76 | var setup_dialog_scn := preload("res://addons/godot-fsharp-tools/fsharp_setup_dialog.tscn") 77 | var create_fsharp_script_scn := preload("res://addons/godot-fsharp-tools/create_fsharp_script_dialog.tscn") 78 | 79 | var setup_dialog: ConfirmationDialog = null 80 | var create_fsharp_script_dialog: ConfirmationDialog = null 81 | 82 | ##### NOTIFICATIONS ##### 83 | 84 | func _enter_tree() -> void: 85 | _setup_fsharp_settings() 86 | _setup_create_fsharp_script_dialog() 87 | add_tool_menu_item(MENU_FSHARP_SETUP, self, "_show_setup_dialog") 88 | add_tool_menu_item(MENU_FSHARP_GENERATE_SCRIPT, create_fsharp_script_dialog, "popup_centered_minsize", Vector2.ZERO) 89 | 90 | var fs := get_editor_interface().get_resource_filesystem() 91 | # warning-ignore:return_value_discarded 92 | fs.connect("filesystem_changed", self, "_on_filesystem_changed") 93 | 94 | func _exit_tree() -> void: 95 | ProjectSettings.set_setting(SETTINGS_FSHARP_AUTOGEN, null) 96 | ProjectSettings.set_setting(SETTINGS_FSHARP_DEFAULT_NAMESPACE, null) 97 | ProjectSettings.set_setting(SETTINGS_FSHARP_DEFAULT_OUTPUT_DIR, null) 98 | remove_tool_menu_item(MENU_FSHARP_GENERATE_SCRIPT) 99 | remove_tool_menu_item(MENU_FSHARP_SETUP) 100 | 101 | ##### CONNECTIONS ##### 102 | 103 | func _show_setup_dialog(_p_ud) -> void: 104 | _setup_setup_dialog() 105 | setup_dialog.popup_centered_minsize() 106 | setup_dialog.name_edit.grab_focus() 107 | 108 | func _on_filesystem_changed() -> void: 109 | if true and "Prerequisites Check": 110 | var dir := Directory.new() 111 | if not (ProjectSettings.has_setting(SETTINGS_FSHARP_AUTOGEN) and 112 | ProjectSettings.has_setting(SETTINGS_FSHARP_DEFAULT_OUTPUT_DIR) and 113 | ProjectSettings.has_setting(SETTINGS_FSHARP_DEFAULT_NAMESPACE) and 114 | dir.dir_exists(ProjectSettings.get_setting(SETTINGS_FSHARP_DEFAULT_OUTPUT_DIR)) 115 | ): 116 | 117 | return 118 | 119 | var top_dir := "res://" 120 | var dirs: Array = [top_dir] 121 | var dir := Directory.new() 122 | var first := true 123 | var csdata := {} 124 | 125 | # generate 'csdata' map 126 | while not dirs.empty(): 127 | var dir_name = dirs.back() 128 | dirs.pop_back() 129 | 130 | if dir.open(dir_name) == OK: 131 | #warning-ignore:return_value_discarded 132 | dir.list_dir_begin() 133 | var file_name = dir.get_next() 134 | while file_name: 135 | if first and not dir_name == top_dir: 136 | first = false 137 | # Ignore hidden content 138 | if not file_name.begins_with("."): 139 | var a_path = dir.get_current_dir() + ("" if first else "/") + file_name 140 | var a_name = file_name.get_basename() 141 | 142 | # If a directory, then add to list of directories to visit 143 | if dir.current_is_dir(): 144 | dirs.push_back(dir.get_current_dir().plus_file(file_name)) 145 | # If a file, check if we already have a record for the same name. 146 | # Only use files with extensions 147 | elif not csdata.has(a_name) and file_name.ends_with(".cs"): 148 | csdata[a_name] = a_path 149 | 150 | # Move on to the next file in this directory 151 | file_name = dir.get_next() 152 | 153 | # We've exhausted all files in this directory. Close the iterator 154 | dir.list_dir_end() 155 | 156 | dirs = [top_dir] 157 | dir = Directory.new() 158 | first = true 159 | 160 | # Remove from 'csdata' entries that already have matching F# scripts 161 | while not dirs.empty(): 162 | var dir_name = dirs.back() as String 163 | dirs.pop_back() 164 | 165 | if dir.open(dir_name) == OK: 166 | #warning-ignore:return_value_discarded 167 | dir.list_dir_begin() 168 | var file_name := dir.get_next() 169 | while file_name: 170 | if first and not dir_name == top_dir: 171 | first = false 172 | # Ignore hidden content 173 | if not file_name.begins_with("."): 174 | 175 | # If a directory, then add to list of directories to visit 176 | if dir.current_is_dir(): 177 | dirs.push_back(dir.get_current_dir().plus_file(file_name)) 178 | # If a file, check if an F# script 179 | elif file_name.ends_with(".fs"): 180 | # If so, remove the F# script's C# class name equivalent from the list of C# scripts. 181 | var a_csname := file_name.get_basename() 182 | a_csname = a_csname.substr(0, len(a_csname) - 2) # remove "Fs" 183 | if csdata.has(a_csname): 184 | # warning-ignore:return_value_discarded 185 | csdata.erase(a_csname) 186 | 187 | # Move on to the next file in this directory 188 | file_name = dir.get_next() 189 | 190 | # We've exhausted all files in this directory. Close the iterator 191 | dir.list_dir_end() 192 | 193 | # scripts that need to be generated 194 | var f := File.new() 195 | var find_typenames := RegEx.new() 196 | # warning-ignore:return_value_discarded 197 | find_typenames.compile("public class (?P.+) : (?P.+)") 198 | for a_csname in csdata: 199 | var path = csdata[a_csname] 200 | var fsname = path.get_file().get_basename() + "Fs" 201 | var fspath = ProjectSettings.get_setting(SETTINGS_FSHARP_DEFAULT_OUTPUT_DIR) 202 | if not fspath: 203 | return 204 | fspath = fspath.plus_file(fsname + ".fs") 205 | if dir.file_exists(fspath): 206 | return 207 | var csharp_classname := "" 208 | var basename := "" 209 | if f.open(fspath, File.WRITE) == OK: 210 | var csf := File.new() 211 | if csf.open(path, File.READ) == OK: 212 | var match_ = find_typenames.search(csf.get_as_text()) 213 | if match_: 214 | csharp_classname = match_.strings[match_.names.classname] as String 215 | basename = match_.strings[match_.names.basename] as String 216 | csf.close() 217 | 218 | var namespace = ProjectSettings.get_setting(SETTINGS_FSHARP_DEFAULT_NAMESPACE) 219 | f.store_string(default_fsharp_file_text % [namespace, fsname, basename]) 220 | f.close() 221 | 222 | if csf.open(path, File.WRITE) == OK: 223 | csf.store_string(default_csharp_file_text % [namespace, csharp_classname, fsname]) 224 | csf.close() 225 | 226 | ##### PRIVATE METHODS ##### 227 | 228 | func _setup_setup_dialog() -> void: 229 | setup_dialog = setup_dialog_scn.instance() as ConfirmationDialog 230 | setup_dialog.call_deferred("init", self) 231 | setup_dialog.theme = get_editor_theme() 232 | add_child(setup_dialog) 233 | 234 | func _setup_create_fsharp_script_dialog() -> void: 235 | create_fsharp_script_dialog = create_fsharp_script_scn.instance() as ConfirmationDialog 236 | create_fsharp_script_dialog.call_deferred("init", self) 237 | create_fsharp_script_dialog.theme = get_editor_theme() 238 | add_child(create_fsharp_script_dialog) 239 | 240 | func _setup_fsharp_settings() -> void: 241 | if not ProjectSettings.has_setting(SETTINGS_FSHARP_AUTOGEN): 242 | ProjectSettings.set_setting(SETTINGS_FSHARP_AUTOGEN, false) 243 | 244 | if not ProjectSettings.has_setting(SETTINGS_FSHARP_DEFAULT_OUTPUT_DIR): 245 | ProjectSettings.set_setting(SETTINGS_FSHARP_DEFAULT_OUTPUT_DIR, "") 246 | 247 | if not ProjectSettings.has_setting(SETTINGS_FSHARP_DEFAULT_NAMESPACE): 248 | ProjectSettings.set_setting(SETTINGS_FSHARP_DEFAULT_NAMESPACE, "") 249 | 250 | func _print_and_clear_output(var p_output: Array) -> void: 251 | for line in p_output: 252 | print(line) 253 | p_output.clear() 254 | 255 | ##### PUBLIC METHODS ##### 256 | 257 | func get_editor_theme() -> Theme: 258 | return get_editor_interface().get_base_control().theme 259 | 260 | func setup_fsharp_project(p_fspath: String) -> void: 261 | var res_final_path := p_fspath 262 | var final_path := ProjectSettings.globalize_path(res_final_path) 263 | var output := [] 264 | 265 | var godot_sharp_path := "" 266 | if true: 267 | var a_path := res_final_path 268 | var start = true 269 | while a_path != "res://": 270 | if not start: 271 | godot_sharp_path += "../" 272 | a_path = a_path.get_base_dir() 273 | start = false 274 | godot_sharp_path += ".mono/assemblies/GodotSharp.dll" 275 | 276 | var root_path = "res://" + ProjectSettings.get_setting("application/config/name") 277 | var csharp_proj_path = ProjectSettings.globalize_path(root_path + ".csproj") 278 | var sln_path = ProjectSettings.globalize_path(root_path + ".sln") 279 | 280 | # Create F# class library and containing directory. 281 | var dir = Directory.new() 282 | var base_dir = final_path.get_base_dir() 283 | var proj_name = final_path.get_file().get_basename() 284 | if not dir.dir_exists(base_dir): 285 | dir.make_dir_recursive(base_dir) 286 | 287 | print("Running `dotnet new classlib -o %s -n %s -lang F#`" % [base_dir, proj_name]) 288 | # warning-ignore:return_value_discarded 289 | OS.execute("dotnet", PoolStringArray(["new", "classlib", "-o", base_dir, "-n", proj_name, "-lang", "F#"]), true, output) 290 | _print_and_clear_output(output) 291 | 292 | # Update F# project settings by rewriting entire file (trust me, it's easier this way) 293 | var fsproj = File.new() 294 | if fsproj.open(final_path, File.WRITE) != OK: 295 | push_error("fsharp_tools/plugin.gd::setup_fsharp_project(): Failed to open F# project file at '%s'." % final_path) 296 | return 297 | 298 | var text = default_fsharp_project_text % godot_sharp_path 299 | fsproj.store_string(text) 300 | fsproj.close() 301 | 302 | # Add the F# library to the solution. 303 | print("Running `dotnet sln %s add %s`" % [sln_path, final_path]) 304 | # warning-ignore:return_value_discarded 305 | OS.execute("dotnet", PoolStringArray(["sln", sln_path, "add", final_path]), true, output) 306 | _print_and_clear_output(output) 307 | 308 | # Add the System.Runtime dependency to the F# library. 309 | print("Running `dotnet add %s package System.Runtime`" % final_path) 310 | # warning-ignore:return_value_discarded 311 | OS.execute("dotnet", PoolStringArray(["add", final_path, "package", "System.Runtime"]), true, output) 312 | _print_and_clear_output(output) 313 | 314 | # Register the F# library to the C# project. 315 | print("Running `dotnet add %s reference %s`" % [csharp_proj_path, final_path]) 316 | # warning-ignore:return_value_discarded 317 | OS.execute("dotnet", PoolStringArray(["add", csharp_proj_path, "reference", final_path]), true) 318 | _print_and_clear_output(output) 319 | 320 | func create_fsharp_script_from_csharp(p_fspath: String, p_cspath: String, p_fsclass: String, p_namespace: String) -> void: 321 | var classname = p_fspath.get_file().get_basename() if not p_fsclass else p_fsclass 322 | 323 | var basename := "" 324 | if true and "Extract C# class name and base type from C# script.": 325 | var regex := RegEx.new() 326 | # warning-ignore:return_value_discarded 327 | regex.compile("public class (?P.+) : (?P.+)") 328 | var f := File.new() 329 | if f.open(p_cspath, File.READ) == OK: 330 | var match_ = regex.search(f.get_as_text()) 331 | if match_: 332 | basename = match_.strings[match_.names.basename] as String 333 | f.close() 334 | 335 | var namespace = p_namespace 336 | if not namespace: 337 | var list := p_fspath.get_base_dir().split("/", false) 338 | namespace = list[list.size() - 1] 339 | 340 | if true and "Create F# script.": 341 | var text = default_fsharp_file_text % [namespace, classname, basename] 342 | var f := File.new() 343 | if f.open(p_fspath, File.WRITE) == OK: 344 | f.store_string(text) 345 | f.close() 346 | 347 | if true and "Update inheritance of C# script.": 348 | var replace_inheritance := RegEx.new() 349 | # warning-ignore:return_value_discarded 350 | replace_inheritance.compile(" : .+\\b") 351 | 352 | var include_namespace := RegEx.new() 353 | # warning-ignore:return_value_discarded 354 | include_namespace.compile("using System;") 355 | var f := File.new() 356 | if f.open(p_cspath, File.READ_WRITE) == OK: 357 | var text = f.get_as_text() 358 | text = replace_inheritance.sub(text, " : %s" % classname) 359 | text = include_namespace.sub(text, ( 360 | """using System; 361 | 362 | using %s;""" 363 | ) % namespace) 364 | f.seek(0) 365 | f.store_string(text) 366 | 367 | 368 | f.close() 369 | --------------------------------------------------------------------------------