├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── auto_release.yaml │ ├── publish.yaml │ ├── spellcheck.yaml │ └── tests.yaml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── Chickensoft.GoDotNet.Tests ├── Chickensoft.GoDotNet.Tests.csproj ├── Chickensoft.GoDotNet.Tests.sln ├── badges │ ├── .gdignore │ ├── branch_coverage.svg │ └── line_coverage.svg ├── coverage.sh ├── coverage │ └── .gdignore ├── icon.svg ├── icon.svg.import ├── nuget.config ├── project.godot └── test │ ├── Tests.cs │ ├── Tests.tscn │ └── src │ ├── AutoloadCacheTest.cs │ ├── extensions │ ├── NodeExtensionsTest.cs │ └── StringExtensionsTest.cs │ ├── nodes │ └── SchedulerTest.cs │ ├── state │ ├── MachineTest.cs │ └── NotifierTest.cs │ └── utils │ └── RngTest.cs ├── Chickensoft.GoDotNet.sln ├── Chickensoft.GoDotNet ├── Chickensoft.GoDotNet.csproj ├── icon.png └── src │ ├── AutoloadCache.cs │ ├── extensions │ ├── NodeExtensions.cs │ └── StringExtensions.cs │ ├── nodes │ └── Scheduler.cs │ ├── state │ ├── Machine.cs │ └── Notifier.cs │ └── utils │ └── Rng.cs ├── LICENSE ├── README.md ├── cspell.json ├── docs ├── renovatebot_pr.png └── spelling_fix.png ├── global.json └── renovate.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------------- # 2 | # Chickensoft C# Style — .editorconfig # 3 | # --------------------------------------------------------------------------- # 4 | # Godot-friendly, K&R coding style with a bit of Dart-style flair thrown in. # 5 | # --------------------------------------------------------------------------- # 6 | # # 7 | # # 8 | # ╓╗_▄╗_╓▄_ # 9 | # ▄▄╟▓▓▓▓▓▓▓▓ # 10 | # ╙▓▓▓▀▀╠╠╦╦╓,_ # 11 | # ,φ╠╠╠╠╠╠╠╠╠╠▒╥ # 12 | # φ╠╠╠╠╠╠╠╠╠╠╠╠╠╠╦ # 13 | # @╠╠╫▌╠╟▌╠╠╠╠╠╠╠╠╠ # 14 | # ╠╠╠▄▄▄▒╠╠╠╠╠╠╠╠╠╠b # 15 | # ╠╠╨███▌╠╠╠╠╠╠╠▒╠╠▒_ ç╓ # 16 | # ╠╠╠╠▒▒╠╠╠╠╠╠╠╠▒Å▄╠╬▒φ╩ε # 17 | # ╚╠╠╠╠╠╠╠╠╠╠╠▒█▒╫█Å╠╠╩ # 18 | # ╠╠╠╠╠╠╠╠╠╠╠╠╠╟╫▒╠╠╩ # 19 | # ╙╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╜ # 20 | # ╙╚╠╠╠╠╠╠╠╠╩╙ # 21 | # ╒ µ # 22 | # ▌ ▓ # 23 | # ^▀▀ "▀ª # 24 | # # 25 | # # 26 | # --------------------------------------------------------------------------- # 27 | # 28 | # Based on: 29 | # - https://github.com/RehanSaeed/EditorConfig/blob/main/.editorconfig 30 | # - https://gist.github.com/FaronBracy/155d8d7ad98b4ceeb526b9f47543db1b 31 | # - various other gists floating around :) 32 | # 33 | # Have a problem? Encounter an issue? 34 | # Come visit our Discord and let us know! https://discord.gg/MjA6HUzzAE 35 | # 36 | # Based on https://github.com/RehanSaeed/EditorConfig/blob/main/.editorconfig 37 | # and https://gist.github.com/FaronBracy/155d8d7ad98b4ceeb526b9f47543db1b 38 | 39 | # This file is the top-most EditorConfig file 40 | root = true 41 | 42 | # All Files 43 | [*] 44 | charset = utf-8 45 | indent_style = space 46 | indent_size = 2 47 | insert_final_newline = true 48 | trim_trailing_whitespace = true 49 | 50 | ########################################## 51 | # File Extension Settings 52 | ########################################## 53 | 54 | # Visual Studio Solution Files 55 | [*.sln] 56 | indent_style = tab 57 | 58 | # Visual Studio XML Project Files 59 | [*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}] 60 | indent_size = 2 61 | 62 | # XML Configuration Files 63 | [*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] 64 | indent_size = 2 65 | 66 | # JSON Files 67 | [*.{json,json5,webmanifest}] 68 | indent_size = 2 69 | 70 | # YAML Files 71 | [*.{yml,yaml}] 72 | indent_size = 2 73 | 74 | # Markdown Files 75 | [*.{md,mdx}] 76 | trim_trailing_whitespace = false 77 | 78 | # Web Files 79 | [*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}] 80 | indent_size = 2 81 | 82 | # Batch Files 83 | [*.{cmd,bat}] 84 | end_of_line = crlf 85 | 86 | # Bash Files 87 | [*.sh] 88 | end_of_line = lf 89 | 90 | # Makefiles 91 | [Makefile] 92 | indent_style = tab 93 | 94 | [*_Generated.cs, *.g.cs, *.generated.cs] 95 | # Ignore a lack of documentation for generated code. Doesn't apply to builds, 96 | # just to viewing generation output. 97 | dotnet_diagnostic.CS1591.severity = none 98 | 99 | ########################################## 100 | # Default .NET Code Style Severities 101 | ########################################## 102 | 103 | [*.{cs,csx,cake,vb,vbx}] 104 | # Default Severity for all .NET Code Style rules below 105 | dotnet_analyzer_diagnostic.severity = warning 106 | 107 | ########################################## 108 | # Language Rules 109 | ########################################## 110 | 111 | # .NET Style Rules 112 | [*.{cs,csx,cake,vb,vbx}] 113 | 114 | # "this." and "Me." qualifiers 115 | dotnet_style_qualification_for_field = false 116 | dotnet_style_qualification_for_property = false 117 | dotnet_style_qualification_for_method = false 118 | dotnet_style_qualification_for_event = false 119 | 120 | # Language keywords instead of framework type names for type references 121 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 122 | dotnet_style_predefined_type_for_member_access = true:warning 123 | 124 | # Modifier preferences 125 | dotnet_style_require_accessibility_modifiers = always:warning 126 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning 127 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning 128 | dotnet_style_readonly_field = true:warning 129 | 130 | # Parentheses preferences 131 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning 132 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning 133 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning 134 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning 135 | 136 | # Expression-level preferences 137 | dotnet_style_object_initializer = true:warning 138 | dotnet_style_collection_initializer = true:warning 139 | dotnet_style_explicit_tuple_names = true:warning 140 | dotnet_style_prefer_inferred_tuple_names = true:warning 141 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning 142 | dotnet_style_prefer_auto_properties = true:warning 143 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 144 | dotnet_diagnostic.IDE0045.severity = suggestion 145 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 146 | dotnet_diagnostic.IDE0046.severity = suggestion 147 | dotnet_style_prefer_compound_assignment = true:warning 148 | dotnet_style_prefer_simplified_interpolation = true:warning 149 | dotnet_style_prefer_simplified_boolean_expressions = true:warning 150 | 151 | # Null-checking preferences 152 | dotnet_style_coalesce_expression = true:warning 153 | dotnet_style_null_propagation = true:warning 154 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning 155 | 156 | # File header preferences 157 | # Keep operators at end of line when wrapping. 158 | dotnet_style_operator_placement_when_wrapping = end_of_line:warning 159 | csharp_style_prefer_null_check_over_type_check = true:warning 160 | 161 | # Code block preferences 162 | csharp_prefer_braces = true:warning 163 | csharp_prefer_simple_using_statement = true:suggestion 164 | dotnet_diagnostic.IDE0063.severity = suggestion 165 | 166 | # C# Style Rules 167 | [*.{cs,csx,cake}] 168 | # 'var' preferences 169 | csharp_style_var_for_built_in_types = true:warning 170 | csharp_style_var_when_type_is_apparent = true:warning 171 | csharp_style_var_elsewhere = true:warning 172 | # Expression-bodied members 173 | csharp_style_expression_bodied_methods = true:warning 174 | csharp_style_expression_bodied_constructors = false:warning 175 | csharp_style_expression_bodied_operators = true:warning 176 | csharp_style_expression_bodied_properties = true:warning 177 | csharp_style_expression_bodied_indexers = true:warning 178 | csharp_style_expression_bodied_accessors = true:warning 179 | csharp_style_expression_bodied_lambdas = true:warning 180 | csharp_style_expression_bodied_local_functions = true:warning 181 | # Pattern matching preferences 182 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 183 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 184 | csharp_style_prefer_switch_expression = true:warning 185 | csharp_style_prefer_pattern_matching = true:warning 186 | csharp_style_prefer_not_pattern = true:warning 187 | # Expression-level preferences 188 | csharp_style_inlined_variable_declaration = true:warning 189 | csharp_prefer_simple_default_expression = true:warning 190 | csharp_style_pattern_local_over_anonymous_function = true:warning 191 | csharp_style_deconstructed_variable_declaration = true:warning 192 | csharp_style_prefer_index_operator = true:warning 193 | csharp_style_prefer_range_operator = true:warning 194 | csharp_style_implicit_object_creation_when_type_is_apparent = true:warning 195 | # "Null" checking preferences 196 | csharp_style_throw_expression = true:warning 197 | csharp_style_conditional_delegate_call = true:warning 198 | # Code block preferences 199 | csharp_prefer_braces = true:warning 200 | csharp_prefer_simple_using_statement = true:suggestion 201 | dotnet_diagnostic.IDE0063.severity = suggestion 202 | # 'using' directive preferences 203 | csharp_using_directive_placement = inside_namespace:warning 204 | # Modifier preferences 205 | # Don't suggest making public methods static. Very annoying. 206 | csharp_prefer_static_local_function = false 207 | # Only suggest making private methods static (if they don't use instance data). 208 | dotnet_code_quality.CA1822.api_surface = private 209 | 210 | ########################################## 211 | # Unnecessary Code Rules 212 | ########################################## 213 | 214 | # .NET Unnecessary code rules 215 | [*.{cs,csx,cake,vb,vbx}] 216 | 217 | dotnet_code_quality_unused_parameters = non_public:suggestion 218 | dotnet_remove_unnecessary_suppression_exclusions = none 219 | dotnet_diagnostic.IDE0079.severity = warning 220 | 221 | # C# Unnecessary code rules 222 | [*.{cs,csx,cake}] 223 | 224 | # Chickensoft Unused Code Additions 225 | # 226 | # Unfortunately for VSCode users, disabling these rules prevents you from 227 | # detecting unused code. Enabling them will trigger the roslyn analyzers' 228 | # automatic code fixes to remove unused code, which is very annoying when 229 | # you're actively coding or using reflection. 230 | # 231 | # I have not found a way to disable automatic fixes while keeping 232 | # warnings/suggestions/etc in the editor. If you find a way, please file an 233 | # issue or open a PR. 234 | 235 | # Don't remove method parameters that are unused. 236 | dotnet_diagnostic.IDE0060.severity = none 237 | dotnet_diagnostic.RCS1163.severity = none 238 | 239 | # Don't remove methods that are unused. 240 | dotnet_diagnostic.IDE0051.severity = none 241 | dotnet_diagnostic.RCS1213.severity = none 242 | 243 | # Use discard variable for unused expression values. 244 | csharp_style_unused_value_expression_statement_preference = discard_variable 245 | 246 | # .NET formatting rules 247 | [*.{cs,csx,cake,vb,vbx}] 248 | 249 | # Organize using directives 250 | dotnet_sort_system_directives_first = true 251 | dotnet_separate_import_directive_groups = false 252 | 253 | # Dotnet namespace options 254 | # 255 | # We don't care about namespaces matching folder structure. Games and apps 256 | # are complicated and you are free to organize them however you like. Change 257 | # this if you want to enforce it. 258 | dotnet_style_namespace_match_folder = false 259 | dotnet_diagnostic.IDE0130.severity = none 260 | 261 | # C# formatting rules 262 | [*.{cs,csx,cake}] 263 | 264 | # Newline options 265 | csharp_new_line_before_open_brace = none 266 | csharp_new_line_before_else = true 267 | csharp_new_line_before_catch = true 268 | csharp_new_line_before_finally = true 269 | csharp_new_line_before_members_in_object_initializers = true 270 | csharp_new_line_before_members_in_anonymous_types = true 271 | csharp_new_line_between_query_expression_clauses = true 272 | 273 | # Indentation options 274 | csharp_indent_switch_labels = true 275 | csharp_indent_case_contents = true 276 | csharp_indent_case_contents_when_block = true 277 | csharp_indent_labels = no_change 278 | csharp_indent_block_contents = true 279 | csharp_indent_braces = false 280 | 281 | # Spacing options 282 | csharp_space_after_cast = false 283 | csharp_space_after_keywords_in_control_flow_statements = true 284 | csharp_space_between_parentheses = false 285 | csharp_space_before_colon_in_inheritance_clause = true 286 | csharp_space_after_colon_in_inheritance_clause = true 287 | csharp_space_around_binary_operators = before_and_after 288 | csharp_space_between_method_declaration_parameter_list_parentheses = false 289 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 290 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 291 | csharp_space_between_method_call_parameter_list_parentheses = false 292 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 293 | csharp_space_between_method_call_name_and_opening_parenthesis = false 294 | csharp_space_after_comma = true 295 | csharp_space_before_comma = false 296 | csharp_space_after_dot = false 297 | csharp_space_before_dot = false 298 | csharp_space_after_semicolon_in_for_statement = true 299 | csharp_space_before_semicolon_in_for_statement = false 300 | csharp_space_around_declaration_statements = false 301 | csharp_space_before_open_square_brackets = false 302 | csharp_space_between_empty_square_brackets = false 303 | csharp_space_between_square_brackets = false 304 | 305 | # Wrap options 306 | csharp_preserve_single_line_statements = false 307 | csharp_preserve_single_line_blocks = true 308 | 309 | # Namespace options 310 | csharp_style_namespace_declarations = file_scoped:warning 311 | 312 | ########################################## 313 | # .NET Naming Rules 314 | ########################################## 315 | [*.{cs,csx,cake,vb,vbx}] 316 | 317 | ########################################## 318 | # Chickensoft Naming Conventions & Styles 319 | # These deviate heavily from Microsoft's Offical Naming Conventions. 320 | ########################################## 321 | 322 | # Allow underscores in names. 323 | dotnet_diagnostic.CA1707.severity = none 324 | 325 | # Styles 326 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 327 | 328 | dotnet_naming_style.upper_case_style.capitalization = all_upper 329 | dotnet_naming_style.upper_case_style.word_separator = _ 330 | 331 | dotnet_naming_style.camel_case_style.capitalization = camel_case 332 | 333 | dotnet_naming_style.camel_case_underscore_style.required_prefix = _ 334 | dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case 335 | 336 | # Use uppercase for all constant fields. 337 | dotnet_naming_rule.constants_uppercase.severity = suggestion 338 | dotnet_naming_rule.constants_uppercase.symbols = constant_fields 339 | dotnet_naming_rule.constants_uppercase.style = upper_case_style 340 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 341 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 342 | dotnet_naming_symbols.constant_fields.required_modifiers = const 343 | 344 | # Non-public fields should be _camelCase 345 | dotnet_naming_rule.non_public_fields_under_camel.severity = suggestion 346 | dotnet_naming_rule.non_public_fields_under_camel.symbols = non_public_fields 347 | dotnet_naming_rule.non_public_fields_under_camel.style = camel_case_underscore_style 348 | dotnet_naming_symbols.non_public_fields.applicable_kinds = field 349 | dotnet_naming_symbols.non_public_fields.required_modifiers = 350 | dotnet_naming_symbols.non_public_fields.applicable_accessibilities = private,private_protected,protected,internal,protected,protected_internal 351 | 352 | # Public fields should be PascalCase 353 | dotnet_naming_rule.public_fields_pascal.severity = suggestion 354 | dotnet_naming_rule.public_fields_pascal.symbols = public_fields 355 | dotnet_naming_rule.public_fields_pascal.style = pascal_case_style 356 | dotnet_naming_symbols.public_fields.applicable_kinds = field 357 | dotnet_naming_symbols.public_fields.required_modifiers = 358 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public 359 | 360 | # Async methods should have "Async" suffix. 361 | # Disabled because it makes tests too verbose. 362 | # dotnet_naming_style.end_in_async.required_suffix = Async 363 | # dotnet_naming_style.end_in_async.capitalization = pascal_case 364 | # dotnet_naming_rule.methods_end_in_async.symbols = methods_async 365 | # dotnet_naming_rule.methods_end_in_async.style = end_in_async 366 | # dotnet_naming_rule.methods_end_in_async.severity = warning 367 | # dotnet_naming_symbols.methods_async.applicable_kinds = method 368 | # dotnet_naming_symbols.methods_async.required_modifiers = async 369 | # dotnet_naming_symbols.methods_async.applicable_accessibilities = * 370 | 371 | ########################################## 372 | # Other Naming Rules 373 | ########################################## 374 | 375 | # All of the following must be PascalCase: 376 | dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property 377 | dotnet_naming_rule.element_rule.symbols = element_group 378 | dotnet_naming_rule.element_rule.style = pascal_case_style 379 | dotnet_naming_rule.element_rule.severity = warning 380 | 381 | # Interfaces use PascalCase and are prefixed with uppercase 'I' 382 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 383 | dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case 384 | dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I 385 | dotnet_naming_symbols.interface_group.applicable_kinds = interface 386 | dotnet_naming_rule.interface_rule.symbols = interface_group 387 | dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style 388 | dotnet_naming_rule.interface_rule.severity = warning 389 | 390 | # Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' 391 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 392 | dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case 393 | dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T 394 | dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter 395 | dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group 396 | dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style 397 | dotnet_naming_rule.type_parameter_rule.severity = warning 398 | 399 | # Function parameters use camelCase 400 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters 401 | dotnet_naming_symbols.parameters_group.applicable_kinds = parameter 402 | dotnet_naming_rule.parameters_rule.symbols = parameters_group 403 | dotnet_naming_rule.parameters_rule.style = camel_case_style 404 | dotnet_naming_rule.parameters_rule.severity = warning 405 | 406 | # Anything not specified uses camel case. 407 | dotnet_naming_rule.unspecified_naming.severity = warning 408 | dotnet_naming_rule.unspecified_naming.symbols = unspecified 409 | dotnet_naming_rule.unspecified_naming.style = camel_case_style 410 | dotnet_naming_symbols.unspecified.applicable_kinds = * 411 | dotnet_naming_symbols.unspecified.applicable_accessibilities = * 412 | 413 | ########################################## 414 | # Chickensoft Rule Overrides 415 | ########################################## 416 | 417 | # Allow using keywords as names 418 | # dotnet_diagnostic.CA1716.severity = none 419 | # Don't require culture info for ToString() 420 | dotnet_diagnostic.CA1304.severity = none 421 | # Don't require a string comparison for comparing strings. 422 | dotnet_diagnostic.CA1310.severity = none 423 | # Don't require a string format specifier. 424 | dotnet_diagnostic.CA1305.severity = none 425 | # Allow protected fields. 426 | dotnet_diagnostic.CA1051.severity = none 427 | # Don't warn about checking values that are supposedly never null. Sometimes 428 | # they are actually null. 429 | dotnet_diagnostic.CS8073.severity = none 430 | # Don't remove seemingly "unnecessary" assignments, as they often have 431 | # intended side-effects. 432 | dotnet_diagnostic.IDE0059.severity = none 433 | # Switch/case should always have a default clause. Tell that to Roslynator. 434 | dotnet_diagnostic.RCS1070.severity = none 435 | # Tell roslynator not to eat unused parameters. 436 | dotnet_diagnostic.RCS1163.severity = none 437 | # Tell dotnet not to remove unused parameters. 438 | dotnet_diagnostic.IDE0060.severity = none 439 | # Tell roslynator not to remove `partial` modifiers. 440 | dotnet_diagnostic.RCS1043.severity = none 441 | # Tell roslynator not to make classes static so aggressively. 442 | dotnet_diagnostic.RCS1102.severity = none 443 | # Roslynator wants to make properties readonly all the time, so stop it. 444 | # The developer knows best when it comes to contract definitions with Godot. 445 | dotnet_diagnostic.RCS1170.severity = none 446 | # Allow expression values to go unused, even without discard variable. 447 | # Otherwise, using Moq would be way too verbose. 448 | dotnet_diagnostic.IDE0058.severity = none 449 | # Don't let roslynator turn every local variable into a const. 450 | # If we did, we'd have to specify the types of local variables far more often, 451 | # and this style prefers type inference. 452 | dotnet_diagnostic.RCS1118.severity = none 453 | # Enums don't need to declare explicit values. Everyone knows they start at 0. 454 | dotnet_diagnostic.RCS1161.severity = none 455 | # Allow unconstrained type parameter to be checked for null. 456 | dotnet_diagnostic.RCS1165.severity = none 457 | # Allow keyword-based names so that parameter names like `@event` can be used. 458 | dotnet_diagnostic.CA1716.severity = none 459 | # Allow exceptions without the standard exception constructors. 460 | dotnet_diagnostic.RCS1194.severity = none 461 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.png filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.github/workflows/auto_release.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will run whenever tests finish running. If tests pass, it will 2 | # look at the last commit message to see if it contains the phrase 3 | # "chore(deps): update all dependencies". 4 | # 5 | # If it finds a commit with that phrase, and the testing workflow has passed, 6 | # it will automatically release a new version of the project by running the 7 | # publish workflow. 8 | # 9 | # The commit message phrase above is always used by renovatebot when opening 10 | # PR's to update dependencies. If you have renovatebot enabled and set to 11 | # automatically merge in dependency updates, this can automatically release and 12 | # publish the updated version of the project. 13 | # 14 | # You can disable this action by setting the DISABLE_AUTO_RELEASE repository 15 | # variable to true. 16 | 17 | name: 🦾 Auto-Release 18 | on: 19 | workflow_run: 20 | workflows: ["🚥 Tests"] 21 | branches: 22 | - main 23 | types: 24 | - completed 25 | 26 | jobs: 27 | auto_release: 28 | name: 🦾 Auto-Release 29 | runs-on: ubuntu-latest 30 | outputs: 31 | should_release: ${{ steps.release.outputs.should_release }} 32 | steps: 33 | - name: 🧾 Checkout 34 | uses: actions/checkout@v3 35 | 36 | - name: 🧑‍🔬 Check Test Results 37 | id: tests 38 | run: | 39 | echo "passed=${{ github.event.workflow_run.conclusion == 'success' }}" >> "$GITHUB_OUTPUT" 40 | 41 | - name: 📄 Check If Dependencies Changed 42 | id: deps 43 | run: | 44 | message=$(git log -1 --pretty=%B) 45 | 46 | if [[ $message == *"chore(deps): update all dependencies"* ]]; then 47 | echo "changed=true" >> "$GITHUB_OUTPUT" 48 | else 49 | echo "changed=false" >> "$GITHUB_OUTPUT" 50 | fi 51 | 52 | - name: 📝 Check Release Status 53 | id: release 54 | run: | 55 | echo "Tests passed: ${{ steps.tests.outputs.passed }}" 56 | echo "Dependencies changed: ${{ steps.deps.outputs.changed }}" 57 | disable_auto_release='${{ vars.DISABLE_AUTO_RELEASE }}' 58 | echo "DISABLE_AUTO_RELEASE=$disable_auto_release" 59 | 60 | if [[ ${{ steps.tests.outputs.passed }} == "true" && ${{ steps.deps.outputs.changed }} == "true" && $disable_auto_release != "true" ]]; then 61 | echo "should_release=true" >> "$GITHUB_OUTPUT" 62 | echo "🦾 Creating a release!" 63 | else 64 | echo "should_release=false" >> "$GITHUB_OUTPUT" 65 | echo "✋ Not creating a release." 66 | fi 67 | 68 | release: 69 | uses: './.github/workflows/publish.yaml' 70 | needs: auto_release 71 | if: needs.auto_release.outputs.should_release == 'true' 72 | secrets: 73 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 74 | with: 75 | bump: patch 76 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: '📦 Publish' 2 | on: 3 | workflow_dispatch: 4 | branches: 5 | - main 6 | inputs: 7 | bump: 8 | description: "Version Bump Method" 9 | type: choice 10 | options: 11 | - major 12 | - minor 13 | - patch 14 | required: true 15 | workflow_call: 16 | secrets: 17 | NUGET_API_KEY: 18 | description: "NuGet API Key" 19 | required: true 20 | inputs: 21 | bump: 22 | description: "Version Bump Method" 23 | type: string 24 | required: true 25 | 26 | jobs: 27 | publish: 28 | name: 📦 Publish 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: 🧾 Checkout 32 | uses: actions/checkout@v3 33 | with: 34 | lfs: true 35 | submodules: 'recursive' 36 | 37 | - name: 🔎 Read Current Project Verson 38 | uses: KageKirin/get-csproj-version@v1.0.0 39 | id: current-version 40 | with: 41 | file: Chickensoft.GoDotNet/Chickensoft.GoDotNet.csproj 42 | xpath: /Project/PropertyGroup/Version 43 | 44 | - name: 🖨 Print Current Version 45 | run: | 46 | echo "Current Version: ${{ steps.current-version.outputs.version }}" 47 | 48 | - name: 🧮 Compute Next Version 49 | uses: chickensoft-games/next-godot-csproj-version@v1 50 | id: next-version 51 | with: 52 | project-version: ${{ steps.current-version.outputs.version }} 53 | godot-version: global.json 54 | bump: ${{ inputs.bump }} 55 | 56 | - name: ✨ Print Next Version 57 | run: | 58 | echo "Next Version: ${{ steps.next-version.outputs.version }}" 59 | 60 | - name: 📝 Change Version 61 | uses: vers-one/dotnet-project-version-updater@v1.3 62 | with: 63 | file: "Chickensoft.GoDotNet/Chickensoft.GoDotNet.csproj" 64 | version: ${{ steps.next-version.outputs.version }} 65 | 66 | - name: ✍️ Commit Changes 67 | run: | 68 | git config user.name "action@github.com" 69 | git config user.email "GitHub Action" 70 | git commit -a -m "chore(version): update version to ${{ steps.next-version.outputs.version }}" 71 | git push 72 | 73 | - name: ✨ Create Release 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | run: gh release create --generate-notes "v${{ steps.next-version.outputs.version }}" 77 | 78 | - name: 💽 Setup .NET SDK 79 | uses: actions/setup-dotnet@v3 80 | with: 81 | # Use the .NET SDK from global.json in the root of the repository. 82 | global-json-file: global.json 83 | 84 | - name: 📦 Publish 85 | run: | 86 | # build the package 87 | dotnet build Chickensoft.GoDotNet/Chickensoft.GoDotNet.csproj -c Release 88 | 89 | # find the built nuget package 90 | nuget_package=$(find . -name "Chickensoft.GoDotNet.*.nupkg") 91 | 92 | echo "📦 Publishing package: $nuget_package" 93 | 94 | # publish the nuget package 95 | dotnet nuget push "$nuget_package" --api-key "${{ secrets.NUGET_API_KEY }}" --source "https://api.nuget.org/v3/index.json" --skip-duplicate 96 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yaml: -------------------------------------------------------------------------------- 1 | name: '🧑‍🏫 Spellcheck' 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | spellcheck: 8 | name: '🧑‍🏫 Spellcheck' 9 | # Only run the workflow if it's not a PR or if it's a PR from a fork. 10 | # This prevents duplicate workflows from running on PR's that originate 11 | # from the repository itself. 12 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: '.' 17 | steps: 18 | - uses: actions/checkout@v3 19 | name: 🧾 Checkout 20 | 21 | - uses: streetsidesoftware/cspell-action@v3 22 | name: 📝 Check Spelling 23 | with: 24 | config: './cspell.json' 25 | incremental_files_only: false 26 | root: '.' 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: 🚥 Tests 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | tests: 8 | name: 🧪 Evaluate Tests on ${{ matrix.os }} 9 | # Only run the workflow if it's not a PR or if it's a PR from a fork. 10 | # This prevents duplicate workflows from running on PR's that originate 11 | # from the repository itself. 12 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | # Don't cancel other OS runners if one fails. 16 | fail-fast: false 17 | matrix: 18 | # Put the operating systems you want to run on here. 19 | # 20 | # You can change windows-2019 to windows-latest, but windows-2019 21 | # was running in half the time. Try it out and see what works best. 22 | os: [ubuntu-latest, macos-latest, windows-2019] 23 | env: 24 | DOTNET_CLI_TELEMETRY_OPTOUT: true 25 | DOTNET_NOLOGO: true 26 | defaults: 27 | run: 28 | # Use bash shells on all platforms. 29 | shell: bash 30 | steps: 31 | - name: 🧾 Checkout 32 | uses: actions/checkout@v3 33 | 34 | - name: 💽 Setup .NET SDK 35 | uses: actions/setup-dotnet@v3 36 | with: 37 | # Use the .NET SDK from global.json in the root of the repository. 38 | global-json-file: global.json 39 | 40 | - name: 📦 Restore Dependencies 41 | run: dotnet restore 42 | 43 | - name: 🤖 Setup Godot 44 | uses: chickensoft-games/setup-godot@v1 45 | with: 46 | # Version must include major, minor, and patch, and be >= 4.0.0 47 | # Pre-release label is optional. 48 | # 49 | # In this case, we are using the version from global.json. 50 | # 51 | # This allows checks on renovatebot PR's to succeed whenever 52 | # renovatebot updates the Godot SDK version. 53 | version: global.json 54 | 55 | - name: 🔬 Verify Setup 56 | run: | 57 | dotnet --version 58 | godot --version 59 | 60 | - name: 🧑‍🔬 Generate .NET Bindings 61 | working-directory: Chickensoft.GoDotNet.Tests 62 | run: godot --headless --build-solutions --quit || exit 0 63 | 64 | - name: 🦺 Build Projects 65 | run: dotnet build 66 | 67 | - name: 🧪 Run Tests 68 | working-directory: Chickensoft.GoDotNet.Tests 69 | run: godot --headless --run-tests --quit-on-finish 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Chickensoft.GoDotNet/nupkg/ 2 | 3 | Chickensoft.GoDotNet.Tests/coverage/* 4 | !Chickensoft.GoDotNet.Tests/coverage/.gdignore 5 | 6 | .godot/ 7 | bin/ 8 | obj/ 9 | .generated/ 10 | .vs/ 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-dotnettools.csharp", 4 | "selcukermaya.se-csproj-extensions", 5 | "josefpihrt-vscode.roslynator", 6 | "streetsidesoftware.code-spell-checker", 7 | "VisualStudioExptTeam.vscodeintellicode", 8 | "DavidAnson.vscode-markdownlint" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // For these launch configurations to work, you need to setup a GODOT4 5 | // environment variable. On mac or linux, this can be done by adding 6 | // the following to your .zshrc, .bashrc, or .bash_profile file: 7 | // export GODOT4="/Applications/Godot.app/Contents/MacOS/Godot" 8 | { 9 | "name": "🧪 Debug Tests", 10 | "type": "coreclr", 11 | "request": "launch", 12 | "preLaunchTask": "build", 13 | "program": "${env:GODOT4}", 14 | "args": [ 15 | // These command line flags are used by GoDotTest to run tests. 16 | "--run-tests", 17 | "--quit-on-finish" 18 | ], 19 | "cwd": "${workspaceFolder}/Chickensoft.GoDotNet.Tests", 20 | "stopAtEntry": false, 21 | }, 22 | { 23 | "name": "🔬 Debug Current Test", 24 | "type": "coreclr", 25 | "request": "launch", 26 | "preLaunchTask": "build", 27 | "program": "${env:GODOT4}", 28 | "args": [ 29 | // These command line flags are used by GoDotTest to run tests. 30 | "--run-tests=${fileBasenameNoExtension}", 31 | "--quit-on-finish" 32 | ], 33 | "cwd": "${workspaceFolder}/Chickensoft.GoDotNet.Tests", 34 | "stopAtEntry": false, 35 | }, 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[csharp]": { 3 | "editor.codeActionsOnSave": { 4 | "source.addMissingImports": true, 5 | "source.fixAll": true, 6 | "source.organizeImports": true 7 | }, 8 | "editor.formatOnPaste": true, 9 | "editor.formatOnSave": true, 10 | "editor.formatOnType": false 11 | }, 12 | "csharp.semanticHighlighting.enabled": true, 13 | "editor.semanticHighlighting.enabled": true, 14 | // C# doc comment colorization gets lost with semantic highlighting, but we 15 | // need semantic highlighting for proper syntax highlighting with record 16 | // shorthand. 17 | // 18 | // Here's a workaround for doc comment highlighting from 19 | // https://github.com/OmniSharp/omnisharp-vscode/issues/3816 20 | "editor.tokenColorCustomizations": { 21 | "[*Dark*]": { 22 | // Themes that include the word "Dark" in them. 23 | "textMateRules": [ 24 | { 25 | "scope": "comment.documentation", 26 | "settings": { 27 | "foreground": "#608B4E" 28 | } 29 | }, 30 | { 31 | "scope": "comment.documentation.attribute", 32 | "settings": { 33 | "foreground": "#C8C8C8" 34 | } 35 | }, 36 | { 37 | "scope": "comment.documentation.cdata", 38 | "settings": { 39 | "foreground": "#E9D585" 40 | } 41 | }, 42 | { 43 | "scope": "comment.documentation.delimiter", 44 | "settings": { 45 | "foreground": "#808080" 46 | } 47 | }, 48 | { 49 | "scope": "comment.documentation.name", 50 | "settings": { 51 | "foreground": "#569CD6" 52 | } 53 | } 54 | ] 55 | }, 56 | "[*Light*]": { 57 | // Themes that include the word "Light" in them. 58 | "textMateRules": [ 59 | { 60 | "scope": "comment.documentation", 61 | "settings": { 62 | "foreground": "#008000" 63 | } 64 | }, 65 | { 66 | "scope": "comment.documentation.attribute", 67 | "settings": { 68 | "foreground": "#282828" 69 | } 70 | }, 71 | { 72 | "scope": "comment.documentation.cdata", 73 | "settings": { 74 | "foreground": "#808080" 75 | } 76 | }, 77 | { 78 | "scope": "comment.documentation.delimiter", 79 | "settings": { 80 | "foreground": "#808080" 81 | } 82 | }, 83 | { 84 | "scope": "comment.documentation.name", 85 | "settings": { 86 | "foreground": "#808080" 87 | } 88 | } 89 | ] 90 | }, 91 | "[*]": { 92 | // Themes that don't include the word "Dark" or "Light" in them. 93 | // These are some bold colors that show up well against most dark and 94 | // light themes. 95 | // 96 | // Change them to something that goes well with your preferred theme :) 97 | "textMateRules": [ 98 | { 99 | "scope": "comment.documentation", 100 | "settings": { 101 | "foreground": "#0091ff" 102 | } 103 | }, 104 | { 105 | "scope": "comment.documentation.attribute", 106 | "settings": { 107 | "foreground": "#8480ff" 108 | } 109 | }, 110 | { 111 | "scope": "comment.documentation.cdata", 112 | "settings": { 113 | "foreground": "#0091ff" 114 | } 115 | }, 116 | { 117 | "scope": "comment.documentation.delimiter", 118 | "settings": { 119 | "foreground": "#aa00ff" 120 | } 121 | }, 122 | { 123 | "scope": "comment.documentation.name", 124 | "settings": { 125 | "foreground": "#ef0074" 126 | } 127 | } 128 | ] 129 | } 130 | }, 131 | "markdownlint.config": { 132 | // Allow html in markdown. 133 | "MD033": false, 134 | // Allow non-unique heading names so we don't break the changelog. 135 | "MD024": false 136 | }, 137 | "markdownlint.ignore": [ 138 | "**/LICENSE" 139 | ], 140 | "omnisharp.enableEditorConfigSupport": true, 141 | "omnisharp.enableMsBuildLoadProjectsOnDemand": false, 142 | "omnisharp.enableRoslynAnalyzers": true, 143 | "omnisharp.maxFindSymbolsItems": 3000, 144 | "omnisharp.organizeImportsOnFormat": true, 145 | "omnisharp.useModernNet": true, 146 | // Remove these if you're happy with your terminal profiles. 147 | "terminal.integrated.defaultProfile.windows": "Git Bash", 148 | "terminal.integrated.profiles.windows": { 149 | "Command Prompt": { 150 | "icon": "terminal-cmd", 151 | "path": [ 152 | "${env:windir}\\Sysnative\\cmd.exe", 153 | "${env:windir}\\System32\\cmd.exe" 154 | ] 155 | }, 156 | "Git Bash": { 157 | "icon": "terminal", 158 | "source": "Git Bash" 159 | }, 160 | "PowerShell": { 161 | "icon": "terminal-powershell", 162 | "source": "PowerShell" 163 | } 164 | }, 165 | "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true 166 | } 167 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "--no-restore" 11 | ], 12 | "problemMatcher": "$msCompile", 13 | "presentation": { 14 | "echo": true, 15 | "reveal": "silent", 16 | "focus": false, 17 | "panel": "shared", 18 | "showReuseMessage": false, 19 | "clear": false 20 | } 21 | }, 22 | { 23 | "label": "coverage", 24 | "group": "test", 25 | "command": "${workspaceFolder}/Chickensoft.GoDotNet.Tests/coverage.sh", 26 | "type": "shell", 27 | "options": { 28 | "cwd": "${workspaceFolder}/Chickensoft.GoDotNet.Tests" 29 | }, 30 | "presentation": { 31 | "echo": true, 32 | "reveal": "always", 33 | "focus": false, 34 | "panel": "shared", 35 | "showReuseMessage": false, 36 | "clear": true 37 | }, 38 | }, 39 | { 40 | "label": "build-solutions", 41 | "group": "test", 42 | "command": "dotnet restore; ${env:GODOT4} --headless --build-solutions --quit || exit 0", 43 | "type": "shell", 44 | "options": { 45 | "cwd": "${workspaceFolder}/Chickensoft.GoDotNet.Tests" 46 | }, 47 | "presentation": { 48 | "echo": true, 49 | "reveal": "silent", 50 | "focus": false, 51 | "panel": "shared", 52 | "showReuseMessage": false, 53 | "clear": false 54 | } 55 | }, 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for taking the time to read this contributing guide and for showing interest in helping this project! 4 | 5 | ## Getting Started 6 | 7 | Need a helping hand to get started? Check out these resources! 8 | 9 | - [Discord Server][discord] 10 | - [Chickensoft Website][chickensoft] 11 | 12 | Please read our [code of conduct](#code-of-conduct). We do our best to treat others fairly and foster a welcoming environment. 13 | 14 | ## Project Setup 15 | 16 | This is a C# nuget package, for use with the .NET SDK 6 or 7. As such, the `dotnet` tool will allow you to restore packages and build projects. 17 | 18 | The `Chickensoft.GoDotNet.Tests` project must be built with the Godot editor at least once before `dotnet build` will succeed. Godot has to generate the .NET bindings for the project, since tests run in an actual game environment. 19 | 20 | ## Coding Guidelines 21 | 22 | Your IDE should automatically adhere to the style guidelines in the provided `.editorconfig` file. Please try to keep lines under 80 characters long whenever possible. 23 | 24 | We try to write tests for our projects to ensure a certain level of quality. We are willing to give you support and guidance if you need help! 25 | 26 | ## Code of Conduct 27 | 28 | We follow the [Contributor Covenant][covenant]. 29 | 30 | In short: 31 | 32 | > We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 33 | 34 | 35 | 36 | [discord]: https://discord.gg/gSjaPgMmYW 37 | [chickensoft]: https://chickensoft.games 38 | [covenant]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ 39 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/Chickensoft.GoDotNet.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | true 5 | 10.0 6 | enable 7 | Chickensoft.GoDotNet.Tests 8 | 9 | 10 | true 11 | 12 | 18 | full 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/Chickensoft.GoDotNet.Tests.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 2012 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.GoDotNet.Tests", "Chickensoft.GoDotNet.Tests.csproj", "{07C3BA9A-CC0E-47EC-996F-91C58DA3E779}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | ExportDebug|Any CPU = ExportDebug|Any CPU 9 | ExportRelease|Any CPU = ExportRelease|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {07C3BA9A-CC0E-47EC-996F-91C58DA3E779}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {07C3BA9A-CC0E-47EC-996F-91C58DA3E779}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {07C3BA9A-CC0E-47EC-996F-91C58DA3E779}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU 15 | {07C3BA9A-CC0E-47EC-996F-91C58DA3E779}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU 16 | {07C3BA9A-CC0E-47EC-996F-91C58DA3E779}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU 17 | {07C3BA9A-CC0E-47EC-996F-91C58DA3E779}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU 18 | EndGlobalSection 19 | EndGlobal 20 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/badges/.gdignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chickensoft-games/GoDotNet/29e0f78466c8295b028307605ba91c2ec01a7eec/Chickensoft.GoDotNet.Tests/badges/.gdignore -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/badges/branch_coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 48 | Code coverage 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Generated by: ReportGenerator 5.1.17.0 98 | 99 | 100 | 101 | Coverage 102 | Coverage 103 | 104 | 100%100% 105 | 106 | 107 | 108 | 109 | 110 | Branch coverage 111 | 112 | 113 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/badges/line_coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 48 | Code coverage 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Generated by: ReportGenerator 5.1.17.0 98 | 99 | 100 | 101 | Coverage 102 | Coverage 103 | 100%100% 104 | 105 | 106 | 107 | 108 | 109 | Line coverage 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # To collect code coverage, you will need the following environment setup: 4 | # 5 | # - A "GODOT4" environment variable pointing to the Godot executable 6 | # - ReportGenerator installed 7 | # 8 | # dotnet tool install -g dotnet-reportgenerator-globaltool 9 | # 10 | # - A version of coverlet > 3.2.0. 11 | # 12 | # As of Jan 2023, this is not yet released. 13 | # 14 | # The included `nuget.config` file will allow you to install a nightly 15 | # version of coverlet from the coverlet nightly nuget feed. 16 | # 17 | # dotnet tool install --global coverlet.console --prerelease. 18 | # 19 | # You can build coverlet yourself, but you will need to edit the path to 20 | # coverlet below to point to your local build of the coverlet dll. 21 | # 22 | # If you need help with coverage, feel free to join the Chickensoft Discord. 23 | # https://chickensoft.games 24 | 25 | dotnet build --no-restore 26 | 27 | coverlet \ 28 | "./.godot/mono/temp/bin/Debug" --verbosity detailed \ 29 | --target $GODOT4 \ 30 | --targetargs "--run-tests --coverage --quit-on-finish" \ 31 | --format "opencover" \ 32 | --output "./coverage/coverage.xml" \ 33 | --exclude-by-file "**/test/**/*.cs" \ 34 | --exclude-by-file "**/*Microsoft.NET.Test.Sdk.Program.cs" \ 35 | --exclude-by-file "**/Godot.SourceGenerators/**/*.cs" \ 36 | --exclude-assemblies-without-sources "missingall" 37 | 38 | # Projects included via will be collected in code coverage. 39 | # If you want to exclude them, replace the string below with the names of 40 | # the assemblies to ignore. e.g., 41 | # ASSEMBLIES_TO_REMOVE="-AssemblyToRemove1;-AssemblyToRemove2" 42 | ASSEMBLIES_TO_REMOVE="-Chickensoft.GoDotNet.Tests" 43 | 44 | reportgenerator \ 45 | -reports:"./coverage/coverage.xml" \ 46 | -targetdir:"./coverage/report" \ 47 | "-assemblyfilters:$ASSEMBLIES_TO_REMOVE" \ 48 | "-classfilters:-GodotPlugins.Game.Main" \ 49 | -reporttypes:"Html;Badges" 50 | 51 | # Copy badges into their own folder. The badges folder should be included in 52 | # source control so that the README.md in the root can reference the badges. 53 | 54 | mkdir -p ./badges 55 | mv ./coverage/report/badge_branchcoverage.svg ./badges/branch_coverage.svg 56 | mv ./coverage/report/badge_linecoverage.svg ./badges/line_coverage.svg 57 | 58 | # Determine OS, open coverage accordingly. 59 | 60 | case "$(uname -s)" in 61 | 62 | Darwin) 63 | echo 'Mac OS X' 64 | open coverage/report/index.htm 65 | ;; 66 | 67 | Linux) 68 | echo 'Linux' 69 | ;; 70 | 71 | CYGWIN*|MINGW32*|MSYS*|MINGW*) 72 | echo 'MS Windows' 73 | start coverage/report/index.htm 74 | ;; 75 | 76 | *) 77 | echo 'Other OS' 78 | ;; 79 | esac 80 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/coverage/.gdignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chickensoft-games/GoDotNet/29e0f78466c8295b028307605ba91c2ec01a7eec/Chickensoft.GoDotNet.Tests/coverage/.gdignore -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://da2tcc2mhkfgi" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/lossy_quality=0.7 20 | compress/hdr_compression=1 21 | compress/bptc_ldr=0 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Chickensoft.GoDotNet.Tests" 14 | run/main_scene="res://test/Tests.tscn" 15 | config/features=PackedStringArray("4.0", "C#", "Mobile") 16 | config/icon="res://icon.svg" 17 | 18 | [dotnet] 19 | 20 | project/assembly_name="Chickensoft.GoDotNet.Tests" 21 | 22 | [rendering] 23 | 24 | renderer/rendering_method="mobile" 25 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/Tests.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet.Tests; 2 | 3 | using System.Reflection; 4 | using Godot; 5 | using GoDotTest; 6 | 7 | public partial class Tests : Node2D { 8 | /// 9 | /// Called when the node enters the scene tree for the first time. 10 | /// 11 | public override void _Ready() 12 | => GoTest.RunTests(Assembly.GetExecutingAssembly(), this); 13 | } 14 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/Tests.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://bv5dxd8hrc5g4"] 2 | 3 | [ext_resource type="Script" path="res://test/Tests.cs" id="1_310o6"] 4 | 5 | [node name="Node2D" type="Node2D"] 6 | script = ExtResource("1_310o6") 7 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/src/AutoloadCacheTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet.Tests; 2 | 3 | using Godot; 4 | using GoDotNet; 5 | using GoDotTest; 6 | using Shouldly; 7 | 8 | public class AutoloadCacheTest : TestClass { 9 | private class TestClass { } 10 | 11 | public AutoloadCacheTest(Node testScene) : base(testScene) { } 12 | 13 | [Test] 14 | public void SavesAndLoadsAutoloadFromCache() { 15 | var autoload = new TestClass(); 16 | AutoloadCache.Write(typeof(TestClass), autoload); 17 | AutoloadCache.Has(typeof(TestClass)).ShouldBeTrue(); 18 | AutoloadCache.Read(typeof(TestClass)).ShouldBeSameAs(autoload); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/src/extensions/NodeExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet.Tests; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using Godot; 6 | using GoDotNet; 7 | using GoDotTest; 8 | using Shouldly; 9 | 10 | public partial class TestAutoloadNode : Node { } 11 | public class NodeExtensionsTest : TestClass { 12 | public NodeExtensionsTest(Node testScene) : base(testScene) { } 13 | 14 | [Test] 15 | public async Task TryAutoloadFindsNothing() { 16 | // Must wait until the next frame so that we are running inside "_Process" 17 | // This ensures that the root node children are initialized. 18 | await TestScene.ToSignal(TestScene.GetTree(), "process_frame"); 19 | var foundAutoload = TestScene.TryAutoload(); 20 | foundAutoload.ShouldBeNull(); 21 | } 22 | 23 | [Test] 24 | public async Task AutoloadThrowsOnNotFound() { 25 | await TestScene.ToSignal(TestScene.GetTree(), "process_frame"); 26 | Should.Throw( 27 | () => TestScene.Autoload() 28 | ); 29 | } 30 | 31 | [Test] 32 | public async Task TryAutoloadFindsAutoloadFromRootAndCache() { 33 | await TestScene.ToSignal(TestScene.GetTree(), "process_frame"); 34 | var autoload = new TestAutoloadNode(); 35 | var root = TestScene.GetNode("/root/"); 36 | root.AddChild(autoload); 37 | var foundAutoload = TestScene.Autoload(); 38 | foundAutoload.ShouldNotBeNull(); 39 | foundAutoload.ShouldBeSameAs(autoload); 40 | var cachedAutoload = TestScene.Autoload(); 41 | cachedAutoload.ShouldNotBeNull(); 42 | cachedAutoload.ShouldBeSameAs(autoload); 43 | root.RemoveChild(autoload); // as of Godot 3.5, this doesn't seem to take 44 | // place right away. :( That's why this test runs after the two above. 45 | autoload.QueueFree(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/src/extensions/StringExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet.Tests; 2 | 3 | using Godot; 4 | using GoDotNet; 5 | using GoDotTest; 6 | using Shouldly; 7 | 8 | public class StringExtensionsTest : TestClass { 9 | public StringExtensionsTest(Node testScene) : base(testScene) { } 10 | 11 | [Test] 12 | public void ToNameCaseWorksOnNull() { 13 | string str = null!; 14 | str.ToNameCase().ShouldBe(""); 15 | } 16 | 17 | [Test] 18 | public void ToNameCaseWorksOnBlank() { 19 | var str = ""; 20 | str.ToNameCase().ShouldBe(""); 21 | } 22 | 23 | [Test] 24 | public void ToNameCaseWorksOnValue() { 25 | "hello".ToNameCase().ShouldBe("Hello"); 26 | " ".ToNameCase().ShouldBe(" "); 27 | "^".ToNameCase().ShouldBe("^"); 28 | "1".ToNameCase().ShouldBe("1"); 29 | "a".ToNameCase().ShouldBe("A"); 30 | "A B C".ToNameCase().ShouldBe("A B C"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/src/nodes/SchedulerTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet.Tests; 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using Godot; 6 | using GoDotLog; 7 | using GoDotNet; 8 | using GoDotTest; 9 | using LightMock; 10 | using LightMock.Generator; 11 | using LightMoq; 12 | using Shouldly; 13 | 14 | public class SchedulerTest : TestClass { 15 | public SchedulerTest(Node testScene) : base(testScene) { } 16 | 17 | [Test] 18 | public void InitializesFromDefaultConstructor() { 19 | var scheduler = new Scheduler(); 20 | scheduler.ShouldBeOfType(typeof(Scheduler)); 21 | } 22 | 23 | [Test] 24 | public void InitializesWithCustomConstructor() { 25 | var log = new Mock(); 26 | var scheduler = new Scheduler(log.Object); 27 | scheduler.Log.ShouldBeSameAs(log.Object); 28 | } 29 | 30 | [Test] 31 | public void InitializesWithCustomConstructorAndDebugging() { 32 | var log = new Mock(); 33 | var isDebugging = true; 34 | var scheduler = new Scheduler(log.Object, isDebugging); 35 | scheduler.Log.ShouldBeSameAs(log.Object); 36 | scheduler._Ready(); 37 | scheduler.IsDebugging.ShouldBe(isDebugging); 38 | } 39 | 40 | [Test] 41 | public void RunsScheduledCallback() { 42 | var scheduler = new Scheduler(); 43 | var called = false; 44 | scheduler.NextFrame(() => called = true); 45 | scheduler._Process(0); 46 | called.ShouldBeTrue(); 47 | } 48 | 49 | [Test] 50 | public void RunsScheduledCallbackAndHandlesErrorInDebug() { 51 | var log = new Mock(); 52 | var scheduler = new Scheduler(log.Object, true); 53 | log.Setup(l => l.Run(The.IsAnyValue, The>.IsAnyValue)) 54 | .Callback>( 55 | (action, errorHandler) => { 56 | try { 57 | action(); 58 | } 59 | catch (Exception e) { 60 | errorHandler(e); 61 | } 62 | } 63 | ); 64 | 65 | log.Setup(l => l.Print( 66 | "A callback scheduled in a previous frame threw " + 67 | "an error with the following stack trace." 68 | )); 69 | log.Setup(l => l.Print(The.IsAnyValue)); 70 | scheduler.NextFrame(() => throw new InvalidOperationException()); 71 | scheduler._Process(0); 72 | log.VerifyAll(); 73 | scheduler._Process(0); 74 | } 75 | 76 | [Test] 77 | public void RunsScheduledCallbackAndHandlesErrorInProduction() { 78 | var log = new Mock(); 79 | var scheduler = new Scheduler(log.Object, false); 80 | log.Setup(l => l.Run( 81 | The.IsAnyValue, The>.IsAnyValue 82 | )).Callback>( 83 | (action, errorHandler) => { 84 | try { 85 | action(); 86 | } 87 | catch (Exception e) { 88 | errorHandler(e); 89 | } 90 | } 91 | ); 92 | scheduler.NextFrame(() => throw new InvalidOperationException()); 93 | scheduler._Process(0); 94 | log.VerifyAll(); 95 | scheduler._Process(0); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/src/state/MachineTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet.Tests; 2 | 3 | using Godot; 4 | using GoDotNet; 5 | using GoDotTest; 6 | using Shouldly; 7 | 8 | public class MachineTest : TestClass { 9 | private interface ITestState : IMachineState { } 10 | private record TestStateA : ITestState { 11 | public bool CanTransitionTo(ITestState state) 12 | => state is TestStateB; 13 | } 14 | private record TestStateB : ITestState { 15 | public bool CanTransitionTo(ITestState state) 16 | => state is TestStateC; 17 | } 18 | private record TestStateC : ITestState { 19 | public bool CanTransitionTo(ITestState state) 20 | => state is TestStateA; 21 | } 22 | 23 | private record TestStateD : ITestState { } 24 | 25 | public MachineTest(Node testScene) : base(testScene) { } 26 | 27 | [Test] 28 | public void Instantiates() { 29 | var machine = new Machine(new TestStateA()); 30 | machine.State.ShouldBe(new TestStateA()); 31 | } 32 | 33 | [Test] 34 | public void InstantiatesWithListener() { 35 | var called = false; 36 | void onChanged(ITestState state) { 37 | state.ShouldBe(new TestStateA()); 38 | called = true; 39 | } 40 | var machine = new Machine(new TestStateA(), onChanged); 41 | called.ShouldBeTrue(); 42 | machine.State.ShouldBe(new TestStateA()); 43 | } 44 | 45 | [Test] 46 | public void UpdatesStateAndAnnounces() { 47 | var machine = new Machine(new TestStateA()); 48 | 49 | var called = false; 50 | void onChanged(ITestState state) { 51 | state.ShouldBe(new TestStateB()); 52 | called = true; 53 | } 54 | 55 | machine.OnChanged += onChanged; 56 | machine.Update(new TestStateB()); 57 | machine.State.ShouldBe(new TestStateB()); 58 | called.ShouldBeTrue(); 59 | } 60 | 61 | [Test] 62 | public void DoesNothingOnSameState() { 63 | var machine = new Machine(new TestStateA()); 64 | var called = false; 65 | void onChanged(ITestState state) => called = true; 66 | machine.OnChanged += onChanged; 67 | machine.Update(new TestStateA()); 68 | called.ShouldBeFalse(); 69 | } 70 | 71 | [Test] 72 | public void ThrowsWhenTransitioningToInvalidNextState() { 73 | var machine = new Machine(new TestStateA()); 74 | machine.State.CanTransitionTo(new TestStateC()).ShouldBeFalse(); 75 | Should.Throw>( 76 | () => machine.Update(new TestStateC()) 77 | ); 78 | } 79 | 80 | [Test] 81 | public void DefaultStateTransitionsToAnything() { 82 | var machine = new Machine(new TestStateD()); 83 | machine.State.CanTransitionTo(new TestStateA()).ShouldBeTrue(); 84 | machine.State.CanTransitionTo(new TestStateB()).ShouldBeTrue(); 85 | machine.State.CanTransitionTo(new TestStateC()).ShouldBeTrue(); 86 | } 87 | 88 | [Test] 89 | public void UpdatesStateFromAnnouncementPreservesOrdering() { 90 | var machine = new Machine(new TestStateA()); 91 | 92 | void onChanged(ITestState state) { 93 | if (state is TestStateB) { 94 | machine.Update(new TestStateC()); 95 | } 96 | } 97 | 98 | machine.OnChanged += onChanged; 99 | machine.Update(new TestStateB()); 100 | machine.State.ShouldBe(new TestStateC()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/src/state/NotifierTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet.Tests; 2 | 3 | using Godot; 4 | using GoDotNet; 5 | using GoDotTest; 6 | using Shouldly; 7 | 8 | public class NotifierTest : TestClass { 9 | public NotifierTest(Node testScene) : base(testScene) { } 10 | 11 | [Test] 12 | public void Instantiates() { 13 | var notifier = new Notifier("a"); 14 | notifier.Value.ShouldBe("a"); 15 | notifier.Previous.ShouldBeNull(); 16 | } 17 | 18 | [Test] 19 | public void InstantiatesWithListener() { 20 | var called = false; 21 | void onChanged(string value, string? previous) { 22 | previous.ShouldBeNull(); 23 | value.ShouldBe("a"); 24 | called = true; 25 | } 26 | var notifier = new Notifier("a", onChanged); 27 | called.ShouldBeTrue(); 28 | notifier.Value.ShouldBe("a"); 29 | notifier.Previous.ShouldBeNull(); 30 | } 31 | 32 | [Test] 33 | public void DoesNothingOnSameValue() { 34 | var notifier = new Notifier("a"); 35 | var called = false; 36 | void onChanged(string value, string? previous) => called = true; 37 | notifier.OnChanged += onChanged; 38 | notifier.Update("a"); 39 | called.ShouldBeFalse(); 40 | notifier.Previous.ShouldBeNull(); 41 | } 42 | 43 | [Test] 44 | public void UpdatesValue() { 45 | var notifier = new Notifier("a"); 46 | var calledOnChanged = false; 47 | var calledOnUpdated = false; 48 | void onChanged(string value, string? previous) { 49 | calledOnChanged = true; 50 | value.ShouldBe("b"); 51 | previous.ShouldBe("a"); 52 | notifier.Previous.ShouldBe("a"); 53 | } 54 | void onUpdated(string value) { 55 | calledOnUpdated = true; 56 | value.ShouldBe("b"); 57 | notifier.Previous.ShouldBe("a"); 58 | } 59 | notifier.OnChanged += onChanged; 60 | notifier.OnUpdated += onUpdated; 61 | notifier.Update("b"); 62 | calledOnChanged.ShouldBeTrue(); 63 | calledOnUpdated.ShouldBeTrue(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.Tests/test/src/utils/RngTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet.Tests; 2 | 3 | using Godot; 4 | using GoDotNet; 5 | using GoDotTest; 6 | using Shouldly; 7 | 8 | public class NotRandomInt : System.Random { 9 | private readonly int _number; 10 | 11 | public int MinValue { get; private set; } 12 | public int MaxValue { get; private set; } 13 | 14 | public NotRandomInt(int number) : base(0) { 15 | _number = number; 16 | } 17 | 18 | public override int Next(int minValue, int maxValue) { 19 | MinValue = minValue; 20 | MaxValue = maxValue; 21 | return _number; 22 | } 23 | 24 | public override int Next() => _number; 25 | } 26 | 27 | public class NotRandomDouble : System.Random { 28 | private readonly double _number; 29 | 30 | public NotRandomDouble(double number) : base(0) { 31 | _number = number; 32 | } 33 | 34 | public override double NextDouble() => _number; 35 | } 36 | 37 | public class RngTest : TestClass { 38 | public RngTest(Node testScene) : base(testScene) { } 39 | 40 | [Test] 41 | public void DefaultInitializer() { 42 | var rng = new Rng(); 43 | rng.Seed.ShouldBeOfType(); 44 | rng.Generator.ShouldBeOfType(); 45 | } 46 | 47 | [Test] 48 | public void SeedInitializer() { 49 | var seed = 123; 50 | var rng = new Rng(seed); 51 | rng.Seed.ShouldBe(seed); 52 | rng.Generator.ShouldBeOfType(); 53 | } 54 | 55 | [Test] 56 | public void ManualInitializer() { 57 | var seed = 123; 58 | var generator = new System.Random(seed); 59 | var rng = new Rng(seed, generator); 60 | rng.Seed.ShouldBe(seed); 61 | rng.Generator.ShouldBe(generator); 62 | } 63 | 64 | [Test] 65 | public void NextInt() { 66 | var rng = new Rng(0, new NotRandomInt(123)); 67 | rng.NextInt().ShouldBe(123); 68 | } 69 | 70 | [Test] 71 | public void NextFloat() { 72 | var rng = new Rng(0, new NotRandomDouble(0.123d)); 73 | rng.NextFloat().ShouldBe(0.123f); 74 | } 75 | 76 | [Test] 77 | public void NextDouble() { 78 | var rng = new Rng(0, new NotRandomDouble(0.123d)); 79 | rng.NextDouble().ShouldBe(0.123d); 80 | } 81 | 82 | [Test] 83 | public void RangeInt() { 84 | var gen = new NotRandomInt(5); 85 | var rng = new Rng(0, gen); 86 | rng.RangeInt(0, 10).ShouldBe(5); 87 | gen.MinValue.ShouldBe(0); 88 | gen.MaxValue.ShouldBe(10); 89 | } 90 | 91 | [Test] 92 | public void RangeFloat() { 93 | var gen = new NotRandomDouble(0.5d); 94 | var rng = new Rng(0, gen); 95 | rng.RangeFloat(1f, 10f).ShouldBe(5.5f); 96 | } 97 | 98 | [Test] 99 | public void RangeDouble() { 100 | var gen = new NotRandomDouble(0.5d); 101 | var rng = new Rng(0, gen); 102 | rng.RangeDouble(1d, 10d).ShouldBe(5.5d); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.GoDotNet", "Chickensoft.GoDotNet\Chickensoft.GoDotNet.csproj", "{FD2D0482-115D-4B55-A95A-5DF468BF19D5}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.GoDotNet.Tests", "Chickensoft.GoDotNet.Tests\Chickensoft.GoDotNet.Tests.csproj", "{73F2185F-1EB8-4817-BCCD-A3887844109D}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {FD2D0482-115D-4B55-A95A-5DF468BF19D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {FD2D0482-115D-4B55-A95A-5DF468BF19D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {FD2D0482-115D-4B55-A95A-5DF468BF19D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {FD2D0482-115D-4B55-A95A-5DF468BF19D5}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {73F2185F-1EB8-4817-BCCD-A3887844109D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {73F2185F-1EB8-4817-BCCD-A3887844109D}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {73F2185F-1EB8-4817-BCCD-A3887844109D}.Release|Any CPU.ActiveCfg = Debug|Any CPU 26 | {73F2185F-1EB8-4817-BCCD-A3887844109D}.Release|Any CPU.Build.0 = Debug|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/Chickensoft.GoDotNet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | true 5 | preview 6 | true 7 | enable 8 | true 9 | Chickensoft.GoDotNet 10 | true 11 | ./nupkg 12 | portable 13 | 14 | Chickensoft.GoDotNet 15 | 1.5.16 16 | State machines, notifiers, and other utilities for C# Godot development. 17 | © 2023 Chickensoft 18 | Chickensoft 19 | Chickensoft 20 | 21 | Chickensoft.GoDotNet 22 | Chickensoft.GoDotNet release. 23 | icon.png 24 | Godot;State Machine;Deterministic;Finite;FSM;Extensions;Notifier;Listener;Observable;Chickensoft;Gamedev;Utility;Utilities 25 | README.md 26 | LICENSE 27 | https://github.com/chickensoft-games/GoDotNet 28 | 29 | git 30 | https://github.com/chickensoft-games/GoDotNet 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | all 46 | runtime; build; native; contentfiles; analyzers 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/icon.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:18ed2854b10643db64f9c5644af05d2dacb5d8e8ae2df2998ef94cad29c3013e 3 | size 30347 4 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/src/AutoloadCache.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | /// 7 | /// A static class used to cache autoloads whenever they are fetched. This 8 | /// prevents from having to fetch the 9 | /// root node children on every invocation. 10 | /// 11 | public class AutoloadCache { 12 | private static readonly Dictionary _cache = new(); 13 | 14 | /// 15 | /// Save a value to the cache. 16 | /// 17 | /// Type of the node. 18 | /// Node to save. 19 | public static void Write(Type type, object value) => _cache.Add(type, value); 20 | /// 21 | /// Read a value from the cache. 22 | /// 23 | /// Type of the node. 24 | /// Node in the cache. 25 | public static object Read(Type type) => _cache[type]; 26 | /// 27 | /// Check if a value is in the cache. 28 | /// 29 | /// Type of the node. 30 | /// True if the value is saved in the cache. 31 | public static bool Has(Type type) => _cache.ContainsKey(type); 32 | } 33 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/src/extensions/NodeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet; 2 | 3 | using System; 4 | using Godot; 5 | 6 | /// 7 | /// A collection of extension methods for Godot nodes. 8 | /// 9 | public static class NodeExtensions { 10 | /// 11 | /// 12 | /// Returns an instance of a Godot Autoload singleton. Implemented as an 13 | /// extension of Godot.Node, for your convenience. 14 | /// 15 | /// 16 | /// This can potentially return the root node of the scene if and only if 17 | /// you specify the type of the root node, which should be unlikely (an 18 | /// edge case that can occur since this searches the root node's children). 19 | /// 20 | /// 21 | /// This respects type inheritance. If multiple autoloads extend the same 22 | /// type, this returns the first autoload that is assignable to the 23 | /// specified type. For best results, ensure the autoload type hierarchy 24 | /// does not overlap. 25 | /// 26 | /// 27 | /// The node (receiver) used to get the scene root. 28 | /// 29 | /// The type of autoload to find. 30 | /// The autoload instance, or throws. 31 | /// Thrown when an autoload of the 32 | /// given type cannot be found. 33 | public static T Autoload(this Node node) where T : Node 34 | => TryAutoload(node) ?? throw new InvalidOperationException( 35 | "No singleton found for type " + typeof(T).Name 36 | ); 37 | 38 | /// 39 | /// Tries to find an autoload of the specified type. If none is found, 40 | /// returns null. 41 | /// 42 | /// The node (receiver) used to get the scene root. 43 | /// 44 | /// The type of autoload to find. 45 | /// The autoload, if found. 46 | public static T? TryAutoload(this Node node) { 47 | var type = typeof(T); 48 | if (AutoloadCache.Has(type)) { return (T)AutoloadCache.Read(type); } 49 | var root = node.GetNode("/root/"); 50 | foreach (var autoload in (Godot.Collections.Array)root.GetChildren()) { 51 | if (autoload is T singleton) { 52 | AutoloadCache.Write(type, singleton); 53 | return singleton; 54 | } 55 | } 56 | return default; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/src/extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet; 2 | 3 | /// 4 | /// String extensions. 5 | /// 6 | public static class StringExtensions { 7 | /// 8 | /// Returns the string with the first letter capitalized. 9 | /// 10 | /// String receiver. 11 | /// The string with the first letter capitalized. 12 | public static string ToNameCase(this string input) => 13 | input switch { 14 | null => "", 15 | "" => "", 16 | _ => input[0].ToString().ToUpper() + input[1..], 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/src/nodes/Scheduler.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using Godot; 7 | using GoDotLog; 8 | 9 | /// 10 | /// The scheduler helps queue up callbacks and run them at the desired time. 11 | /// 12 | public partial class Scheduler : Node { 13 | public ILog Log { get; } 14 | private readonly Queue _actions = new(); 15 | private readonly Dictionary _stackTraces = new(); 16 | private readonly Dictionary _callers = new(); 17 | private readonly bool? _isDebuggingOverride; 18 | private bool _isDebugBuild; 19 | 20 | /// True if the scheduler is running in debug mode. 21 | public bool IsDebugging => _isDebuggingOverride ?? _isDebugBuild; 22 | 23 | /// Creates a new scheduler. 24 | public Scheduler() { 25 | Log = new GDLog(nameof(Scheduler)); 26 | } 27 | 28 | /// Creates a new scheduler. 29 | /// Scheduler log. 30 | /// Debug mode (true or false, or null 31 | /// to infer). 32 | public Scheduler(ILog log, bool? isDebuggingOverride = null) { 33 | Log = log; 34 | _isDebuggingOverride = isDebuggingOverride; 35 | } 36 | 37 | /// 38 | public override void _Ready() 39 | => _isDebugBuild = OS.IsDebugBuild(); 40 | 41 | /// 42 | public override void _Process(double delta) { 43 | if (_actions.Count == 0) { return; } 44 | do { 45 | var action = _actions.Dequeue(); 46 | Log.Run( 47 | action, 48 | IsDebugging 49 | ? HandleScheduledActionError(action) 50 | : (e) => { } 51 | ); 52 | } while (_actions.Count > 0); 53 | } 54 | 55 | private Action HandleScheduledActionError(Action action) 56 | => (Exception e) => { 57 | var stackTrace = _stackTraces[action]; 58 | _stackTraces.Remove(action); 59 | Log.Print( 60 | "A callback scheduled in a previous frame threw an error with " + 61 | "the following stack trace." 62 | ); 63 | Log.Print(stackTrace); 64 | }; 65 | 66 | /// 67 | /// Schedule a callback to run on the next frame. 68 | /// 69 | /// Callback to run. 70 | public void NextFrame(Action action) { 71 | _actions.Enqueue(action); 72 | if (IsDebugging) { 73 | _stackTraces.Add(action, new StackTrace()); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/src/state/Machine.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | /// 7 | /// Exception thrown when attempting to transition between states 8 | /// that are incompatible. 9 | /// 10 | /// Machine state. 11 | public class InvalidStateTransitionException : Exception { 12 | /// Current state. 13 | public TState Current; 14 | /// Attempted next state which was invalid. 15 | public TState Desired; 16 | 17 | /// 18 | /// Creates a new invalid state transition exception. 19 | /// 20 | /// Current state. 21 | /// Attempted next state which was invalid. 22 | public InvalidStateTransitionException(TState current, TState desired) : 23 | base($"Invalid state transition between ${current} and ${desired}.") { 24 | Current = current; 25 | Desired = desired; 26 | } 27 | } 28 | 29 | /// 30 | /// A record type that all machine states must inherit from. 31 | /// 32 | /// Because records are reference types with value-based equality, states 33 | /// can be compared easily by the state machine. 34 | /// 35 | /// 36 | /// All state types must implement `CanTransitionTo` which returns true 37 | /// if a given state can be transitioned to from the current state. 38 | /// 39 | /// 40 | /// Machine state type. 41 | public interface IMachineState { 42 | /// 43 | /// Determines whether the given state can be transitioned to from the 44 | /// current state. 45 | /// 46 | /// The requested next state. 47 | /// True to allow the state transition, false to prevent. 48 | bool CanTransitionTo(TState state) => true; 49 | } 50 | 51 | /// 52 | /// Read-only interface for a machine. Expose machines as this interface 53 | /// when you want to allow them to be observed and read, but not updated. 54 | /// 55 | /// 56 | public interface IReadOnlyMachine where TState : IMachineState { 57 | /// 58 | /// Event handler for when the machine's state changes. 59 | /// 60 | /// The new state of the machine. 61 | delegate void Changed(TState state); 62 | 63 | /// Event emitted when the machine's state changes. 64 | event Changed? OnChanged; 65 | 66 | /// The current state of the machine. 67 | TState State { get; } 68 | } 69 | 70 | /// 71 | /// 72 | /// A simple implementation of a state machine. Events an emit when the state 73 | /// is changed. 74 | /// 75 | /// 76 | /// Not intended to be subclassed — instead, use instances of this in a 77 | /// compositional pattern. 78 | /// 79 | /// 80 | /// States can implement `CanTransitionTo` to prevent transitions to invalid 81 | /// states. 82 | /// 83 | /// 84 | /// Type of state used by the machine. 85 | public sealed class Machine : IReadOnlyMachine 86 | where TState : IMachineState { 87 | /// 88 | /// Creates a new machine with the given initial state. 89 | /// 90 | /// Initial state of the machine. 91 | /// 92 | public Machine( 93 | TState state, 94 | IReadOnlyMachine.Changed? onChanged = null 95 | ) { 96 | State = state; 97 | if (onChanged != null) { OnChanged += onChanged; } 98 | Announce(); 99 | } 100 | 101 | /// 102 | public TState State { get; private set; } 103 | 104 | /// 105 | public event IReadOnlyMachine.Changed? OnChanged; 106 | 107 | /// 108 | /// Whether we're currently in the process of changing the state (or not). 109 | /// 110 | public bool IsBusy { get; private set; } 111 | 112 | private Queue PendingTransitions { get; set; } 113 | = new Queue(); 114 | 115 | /// 116 | /// Adds a value to the queue of pending transitions. If the next state 117 | /// is equivalent to the current state, the state will not be changed. 118 | /// If the next state cannot be transitioned to from the current state, 119 | /// the state will not be changed and a warning will be issued before 120 | /// attempting to transition to any subsequent queued states. 121 | /// 122 | /// State for the machine to transition to. 123 | /// 124 | public void Update(TState value) { 125 | // Because machine state can be updated when firing state events from 126 | // previous state updates, we need to make sure we don't allow another 127 | // announce loop to begin while we're already announcing state updates. 128 | // 129 | // Instead, we just make sure we add the transition to the list of 130 | // pending transitions. State machines are guaranteed to enter each state 131 | // requested in the order they are requested (or throw an error if the 132 | // requested sequence is not comprised of valid transitions). 133 | PendingTransitions.Enqueue(value); 134 | 135 | if (IsBusy) { 136 | return; 137 | } 138 | 139 | IsBusy = true; 140 | 141 | while (PendingTransitions.Count > 0) { 142 | var state = PendingTransitions.Dequeue(); 143 | if (State.Equals(state)) { 144 | continue; 145 | } 146 | 147 | if (State.CanTransitionTo(state)) { 148 | State = state; 149 | Announce(); 150 | } 151 | else { 152 | IsBusy = false; 153 | throw new InvalidStateTransitionException(State, state); 154 | } 155 | } 156 | 157 | IsBusy = false; 158 | } 159 | 160 | /// 161 | /// Announces the current state to any listeners. 162 | /// calls this automatically if the new state is 163 | /// different from the previous state. 164 | ///
165 | /// Call this whenever you want to force a re-announcement. 166 | ///
167 | public void Announce() => OnChanged?.Invoke(State); 168 | } 169 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/src/state/Notifier.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet; 2 | /// 3 | /// Read-only interface for a notifier. Expose notifiers as this interface 4 | /// when you want to allow them to be observed and read, but not updated. 5 | /// 6 | /// Notifier value type. 7 | public interface IReadOnlyNotifier { 8 | /// 9 | /// Signature of the event handler for when the value changes. The current 10 | /// and previous values are provided to allow listeners to react to changes. 11 | /// 12 | /// Current value of the notifier. 13 | /// Previous value of the notifier. 14 | delegate void Changed(TData current, TData? previous); 15 | 16 | /// 17 | /// Signature of the event handler for when the value changes. 18 | /// 19 | /// Current value of the notifier. 20 | delegate void Updated(TData value); 21 | 22 | /// 23 | /// Event emitted when the current value of the notifier has changed. 24 | /// Event listeners will receive both the current and previous values. 25 | /// 26 | event Changed? OnChanged; 27 | 28 | /// 29 | /// Event emitted when the current value of the notifier has changed. 30 | /// Event listeners will receive only the current value. Subscribe to 31 | /// if you need both the current and previous values. 32 | /// 33 | event Updated? OnUpdated; 34 | 35 | /// Current notifier value. 36 | TData Value { get; } 37 | } 38 | 39 | /// 40 | /// An object which stores a value and emits an event whenever the value 41 | /// changes. 42 | /// 43 | /// Type of data emitted. 44 | public sealed class Notifier : IReadOnlyNotifier { 45 | /// 46 | /// Creates a new notifier with the given initial value and optional 47 | /// event handler. 48 | /// 49 | /// Initial value (should not be null). 50 | /// Event handler, if any. 51 | public Notifier( 52 | TData initialValue, 53 | IReadOnlyNotifier.Changed? onChanged = null 54 | ) { 55 | if (onChanged != null) { OnChanged += onChanged; } 56 | Value = initialValue; 57 | Previous = default; 58 | Announce(); 59 | } 60 | 61 | /// 62 | /// Previous value of the notifier, if any. This is `default` when the 63 | /// notifier has just been created. 64 | /// 65 | public TData? Previous { get; private set; } 66 | 67 | /// 68 | public TData Value { get; private set; } 69 | 70 | /// 71 | public event IReadOnlyNotifier.Changed? OnChanged; 72 | 73 | /// 74 | public event IReadOnlyNotifier.Updated? OnUpdated; 75 | 76 | /// 77 | /// Updates the notifier value. Any listeners will be called with the 78 | /// new and previous values if the new value is not equal to the old value 79 | /// (as determined by Object.Equals). 80 | /// 81 | /// New value of the notifier. 82 | public void Update(TData value) { 83 | if (Equals(Value, value)) { return; } 84 | Previous = Value; 85 | Value = value; 86 | Announce(); 87 | } 88 | 89 | /// 90 | /// Announces the current and previous values to any listeners. 91 | /// calls this automatically if the new value is 92 | /// different from the previous value. 93 | ///
94 | /// Call this whenever you want to force a re-announcement. 95 | ///
96 | public void Announce() { 97 | OnChanged?.Invoke(Value, Previous); 98 | OnUpdated?.Invoke(Value); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Chickensoft.GoDotNet/src/utils/Rng.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GoDotNet; 2 | 3 | /// 4 | /// A random number generator that wraps and 5 | /// provides additional convenience functions for working with floats, doubles, 6 | /// and other types. 7 | /// 8 | public interface IRng { 9 | /// 10 | /// Seed used for the random number generator. 11 | /// 12 | int Seed { get; } 13 | /// 14 | /// Random number generator used to produce random numbers. 15 | /// 16 | System.Random Generator { get; } 17 | /// 18 | /// Returns a non-negative random integer. 19 | /// 20 | /// A 32-bit signed integer that is greater than or equal to 0 and 21 | /// less than int.MaxValue 22 | int NextInt(); 23 | /// 24 | /// Returns a random floating-point number that is greater than or equal to 25 | /// 0.0, and less than 1.0. 26 | /// 27 | float NextFloat(); 28 | /// 29 | /// Returns a random floating-point number that is greater than or equal to 30 | /// 0.0, and less than 1.0. 31 | /// 32 | double NextDouble(); 33 | /// 34 | /// Generates a random integer between min (inclusive) and max (exclusive). 35 | /// 36 | /// Minimum value (inclusive). 37 | /// Maximum value (exclusive). 38 | /// An integer between [min, max) 39 | int RangeInt(int min, int max); 40 | /// 41 | /// Generates a random float between min (inclusive) and max (exclusive). 42 | /// 43 | /// Minimum value (inclusive). 44 | /// Maximum value (exclusive). 45 | /// A float between [min, max) 46 | float RangeFloat(float min, float max); 47 | /// 48 | /// Generates a random double between min (inclusive) and max (exclusive). 49 | /// 50 | /// Minimum value (inclusive). 51 | /// Maximum value (exclusive). 52 | /// A double between [min, max) 53 | double RangeDouble(double min, double max); 54 | } 55 | 56 | /// 57 | /// Random number generator default implementation. 58 | /// 59 | public record Rng : IRng { 60 | /// 61 | public int Seed { get; init; } 62 | /// 63 | public System.Random Generator { get; private set; } 64 | 65 | /// 66 | /// Creates a new random number generator with the current value of 67 | /// as the seed. 68 | /// 69 | public Rng() { 70 | Seed = System.Environment.TickCount; 71 | Generator = new System.Random(Seed); 72 | } 73 | 74 | /// 75 | /// Creates a new random number generator with the given seed. 76 | /// 77 | /// Random number generator seed. 78 | public Rng(int seed) { 79 | Generator = new System.Random(seed); 80 | Seed = seed; 81 | } 82 | 83 | /// 84 | /// Creates a new random number generator with the given seed and generator. 85 | /// 86 | /// Random number generator seed. 87 | /// Random number generator. 88 | public Rng(int seed, System.Random generator) { 89 | Generator = generator; 90 | Seed = seed; 91 | } 92 | 93 | /// 94 | public int NextInt() => Generator.Next(); 95 | 96 | /// 97 | public float NextFloat() => (float)Generator.NextDouble(); 98 | 99 | /// 100 | public double NextDouble() => Generator.NextDouble(); 101 | 102 | /// 103 | public int RangeInt(int min, int max) => Generator.Next(min, max); 104 | 105 | /// 106 | public float RangeFloat(float min, float max) 107 | => (float)( 108 | (Generator.NextDouble() * ((double)max - (double)min)) + (double)min 109 | ); 110 | 111 | /// 112 | public double RangeDouble(double min, double max) 113 | => (Generator.NextDouble() * (max - min)) + min; 114 | } 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 Chickensoft 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chickensoft.GoDotNet 2 | 3 | [![Chickensoft Badge][chickensoft-badge]][chickensoft-website] [![Discord][discord-badge]][discord] [![Read the docs][read-the-docs-badge]][docs] ![line coverage][line-coverage] ![branch coverage][branch-coverage] 4 | 5 | > ⛔️🧨💥 **This package is no longer maintained and has been superseded by other projects.** 6 | > 7 | > For better alternatives to the tools provided here, please see the following: 8 | > 9 | > - State machines — use [LogicBlocks]. 10 | > - Notifiers - use C#'s [BehaviorSubject] from the [Reactive Extensions]. 11 | > - Scheduler - use [Godot's built-in call_deferred mechanism][call-deferred]. 12 | 13 | State machines, notifiers, and other utilities for C# Godot development. 14 | 15 | --- 16 | 17 |

18 | Chickensoft.GoDotNet 19 |

20 | 21 | > 🚨 Looking for node-based dependency injection with providers and dependents? That functionality has been moved to it's own package, [GoDotDep][go_dot_dep]! 22 | 23 | While developing our own game, we couldn't find any simple C# solutions for simple state machines, notifiers, and mechanisms for avoiding unnecessary marshalling with Godot. So, we built our own systems — hopefully you can benefit from them, too! 24 | 25 | > Are you on discord? If you're building games with Godot and C#, we'd love to see you in the [Chickensoft Discord server][discord]! 26 | 27 | ## Installation 28 | 29 | Find the latest version of [GoDotNet][go_dot_net_nuget] on nuget. 30 | 31 | In your `*.csproj`, add the following snippet in your ``, save, and run `dotnet restore`. Make sure to replace `*VERSION*` with the latest version. 32 | 33 | ```xml 34 | 35 | ``` 36 | 37 | GoDotNet is itself written in C# 10 for `netstandard2.1` (the highest language version currently supported by Godot). If you want to setup your project the same way, look no further than the [`GoDotNet.csproj`](GoDotNet.csproj) file for inspiration! 38 | 39 | ## Logging 40 | 41 | Internally, GoDotNet uses [GoDotLog] for logging. GoDotLog allows you to easily create loggers that output nicely formatted, prefixed messages (in addition to asserts and other exception-aware execution utilities). 42 | 43 | ## Autoloads 44 | 45 | An autoload can be fetched easily from any node. Once an autoload is found on the root child, GoDotNet caches it's type, allowing it to be looked up instantaneously without calling into Godot. 46 | 47 | ```csharp 48 | public class MyEntity : Node { 49 | private MyAutoloadType _myAutoload => this.Autoload(); 50 | 51 | public override void _Ready() { 52 | _myAutoload.DoSomething(); 53 | var otherAutoload = this.Autoload(); 54 | } 55 | } 56 | ``` 57 | 58 | ## Scheduling 59 | 60 | A `Scheduler` node is included which allows callbacks to be run on the next frame, similar to [CallDeferred][call-deferred]. Unlike `CallDeferred`, the scheduler uses vanilla C# to avoid marshalling types to Godot. Since Godot cannot marshal objects that don't extend `Godot.Object`/`Godot.Reference`, this utility is provided to perform the same function for records, custom types, and C# collections which otherwise couldn't be marshaled between C# and Godot. 61 | 62 | Create a new autoload which extends the scheduler: 63 | 64 | ```csharp 65 | using GoDotNet; 66 | 67 | public class GameScheduler : Scheduler { } 68 | ``` 69 | 70 | Add it to your `project.godot` file (preferably the first entry): 71 | 72 | ```ini 73 | [autoload] 74 | 75 | GameScheduler="*res://autoload_folder/GameScheduler.cs" 76 | ``` 77 | 78 | ...and simply schedule a callback to run on the next frame: 79 | 80 | ```csharp 81 | this.Autoload().NextFrame( 82 | () => _log.Print("I won't execute until the next frame.") 83 | ) 84 | ``` 85 | 86 | ## State Machines 87 | 88 | GoDotNet provides a simple state machine implementation that emits a C# event when the state changes (since [Godot signals are more fragile](#signals-and-events)). If you try to update the machine to a state that isn't a valid transition from the current state, it throws an exception. The machine requires an initial state to avoid nullability issues during construction. 89 | 90 | State machines are not extensible — instead, GoDotNet almost always prefers the pattern of [composition over inheritance][composition-inheritance]. The state machine relies on state equality to determine if the state has changed to avoid issuing unnecessary events. Using `record` or other value types for the state makes equality checking work automatically for free. 91 | 92 | States used with a state machine must implement `IMachineState`, where T is just the type of the machine state. Your machine states can optionally implement the method `CanTransitionTo(IMachineState state)`, which should return true if the given "next state" is a valid transition. Otherwise, the default implementation returns `true` to allow transitions to any state. 93 | 94 | To create states for use with a machine, create an interface which implements `IMachineState`. Then, create record types for each state which implement your interface, optionally overriding `CanTransitionTo` for any states which only allow transitions to specific states. 95 | 96 | ```csharp 97 | public interface IGameState : IMachineState { } 98 | 99 | public record GameMainMenuState : IGameState { 100 | public bool CanTransitionTo(IGameState state) => state is GameLoadingState; 101 | } 102 | 103 | public record GameLoadingState : IGameState { 104 | public bool CanTransitionTo(IGameState state) => state is GamePlayingState; 105 | } 106 | 107 | // States can store values! 108 | public record GamePlayingState(string PlayerName) { 109 | public bool CanTransitionTo(IGameState state) => state is GameMainMenuState; 110 | } 111 | ``` 112 | 113 | Simply omit implementing `CanTransitionTo` for any states which should allow transitions to any other state. 114 | 115 | ```csharp 116 | public interface GameSuspended : IGameState { } 117 | ``` 118 | 119 | Machines are fairly simple to use: create one with an initial state (and optionally register a machine state change event handler). A state machine will announce the state has changed as soon as it is constructed. 120 | 121 | ```csharp 122 | public class GameManager : Node { 123 | private readonly Machine _machine; 124 | 125 | public override void _Ready() { 126 | _machine = new Machine(new GameMainMenuState(), OnChanged); 127 | } 128 | 129 | /// Starts the game. 130 | public void Start(string name) { 131 | _machine.Update(new GameLoadingState()); 132 | // do your loading... 133 | // ... 134 | // start the game! 135 | _machine.Update(new GamePlayingState(name); 136 | // Read the current state at any time: 137 | var state = _machine.State; 138 | if (state is GamePlayingState) { /* ... */ } 139 | } 140 | 141 | /// Goes back to the menu. 142 | public void GoToMenu() => _machine.Update(new GameMainMenuState()); 143 | 144 | public void OnChanged(IGameState state) { 145 | if (state is GamePlayingState playingState) { 146 | var playerName = playingState.Name(); 147 | // ... 148 | } 149 | } 150 | } 151 | ``` 152 | 153 | ### Read-only State Machines 154 | 155 | If you want another object to only be able to read the current state of a state machine and subscribe to changes, but not be able to update the state of the machine, you can expose the machine as an `IReadOnlyMachine` instead of as a `Machine`. 156 | 157 | ```csharp 158 | public class AnObjectThatOnlyListensToAMachine { 159 | public IReadOnlyMachine Machine { get; set; } 160 | 161 | // This object can listen and respond to state changes in Machine, but 162 | // cannot mutate the state of the machine via `Machine.Update` :) 163 | } 164 | ``` 165 | 166 | ## Notifiers 167 | 168 | A notifier is an object which emits a signal when its value changes. Notifiers are similar to state machines, but they don't care about transitions. Any update that changes the value (determined by comparing the new value with the previous value using `Object.Equals`) will emit a signal. Like state machines, the value of a notifier can never be `null` — make sure you initialize with a valid value! 169 | 170 | Using "value" types (primitive types, records, and structs) with a notifier is a natural fit since notifiers check equality to determine if the value has changed. Like state machines, notifiers also invoke an event to announce their value as soon as they are constructed. 171 | 172 | ```csharp 173 | var notifier = new Notifier("Player", OnPlayerNameChanged); 174 | notifier.Update("Godot"); 175 | 176 | // Elsewhere... 177 | 178 | private void OnPlayerNameChanged(string name, string previous) { 179 | _log.Print($"Player name changed from ${previous} to ${name}"); 180 | } 181 | ``` 182 | 183 | > To easily inject dependencies to descendent nodes, check out [go_dot_dep]. 184 | 185 | ### Read-only Notifiers 186 | 187 | As with state machines, a notifiers can be referenced as an `IReadOnlyNotifier` to prevent objects using them from causing unwanted changes. The object(s) that own the notifier can reference it as a `Notifier` and mutate it accordingly, while the listener objects can simply reference it as an `IReadOnlyNotifier`. 188 | 189 | ```csharp 190 | public class AnObjectThatOnlyListensToANotifier { 191 | public IReadOnlyNotifier Notifier { get; set; } 192 | 193 | // This object can listen and respond to changes in the Notifier's value, but 194 | // cannot mutate the value of the notifier via `Notifier.Update` :) 195 | } 196 | ``` 197 | 198 | ## Signals and Events 199 | 200 | Godot supports emitting [signals] from C#. Because Godot signals pass through the Godot engine, any arguments given to the signal must be marshalled through Godot, forcing them to be classes which extend `Godot.Object`/`Godot.Reference` (records aren't allowed). Likewise, all the fields in the class must also be the same kind of types so they can be marshalled, and so on. 201 | 202 | It's not possible to have static typing with signal parameters, so you don't find out until runtime if you accidentally passed the wrong parameter. The closest you can do is the following, which wouldn't break at compile time if the receiving function signature happened to be wrong. 203 | 204 | ```csharp 205 | public class ObjectType { 206 | [Signal] 207 | public delegate void DoSomething(string value); 208 | } 209 | 210 | public class MyNode : Node { 211 | // Inside your node 212 | public override void _Ready() { 213 | _ = object.Connect( 214 | nameof(ObjectType.DoSomething), 215 | this, 216 | nameof(MyDoSomething) 217 | ); 218 | } 219 | 220 | private void DoSomething(int value) { 221 | // WARNING: Value should be string, not int!! 222 | } 223 | } 224 | ``` 225 | 226 | Because of these limitations, GoDotNet will avoid Godot signals except when necessary to interact with Godot components. For communication between C# game logic, it will typically be preferable to use C# events instead. 227 | 228 | ```csharp 229 | // Declare an event signature — no [Signal] attribute necessary. 230 | public delegate void Changed(Type1 value1, Type2 value2); 231 | 232 | // Add an event field to your emitter 233 | public event Changed? OnChanged; 234 | 235 | // Trigger an event from your emitter: 236 | OnChanged?.Invoke(argument1, argument2); 237 | 238 | // Listen to an event in your receiver: 239 | var emitter = new MyEmitterObject(); 240 | emitter.OnChanged += MyOnChangedHandler; 241 | 242 | // Event handler in your receiver: 243 | private void MyOnChangedHandler(Type1 value1, Type2 value2) { 244 | // respond to event we received 245 | } 246 | ``` 247 | 248 | ## Random Number Generator 249 | 250 | GoDotNet includes a testable convenience wrapper for `System.Random`. 251 | 252 | ```csharp 253 | public partial class MyNode : Node { 254 | public IRng Random { get; set; } = new Rng(); 255 | 256 | public override void _Ready() { 257 | int i = Rng.NextInt(); // 0 (inclusive) to int.MaxValue (exclusive) 258 | float f = Rng.NextFloat(); // 0 (inclusive) to 1 (exclusive) 259 | double d = Rng.NextDouble(); // 0 (inclusive) to 1 (exclusive) 260 | 261 | // 0 (inclusive) to 25 (exclusive) 262 | int ir = Rng.RangeInt(0, 25); 263 | 264 | // 0.0f (inclusive) to 25.0f (exclusive) 265 | float fr = Rng.RangeFloat(0f, 25f); 266 | 267 | // 0.0d (inclusive) to 25.0d (exclusive) 268 | double dr = Rng.RangeDouble(0d, 25d); 269 | } 270 | } 271 | ``` 272 | 273 | --- 274 | 275 | 🐣 Package generated from a 🐤 Chickensoft Template — 276 | 277 | 278 | 279 | 280 | [chickensoft-badge]: https://raw.githubusercontent.com/chickensoft-games/chickensoft_site/main/static/img/badges/chickensoft_badge.svg 281 | [chickensoft-website]: https://chickensoft.games 282 | [discord-badge]: https://raw.githubusercontent.com/chickensoft-games/chickensoft_site/main/static/img/badges/discord_badge.svg 283 | [discord]: https://discord.gg/gSjaPgMmYW 284 | [read-the-docs-badge]: https://raw.githubusercontent.com/chickensoft-games/chickensoft_site/main/static/img/badges/read_the_docs_badge.svg 285 | [docs]: https://chickensoft.games/docsickensoft%20Discord-%237289DA.svg?style=flat&logo=discord&logoColor=white 286 | [line-coverage]: Chickensoft.GoDotNet.Tests/badges/line_coverage.svg 287 | [branch-coverage]: Chickensoft.GoDotNet.Tests/badges/branch_coverage.svg 288 | 289 | 290 | [go_dot_dep]: https://github.com/chickensoft-games/go_dot_dep 291 | [go_dot_net_nuget]: https://www.nuget.org/packages/Chickensoft.GoDotNet/ 292 | [GoDotLog]: https://github.com/chickensoft-games/go_dot_log 293 | [call-deferred]: https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-method-call-deferred 294 | [signals]: https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_features.html#c-signals 295 | [composition-inheritance]: https://en.wikipedia.org/wiki/Composition_over_inheritance 296 | [LogicBlocks]: https://github.com/chickensoft-games/LogicBlocks 297 | [BehaviorSubject]: https://learn.microsoft.com/en-us/previous-versions/dotnet/reactive-extensions/hh211949 298 | [Reactive Extensions]: https://www.nuget.org/packages/System.Reactive/ 299 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "**/*.*" 4 | ], 5 | "ignorePaths": [ 6 | "**/*.tscn", 7 | "**/*.import", 8 | "Chickensoft.GoDotNet.Tests/badges/**/*.*", 9 | "Chickensoft.GoDotNet.Tests/coverage/**/*.*", 10 | "Chickensoft.GoDotNet.Tests/.godot/**/*.*", 11 | "**/obj/**/*.*", 12 | "**/bin/**/*.*", 13 | "Chickensoft.GoDotNet/nupkg/**/*.*" 14 | ], 15 | "words": [ 16 | "assemblyfilters", 17 | "Autoload", 18 | "autoloads", 19 | "automerge", 20 | "branchcoverage", 21 | "brandedoutcast", 22 | "camelcase", 23 | "chickenpackage", 24 | "Chickensoft", 25 | "classfilters", 26 | "contentfiles", 27 | "CYGWIN", 28 | "endregion", 29 | "Gamedev", 30 | "globaltool", 31 | "godotengine", 32 | "godotpackage", 33 | "inheritdoc", 34 | "issuecomment", 35 | "lcov", 36 | "linecoverage", 37 | "methodcoverage", 38 | "missingall", 39 | "msbuild", 40 | "MSYS", 41 | "nameof", 42 | "netstandard", 43 | "NOLOGO", 44 | "nupkg", 45 | "Omnisharp", 46 | "opencover", 47 | "OPTOUT", 48 | "paramref", 49 | "pascalcase", 50 | "renovatebot", 51 | "reportgenerator", 52 | "reporttypes", 53 | "Shouldly", 54 | "structs", 55 | "subfolders", 56 | "targetargs", 57 | "targetdir", 58 | "tscn", 59 | "typeof", 60 | "typeparam", 61 | "typeparamref", 62 | "ulong", 63 | "Xunit" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /docs/renovatebot_pr.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b63d75d932457b93f79f54e5c4192d201a29aefb4e437a3732c63099ec8f78a1 3 | size 148110 4 | -------------------------------------------------------------------------------- /docs/spelling_fix.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:870bad263d4bbf803f2278af6e4346cf1e9c07c48c51d85c40ad9b5b63f761b2 3 | size 71136 4 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.100-preview.7.23376.3", 4 | "rollForward": "latestMinor" 5 | }, 6 | "msbuild-sdks": { 7 | "Godot.NET.Sdk": "4.1.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":semanticCommits" 6 | ], 7 | "prHourlyLimit": 2, 8 | "versioning": "loose", 9 | "packageRules": [ 10 | { 11 | "matchPackagePatterns": [ 12 | "*" 13 | ], 14 | "groupName": "all dependencies", 15 | "groupSlug": "all-deps", 16 | "automerge": true 17 | }, 18 | { 19 | "matchPackagePrefixes": [ 20 | "GodotSharp", 21 | "Godot.NET.Sdk" 22 | ], 23 | "allowedVersions": "/^(\\d+\\.\\d+\\.\\d+)(-(beta|rc)\\.(\\d+)(\\.\\d+)*)?$/" 24 | }, 25 | { 26 | "matchPackagePrefixes": [ 27 | "Chickensoft" 28 | ], 29 | "allowedVersions": "/^(\\d+\\.\\d+\\.\\d+)(-godot(\\d+\\.)+\\d+(-.*)?)?$/" 30 | } 31 | ] 32 | } 33 | --------------------------------------------------------------------------------