Submodule Plugins/UnrealSharp deleted from a5e501bc43
									
								
							
							
								
								
									
										2
									
								
								Plugins/UnrealSharp/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Plugins/UnrealSharp/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| # Auto detect text files and perform LF normalization | ||||
| * text=auto | ||||
							
								
								
									
										84
									
								
								Plugins/UnrealSharp/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								Plugins/UnrealSharp/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | ||||
| # Visual Studio 2015 user specific files | ||||
| .vs/ | ||||
|  | ||||
| # Compiled Object files | ||||
| *.slo | ||||
| *.lo | ||||
| *.o | ||||
| *.obj | ||||
|  | ||||
| # Precompiled Headers | ||||
| *.gch | ||||
| *.pch | ||||
|  | ||||
| # Compiled Dynamic libraries | ||||
| *.so | ||||
| *.dylib | ||||
| *.dll | ||||
|  | ||||
| # Fortran module files | ||||
| *.mod | ||||
|  | ||||
| # Compiled Static libraries | ||||
| *.lai | ||||
| *.la | ||||
| *.a | ||||
| *.lib | ||||
|  | ||||
| # Executables | ||||
| *.exe | ||||
| *.out | ||||
| *.app | ||||
| *.ipa | ||||
|  | ||||
| # These project files can be generated by the engine | ||||
| *.xcodeproj | ||||
| *.xcworkspace | ||||
| *.suo | ||||
| *.opensdf | ||||
| *.sdf | ||||
| *.VC.db | ||||
| *.VC.opendb | ||||
|  | ||||
| # Precompiled Assets | ||||
| SourceArt/**/*.png | ||||
| SourceArt/**/*.tga | ||||
|  | ||||
| # Binary Files | ||||
| Binaries/* | ||||
| Plugins/*/Binaries/* | ||||
|  | ||||
| # Builds | ||||
| Build/* | ||||
|  | ||||
| # Whitelist PakBlacklist-<BuildConfiguration>.txt files | ||||
| !Build/*/ | ||||
| Build/*/** | ||||
| !Build/*/PakBlacklist*.txt | ||||
|  | ||||
| # Don't ignore icon files in Build | ||||
| !Build/**/*.ico | ||||
|  | ||||
| # Built data for maps | ||||
| *_BuiltData.uasset | ||||
|  | ||||
| # Configuration files generated by the Editor | ||||
| Saved/* | ||||
|  | ||||
| # Compiled source files for the engine to use | ||||
| Intermediate/* | ||||
| Plugins/*/Intermediate/* | ||||
|  | ||||
| # Cache files for the editor to use | ||||
| DerivedDataCache/* | ||||
|  | ||||
| *.user | ||||
|  | ||||
| .idea/ | ||||
|  | ||||
| bin/ | ||||
| Generated/ | ||||
| obj/ | ||||
|  | ||||
| *.props | ||||
| !Directory.Packages.props | ||||
							
								
								
									
										117
									
								
								Plugins/UnrealSharp/Config/Default.UnrealSharpTypes.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								Plugins/UnrealSharp/Config/Default.UnrealSharpTypes.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,117 @@ | ||||
| { | ||||
|     "Structs": { | ||||
|         "CustomTypes": [  | ||||
|             "FInstancedStruct"  | ||||
|         ], | ||||
|         "BlittableTypes": [ | ||||
|             { | ||||
|                 "Name": "EStreamingSourcePriority" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "ETriggerEvent" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector2D", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector2D" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector_NetQuantize", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector_NetQuantize10", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector_NetQuantize100", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector_NetQuantizeNormal", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector2f", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector2f" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector3f", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector3f" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FVector4f", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector4f" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FQuatf4", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FVector4f" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FRotator", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FRotator" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FMatrix44f", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FMatrix44f" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FTransform", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FTransform" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FTimerHandle", | ||||
|                 "ManagedType": "UnrealSharp.Engine.FTimerHandle" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FInputActionValue", | ||||
|                 "ManagedType": "UnrealSharp.EnhancedInput.FInputActionValue" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FRandomStream", | ||||
|                 "ManagedType": "UnrealSharp.CoreUObject.FRandomStream" | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FSubsystemCollectionBaseRef", | ||||
|                 "ManagedType": "UnrealSharp.UnrealSharpCore.FSubsystemCollectionBaseRef" | ||||
|             } | ||||
|         ], | ||||
|         "NativelyTranslatableTypes": [ | ||||
|             { | ||||
|                 "Name": "FMoverDataCollection", | ||||
|                 "HasDestructor": false | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FPaintContext", | ||||
|                 "HasDestructor": false | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FGeometry", | ||||
|                 "HasDestructor": false | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FGameplayEffectSpec", | ||||
|                 "HasDestructor": true | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FGameplayEffectSpecHandle", | ||||
|                 "HasDestructor": true | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FKeyEvent", | ||||
|                 "HasDestructor": false | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FInputEvent", | ||||
|                 "HasDestructor": false | ||||
|             }, | ||||
|             { | ||||
|                 "Name": "FKey", | ||||
|                 "HasDestructor": false | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										5
									
								
								Plugins/UnrealSharp/Config/DefaultUnrealSharp.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Plugins/UnrealSharp/Config/DefaultUnrealSharp.ini
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| [CoreRedirects] | ||||
| +ClassRedirects=(OldName="/Script/CSharpForUE.CSClass",NewName="/Script/UnrealSharpCore.CSClass") | ||||
| +StructRedirects=(OldName="/Script/CSharpForUE.CSScriptStruct",NewName="/Script/UnrealSharpCore.CSScriptStruct") | ||||
| +EnumRedirects=(OldName="/Script/CSharpForUE.CSEnum",NewName="/Script/UnrealSharpCore.CSEnum") | ||||
| +ClassRedirects=(OldName="/Script/UnrealSharpCore.CSDeveloperSettings",NewName="/Script/UnrealSharpCore.CSUnrealSharpSettings") | ||||
							
								
								
									
										8
									
								
								Plugins/UnrealSharp/Config/FilterPlugin.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Plugins/UnrealSharp/Config/FilterPlugin.ini
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| [FilterPlugin] | ||||
| ; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and | ||||
| ; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. | ||||
| ; | ||||
| ; Examples: | ||||
| ;    /README.txt | ||||
| ;    /Extras/... | ||||
| ;    /Binaries/ThirdParty/*.dll | ||||
							
								
								
									
										18
									
								
								Plugins/UnrealSharp/Directory.Packages.props
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Plugins/UnrealSharp/Directory.Packages.props
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| <Project> | ||||
|   <PropertyGroup> | ||||
|     <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> | ||||
|   </PropertyGroup> | ||||
|   <ItemGroup> | ||||
|     <PackageVersion Include="CommandLineParser" Version="2.9.1" /> | ||||
|     <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> | ||||
|     <PackageVersion Include="LanguageExt.Core" Version="4.4.9" /> | ||||
|     <PackageVersion Include="Mono.Cecil" Version="0.11.6" /> | ||||
|     <PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.13.9" /> | ||||
|     <PackageVersion Include="Microsoft.Build" Version="17.13.9" /> | ||||
|     <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" /> | ||||
|     <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" /> | ||||
|     <PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.13.0" /> | ||||
|     <PackageVersion Include="Microsoft.CSharp" Version="4.7.0" /> | ||||
|     <PackageVersion Include="Microsoft.Build.Locator" Version="1.9.1" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
							
								
								
									
										21
									
								
								Plugins/UnrealSharp/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Plugins/UnrealSharp/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2025 UnrealSharp | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
| @ -0,0 +1,47 @@ | ||||
| // Licensed to the .NET Foundation under one or more agreements. | ||||
| // The .NET Foundation licenses this file to you under the MIT license. | ||||
|  | ||||
| #ifndef __CORECLR_DELEGATES_H__ | ||||
| #define __CORECLR_DELEGATES_H__ | ||||
|  | ||||
| #include <stdint.h> | ||||
|  | ||||
| #if defined(_WIN32) | ||||
|     #define CORECLR_DELEGATE_CALLTYPE __stdcall | ||||
|     #ifdef _WCHAR_T_DEFINED | ||||
|         typedef wchar_t char_t; | ||||
|     #else | ||||
|         typedef unsigned short char_t; | ||||
|     #endif | ||||
| #else | ||||
|     #define CORECLR_DELEGATE_CALLTYPE | ||||
|     typedef char char_t; | ||||
| #endif | ||||
|  | ||||
| #define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1) | ||||
|  | ||||
| // Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer | ||||
| typedef int (CORECLR_DELEGATE_CALLTYPE *load_assembly_and_get_function_pointer_fn)( | ||||
|     const char_t *assembly_path      /* Fully qualified path to assembly */, | ||||
|     const char_t *type_name          /* Assembly qualified type name */, | ||||
|     const char_t *method_name        /* Public static method name compatible with delegateType */, | ||||
|     const char_t *delegate_type_name /* Assembly qualified delegate type name or null | ||||
|                                         or UNMANAGEDCALLERSONLY_METHOD if the method is marked with | ||||
|                                         the UnmanagedCallersOnlyAttribute. */, | ||||
|     void         *reserved           /* Extensibility parameter (currently unused and must be 0) */, | ||||
|     /*out*/ void **delegate          /* Pointer where to store the function pointer result */); | ||||
|  | ||||
| // Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default) | ||||
| typedef int (CORECLR_DELEGATE_CALLTYPE *component_entry_point_fn)(void *arg, int32_t arg_size_in_bytes); | ||||
|  | ||||
| typedef int (CORECLR_DELEGATE_CALLTYPE *get_function_pointer_fn)( | ||||
|     const char_t *type_name          /* Assembly qualified type name */, | ||||
|     const char_t *method_name        /* Public static method name compatible with delegateType */, | ||||
|     const char_t *delegate_type_name /* Assembly qualified delegate type name or null, | ||||
|                                         or UNMANAGEDCALLERSONLY_METHOD if the method is marked with | ||||
|                                         the UnmanagedCallersOnlyAttribute. */, | ||||
|     void         *load_context       /* Extensibility parameter (currently unused and must be 0) */, | ||||
|     void         *reserved           /* Extensibility parameter (currently unused and must be 0) */, | ||||
|     /*out*/ void **delegate          /* Pointer where to store the function pointer result */); | ||||
|  | ||||
| #endif // __CORECLR_DELEGATES_H__ | ||||
							
								
								
									
										323
									
								
								Plugins/UnrealSharp/Managed/DotNetRuntime/inc/hostfxr.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								Plugins/UnrealSharp/Managed/DotNetRuntime/inc/hostfxr.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,323 @@ | ||||
| // Licensed to the .NET Foundation under one or more agreements. | ||||
| // The .NET Foundation licenses this file to you under the MIT license. | ||||
|  | ||||
| #ifndef __HOSTFXR_H__ | ||||
| #define __HOSTFXR_H__ | ||||
|  | ||||
| #include <stddef.h> | ||||
| #include <stdint.h> | ||||
|  | ||||
| #if defined(_WIN32) | ||||
|     #define HOSTFXR_CALLTYPE __cdecl | ||||
|     #ifdef _WCHAR_T_DEFINED | ||||
|         typedef wchar_t char_t; | ||||
|     #else | ||||
|         typedef unsigned short char_t; | ||||
|     #endif | ||||
| #else | ||||
|     #define HOSTFXR_CALLTYPE | ||||
|     typedef char char_t; | ||||
| #endif | ||||
|  | ||||
| enum hostfxr_delegate_type | ||||
| { | ||||
|     hdt_com_activation, | ||||
|     hdt_load_in_memory_assembly, | ||||
|     hdt_winrt_activation, | ||||
|     hdt_com_register, | ||||
|     hdt_com_unregister, | ||||
|     hdt_load_assembly_and_get_function_pointer, | ||||
|     hdt_get_function_pointer, | ||||
| }; | ||||
|  | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_fn)(const int argc, const char_t **argv); | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_startupinfo_fn)( | ||||
|     const int argc, | ||||
|     const char_t **argv, | ||||
|     const char_t *host_path, | ||||
|     const char_t *dotnet_root, | ||||
|     const char_t *app_path); | ||||
| typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)( | ||||
|     const int argc, | ||||
|     const char_t** argv, | ||||
|     const char_t* host_path, | ||||
|     const char_t* dotnet_root, | ||||
|     const char_t* app_path, | ||||
|     int64_t bundle_header_offset); | ||||
|  | ||||
| typedef void(HOSTFXR_CALLTYPE *hostfxr_error_writer_fn)(const char_t *message); | ||||
|  | ||||
| // | ||||
| // Sets a callback which is to be used to write errors to. | ||||
| // | ||||
| // Parameters: | ||||
| //     error_writer | ||||
| //         A callback function which will be invoked every time an error is to be reported. | ||||
| //         Or nullptr to unregister previously registered callback and return to the default behavior. | ||||
| // Return value: | ||||
| //     The previously registered callback (which is now unregistered), or nullptr if no previous callback | ||||
| //     was registered | ||||
| // | ||||
| // The error writer is registered per-thread, so the registration is thread-local. On each thread | ||||
| // only one callback can be registered. Subsequent registrations overwrite the previous ones. | ||||
| // | ||||
| // By default no callback is registered in which case the errors are written to stderr. | ||||
| // | ||||
| // Each call to the error writer is sort of like writing a single line (the EOL character is omitted). | ||||
| // Multiple calls to the error writer may occure for one failure. | ||||
| // | ||||
| // If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer | ||||
| // will be propagated to hostpolicy for the duration of the call. This means that errors from | ||||
| // both hostfxr and hostpolicy will be reporter through the same error writer. | ||||
| // | ||||
| typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE *hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer); | ||||
|  | ||||
| typedef void* hostfxr_handle; | ||||
| struct hostfxr_initialize_parameters | ||||
| { | ||||
|     size_t size; | ||||
|     const char_t *host_path; | ||||
|     const char_t *dotnet_root; | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Initializes the hosting components for a dotnet command line running an application | ||||
| // | ||||
| // Parameters: | ||||
| //    argc | ||||
| //      Number of argv arguments | ||||
| //    argv | ||||
| //      Command-line arguments for running an application (as if through the dotnet executable). | ||||
| //      Only command-line arguments which are accepted by runtime installation are supported, SDK/CLI commands are not supported. | ||||
| //      For example 'app.dll app_argument_1 app_argument_2`. | ||||
| //    parameters | ||||
| //      Optional. Additional parameters for initialization | ||||
| //    host_context_handle | ||||
| //      On success, this will be populated with an opaque value representing the initialized host context | ||||
| // | ||||
| // Return value: | ||||
| //    Success          - Hosting components were successfully initialized | ||||
| //    HostInvalidState - Hosting components are already initialized | ||||
| // | ||||
| // This function parses the specified command-line arguments to determine the application to run. It will | ||||
| // then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and | ||||
| // dependencies and prepare everything needed to load the runtime. | ||||
| // | ||||
| // This function only supports arguments for running an application. It does not support SDK commands. | ||||
| // | ||||
| // This function does not load the runtime. | ||||
| // | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_dotnet_command_line_fn)( | ||||
|     int argc, | ||||
|     const char_t **argv, | ||||
|     const struct hostfxr_initialize_parameters *parameters, | ||||
|     /*out*/ hostfxr_handle *host_context_handle); | ||||
|  | ||||
| // | ||||
| // Initializes the hosting components using a .runtimeconfig.json file | ||||
| // | ||||
| // Parameters: | ||||
| //    runtime_config_path | ||||
| //      Path to the .runtimeconfig.json file | ||||
| //    parameters | ||||
| //      Optional. Additional parameters for initialization | ||||
| //    host_context_handle | ||||
| //      On success, this will be populated with an opaque value representing the initialized host context | ||||
| // | ||||
| // Return value: | ||||
| //    Success                            - Hosting components were successfully initialized | ||||
| //    Success_HostAlreadyInitialized     - Config is compatible with already initialized hosting components | ||||
| //    Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components | ||||
| //    CoreHostIncompatibleConfig         - Config is incompatible with already initialized hosting components | ||||
| // | ||||
| // This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed | ||||
| // to load the runtime. It will only process the .deps.json from frameworks (not any app/component that | ||||
| // may be next to the .runtimeconfig.json). | ||||
| // | ||||
| // This function does not load the runtime. | ||||
| // | ||||
| // If called when the runtime has already been loaded, this function will check if the specified runtime | ||||
| // config is compatible with the existing runtime. | ||||
| // | ||||
| // Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful | ||||
| // initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that | ||||
| // the difference in properties is acceptable. | ||||
| // | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_runtime_config_fn)( | ||||
|     const char_t *runtime_config_path, | ||||
|     const struct hostfxr_initialize_parameters *parameters, | ||||
|     /*out*/ hostfxr_handle *host_context_handle); | ||||
|  | ||||
| // | ||||
| // Gets the runtime property value for an initialized host context | ||||
| // | ||||
| // Parameters: | ||||
| //     host_context_handle | ||||
| //       Handle to the initialized host context | ||||
| //     name | ||||
| //       Runtime property name | ||||
| //     value | ||||
| //       Out parameter. Pointer to a buffer with the property value. | ||||
| // | ||||
| // Return value: | ||||
| //     The error code result. | ||||
| // | ||||
| // The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only | ||||
| // guaranteed until any of the below occur: | ||||
| //   - a 'run' method is called for the host context | ||||
| //   - properties are changed via hostfxr_set_runtime_property_value | ||||
| //   - the host context is closed via 'hostfxr_close' | ||||
| // | ||||
| // If host_context_handle is nullptr and an active host context exists, this function will get the | ||||
| // property value for the active host context. | ||||
| // | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_property_value_fn)( | ||||
|     const hostfxr_handle host_context_handle, | ||||
|     const char_t *name, | ||||
|     /*out*/ const char_t **value); | ||||
|  | ||||
| // | ||||
| // Sets the value of a runtime property for an initialized host context | ||||
| // | ||||
| // Parameters: | ||||
| //     host_context_handle | ||||
| //       Handle to the initialized host context | ||||
| //     name | ||||
| //       Runtime property name | ||||
| //     value | ||||
| //       Value to set | ||||
| // | ||||
| // Return value: | ||||
| //     The error code result. | ||||
| // | ||||
| // Setting properties is only supported for the first host context, before the runtime has been loaded. | ||||
| // | ||||
| // If the property already exists in the host context, it will be overwritten. If value is nullptr, the | ||||
| // property will be removed. | ||||
| // | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_set_runtime_property_value_fn)( | ||||
|     const hostfxr_handle host_context_handle, | ||||
|     const char_t *name, | ||||
|     const char_t *value); | ||||
|  | ||||
| // | ||||
| // Gets all the runtime properties for an initialized host context | ||||
| // | ||||
| // Parameters: | ||||
| //     host_context_handle | ||||
| //       Handle to the initialized host context | ||||
| //     count | ||||
| //       [in] Size of the keys and values buffers | ||||
| //       [out] Number of properties returned (size of keys/values buffers used). If the input value is too | ||||
| //             small or keys/values is nullptr, this is populated with the number of available properties | ||||
| //     keys | ||||
| //       Array of pointers to buffers with runtime property keys | ||||
| //     values | ||||
| //       Array of pointers to buffers with runtime property values | ||||
| // | ||||
| // Return value: | ||||
| //     The error code result. | ||||
| // | ||||
| // The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only | ||||
| // guaranteed until any of the below occur: | ||||
| //   - a 'run' method is called for the host context | ||||
| //   - properties are changed via hostfxr_set_runtime_property_value | ||||
| //   - the host context is closed via 'hostfxr_close' | ||||
| // | ||||
| // If host_context_handle is nullptr and an active host context exists, this function will get the | ||||
| // properties for the active host context. | ||||
| // | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_properties_fn)( | ||||
|     const hostfxr_handle host_context_handle, | ||||
|     /*inout*/ size_t * count, | ||||
|     /*out*/ const char_t **keys, | ||||
|     /*out*/ const char_t **values); | ||||
|  | ||||
| // | ||||
| // Load CoreCLR and run the application for an initialized host context | ||||
| // | ||||
| // Parameters: | ||||
| //     host_context_handle | ||||
| //       Handle to the initialized host context | ||||
| // | ||||
| // Return value: | ||||
| //     If the app was successfully run, the exit code of the application. Otherwise, the error code result. | ||||
| // | ||||
| // The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line. | ||||
| // | ||||
| // This function will not return until the managed application exits. | ||||
| // | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_run_app_fn)(const hostfxr_handle host_context_handle); | ||||
|  | ||||
| // | ||||
| // Gets a typed delegate from the currently loaded CoreCLR or from a newly created one. | ||||
| // | ||||
| // Parameters: | ||||
| //     host_context_handle | ||||
| //       Handle to the initialized host context | ||||
| //     type | ||||
| //       Type of runtime delegate requested | ||||
| //     delegate | ||||
| //       An out parameter that will be assigned the delegate. | ||||
| // | ||||
| // Return value: | ||||
| //     The error code result. | ||||
| // | ||||
| // If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config, | ||||
| // then all delegate types are supported. | ||||
| // If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line, | ||||
| // then only the following delegate types are currently supported: | ||||
| //     hdt_load_assembly_and_get_function_pointer | ||||
| //     hdt_get_function_pointer | ||||
| // | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_delegate_fn)( | ||||
|     const hostfxr_handle host_context_handle, | ||||
|     enum hostfxr_delegate_type type, | ||||
|     /*out*/ void **delegate); | ||||
|  | ||||
| // | ||||
| // Closes an initialized host context | ||||
| // | ||||
| // Parameters: | ||||
| //     host_context_handle | ||||
| //       Handle to the initialized host context | ||||
| // | ||||
| // Return value: | ||||
| //     The error code result. | ||||
| // | ||||
| typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_close_fn)(const hostfxr_handle host_context_handle); | ||||
|  | ||||
| struct hostfxr_dotnet_environment_sdk_info | ||||
| { | ||||
|     size_t size; | ||||
|     const char_t* version; | ||||
|     const char_t* path; | ||||
| }; | ||||
|  | ||||
| typedef void(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_result_fn)( | ||||
|     const struct hostfxr_dotnet_environment_info* info, | ||||
|     void* result_context); | ||||
|  | ||||
| struct hostfxr_dotnet_environment_framework_info | ||||
| { | ||||
|     size_t size; | ||||
|     const char_t* name; | ||||
|     const char_t* version; | ||||
|     const char_t* path; | ||||
| }; | ||||
|  | ||||
| struct hostfxr_dotnet_environment_info | ||||
| { | ||||
|     size_t size; | ||||
|  | ||||
|     const char_t* hostfxr_version; | ||||
|     const char_t* hostfxr_commit_hash; | ||||
|  | ||||
|     size_t sdk_count; | ||||
|     const hostfxr_dotnet_environment_sdk_info* sdks; | ||||
|  | ||||
|     size_t framework_count; | ||||
|     const hostfxr_dotnet_environment_framework_info* frameworks; | ||||
| }; | ||||
|  | ||||
| #endif //__HOSTFXR_H__ | ||||
							
								
								
									
										99
									
								
								Plugins/UnrealSharp/Managed/DotNetRuntime/inc/nethost.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								Plugins/UnrealSharp/Managed/DotNetRuntime/inc/nethost.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| // Licensed to the .NET Foundation under one or more agreements. | ||||
| // The .NET Foundation licenses this file to you under the MIT license. | ||||
|  | ||||
| #ifndef __NETHOST_H__ | ||||
| #define __NETHOST_H__ | ||||
|  | ||||
| #include <stddef.h> | ||||
|  | ||||
| #ifdef _WIN32 | ||||
|     #ifdef NETHOST_EXPORT | ||||
|         #define NETHOST_API __declspec(dllexport) | ||||
|     #else | ||||
|         // Consuming the nethost as a static library | ||||
|         // Shouldn't export attempt to dllimport. | ||||
|         #ifdef NETHOST_USE_AS_STATIC | ||||
|             #define NETHOST_API  | ||||
|         #else | ||||
|             #define NETHOST_API __declspec(dllimport) | ||||
|         #endif | ||||
|     #endif | ||||
|  | ||||
|     #define NETHOST_CALLTYPE __stdcall | ||||
|     #ifdef _WCHAR_T_DEFINED | ||||
|         typedef wchar_t char_t; | ||||
|     #else | ||||
|         typedef unsigned short char_t; | ||||
|     #endif | ||||
| #else | ||||
|     #ifdef NETHOST_EXPORT | ||||
|         #define NETHOST_API __attribute__((__visibility__("default"))) | ||||
|     #else | ||||
|         #define NETHOST_API | ||||
|     #endif | ||||
|  | ||||
|     #define NETHOST_CALLTYPE | ||||
|     typedef char char_t; | ||||
| #endif | ||||
|  | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
|  | ||||
| // Parameters for get_hostfxr_path | ||||
| // | ||||
| // Fields: | ||||
| //   size | ||||
| //     Size of the struct. This is used for versioning. | ||||
| // | ||||
| //   assembly_path | ||||
| //     Path to the compenent's assembly. | ||||
| //     If specified, hostfxr is located as if the assembly_path is the apphost | ||||
| // | ||||
| //   dotnet_root | ||||
| //     Path to directory containing the dotnet executable. | ||||
| //     If specified, hostfxr is located as if an application is started using | ||||
| //     'dotnet app.dll', which means it will be searched for under the dotnet_root | ||||
| //     path and the assembly_path is ignored. | ||||
| // | ||||
| struct get_hostfxr_parameters { | ||||
|     size_t size; | ||||
|     const char_t *assembly_path; | ||||
|     const char_t *dotnet_root; | ||||
| }; | ||||
|  | ||||
| // | ||||
| // Get the path to the hostfxr library | ||||
| // | ||||
| // Parameters: | ||||
| //   buffer | ||||
| //     Buffer that will be populated with the hostfxr path, including a null terminator. | ||||
| // | ||||
| //   buffer_size | ||||
| //     [in] Size of buffer in char_t units. | ||||
| //     [out] Size of buffer used in char_t units. If the input value is too small | ||||
| //           or buffer is nullptr, this is populated with the minimum required size | ||||
| //           in char_t units for a buffer to hold the hostfxr path | ||||
| // | ||||
| //   get_hostfxr_parameters | ||||
| //     Optional. Parameters that modify the behaviour for locating the hostfxr library. | ||||
| //     If nullptr, hostfxr is located using the enviroment variable or global registration | ||||
| // | ||||
| // Return value: | ||||
| //   0 on success, otherwise failure | ||||
| //   0x80008098 - buffer is too small (HostApiBufferTooSmall) | ||||
| // | ||||
| // Remarks: | ||||
| //   The full search for the hostfxr library is done on every call. To minimize the need | ||||
| //   to call this function multiple times, pass a large buffer (e.g. PATH_MAX). | ||||
| // | ||||
| NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path( | ||||
|     char_t * buffer, | ||||
|     size_t * buffer_size, | ||||
|     const struct get_hostfxr_parameters *parameters); | ||||
|  | ||||
| #ifdef __cplusplus | ||||
| } // extern "C" | ||||
| #endif | ||||
|  | ||||
| #endif // __NETHOST_H__ | ||||
							
								
								
									
										252
									
								
								Plugins/UnrealSharp/Managed/Shared/DotNetUtilities.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								Plugins/UnrealSharp/Managed/Shared/DotNetUtilities.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,252 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Collections.ObjectModel; | ||||
| using System.Diagnostics; | ||||
| using System.IO; | ||||
| using System.Text; | ||||
|  | ||||
| namespace UnrealSharp.Shared; | ||||
|  | ||||
| public static class DotNetUtilities | ||||
| { | ||||
| 	public const string DOTNET_MAJOR_VERSION = "9.0"; | ||||
| 	public const string DOTNET_MAJOR_VERSION_DISPLAY = "net" + DOTNET_MAJOR_VERSION; | ||||
|  | ||||
|     public static string FindDotNetExecutable() | ||||
|     { | ||||
| 		const string DOTNET_WIN = "dotnet.exe"; | ||||
| 		const string DOTNET_UNIX = "dotnet"; | ||||
|  | ||||
| 		var dotnetExe = OperatingSystem.IsWindows() ? DOTNET_WIN : DOTNET_UNIX; | ||||
|  | ||||
|     	var pathVariable = Environment.GetEnvironmentVariable("PATH"); | ||||
|  | ||||
|     	if (pathVariable == null) | ||||
|     	{ | ||||
|     		throw new Exception($"Couldn't find {dotnetExe}!"); | ||||
|     	} | ||||
|  | ||||
|     	var paths = pathVariable.Split(Path.PathSeparator); | ||||
|  | ||||
|     	foreach (var path in paths) | ||||
|     	{ | ||||
|     		// This is a hack to avoid using the dotnet.exe from the Unreal Engine installation directory. | ||||
|     		// Can't use the dotnet.exe from the Unreal Engine installation directory because it's .NET 6.0 | ||||
|     		if (!path.Contains(@"\dotnet\")) | ||||
|     		{ | ||||
|     			continue; | ||||
|     		} | ||||
|  | ||||
|     		var dotnetExePath = Path.Combine(path, dotnetExe); | ||||
|  | ||||
|     		if (File.Exists(dotnetExePath)) | ||||
|     		{ | ||||
|     			return dotnetExePath; | ||||
|     		} | ||||
|     	} | ||||
|  | ||||
|     	if ( OperatingSystem.IsMacOS() ) { | ||||
| 			if ( File.Exists( "/usr/local/share/dotnet/dotnet" ) ) { | ||||
| 				return "/usr/local/share/dotnet/dotnet"; | ||||
| 			} | ||||
| 			if ( File.Exists( "/opt/homebrew/bin/dotnet" ) ) { | ||||
| 				return "/opt/homebrew/bin/dotnet"; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		throw new Exception($"Couldn't find {dotnetExe} in PATH!"); | ||||
|     } | ||||
|  | ||||
|     public static string GetLatestDotNetSdkPath() | ||||
|     { | ||||
| 	    string dotNetExecutable = FindDotNetExecutable(); | ||||
| 	    string dotNetExecutableDirectory = Path.GetDirectoryName(dotNetExecutable)!; | ||||
| 	    string dotNetSdkDirectory = Path.Combine(dotNetExecutableDirectory!, "sdk"); | ||||
|  | ||||
| 	    string[] folderPaths = Directory.GetDirectories(dotNetSdkDirectory); | ||||
|  | ||||
| 	    string highestVersion = "0.0.0"; | ||||
|  | ||||
| 	    foreach (string folderPath in folderPaths) | ||||
| 	    { | ||||
| 		    string folderName = Path.GetFileName(folderPath); | ||||
|  | ||||
| 		    if (string.IsNullOrEmpty(folderName) || !char.IsDigit(folderName[0])) | ||||
| 		    { | ||||
| 			    continue; | ||||
| 		    } | ||||
|  | ||||
| 		    if (string.Compare(folderName, highestVersion, StringComparison.Ordinal) > 0) | ||||
| 		    { | ||||
| 			    highestVersion = folderName; | ||||
| 		    } | ||||
| 	    } | ||||
|  | ||||
| 	    if (highestVersion == "0.0.0") | ||||
| 	    { | ||||
| 		    throw new Exception("Failed to find the latest .NET SDK version."); | ||||
| 	    } | ||||
|  | ||||
| 	    if (!highestVersion.StartsWith(DOTNET_MAJOR_VERSION)) | ||||
| 	    { | ||||
| 		    throw new Exception($"Failed to find the latest .NET SDK version. Expected version to start with {DOTNET_MAJOR_VERSION} but found: {highestVersion}"); | ||||
| 	    } | ||||
|  | ||||
| 	    return Path.Combine(dotNetSdkDirectory, highestVersion); | ||||
|     } | ||||
|  | ||||
|     public static void BuildSolution(string projectRootDirectory, string managedBinariesPath) | ||||
|     { | ||||
|     	if (!Directory.Exists(projectRootDirectory)) | ||||
|     	{ | ||||
|     		throw new Exception($"Couldn't find project root directory: {projectRootDirectory}"); | ||||
|     	} | ||||
|  | ||||
| 	    if (!Directory.Exists(managedBinariesPath)) | ||||
| 	    { | ||||
| 		    Directory.CreateDirectory(managedBinariesPath); | ||||
| 	    } | ||||
|  | ||||
|     	Collection<string> arguments = new Collection<string> | ||||
| 		{ | ||||
| 			"publish", | ||||
| 			$"-p:PublishDir=\"{managedBinariesPath}\"" | ||||
| 		}; | ||||
|  | ||||
| 	    InvokeDotNet(arguments, projectRootDirectory); | ||||
|     } | ||||
|  | ||||
|     public static bool InvokeDotNet(Collection<string> arguments, string? workingDirectory = null) | ||||
|     { | ||||
| 	    string dotnetPath = FindDotNetExecutable(); | ||||
|  | ||||
| 	    var startInfo = new ProcessStartInfo | ||||
| 	    { | ||||
| 		    FileName = dotnetPath, | ||||
| 		    RedirectStandardOutput = true, | ||||
| 		    RedirectStandardError = true | ||||
| 	    }; | ||||
|  | ||||
| 	    foreach (string argument in arguments) | ||||
| 	    { | ||||
| 		    startInfo.ArgumentList.Add(argument); | ||||
| 	    } | ||||
|  | ||||
| 	    if (workingDirectory != null) | ||||
| 	    { | ||||
| 		    startInfo.WorkingDirectory = workingDirectory; | ||||
| 	    } | ||||
|  | ||||
| 	    // Set the MSBuild environment variables to the latest .NET SDK that U# supports. | ||||
| 	    // Otherwise, we'll use the .NET SDK that comes with the Unreal Engine. | ||||
| 	    { | ||||
| 		    string latestDotNetSdkPath = GetLatestDotNetSdkPath(); | ||||
| 		    startInfo.Environment["MSBuildExtensionsPath"] = latestDotNetSdkPath; | ||||
| 		    startInfo.Environment["MSBUILD_EXE_PATH"] = $@"{latestDotNetSdkPath}\MSBuild.dll"; | ||||
| 		    startInfo.Environment["MSBuildSDKsPath"] = $@"{latestDotNetSdkPath}\Sdks"; | ||||
| 	    } | ||||
|  | ||||
| 	    using Process process = new Process(); | ||||
| 	    process.StartInfo = startInfo; | ||||
|  | ||||
| 	    try | ||||
| 	    { | ||||
| 		    StringBuilder outputBuilder = new StringBuilder(); | ||||
| 		    process.OutputDataReceived += (sender, e) => | ||||
| 		    { | ||||
| 			    if (e.Data != null) | ||||
| 			    { | ||||
| 				    outputBuilder.AppendLine(e.Data); | ||||
| 			    } | ||||
| 		    }; | ||||
|              | ||||
| 		    process.ErrorDataReceived += (sender, e) => | ||||
| 		    { | ||||
| 			    if (e.Data != null) | ||||
| 			    { | ||||
| 				    outputBuilder.AppendLine(e.Data); | ||||
| 			    } | ||||
| 		    }; | ||||
|              | ||||
| 		    if (!process.Start()) | ||||
| 		    { | ||||
| 			    throw new Exception("Failed to start process"); | ||||
| 		    } | ||||
|              | ||||
| 		    process.BeginErrorReadLine(); | ||||
| 		    process.BeginOutputReadLine(); | ||||
| 		    process.WaitForExit(); | ||||
|  | ||||
| 		    if (process.ExitCode != 0) | ||||
| 		    { | ||||
| 			    string errorMessage = outputBuilder.ToString(); | ||||
| 			     | ||||
| 			    if (string.IsNullOrEmpty(errorMessage)) | ||||
| 			    { | ||||
| 				    errorMessage = "Process exited with non-zero exit code but no output was captured."; | ||||
| 			    } | ||||
| 			     | ||||
| 			    throw new Exception($"Process failed with exit code {process.ExitCode}: {errorMessage}"); | ||||
| 		    } | ||||
| 	    } | ||||
| 	    catch (Exception ex) | ||||
| 	    { | ||||
| 		    Console.WriteLine($"An error occurred: {ex.Message}"); | ||||
| 		    return false; | ||||
| 	    } | ||||
| 	     | ||||
| 	    return true; | ||||
|     } | ||||
|  | ||||
|     public static bool InvokeUSharpBuildTool(string action, | ||||
| 	    string managedBinariesPath, | ||||
| 	    string projectName, | ||||
| 	    string pluginDirectory, | ||||
| 	    string projectDirectory, | ||||
| 	    string engineDirectory, | ||||
| 	    IEnumerable<KeyValuePair<string, string>>? additionalArguments = null) | ||||
|     { | ||||
| 	    string dotNetExe = FindDotNetExecutable(); | ||||
| 	    string unrealSharpBuildToolPath = Path.Combine(managedBinariesPath, "UnrealSharpBuildTool.dll"); | ||||
|  | ||||
| 	    if (!File.Exists(unrealSharpBuildToolPath)) | ||||
| 	    { | ||||
| 		    throw new Exception($"Failed to find UnrealSharpBuildTool.dll at: {unrealSharpBuildToolPath}"); | ||||
| 	    } | ||||
|  | ||||
| 	    Collection<string> arguments = new Collection<string> | ||||
| 	    { | ||||
| 		    unrealSharpBuildToolPath, | ||||
|  | ||||
| 		    "--Action", | ||||
| 		    action, | ||||
|  | ||||
| 		    "--EngineDirectory", | ||||
| 		    $"{engineDirectory}", | ||||
|  | ||||
| 		    "--ProjectDirectory", | ||||
| 		    $"{projectDirectory}", | ||||
|  | ||||
| 		    "--ProjectName", | ||||
| 		    projectName, | ||||
|  | ||||
| 		    "--PluginDirectory", | ||||
| 		    $"{pluginDirectory}", | ||||
|  | ||||
| 		    "--DotNetPath", | ||||
| 		    $"{dotNetExe}" | ||||
| 	    }; | ||||
|  | ||||
| 	    if (additionalArguments != null) | ||||
| 	    { | ||||
| 		    arguments.Add("--AdditionalArgs"); | ||||
|  | ||||
| 		    foreach (KeyValuePair<string, string> argument in additionalArguments) | ||||
| 		    { | ||||
| 			    arguments.Add($"{argument.Key}={argument.Value}"); | ||||
| 		    } | ||||
| 	    } | ||||
|  | ||||
| 	    return InvokeDotNet(arguments); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| namespace UnrealSharp.Binds; | ||||
|  | ||||
| public static class NativeBinds | ||||
| { | ||||
|     private unsafe static delegate* unmanaged[Cdecl]<char*, char*, int, IntPtr> _getBoundFunction = null; | ||||
|  | ||||
|     public unsafe static void InitializeNativeBinds(IntPtr bindsCallbacks) | ||||
|     { | ||||
|         if (_getBoundFunction != null) | ||||
|         { | ||||
|             throw new Exception("NativeBinds.InitializeNativeBinds called twice"); | ||||
|         } | ||||
|  | ||||
|         _getBoundFunction = (delegate* unmanaged[Cdecl]<char*, char*, int, IntPtr>)bindsCallbacks; | ||||
|     } | ||||
|  | ||||
|     public unsafe static IntPtr TryGetBoundFunction(string outerName, string functionName, int functionSize) | ||||
|     { | ||||
|         if (_getBoundFunction == null) | ||||
|         { | ||||
|             throw new Exception("NativeBinds not initialized"); | ||||
|         } | ||||
|  | ||||
|         IntPtr functionPtr = IntPtr.Zero; | ||||
|         fixed (char* outerNamePtr = outerName) | ||||
|         fixed (char* functionNamePtr = functionName) | ||||
|         { | ||||
|             functionPtr = _getBoundFunction(outerNamePtr, functionNamePtr, functionSize); | ||||
|         } | ||||
|  | ||||
|         if (functionPtr == IntPtr.Zero) | ||||
|         { | ||||
|             throw new Exception($"Failed to find bound function {functionName} in {outerName}"); | ||||
|         } | ||||
|  | ||||
|         return functionPtr; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,4 @@ | ||||
| namespace UnrealSharp.Binds; | ||||
|  | ||||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] | ||||
| public class NativeCallbacksAttribute : Attribute; | ||||
| @ -0,0 +1,9 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
|     </PropertyGroup> | ||||
| </Project> | ||||
| @ -0,0 +1,3 @@ | ||||
| namespace UnrealSharp.Core.Attributes; | ||||
|  | ||||
| public class BindingAttribute : Attribute; | ||||
| @ -0,0 +1,4 @@ | ||||
| namespace UnrealSharp.Core.Attributes; | ||||
|  | ||||
| [AttributeUsage(AttributeTargets.Struct)] | ||||
| public class BlittableTypeAttribute : Attribute; | ||||
| @ -0,0 +1,17 @@ | ||||
| namespace UnrealSharp.Core.Attributes; | ||||
|  | ||||
| /// <summary> | ||||
| /// Used to mark a type as generated. Don't use this attribute in your code. | ||||
| /// It's public since glue for user code is generated in the user's project. | ||||
| /// </summary> | ||||
| public class GeneratedTypeAttribute : Attribute | ||||
| { | ||||
|     public GeneratedTypeAttribute(string engineName, string fullName = "") | ||||
|     { | ||||
|         EngineName = engineName; | ||||
|         FullName = fullName; | ||||
|     } | ||||
|  | ||||
|     public string EngineName; | ||||
|     public string FullName; | ||||
| } | ||||
| @ -0,0 +1,42 @@ | ||||
| using System.Runtime.InteropServices; | ||||
|  | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| /// <summary> | ||||
| /// Struct representing a handle to a delegate. | ||||
| /// </summary> | ||||
| [StructLayout(LayoutKind.Sequential)] | ||||
| public struct FDelegateHandle : IEquatable<FDelegateHandle> | ||||
| { | ||||
|     public ulong ID; | ||||
|  | ||||
|     public void Reset() | ||||
|     { | ||||
|         ID = 0; | ||||
|     } | ||||
|  | ||||
|     public static bool operator ==(FDelegateHandle a, FDelegateHandle b) | ||||
|     { | ||||
|         return a.Equals(b); | ||||
|     } | ||||
|  | ||||
|     public static bool operator !=(FDelegateHandle a, FDelegateHandle b) | ||||
|     { | ||||
|         return !a.Equals(b); | ||||
|     } | ||||
|  | ||||
|     public override bool Equals(object? obj) | ||||
|     { | ||||
|         return obj is FDelegateHandle handle && Equals(handle); | ||||
|     } | ||||
|  | ||||
|     public bool Equals(FDelegateHandle other) | ||||
|     { | ||||
|         return ID == other.ID; | ||||
|     } | ||||
|      | ||||
|     public override int GetHashCode() | ||||
|     { | ||||
|         return ID.GetHashCode(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,22 @@ | ||||
| using UnrealSharp.Binds; | ||||
|  | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| [NativeCallbacks] | ||||
| public static unsafe partial class FCSManagerExporter | ||||
| { | ||||
|     public static delegate* unmanaged<IntPtr, IntPtr> FindManagedObject; | ||||
|     public static delegate* unmanaged<IntPtr, IntPtr, IntPtr> FindOrCreateManagedInterfaceWrapper; | ||||
|     public static delegate* unmanaged<IntPtr> GetCurrentWorldContext; | ||||
|     public static delegate* unmanaged<IntPtr> GetCurrentWorldPtr; | ||||
|      | ||||
|     public static UnrealSharpObject WorldContextObject | ||||
|     { | ||||
|         get | ||||
|         { | ||||
|             IntPtr worldContextObject = CallGetCurrentWorldContext(); | ||||
|             IntPtr handle = CallFindManagedObject(worldContextObject); | ||||
|             return GCHandleUtilities.GetObjectFromHandlePtr<UnrealSharpObject>(handle)!; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,99 @@ | ||||
| using System.Collections.Concurrent; | ||||
| using System.Reflection; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Runtime.Loader; | ||||
|  | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| public static class GCHandleUtilities | ||||
| { | ||||
|     private static readonly ConcurrentDictionary<AssemblyLoadContext, ConcurrentDictionary<GCHandle, object>> StrongRefsByAssembly = new(); | ||||
|  | ||||
|     [MethodImpl(MethodImplOptions.NoInlining)] | ||||
|     private static void OnAlcUnloading(AssemblyLoadContext alc) | ||||
|     { | ||||
|         StrongRefsByAssembly.TryRemove(alc, out _); | ||||
|     } | ||||
|  | ||||
|     public static GCHandle AllocateStrongPointer(object value, object allocator) | ||||
|     { | ||||
|         return AllocateStrongPointer(value, allocator.GetType().Assembly); | ||||
|     } | ||||
|      | ||||
|     public static GCHandle AllocateStrongPointer(object value, Assembly alc) | ||||
|     { | ||||
|         AssemblyLoadContext? assemblyLoadContext = AssemblyLoadContext.GetLoadContext(alc); | ||||
|          | ||||
|         if (assemblyLoadContext == null) | ||||
|         { | ||||
|             throw new InvalidOperationException("AssemblyLoadContext is null."); | ||||
|         } | ||||
|  | ||||
|         return AllocateStrongPointer(value, assemblyLoadContext); | ||||
|     } | ||||
|  | ||||
|     public static GCHandle AllocateStrongPointer(object value, AssemblyLoadContext loadContext) | ||||
|     { | ||||
|         GCHandle weakHandle = GCHandle.Alloc(value, GCHandleType.Weak); | ||||
|         ConcurrentDictionary<GCHandle, object> strongReferences = StrongRefsByAssembly.GetOrAdd(loadContext, alcInstance => | ||||
|         { | ||||
|             alcInstance.Unloading += OnAlcUnloading; | ||||
|             return new ConcurrentDictionary<GCHandle, object>(); | ||||
|         }); | ||||
|  | ||||
|         strongReferences.TryAdd(weakHandle, value); | ||||
|  | ||||
|         return weakHandle; | ||||
|     } | ||||
|  | ||||
|     [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
|     public static GCHandle AllocateWeakPointer(object value) => GCHandle.Alloc(value, GCHandleType.Weak); | ||||
|          | ||||
|     [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
|     public static GCHandle AllocatePinnedPointer(object value) => GCHandle.Alloc(value, GCHandleType.Pinned); | ||||
|  | ||||
|     [MethodImpl(MethodImplOptions.NoInlining)] | ||||
|     public static void Free(GCHandle handle, Assembly? assembly) | ||||
|     { | ||||
|         if (assembly != null) | ||||
|         { | ||||
|             AssemblyLoadContext? assemblyLoadContext = AssemblyLoadContext.GetLoadContext(assembly); | ||||
|              | ||||
|             if (assemblyLoadContext == null) | ||||
|             { | ||||
|                 throw new InvalidOperationException("AssemblyLoadContext is null."); | ||||
|             } | ||||
|              | ||||
|             if (StrongRefsByAssembly.TryGetValue(assemblyLoadContext, out ConcurrentDictionary<GCHandle, object>? strongReferences)) | ||||
|             { | ||||
|                 strongReferences.TryRemove(handle, out _); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         handle.Free(); | ||||
|     } | ||||
|          | ||||
|     public static T? GetObjectFromHandlePtr<T>(IntPtr handle) | ||||
|     { | ||||
|         if (handle == IntPtr.Zero) | ||||
|         { | ||||
|             return default; | ||||
|         } | ||||
|          | ||||
|         GCHandle subObjectGcHandle = GCHandle.FromIntPtr(handle); | ||||
|         if (!subObjectGcHandle.IsAllocated) | ||||
|         { | ||||
|             return default; | ||||
|         } | ||||
|          | ||||
|         object? subObject = subObjectGcHandle.Target; | ||||
|         if (subObject is T typedObject) | ||||
|         { | ||||
|             return typedObject; | ||||
|         } | ||||
|  | ||||
|         return default; | ||||
|          | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| using UnrealSharp.Binds; | ||||
|  | ||||
| namespace UnrealSharp.Core.Interop; | ||||
|  | ||||
| [NativeCallbacks] | ||||
| public static unsafe partial class FScriptArrayExporter | ||||
| { | ||||
|     public static delegate* unmanaged<UnmanagedArray*, IntPtr> GetData; | ||||
|     public static delegate* unmanaged<UnmanagedArray*, int, NativeBool> IsValidIndex; | ||||
|     public static delegate* unmanaged<UnmanagedArray*, int> Num; | ||||
|     public static delegate* unmanaged<UnmanagedArray*, void> Destroy; | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| using UnrealSharp.Binds; | ||||
|  | ||||
| namespace UnrealSharp.Interop; | ||||
|  | ||||
| [NativeCallbacks] | ||||
| public unsafe partial class FStringExporter | ||||
| { | ||||
|     public static delegate* unmanaged<IntPtr, char*, void> MarshalToNativeString; | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| using UnrealSharp.Binds; | ||||
|  | ||||
| namespace UnrealSharp.Core.Interop; | ||||
|  | ||||
| [NativeCallbacks] | ||||
| public unsafe partial class ManagedHandleExporter | ||||
| { | ||||
|     public static delegate* unmanaged<IntPtr, IntPtr, void> StoreManagedHandle; | ||||
|     public static delegate* unmanaged<IntPtr, IntPtr> LoadManagedHandle; | ||||
|     public static delegate* unmanaged<IntPtr, IntPtr, int, void> StoreUnmanagedMemory; | ||||
|     public static delegate* unmanaged<IntPtr, IntPtr, int, void> LoadUnmanagedMemory; | ||||
| } | ||||
| @ -0,0 +1,6 @@ | ||||
| using UnrealSharp.Log; | ||||
|  | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| [CustomLog] | ||||
| public static partial class LogUnrealSharpCore; | ||||
| @ -0,0 +1,31 @@ | ||||
| using System.Runtime.InteropServices; | ||||
|  | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| [StructLayout(LayoutKind.Sequential)] | ||||
| public unsafe struct ManagedCallbacks | ||||
| {    | ||||
|     public delegate* unmanaged<IntPtr, IntPtr, char**, IntPtr> ScriptManagerBridge_CreateManagedObject; | ||||
|     public delegate* unmanaged<IntPtr, IntPtr, IntPtr> ScriptManagerBridge_CreateNewManagedObjectWrapper; | ||||
|     public delegate* unmanaged<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, int> ScriptManagerBridge_InvokeManagedMethod; | ||||
|     public delegate* unmanaged<IntPtr, void> ScriptManagerBridge_InvokeDelegate; | ||||
|     public delegate* unmanaged<IntPtr, char*, IntPtr> ScriptManagerBridge_LookupManagedMethod; | ||||
|     public delegate* unmanaged<IntPtr, char*, IntPtr> ScriptManagedBridge_LookupManagedType; | ||||
|     public delegate* unmanaged<IntPtr, IntPtr, void> ScriptManagedBridge_Dispose; | ||||
|     public delegate* unmanaged<IntPtr, void> ScriptManagedBridge_FreeHandle; | ||||
|  | ||||
|     public static void Initialize(IntPtr outManagedCallbacks) | ||||
|     { | ||||
|         *(ManagedCallbacks*)outManagedCallbacks = new ManagedCallbacks | ||||
|         { | ||||
|             ScriptManagerBridge_CreateManagedObject = &UnmanagedCallbacks.CreateNewManagedObject, | ||||
|             ScriptManagerBridge_CreateNewManagedObjectWrapper = &UnmanagedCallbacks.CreateNewManagedObjectWrapper, | ||||
|             ScriptManagerBridge_InvokeManagedMethod = &UnmanagedCallbacks.InvokeManagedMethod, | ||||
|             ScriptManagerBridge_InvokeDelegate = &UnmanagedCallbacks.InvokeDelegate, | ||||
|             ScriptManagerBridge_LookupManagedMethod = &UnmanagedCallbacks.LookupManagedMethod, | ||||
|             ScriptManagedBridge_LookupManagedType = &UnmanagedCallbacks.LookupManagedType, | ||||
|             ScriptManagedBridge_Dispose = &UnmanagedCallbacks.Dispose, | ||||
|             ScriptManagedBridge_FreeHandle = &UnmanagedCallbacks.FreeHandle, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,25 @@ | ||||
| namespace UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| public static class BitfieldBoolMarshaller | ||||
| { | ||||
|     private const int BoolSize = sizeof(NativeBool); | ||||
|      | ||||
|     public static void ToNative(IntPtr valuePtr, byte fieldMask, bool value) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             var byteValue = (byte*)valuePtr; | ||||
|             var mask = value ? fieldMask : byte.MinValue; | ||||
|             *byteValue = (byte)((*byteValue & ~fieldMask) | mask); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static bool FromNative(IntPtr valuePtr, byte fieldMask) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             var byteValue = (byte*)valuePtr; | ||||
|             return (*byteValue & fieldMask) != 0; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,42 @@ | ||||
| using System.Runtime.CompilerServices; | ||||
|  | ||||
| namespace UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| public static class BlittableMarshaller<T> where T : unmanaged, allows ref struct | ||||
| { | ||||
|     [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
|     public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             ToNative(nativeBuffer, arrayIndex, obj, sizeof(T)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
|     public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj, int size) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             *(T*)(nativeBuffer + arrayIndex * size) = obj; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
|     public static T FromNative(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             return FromNative(nativeBuffer, arrayIndex, sizeof(T)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
|     public static T FromNative(IntPtr nativeBuffer, int arrayIndex, int size) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             return *(T*)(nativeBuffer + arrayIndex * size); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,14 @@ | ||||
| namespace UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| public static class BoolMarshaller | ||||
| { | ||||
|     public static void ToNative(IntPtr nativeBuffer, int arrayIndex, bool obj) | ||||
|     { | ||||
|         BlittableMarshaller<NativeBool>.ToNative(nativeBuffer, arrayIndex, obj.ToNativeBool()); | ||||
|     } | ||||
|      | ||||
|     public static bool FromNative(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         return BlittableMarshaller<NativeBool>.FromNative(nativeBuffer, arrayIndex).ToManagedBool(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,16 @@ | ||||
| namespace UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| public static class EnumMarshaller<T> where T : Enum | ||||
| { | ||||
|     public static T FromNative(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         byte value = BlittableMarshaller<byte>.FromNative(nativeBuffer, arrayIndex); | ||||
|         return (T) Enum.ToObject(typeof(T), value); | ||||
|     } | ||||
|      | ||||
|     public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj) | ||||
|     { | ||||
|         byte value = Convert.ToByte(obj); | ||||
|         BlittableMarshaller<byte>.ToNative(nativeBuffer, arrayIndex, value); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,14 @@ | ||||
| using System.Diagnostics.Contracts; | ||||
|  | ||||
| namespace UnrealSharp; | ||||
|  | ||||
| public interface MarshalledStruct<Self> where Self : MarshalledStruct<Self>, allows ref struct | ||||
| { | ||||
|     public static abstract IntPtr GetNativeClassPtr(); | ||||
|  | ||||
|     public static abstract int GetNativeDataSize(); | ||||
|  | ||||
|     public static abstract Self FromNative(IntPtr buffer); | ||||
|  | ||||
|     public void ToNative(IntPtr buffer); | ||||
| } | ||||
| @ -0,0 +1,8 @@ | ||||
| namespace UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| public static class MarshallingDelegates<T> | ||||
| { | ||||
|     public delegate void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj); | ||||
|     public delegate T FromNative(IntPtr nativeBuffer, int arrayIndex); | ||||
|     public delegate void DestructInstance(IntPtr nativeBuffer, int arrayIndex); | ||||
| } | ||||
| @ -0,0 +1,27 @@ | ||||
| namespace UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| public static class ObjectMarshaller<T> where T : UnrealSharpObject | ||||
| { | ||||
|     public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj) | ||||
|     { | ||||
|         IntPtr uObjectPosition = nativeBuffer + arrayIndex * IntPtr.Size; | ||||
|  | ||||
|         unsafe | ||||
|         { | ||||
|             *(IntPtr*)uObjectPosition = obj?.NativeObject ?? IntPtr.Zero; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public static T FromNative(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         IntPtr uObjectPointer = BlittableMarshaller<IntPtr>.FromNative(nativeBuffer, arrayIndex); | ||||
|          | ||||
|         if (uObjectPointer == IntPtr.Zero) | ||||
|         { | ||||
|             return null!; | ||||
|         } | ||||
|          | ||||
|         IntPtr handle = FCSManagerExporter.CallFindManagedObject(uObjectPointer); | ||||
|         return GCHandleUtilities.GetObjectFromHandlePtr<T>(handle)!; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,43 @@ | ||||
| using UnrealSharp.Interop; | ||||
|  | ||||
| namespace UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| public static class StringMarshaller | ||||
| { | ||||
|     public static void ToNative(IntPtr nativeBuffer, int arrayIndex, string obj) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(obj))  | ||||
|             { | ||||
|                 //Guard against C# null strings (use string.Empty instead) | ||||
|                 obj = string.Empty;  | ||||
|             } | ||||
|              | ||||
|             IntPtr unrealString = nativeBuffer + arrayIndex * sizeof(UnmanagedArray); | ||||
|              | ||||
|             fixed (char* stringPtr = obj) | ||||
|             { | ||||
|                 FStringExporter.CallMarshalToNativeString(unrealString, stringPtr); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public static string FromNative(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             UnmanagedArray unrealString = BlittableMarshaller<UnmanagedArray>.FromNative(nativeBuffer, arrayIndex); | ||||
|             return unrealString.Data == IntPtr.Zero ? string.Empty : new string((char*) unrealString.Data); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public static void DestructInstance(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             UnmanagedArray* unrealString = (UnmanagedArray*) (nativeBuffer + arrayIndex * sizeof(UnmanagedArray)); | ||||
|             unrealString->Destroy(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,14 @@ | ||||
| namespace UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| public static class StructMarshaller<T> where T : MarshalledStruct<T> | ||||
| { | ||||
|     public static T FromNative(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         return T.FromNative(nativeBuffer + arrayIndex * T.GetNativeDataSize()); | ||||
|     } | ||||
|  | ||||
|     public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj) | ||||
|     { | ||||
|         obj.ToNative(nativeBuffer + arrayIndex * T.GetNativeDataSize()); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| namespace UnrealSharp.Engine.Core.Modules; | ||||
|  | ||||
| public interface IModuleInterface  | ||||
| { | ||||
|     public void StartupModule(); | ||||
|     public void ShutdownModule(); | ||||
| } | ||||
| @ -0,0 +1,22 @@ | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| // Bools are not blittable, so we need to convert them to bytes | ||||
| public enum NativeBool : byte | ||||
| { | ||||
|     False = 0, | ||||
|     True = 1 | ||||
| } | ||||
|  | ||||
| public static class BoolConverter | ||||
| {  | ||||
|     public static NativeBool ToNativeBool(this bool value) | ||||
|     { | ||||
|         return value ? NativeBool.True : NativeBool.False; | ||||
|     } | ||||
|  | ||||
|     public static bool ToManagedBool(this NativeBool value) | ||||
|     { | ||||
|         byte byteValue = (byte) value; | ||||
|         return byteValue != 0; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,65 @@ | ||||
| using System.Runtime.InteropServices; | ||||
| using UnrealSharp.Core.Interop; | ||||
| using UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| [StructLayout(LayoutKind.Sequential)] | ||||
| public struct UnmanagedArray | ||||
| { | ||||
|     public IntPtr Data; | ||||
|     public int ArrayNum; | ||||
|     public int ArrayMax; | ||||
|      | ||||
|     public void Destroy() | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             fixed (UnmanagedArray* ptr = &this) | ||||
|             { | ||||
|                 FScriptArrayExporter.CallDestroy(ptr); | ||||
|             } | ||||
|              | ||||
|             Data = IntPtr.Zero; | ||||
|             ArrayNum = 0; | ||||
|             ArrayMax = 0; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public List<T> ToBlittableList<T>() where T : unmanaged | ||||
|     { | ||||
|         List<T> list = new List<T>(ArrayNum); | ||||
|          | ||||
|         unsafe | ||||
|         { | ||||
|             T* data = (T*) Data.ToPointer(); | ||||
|             for (int i = 0; i < ArrayNum; i++) | ||||
|             { | ||||
|                 list.Add(data[i]); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return list; | ||||
|     } | ||||
|      | ||||
|     public List<T> ToListWithMarshaller<T>(Func<IntPtr, int, T> resolver) | ||||
|     { | ||||
|         List<T> list = new List<T>(ArrayNum); | ||||
|  | ||||
|         for (int i = 0; i < ArrayNum; i++) | ||||
|         { | ||||
|             list.Add(resolver(Data, i)); | ||||
|         } | ||||
|  | ||||
|         return list; | ||||
|     } | ||||
|      | ||||
|     public void ForEachWithMarshaller<T>(Func<IntPtr, int, T> resolver, Action<T> action) | ||||
|     { | ||||
|         for (int i = 0; i < ArrayNum; i++) | ||||
|         { | ||||
|             T item = resolver(Data, i); | ||||
|             action(item); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,254 @@ | ||||
| using System.Reflection; | ||||
| using System.Runtime.InteropServices; | ||||
| using UnrealSharp.Core.Attributes; | ||||
| using UnrealSharp.Core.Marshallers; | ||||
|  | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| public static class UnmanagedCallbacks | ||||
| { | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static unsafe IntPtr CreateNewManagedObject(IntPtr nativeObject, IntPtr typeHandlePtr, char** error) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             if (nativeObject == IntPtr.Zero) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(nativeObject)); | ||||
|             } | ||||
|              | ||||
|             Type? type = GCHandleUtilities.GetObjectFromHandlePtr<Type>(typeHandlePtr); | ||||
|              | ||||
|             if (type == null) | ||||
|             { | ||||
|                 throw new InvalidOperationException("The provided type handle does not point to a valid type."); | ||||
|             } | ||||
|  | ||||
|             return UnrealSharpObject.Create(type, nativeObject); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             LogUnrealSharpCore.LogError($"Failed to create new managed object: {ex.Message}"); | ||||
|             *error = (char*)Marshal.StringToHGlobalUni(ex.ToString()); | ||||
|         } | ||||
|  | ||||
|         return IntPtr.Zero; | ||||
|     } | ||||
|  | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static IntPtr CreateNewManagedObjectWrapper(IntPtr managedObjectHandle, IntPtr typeHandlePtr) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             if (managedObjectHandle == IntPtr.Zero) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(managedObjectHandle)); | ||||
|             } | ||||
|              | ||||
|             Type? type = GCHandleUtilities.GetObjectFromHandlePtr<Type>(typeHandlePtr); | ||||
|              | ||||
|             if (type is null) | ||||
|             { | ||||
|                 throw new InvalidOperationException("The provided type handle does not point to a valid type."); | ||||
|             } | ||||
|              | ||||
|             object? managedObject = GCHandleUtilities.GetObjectFromHandlePtr<object>(managedObjectHandle); | ||||
|             if (managedObject is null) | ||||
|             { | ||||
|                 throw new InvalidOperationException("The provided managed object handle does not point to a valid object."); | ||||
|             } | ||||
|  | ||||
|             MethodInfo? wrapMethod = type.GetMethod("Wrap", BindingFlags.Public | BindingFlags.Static); | ||||
|             if (wrapMethod is null) | ||||
|             { | ||||
|                 throw new InvalidOperationException("The provided type does not have a static Wrap method."); | ||||
|             } | ||||
|              | ||||
|             object? createdObject = wrapMethod.Invoke(null, [managedObject]); | ||||
|             if (createdObject is null) | ||||
|             { | ||||
|                 throw new InvalidOperationException("The Wrap method did not return a valid object."); | ||||
|             } | ||||
|  | ||||
|             return GCHandle.ToIntPtr(GCHandleUtilities.AllocateStrongPointer(createdObject, createdObject.GetType().Assembly)); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             LogUnrealSharpCore.LogError($"Failed to create new managed object: {ex.Message}"); | ||||
|         } | ||||
|  | ||||
|         return IntPtr.Zero; | ||||
|     } | ||||
|      | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static unsafe IntPtr LookupManagedMethod(IntPtr typeHandlePtr, char* methodName) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             Type? type = GCHandleUtilities.GetObjectFromHandlePtr<Type>(typeHandlePtr); | ||||
|              | ||||
|             if (type == null) | ||||
|             { | ||||
|                 throw new Exception("Invalid type handle"); | ||||
|             } | ||||
|              | ||||
|             string methodNameString = new string(methodName); | ||||
|             BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; | ||||
|             Type? currentType = type; | ||||
|              | ||||
|             while (currentType != null) | ||||
|             { | ||||
|                 MethodInfo? method = currentType.GetMethod(methodNameString, flags); | ||||
|  | ||||
|                 if (method != null) | ||||
|                 { | ||||
|                     IntPtr functionPtr = method.MethodHandle.GetFunctionPointer(); | ||||
|                     GCHandle methodHandle = GCHandleUtilities.AllocateStrongPointer(functionPtr, type.Assembly); | ||||
|                     return GCHandle.ToIntPtr(methodHandle); | ||||
|                 } | ||||
|                  | ||||
|                 currentType = currentType.BaseType; | ||||
|             } | ||||
|  | ||||
|             return IntPtr.Zero; | ||||
|         } | ||||
|         catch (Exception e) | ||||
|         { | ||||
|             LogUnrealSharpCore.LogError($"Exception while trying to look up managed method: {e.Message}"); | ||||
|         } | ||||
|  | ||||
|         return IntPtr.Zero; | ||||
|     } | ||||
|      | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static unsafe IntPtr LookupManagedType(IntPtr assemblyHandle, char* fullTypeName) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             string fullTypeNameString = new string(fullTypeName); | ||||
|             Assembly? loadedAssembly = GCHandleUtilities.GetObjectFromHandlePtr<Assembly>(assemblyHandle); | ||||
|  | ||||
|             if (loadedAssembly == null) | ||||
|             { | ||||
|                 throw new InvalidOperationException("The provided assembly handle does not point to a valid assembly."); | ||||
|             } | ||||
|  | ||||
|             return FindTypeInAssembly(loadedAssembly, fullTypeNameString); | ||||
|         } | ||||
|         catch (TypeLoadException ex) | ||||
|         { | ||||
|             LogUnrealSharpCore.LogError($"TypeLoadException while trying to look up managed type: {ex.Message}"); | ||||
|             return IntPtr.Zero; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private static IntPtr FindTypeInAssembly(Assembly assembly, string fullTypeName) | ||||
|     { | ||||
|         Type[] types = assembly.GetTypes(); | ||||
|         foreach (Type type in types) | ||||
|         { | ||||
|             foreach (CustomAttributeData attributeData in type.CustomAttributes) | ||||
|             { | ||||
|                 if (attributeData.AttributeType.FullName != typeof(GeneratedTypeAttribute).FullName) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 if (attributeData.ConstructorArguments.Count != 2) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 string fullName = (string)attributeData.ConstructorArguments[1].Value!; | ||||
|                 if (fullName == fullTypeName) | ||||
|                 { | ||||
|                     return GCHandle.ToIntPtr(GCHandleUtilities.AllocateStrongPointer(type, assembly)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return IntPtr.Zero; | ||||
|     } | ||||
|      | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static unsafe int InvokeManagedMethod(IntPtr managedObjectHandle, | ||||
|         IntPtr methodHandlePtr,  | ||||
|         IntPtr argumentsBuffer,  | ||||
|         IntPtr returnValueBuffer,  | ||||
|         IntPtr exceptionTextBuffer) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             IntPtr? methodHandle = GCHandleUtilities.GetObjectFromHandlePtr<IntPtr>(methodHandlePtr); | ||||
|             object? managedObject = GCHandleUtilities.GetObjectFromHandlePtr<object>(managedObjectHandle); | ||||
|              | ||||
|             if (methodHandle == null || managedObject == null) | ||||
|             { | ||||
|                 throw new Exception("Invalid method or target handle"); | ||||
|             } | ||||
|              | ||||
|             delegate*<object, IntPtr, IntPtr, void> methodPtr = (delegate*<object, IntPtr, IntPtr, void>) methodHandle; | ||||
|             methodPtr(managedObject, argumentsBuffer, returnValueBuffer); | ||||
|             return 0; | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             StringMarshaller.ToNative(exceptionTextBuffer, 0, ex.ToString()); | ||||
|             LogUnrealSharpCore.LogError($"Exception during InvokeManagedMethod: {ex.Message}"); | ||||
|             return 1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static void InvokeDelegate(IntPtr delegatePtr) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             Delegate? foundDelegate = GCHandleUtilities.GetObjectFromHandlePtr<Delegate>(delegatePtr); | ||||
|              | ||||
|             if (foundDelegate == null) | ||||
|             { | ||||
|                 throw new Exception("Invalid delegate handle"); | ||||
|             } | ||||
|  | ||||
|             foundDelegate.DynamicInvoke(); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             LogUnrealSharpCore.LogError($"Exception during InvokeDelegate: {ex.Message}"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static void Dispose(IntPtr handle, IntPtr assemblyHandle) | ||||
|     { | ||||
|         GCHandle foundHandle = GCHandle.FromIntPtr(handle); | ||||
|          | ||||
|         if (!foundHandle.IsAllocated) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         if (foundHandle.Target is IDisposable disposable) | ||||
|         { | ||||
|             disposable.Dispose(); | ||||
|         } | ||||
|  | ||||
|         Assembly? foundAssembly = GCHandleUtilities.GetObjectFromHandlePtr<Assembly>(assemblyHandle); | ||||
|         GCHandleUtilities.Free(foundHandle, foundAssembly); | ||||
|     } | ||||
|  | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static void FreeHandle(IntPtr handle) | ||||
|     { | ||||
|         GCHandle foundHandle = GCHandle.FromIntPtr(handle); | ||||
|         if (!foundHandle.IsAllocated) return; | ||||
|          | ||||
|         if (foundHandle.Target is IDisposable disposable) | ||||
|         { | ||||
|             disposable.Dispose(); | ||||
|         } | ||||
|              | ||||
|         foundHandle.Free(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,21 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\UnrealSharp.Binds\UnrealSharp.Binds.csproj" /> | ||||
|       <ProjectReference Include="..\UnrealSharp.Log\UnrealSharp.Log.csproj" /> | ||||
|  | ||||
|         <ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj"> | ||||
|             <OutputItemType>Analyzer</OutputItemType> | ||||
|             <ReferenceOutputAssembly>false</ReferenceOutputAssembly> | ||||
|         </ProjectReference> | ||||
|     </ItemGroup> | ||||
|  | ||||
|  | ||||
| </Project> | ||||
| @ -0,0 +1,44 @@ | ||||
| using System.Reflection; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Runtime.InteropServices; | ||||
|  | ||||
| namespace UnrealSharp.Core; | ||||
|  | ||||
| /// <summary> | ||||
| /// Represents a UObject in Unreal Engine. Don't inherit from this class directly, use a CoreUObject.Object instead. | ||||
| /// </summary> | ||||
| public class UnrealSharpObject : IDisposable | ||||
| { | ||||
|     internal static unsafe IntPtr Create(Type typeToCreate, IntPtr nativeObjectPtr) | ||||
|     { | ||||
|         const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; | ||||
|         ConstructorInfo? foundDefaultCtor = typeToCreate.GetConstructor(bindingFlags, Type.EmptyTypes); | ||||
|              | ||||
|         if (foundDefaultCtor == null) | ||||
|         { | ||||
|             LogUnrealSharpCore.LogError("Failed to find default constructor for type: " + typeToCreate.FullName); | ||||
|             return IntPtr.Zero; | ||||
|         } | ||||
|              | ||||
|         delegate*<object, void> foundConstructor = (delegate*<object, void>) foundDefaultCtor.MethodHandle.GetFunctionPointer(); | ||||
|              | ||||
|         UnrealSharpObject createdObject = (UnrealSharpObject) RuntimeHelpers.GetUninitializedObject(typeToCreate); | ||||
|         createdObject.NativeObject = nativeObjectPtr; | ||||
|              | ||||
|         foundConstructor(createdObject); | ||||
|              | ||||
|         return GCHandle.ToIntPtr(GCHandleUtilities.AllocateStrongPointer(createdObject, typeToCreate.Assembly)); | ||||
|     } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// The pointer to the UObject that this C# object represents. | ||||
|     /// </summary> | ||||
|     public IntPtr NativeObject { get; private set; } | ||||
|      | ||||
|     /// <inheritdoc /> | ||||
|     public virtual void Dispose() | ||||
|     { | ||||
|         NativeObject = IntPtr.Zero; | ||||
|         GC.SuppressFinalize(this); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,11 @@ | ||||
| using UnrealSharp.Binds; | ||||
| using UnrealSharp.Core; | ||||
|  | ||||
| namespace UnrealSharp.Editor.Interop; | ||||
|  | ||||
| [NativeCallbacks] | ||||
| public static unsafe partial class FUnrealSharpEditorModuleExporter | ||||
| { | ||||
|     public static delegate* unmanaged<FManagedUnrealSharpEditorCallbacks, void> InitializeUnrealSharpEditorCallbacks; | ||||
|     public static delegate* unmanaged<out UnmanagedArray, void> GetProjectPaths; | ||||
| } | ||||
| @ -0,0 +1,6 @@ | ||||
| using UnrealSharp.Log; | ||||
|  | ||||
| namespace UnrealSharp.Editor; | ||||
|  | ||||
| [CustomLog] | ||||
| public static partial class LogUnrealSharpEditor; | ||||
| @ -0,0 +1,38 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <CopyLocalLockFileAssembliesName>true</CopyLocalLockFileAssembliesName> | ||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
|         <EnableDynamicLoading>true</EnableDynamicLoading> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <DefineConstants>WITH_EDITOR</DefineConstants> | ||||
|         <DefineConstants Condition="'$(DisableWithEditor)' == 'true'">$(DefineConstants.Replace('WITH_EDITOR;', '').Replace('WITH_EDITOR', ''))</DefineConstants> | ||||
|         <DefineConstants Condition="'$(DefineAdditionalConstants)' != ''">$(DefineConstants);$(DefineAdditionalConstants)</DefineConstants> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <ProjectReference Include="..\UnrealSharp.Plugins\UnrealSharp.Plugins.csproj" /> | ||||
|         <ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj"> | ||||
|             <OutputItemType>Analyzer</OutputItemType> | ||||
|             <ReferenceOutputAssembly>false</ReferenceOutputAssembly> | ||||
|         </ProjectReference> | ||||
|       <ProjectReference Include="..\UnrealSharp\UnrealSharp.csproj" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <Reference Include="UnrealSharpWeaver"> | ||||
|         <HintPath>..\..\..\Binaries\Managed\UnrealSharpWeaver.dll</HintPath> | ||||
|       </Reference> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" /> | ||||
|         <PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @ -0,0 +1,230 @@ | ||||
| using System.Diagnostics; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Text; | ||||
| using Microsoft.Build.Evaluation; | ||||
| using Microsoft.Build.Execution; | ||||
| using Microsoft.Build.Framework; | ||||
| using UnrealSharp.Core; | ||||
| using UnrealSharp.Core.Marshallers; | ||||
| using UnrealSharp.Editor.Interop; | ||||
| using UnrealSharp.Engine.Core.Modules; | ||||
| using UnrealSharp.Shared; | ||||
| using UnrealSharpWeaver; | ||||
|  | ||||
| namespace UnrealSharp.Editor; | ||||
|  | ||||
| // TODO: Automate managed callbacks so we easily can make calls from native to managed. | ||||
| [StructLayout(LayoutKind.Sequential)] | ||||
| public unsafe struct FManagedUnrealSharpEditorCallbacks | ||||
| { | ||||
|     public delegate* unmanaged<char*, char*, char*, LoggerVerbosity, IntPtr, NativeBool, NativeBool> BuildProjects; | ||||
|     public delegate* unmanaged<void> ForceManagedGC; | ||||
|     public delegate* unmanaged<char*, IntPtr, NativeBool> OpenSolution; | ||||
|     public delegate* unmanaged<char*, void> AddProjectToCollection; | ||||
|  | ||||
|     public FManagedUnrealSharpEditorCallbacks() | ||||
|     { | ||||
|         BuildProjects = &ManagedUnrealSharpEditorCallbacks.Build; | ||||
|         ForceManagedGC = &ManagedUnrealSharpEditorCallbacks.ForceManagedGC; | ||||
|         OpenSolution = &ManagedUnrealSharpEditorCallbacks.OpenSolution; | ||||
|         AddProjectToCollection = &ManagedUnrealSharpEditorCallbacks.AddProjectToCollection; | ||||
|     } | ||||
| } | ||||
|  | ||||
| class ErrorCollectingLogger : ILogger | ||||
| { | ||||
|     public StringBuilder ErrorLog { get; } = new(); | ||||
|     public LoggerVerbosity Verbosity { get; set; } | ||||
|     public string Parameters { get; set; } = string.Empty; | ||||
|  | ||||
|     public ErrorCollectingLogger(LoggerVerbosity verbosity = LoggerVerbosity.Normal) | ||||
|     { | ||||
|         Verbosity = verbosity; | ||||
|     } | ||||
|  | ||||
|     public void Initialize(IEventSource eventSource) | ||||
|     { | ||||
|         eventSource.ErrorRaised += (sender, e) => | ||||
|         { | ||||
|             string fileName = Path.GetFileName(e.File); | ||||
|  | ||||
|             ErrorLog.AppendLine($"{fileName}({e.LineNumber},{e.ColumnNumber}): {e.Message}"); | ||||
|             ErrorLog.AppendLine(); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public void Shutdown() | ||||
|     { | ||||
|     } | ||||
| } | ||||
|  | ||||
| public static class ManagedUnrealSharpEditorCallbacks | ||||
| { | ||||
|     private static readonly ProjectCollection ProjectCollection = new(); | ||||
|     private static readonly BuildManager UnrealSharpBuildManager = new("UnrealSharpBuildManager"); | ||||
|  | ||||
|     public static void Initialize() | ||||
|     { | ||||
|         FUnrealSharpEditorModuleExporter.CallGetProjectPaths(out UnmanagedArray projectPaths); | ||||
|         List<string> projectPathsList = projectPaths.ToListWithMarshaller(StringMarshaller.FromNative); | ||||
|  | ||||
|         foreach (string projectPath in projectPathsList) | ||||
|         { | ||||
|             ProjectCollection.LoadProject(projectPath); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static unsafe NativeBool Build(char* solutionPath, | ||||
|         char* outputPath, | ||||
|         char* buildConfiguration, | ||||
|         LoggerVerbosity loggerVerbosity, | ||||
|         IntPtr exceptionBuffer, | ||||
|         NativeBool buildSolution) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             string buildConfigurationString = new string(buildConfiguration); | ||||
|  | ||||
|             if (buildSolution == NativeBool.True) | ||||
|             { | ||||
|                 ErrorCollectingLogger logger = new ErrorCollectingLogger(loggerVerbosity); | ||||
|                 BuildParameters buildParameters = new(ProjectCollection) | ||||
|                 { | ||||
|                     Loggers = new List<ILogger> { logger } | ||||
|                 }; | ||||
|  | ||||
|                 Dictionary<string, string?> globalProperties = new() | ||||
|                 { | ||||
|                     ["Configuration"] = buildConfigurationString, | ||||
|                 }; | ||||
|  | ||||
|                 BuildRequestData buildRequest = new BuildRequestData( | ||||
|                     new string(solutionPath), | ||||
|                     globalProperties, | ||||
|                     null, | ||||
|                     new[] { "Build" }, | ||||
|                     null | ||||
|                 ); | ||||
|  | ||||
|                 BuildResult result = UnrealSharpBuildManager.Build(buildParameters, buildRequest); | ||||
|                 if (result.OverallResult == BuildResultCode.Failure) | ||||
|                 { | ||||
|                     throw new Exception(logger.ErrorLog.ToString()); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Weave(outputPath, buildConfigurationString); | ||||
|         } | ||||
|         catch (Exception exception) | ||||
|         { | ||||
|             StringMarshaller.ToNative(exceptionBuffer, 0, exception.Message); | ||||
|             return NativeBool.False; | ||||
|         } | ||||
|  | ||||
|         return NativeBool.True; | ||||
|     } | ||||
|  | ||||
|     static unsafe void Weave(char* outputPath, string buildConfiguration) | ||||
|     { | ||||
|         List<string> assemblyPaths = new(); | ||||
|         foreach (Project? projectFile in ProjectCollection.LoadedProjects | ||||
|                      .Where(p => p.GetPropertyValue("ExcludeFromWeaver") != "true")) | ||||
|         { | ||||
|             string projectName = Path.GetFileNameWithoutExtension(projectFile.FullPath); | ||||
|             string assemblyPath = Path.Combine(projectFile.DirectoryPath, "bin", | ||||
|                 buildConfiguration, DotNetUtilities.DOTNET_MAJOR_VERSION_DISPLAY, projectName + ".dll"); | ||||
|  | ||||
|             assemblyPaths.Add(assemblyPath); | ||||
|         } | ||||
|  | ||||
|         WeaverOptions weaverOptions = new WeaverOptions | ||||
|         { | ||||
|             AssemblyPaths = assemblyPaths, | ||||
|             OutputDirectory = new string(outputPath), | ||||
|         }; | ||||
|  | ||||
|         Program.Weave(weaverOptions); | ||||
|     } | ||||
|      | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static void ForceManagedGC() | ||||
|     { | ||||
|         GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); | ||||
|         GC.WaitForPendingFinalizers(); | ||||
|         GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); | ||||
|     } | ||||
|  | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static unsafe NativeBool OpenSolution(char* solutionPath, IntPtr exceptionBuffer) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             string solutionFilePath = new (solutionPath); | ||||
|  | ||||
|             if (!File.Exists(solutionFilePath)) | ||||
|             { | ||||
|                 throw new FileNotFoundException($"Solution not found at path \"{solutionFilePath}\""); | ||||
|             } | ||||
|  | ||||
|             ProcessStartInfo? startInfo = null; | ||||
|  | ||||
|             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||||
|             { | ||||
|                 startInfo = new ProcessStartInfo("cmd.exe", $"/c start \"\" \"{solutionFilePath}\""); | ||||
|             } | ||||
|             else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) | ||||
|             { | ||||
|                 startInfo = new ProcessStartInfo("open", solutionFilePath); | ||||
|             } | ||||
|             else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) | ||||
|             { | ||||
|                 startInfo = new ProcessStartInfo("xdg-open", solutionFilePath); | ||||
|             } | ||||
|  | ||||
|             if (startInfo == null) | ||||
|             { | ||||
|                 throw new PlatformNotSupportedException("Unsupported platform."); | ||||
|             } | ||||
|  | ||||
|             startInfo.WorkingDirectory = Path.GetDirectoryName(solutionFilePath); | ||||
|             startInfo.Environment["MsBuildExtensionPath"] = null; | ||||
|             startInfo.Environment["MSBUILD_EXE_PATH"] = null; | ||||
|             startInfo.Environment["MsBuildSDKsPath"] = null; | ||||
|  | ||||
|             Process.Start(startInfo); | ||||
|         } | ||||
|         catch (Exception exception) | ||||
|         { | ||||
|             StringMarshaller.ToNative(exceptionBuffer, 0, exception.Message); | ||||
|             return NativeBool.False; | ||||
|         } | ||||
|  | ||||
|         return NativeBool.True; | ||||
|     } | ||||
|      | ||||
|     [UnmanagedCallersOnly] | ||||
|     public static unsafe void AddProjectToCollection(char* projectPath) | ||||
|     { | ||||
|         string projectPathString = new string(projectPath); | ||||
|          | ||||
|         if (ProjectCollection.LoadedProjects.All(p => p.FullPath != projectPathString)) | ||||
|         { | ||||
|             ProjectCollection.LoadProject(projectPathString); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class FUnrealSharpEditor : IModuleInterface | ||||
| { | ||||
|     public void StartupModule() | ||||
|     { | ||||
|         FManagedUnrealSharpEditorCallbacks callbacks = new FManagedUnrealSharpEditorCallbacks(); | ||||
|         FUnrealSharpEditorModuleExporter.CallInitializeUnrealSharpEditorCallbacks(callbacks); | ||||
|         ManagedUnrealSharpEditorCallbacks.Initialize(); | ||||
|     } | ||||
|  | ||||
|     public void ShutdownModule() | ||||
|     { | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,98 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
|  | ||||
| namespace UnrealSharp.EditorSourceGenerators; | ||||
|  | ||||
| struct AssemblyClassInfo(string fullName, string filePath) | ||||
| { | ||||
|     public string FullName = fullName; | ||||
|     public string FilePath = filePath; | ||||
| } | ||||
|  | ||||
| [Generator] | ||||
| public class ClassFilePathGenerator : IIncrementalGenerator | ||||
| { | ||||
|     private static string GetRelativePath(string filePath) | ||||
|     { | ||||
|         filePath = filePath.Replace("\\", "/"); | ||||
|          | ||||
|         int index = filePath.IndexOf("/Script", StringComparison.OrdinalIgnoreCase); | ||||
|         if (index >= 0) | ||||
|         { | ||||
|             return filePath.Substring(index); | ||||
|         } | ||||
|  | ||||
|         return filePath; | ||||
|     } | ||||
|  | ||||
|     public void Initialize(IncrementalGeneratorInitializationContext context) | ||||
|     { | ||||
|         var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider( | ||||
|             static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax, | ||||
|             static (context, _) => | ||||
|             { | ||||
|                 return context.SemanticModel.GetDeclaredSymbol(context.Node) is INamedTypeSymbol classSymbol | ||||
|                     ? (new[] { new AssemblyClassInfo(classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), context.Node.SyntaxTree.FilePath) }) | ||||
|                     : Array.Empty<AssemblyClassInfo>(); | ||||
|             }) | ||||
|             .SelectMany((results, _) => results) | ||||
|             .Where(i => i.FilePath != null) | ||||
|             .Collect() | ||||
|             .Combine(context.CompilationProvider); | ||||
|  | ||||
|         context.RegisterSourceOutput(syntaxProvider, (outputContext, info) => | ||||
|         { | ||||
|             var (classes, compilation) = info; | ||||
|  | ||||
|             Dictionary<string, string> classDictionary = new Dictionary<string, string>(); | ||||
|  | ||||
|             foreach (AssemblyClassInfo classInfo in classes) | ||||
|             { | ||||
|                 string className = classInfo.FullName; | ||||
|                 string? relativeFilePath = GetRelativePath(classInfo.FilePath); | ||||
|  | ||||
|                 if (!string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(relativeFilePath)) | ||||
|                 { | ||||
|                     classDictionary[className] = relativeFilePath; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             StringBuilder sourceBuilder = new StringBuilder(); | ||||
|  | ||||
|             sourceBuilder.AppendLine("using System.Collections.Generic;"); | ||||
|             sourceBuilder.AppendLine("using System.Runtime.CompilerServices;"); | ||||
|             sourceBuilder.AppendLine($"namespace {compilation.AssemblyName};"); | ||||
|  | ||||
|             sourceBuilder.AppendLine("public static class ClassFileMap"); | ||||
|             sourceBuilder.AppendLine("{"); | ||||
|  | ||||
|             sourceBuilder.AppendLine("    [ModuleInitializer]"); | ||||
|             sourceBuilder.AppendLine("    public static void Initialize()"); | ||||
|             sourceBuilder.AppendLine("    {"); | ||||
|  | ||||
|             foreach (KeyValuePair<string, string> kvp in classDictionary) | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"        AddClassFile(\"{kvp.Key}\", \"{kvp.Value}\");"); | ||||
|             } | ||||
|  | ||||
|             sourceBuilder.AppendLine("    }"); | ||||
|  | ||||
|             sourceBuilder.AppendLine("    public unsafe static void AddClassFile(string className, string filePath)"); | ||||
|             sourceBuilder.AppendLine("    {"); | ||||
|             sourceBuilder.AppendLine("        fixed (char* ptr1 = className)"); | ||||
|             sourceBuilder.AppendLine("        fixed (char* ptr2 = filePath)"); | ||||
|             sourceBuilder.AppendLine("        {"); | ||||
|             sourceBuilder.AppendLine("            UnrealSharp.Interop.FCSTypeRegistryExporter.CallRegisterClassToFilePath(ptr1, ptr2);"); | ||||
|             sourceBuilder.AppendLine("        }"); | ||||
|             sourceBuilder.AppendLine("    }"); | ||||
|  | ||||
|             sourceBuilder.AppendLine("}"); | ||||
|  | ||||
|             outputContext.AddSource("ClassFileMap.generated.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,17 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <LangVersion>latest</LangVersion> | ||||
|         <TargetFramework>netstandard2.0</TargetFramework> | ||||
|         <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||||
|         <IsRoslynComponent>true</IsRoslynComponent> | ||||
|         <NoWarn>$(NoWarn);1570;0649;0169;0108;0109</NoWarn> | ||||
|         <Nullable>enable</Nullable> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" /> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @ -0,0 +1,76 @@ | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace UnrealSharp.ExtensionSourceGenerators; | ||||
|  | ||||
| public class ActorComponentExtensionGenerator : ExtensionGenerator | ||||
| { | ||||
|     public override void Generate(ref StringBuilder builder, INamedTypeSymbol classSymbol) | ||||
|     { | ||||
|         GenerateConstructMethod(ref builder, classSymbol); | ||||
|         GenerateComponentGetter(ref builder, classSymbol); | ||||
|     } | ||||
|      | ||||
|     private void GenerateConstructMethod(ref StringBuilder stringBuilder, INamedTypeSymbol classSymbol) | ||||
|     { | ||||
|         string fullTypeName = classSymbol.ToDisplayString(); | ||||
|          | ||||
|         stringBuilder.AppendLine(); | ||||
|         stringBuilder.AppendLine("     /// <summary>"); | ||||
|         stringBuilder.AppendLine("     /// Constructs a new component of the specified class, and attaches it to the specified actor."); | ||||
|         stringBuilder.AppendLine("     /// </summary>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"owner\">The actor to attach the component to.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"bManualAttachment\">If true, the component will not be attached to the actor's root component.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"relativeTransform\">The relative transform of the component to the actor.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <returns>The constructed component.</returns>"); | ||||
|         stringBuilder.AppendLine($"     public static {fullTypeName} Construct(UnrealSharp.Engine.AActor owner, bool bManualAttachment, FTransform relativeTransform)"); | ||||
|         stringBuilder.AppendLine("     {"); | ||||
|         stringBuilder.AppendLine($"         return owner.AddComponentByClass<{fullTypeName}>(bManualAttachment, relativeTransform);"); | ||||
|         stringBuilder.AppendLine("     }"); | ||||
|          | ||||
|         stringBuilder.AppendLine(); | ||||
|         stringBuilder.AppendLine("     /// <summary>"); | ||||
|         stringBuilder.AppendLine("     /// Constructs a new component of the specified class, and attaches it to the specified actor."); | ||||
|         stringBuilder.AppendLine("     /// </summary>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"owner\">The actor to attach the component to.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"componentClass\">The class of the component to construct.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"bManualAttachment\">If true, the component will not be attached to the actor's root component.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"relativeTransform\">The relative transform of the component to the actor.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <returns>The constructed component.</returns>"); | ||||
|         stringBuilder.AppendLine($"     public static {fullTypeName} Construct(UnrealSharp.Engine.AActor owner, TSubclassOf<UActorComponent> componentClass, bool bManualAttachment, FTransform relativeTransform)"); | ||||
|         stringBuilder.AppendLine("     {"); | ||||
|         stringBuilder.AppendLine($"         return ({fullTypeName}) owner.AddComponentByClass(componentClass, bManualAttachment, relativeTransform);"); | ||||
|         stringBuilder.AppendLine("     }"); | ||||
|          | ||||
|         stringBuilder.AppendLine(); | ||||
|         stringBuilder.AppendLine("     /// <summary>"); | ||||
|         stringBuilder.AppendLine("     /// Constructs a new component of the specified class, and attaches it to the specified actor."); | ||||
|         stringBuilder.AppendLine("     /// </summary>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"owner\">The actor to attach the component to.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <returns>The constructed component.</returns>"); | ||||
|         stringBuilder.AppendLine($"     public static {fullTypeName} Construct(UnrealSharp.Engine.AActor owner)"); | ||||
|         stringBuilder.AppendLine("     {"); | ||||
|         stringBuilder.AppendLine($"         return ({fullTypeName}) owner.AddComponentByClass(typeof({fullTypeName}), false, new FTransform());"); | ||||
|         stringBuilder.AppendLine("     }"); | ||||
|     } | ||||
|      | ||||
|     private void GenerateComponentGetter(ref StringBuilder stringBuilder, INamedTypeSymbol classSymbol) | ||||
|     { | ||||
|         string fullTypeName = classSymbol.ToDisplayString(); | ||||
|         stringBuilder.AppendLine(); | ||||
|         stringBuilder.AppendLine("     /// <summary>"); | ||||
|         stringBuilder.AppendLine("     /// Gets the component of the specified class attached to the specified actor."); | ||||
|         stringBuilder.AppendLine("     /// </summary>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"owner\">The actor to get the component from.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <returns>The component if found, otherwise null.</returns>"); | ||||
|         stringBuilder.AppendLine($"     public static new {fullTypeName}? Get(UnrealSharp.Engine.AActor owner)"); | ||||
|         stringBuilder.AppendLine("     {"); | ||||
|         stringBuilder.AppendLine($"        UActorComponent? foundComponent = owner.GetComponentByClass<{fullTypeName}>(typeof({fullTypeName}));"); | ||||
|         stringBuilder.AppendLine("        if (foundComponent != null)"); | ||||
|         stringBuilder.AppendLine("        {"); | ||||
|         stringBuilder.AppendLine($"            return ({fullTypeName}) foundComponent;"); | ||||
|         stringBuilder.AppendLine("        }"); | ||||
|         stringBuilder.AppendLine("        return null;"); | ||||
|         stringBuilder.AppendLine("     }"); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,48 @@ | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace UnrealSharp.ExtensionSourceGenerators; | ||||
|  | ||||
| public class ActorExtensionGenerator : ExtensionGenerator | ||||
| { | ||||
|     public override void Generate(ref StringBuilder builder, INamedTypeSymbol classSymbol) | ||||
|     { | ||||
|         GenerateSpawnMethod(ref builder, classSymbol); | ||||
|         GenerateGetActorsOfClassMethod(ref builder, classSymbol); | ||||
|     } | ||||
|      | ||||
|     private void GenerateSpawnMethod(ref StringBuilder stringBuilder, INamedTypeSymbol classSymbol) | ||||
|     { | ||||
|         string fullTypeName = classSymbol.ToDisplayString(); | ||||
|         stringBuilder.AppendLine(); | ||||
|         stringBuilder.AppendLine("     /// <summary>"); | ||||
|         stringBuilder.AppendLine("     /// Spawns an actor of the specified class."); | ||||
|         stringBuilder.AppendLine("     /// </summary>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"worldContextObject\">The object to spawn the actor in.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"actorClass\">The class of the actor to spawn.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"spawnTransform\">The transform to spawn the actor at.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"spawnMethod\">The method to handle collisions when spawning the actor.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"instigator\">The actor that caused the actor to be spawned.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"owner\">The actor that owns the spawned actor.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <returns>The spawned actor.</returns>"); | ||||
|         stringBuilder.AppendLine($"     public static {fullTypeName} Spawn(TSubclassOf<{fullTypeName}> actorClass = default, FTransform spawnTransform = default, UnrealSharp.Engine.ESpawnActorCollisionHandlingMethod spawnMethod = ESpawnActorCollisionHandlingMethod.Undefined, APawn? instigator = null, AActor? owner = null)"); | ||||
|         stringBuilder.AppendLine("     {"); | ||||
|         stringBuilder.AppendLine($"         return SpawnActor<{fullTypeName}>(actorClass, spawnTransform, spawnMethod, instigator, owner);"); | ||||
|         stringBuilder.AppendLine("     }"); | ||||
|     } | ||||
|      | ||||
|     private void GenerateGetActorsOfClassMethod(ref StringBuilder stringBuilder, INamedTypeSymbol classSymbol) | ||||
|     { | ||||
|         string fullTypeName = classSymbol.ToDisplayString(); | ||||
|         stringBuilder.AppendLine(); | ||||
|         stringBuilder.AppendLine("     /// <summary>"); | ||||
|         stringBuilder.AppendLine("     /// Gets all actors of the specified class in the world."); | ||||
|         stringBuilder.AppendLine("     /// </summary>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"worldContextObject\">The object to get the actors from.</param>"); | ||||
|         stringBuilder.AppendLine("     /// <param name=\"outActors\">The list to store the actors in.</param>"); | ||||
|         stringBuilder.AppendLine($"     public static new void GetAllActorsOfClass(out IList<{fullTypeName}> outActors)"); | ||||
|         stringBuilder.AppendLine("     {"); | ||||
|         stringBuilder.AppendLine("         UGameplayStatics.GetAllActorsOfClass(out outActors);"); | ||||
|         stringBuilder.AppendLine("     }"); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,93 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
|  | ||||
| namespace UnrealSharp.ExtensionSourceGenerators; | ||||
|  | ||||
| [Generator] | ||||
| public class ClassExtenderGenerator : IIncrementalGenerator | ||||
| {     | ||||
|     private static bool IsA(ITypeSymbol classSymbol, ITypeSymbol otherSymbol) | ||||
|     { | ||||
|         var currentSymbol = classSymbol.BaseType; | ||||
|  | ||||
|         while (currentSymbol != null) | ||||
|         { | ||||
|             if (SymbolEqualityComparer.Default.Equals(currentSymbol, otherSymbol)) | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             currentSymbol = currentSymbol.BaseType; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     public void Initialize(IncrementalGeneratorInitializationContext context) | ||||
|     { | ||||
|         var actorExtensionGenerator = new ActorExtensionGenerator(); | ||||
|         var actorComponentExtensionGenerator = new ActorComponentExtensionGenerator(); | ||||
|  | ||||
|         var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider<(INamedTypeSymbol Symbol, ExtensionGenerator Generator)>( | ||||
|             static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { BaseList: not null }, | ||||
|             (context, _) => | ||||
|             { | ||||
|                 if (context.SemanticModel.GetTypeInfo(context.Node).Type is not INamedTypeSymbol typeSymbol) | ||||
|                 { | ||||
|                     return (null!, null!); | ||||
|                 } | ||||
|  | ||||
|                 if (IsA(typeSymbol, context.SemanticModel.Compilation.GetTypeByMetadataName("UnrealSharp.Engine.AActor")!)) | ||||
|                 { | ||||
|                     return (typeSymbol, actorExtensionGenerator); | ||||
|                 } | ||||
|                 else if (IsA(typeSymbol, context.SemanticModel.Compilation.GetTypeByMetadataName("UnrealSharp.Engine.UActorComponent")!)) | ||||
|                 { | ||||
|                     return (typeSymbol, actorComponentExtensionGenerator); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     return (null!, null!); | ||||
|                 } | ||||
|             }) | ||||
|             .Where(classDecl => classDecl.Symbol != null); | ||||
|  | ||||
|         context.RegisterSourceOutput(syntaxProvider, (outputContext, classDecl) => | ||||
|         { | ||||
|             StringBuilder stringBuilder = new StringBuilder(); | ||||
|             stringBuilder.AppendLine("#nullable disable"); | ||||
|             stringBuilder.AppendLine(); | ||||
|  | ||||
|             stringBuilder.AppendLine("using UnrealSharp.Engine;"); | ||||
|             stringBuilder.AppendLine("using UnrealSharp.CoreUObject;"); | ||||
|             stringBuilder.AppendLine("using UnrealSharp;"); | ||||
|             stringBuilder.AppendLine(); | ||||
|  | ||||
|             stringBuilder.AppendLine($"namespace {classDecl.Symbol.ContainingNamespace};"); | ||||
|             stringBuilder.AppendLine(); | ||||
|             stringBuilder.AppendLine($"public partial class {classDecl.Symbol.Name}"); | ||||
|             stringBuilder.AppendLine("{"); | ||||
|  | ||||
|             classDecl.Generator.Generate(ref stringBuilder, classDecl.Symbol); | ||||
|  | ||||
|             stringBuilder.AppendLine("}"); | ||||
|             outputContext.AddSource($"{classDecl.Symbol.Name}.generated.extension.cs", SourceText.From(stringBuilder.ToString(), Encoding.UTF8)); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private class ClassSyntaxReceiver : ISyntaxReceiver | ||||
|     { | ||||
|         public List<ClassDeclarationSyntax> CandidateClasses { get; } = []; | ||||
|  | ||||
|         public void OnVisitSyntaxNode(SyntaxNode syntaxNode) | ||||
|         { | ||||
|             if (syntaxNode is ClassDeclarationSyntax { BaseList: not null } classDeclaration) | ||||
|             { | ||||
|                 CandidateClasses.Add(classDeclaration); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace UnrealSharp.ExtensionSourceGenerators; | ||||
|  | ||||
| public abstract class ExtensionGenerator | ||||
| { | ||||
|     public abstract void Generate(ref StringBuilder builder, INamedTypeSymbol classSymbol); | ||||
| } | ||||
| @ -0,0 +1,17 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <LangVersion>latest</LangVersion> | ||||
|         <TargetFramework>netstandard2.0</TargetFramework> | ||||
|         <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||||
|         <IsRoslynComponent>true</IsRoslynComponent> | ||||
|         <Nullable>enable</Nullable> | ||||
| 	    <NoWarn>$(NoWarn);1570;0649;0169;0108;0109</NoWarn> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" /> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @ -0,0 +1,7 @@ | ||||
| namespace UnrealSharp.Log; | ||||
|  | ||||
| [AttributeUsage(AttributeTargets.Class, Inherited = false)] | ||||
| public class CustomLog(ELogVerbosity verbosity = ELogVerbosity.Display) : Attribute | ||||
| { | ||||
|     private ELogVerbosity _verbosity = verbosity; | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| using UnrealSharp.Binds; | ||||
|  | ||||
| namespace UnrealSharp.Log; | ||||
|  | ||||
| [NativeCallbacks] | ||||
| public static unsafe partial class FMsgExporter | ||||
| { | ||||
|     public static delegate* unmanaged<char*, ELogVerbosity, char*, void> Log; | ||||
| } | ||||
| @ -0,0 +1,48 @@ | ||||
| namespace UnrealSharp.Log; | ||||
|  | ||||
| public enum ELogVerbosity : byte | ||||
| { | ||||
|     /** Not used */ | ||||
|     NoLogging		= 0, | ||||
|  | ||||
|     /** Always prints a fatal error to console (and log file) and crashes (even if logging is disabled) */ | ||||
|     Fatal, | ||||
|  | ||||
|     /** | ||||
|      * Prints an error to console (and log file). | ||||
|      * Commandlets and the editor collect and report errors. Error messages result in commandlet failure. | ||||
|      */ | ||||
|     Error, | ||||
|  | ||||
|     /** | ||||
|      * Prints a warning to console (and log file). | ||||
|      * Commandlets and the editor collect and report warnings. Warnings can be treated as an error. | ||||
|      */ | ||||
|     Warning, | ||||
|  | ||||
|     /** Prints a message to console (and log file) */ | ||||
|     Display, | ||||
|  | ||||
|     /** Prints a message to a log file (does not print to console) */ | ||||
|     Log, | ||||
|  | ||||
|     /** | ||||
|      * Prints a verbose message to a log file (if Verbose logging is enabled for the given category, | ||||
|      * usually used for detailed logging) | ||||
|      */ | ||||
|     Verbose, | ||||
|  | ||||
|     /** | ||||
|      * Prints a verbose message to a log file (if VeryVerbose logging is enabled, | ||||
|      * usually used for detailed logging that would otherwise spam output) | ||||
|      */ | ||||
|     VeryVerbose, | ||||
|  | ||||
|     // Log masks and special Enum values | ||||
|  | ||||
|     All				= VeryVerbose, | ||||
|     NumVerbosity, | ||||
|     VerbosityMask	= 0xf, | ||||
|     SetColor		= 0x40, // not actually a verbosity, used to set the color of an output device  | ||||
|     BreakOnLog		= 0x80 | ||||
| } | ||||
| @ -0,0 +1,41 @@ | ||||
| namespace UnrealSharp.Log; | ||||
|  | ||||
| public static class UnrealLogger | ||||
| { | ||||
|     public static void Log(string logName, string message, ELogVerbosity logVerbosity = ELogVerbosity.Display) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             fixed (char* logNamePtr = logName) | ||||
|             fixed (char* stringPtr = message) | ||||
|             { | ||||
|                 FMsgExporter.CallLog(logNamePtr, logVerbosity, stringPtr); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public static void LogWarning(string logName, string message) | ||||
|     { | ||||
|         Log(logName, message, ELogVerbosity.Warning); | ||||
|     } | ||||
|      | ||||
|     public static void LogError(string logName, string message) | ||||
|     { | ||||
|         Log(logName, message, ELogVerbosity.Error); | ||||
|     } | ||||
|      | ||||
|     public static void LogFatal(string logName, string message) | ||||
|     { | ||||
|         Log(logName, message, ELogVerbosity.Fatal); | ||||
|     } | ||||
|      | ||||
|     public static void LogVerbose(string logName, string message) | ||||
|     { | ||||
|         Log(logName, message, ELogVerbosity.Verbose); | ||||
|     } | ||||
|      | ||||
|     public static void LogVeryVerbose(string logName, string message) | ||||
|     { | ||||
|         Log(logName, message, ELogVerbosity.VeryVerbose); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,18 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <ProjectReference Include="..\UnrealSharp.Binds\UnrealSharp.Binds.csproj" /> | ||||
|         <ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj"> | ||||
|             <OutputItemType>Analyzer</OutputItemType> | ||||
|             <ReferenceOutputAssembly>false</ReferenceOutputAssembly> | ||||
|         </ProjectReference> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @ -0,0 +1,6 @@ | ||||
| using UnrealSharp.Log; | ||||
|  | ||||
| namespace UnrealSharp.Plugins; | ||||
|  | ||||
| [CustomLog] | ||||
| public static partial class LogUnrealSharpPlugins; | ||||
| @ -0,0 +1,40 @@ | ||||
| using System.Runtime.InteropServices; | ||||
| using Microsoft.Build.Locator; | ||||
| using UnrealSharp.Binds; | ||||
| using UnrealSharp.Core; | ||||
| using UnrealSharp.Shared; | ||||
|  | ||||
| namespace UnrealSharp.Plugins; | ||||
|  | ||||
| public static class Main | ||||
| { | ||||
|     internal static DllImportResolver _dllImportResolver = null!; | ||||
|  | ||||
|     [UnmanagedCallersOnly] | ||||
|     private static unsafe NativeBool InitializeUnrealSharp(char* workingDirectoryPath, nint assemblyPath, PluginsCallbacks* pluginCallbacks, IntPtr bindsCallbacks, IntPtr managedCallbacks) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             #if WITH_EDITOR | ||||
|             string dotnetSdk = DotNetUtilities.GetLatestDotNetSdkPath(); | ||||
|             MSBuildLocator.RegisterMSBuildPath(dotnetSdk); | ||||
|             #endif | ||||
|              | ||||
|             AppDomain.CurrentDomain.SetData("APP_CONTEXT_BASE_DIRECTORY", new string(workingDirectoryPath)); | ||||
|  | ||||
|             // Initialize plugin and managed callbacks | ||||
|             *pluginCallbacks = PluginsCallbacks.Create(); | ||||
|              | ||||
|             NativeBinds.InitializeNativeBinds(bindsCallbacks); | ||||
|             ManagedCallbacks.Initialize(managedCallbacks); | ||||
|  | ||||
|             LogUnrealSharpPlugins.Log("UnrealSharp successfully setup!"); | ||||
|             return NativeBool.True; | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             LogUnrealSharpPlugins.LogError($"Error initializing UnrealSharp: {ex.Message}"); | ||||
|             return NativeBool.False; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,93 @@ | ||||
| using System.Reflection; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Runtime.Loader; | ||||
| using UnrealSharp.Engine.Core.Modules; | ||||
|  | ||||
| namespace UnrealSharp.Plugins; | ||||
|  | ||||
| public class Plugin | ||||
| { | ||||
|     public Plugin(AssemblyName assemblyName, bool isCollectible, string assemblyPath) | ||||
|     { | ||||
|         AssemblyName = assemblyName; | ||||
|         AssemblyPath = assemblyPath; | ||||
|          | ||||
|         string pluginLoadContextName = assemblyName.Name! + "_AssemblyLoadContext"; | ||||
|         LoadContext = new PluginLoadContext(pluginLoadContextName, new AssemblyDependencyResolver(assemblyPath), isCollectible); | ||||
|         WeakRefLoadContext = new WeakReference(LoadContext); | ||||
|     } | ||||
|      | ||||
|     public AssemblyName AssemblyName { get; } | ||||
|     public string AssemblyPath; | ||||
|      | ||||
|     public PluginLoadContext? LoadContext { get; private set; } | ||||
|     public WeakReference? WeakRefLoadContext { get ; } | ||||
|      | ||||
|     public WeakReference? WeakRefAssembly { get; private set; } | ||||
|     public List<IModuleInterface> ModuleInterfaces { get; } = []; | ||||
|  | ||||
|     public bool IsLoadContextAlive | ||||
|     { | ||||
|         [MethodImpl(MethodImplOptions.NoInlining)] | ||||
|         get => WeakRefLoadContext != null && WeakRefLoadContext.IsAlive; | ||||
|     } | ||||
|  | ||||
|     public bool Load() | ||||
|     { | ||||
|         if (LoadContext == null || (WeakRefAssembly != null && WeakRefAssembly.IsAlive)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|         Assembly assembly = LoadContext.LoadFromAssemblyName(AssemblyName); | ||||
|         WeakRefAssembly = new WeakReference(assembly); | ||||
|          | ||||
|         Type[] types = assembly.GetTypes(); | ||||
|              | ||||
|         foreach (Type type in types) | ||||
|         { | ||||
|             if (type == typeof(IModuleInterface) || !typeof(IModuleInterface).IsAssignableFrom(type)) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if (Activator.CreateInstance(type) is not IModuleInterface moduleInterface) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             moduleInterface.StartupModule(); | ||||
|             ModuleInterfaces.Add(moduleInterface); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     [MethodImpl(MethodImplOptions.NoInlining)] | ||||
|     public void Unload() | ||||
|     { | ||||
|         ShutdownModule(); | ||||
|  | ||||
|         if (LoadContext == null) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         PluginLoadContext.RemoveAssemblyFromCache(AssemblyName.Name); | ||||
|          | ||||
|         LoadContext.Unload(); | ||||
|         LoadContext = null; | ||||
|         WeakRefAssembly = null; | ||||
|     } | ||||
|  | ||||
|     [MethodImpl(MethodImplOptions.NoInlining)] | ||||
|     public void ShutdownModule() | ||||
|     { | ||||
|         foreach (IModuleInterface moduleInterface in ModuleInterfaces) | ||||
|         { | ||||
|             moduleInterface.ShutdownModule(); | ||||
|         } | ||||
|  | ||||
|         ModuleInterfaces.Clear(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,84 @@ | ||||
| using System.Reflection; | ||||
| using System.Runtime.Loader; | ||||
| using UnrealSharp.Binds; | ||||
| using UnrealSharp.Core; | ||||
|  | ||||
| namespace UnrealSharp.Plugins; | ||||
|  | ||||
| public class PluginLoadContext : AssemblyLoadContext | ||||
| { | ||||
|     public PluginLoadContext(string assemblyName, AssemblyDependencyResolver resolver, bool isCollectible) : base(assemblyName, isCollectible) | ||||
|     { | ||||
|         _resolver = resolver; | ||||
|     } | ||||
|  | ||||
|     private readonly AssemblyDependencyResolver _resolver; | ||||
|     private static readonly Dictionary<string, WeakReference<Assembly>> LoadedAssemblies = new(); | ||||
|      | ||||
|     static PluginLoadContext() | ||||
|     { | ||||
|         AddAssembly(typeof(PluginLoader).Assembly); | ||||
|         AddAssembly(typeof(NativeBinds).Assembly); | ||||
|         AddAssembly(typeof(UnrealSharpObject).Assembly); | ||||
|         AddAssembly(typeof(UnrealSharpModule).Assembly); | ||||
|     } | ||||
|      | ||||
|     private static void AddAssembly(Assembly assembly) | ||||
|     { | ||||
|         LoadedAssemblies[assembly.GetName().Name!] = new WeakReference<Assembly>(assembly); | ||||
|     } | ||||
|      | ||||
|     public static void RemoveAssemblyFromCache(string assemblyName) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(assemblyName)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         LoadedAssemblies.Remove(assemblyName); | ||||
|     } | ||||
|  | ||||
|     protected override Assembly? Load(AssemblyName assemblyName) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(assemblyName.Name)) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|          | ||||
|         if (LoadedAssemblies.TryGetValue(assemblyName.Name, out WeakReference<Assembly>? weakRef) && weakRef.TryGetTarget(out Assembly? cachedAssembly)) | ||||
|         { | ||||
|             return cachedAssembly; | ||||
|         } | ||||
|  | ||||
|         string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); | ||||
|  | ||||
|         if (string.IsNullOrEmpty(assemblyPath)) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         using FileStream assemblyFile = File.Open(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read); | ||||
|         string pdbPath = Path.ChangeExtension(assemblyPath, ".pdb"); | ||||
|  | ||||
|         Assembly? loadedAssembly; | ||||
|         if (!File.Exists(pdbPath)) | ||||
|         { | ||||
|             loadedAssembly = LoadFromStream(assemblyFile); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read); | ||||
|             loadedAssembly = LoadFromStream(assemblyFile, pdbFile); | ||||
|         } | ||||
|          | ||||
|         LoadedAssemblies[assemblyName.Name] = new WeakReference<Assembly>(loadedAssembly); | ||||
|         return loadedAssembly; | ||||
|     } | ||||
|  | ||||
|     protected override nint LoadUnmanagedDll(string unmanagedDllName) | ||||
|     { | ||||
|         string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); | ||||
|  | ||||
|         return libraryPath != null ? LoadUnmanagedDllFromPath(libraryPath) : nint.Zero; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,138 @@ | ||||
| using System.Diagnostics; | ||||
| using System.Reflection; | ||||
| using System.Runtime.CompilerServices; | ||||
|  | ||||
| namespace UnrealSharp.Plugins; | ||||
|  | ||||
| public static class PluginLoader | ||||
| { | ||||
|     public static readonly List<Plugin> LoadedPlugins = []; | ||||
|  | ||||
|     public static Assembly? LoadPlugin(string assemblyPath, bool isCollectible) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             AssemblyName assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyPath)); | ||||
|  | ||||
|             foreach (Plugin loadedPlugin in LoadedPlugins) | ||||
|             { | ||||
|                 if (!loadedPlugin.IsLoadContextAlive) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|                  | ||||
|                 if (loadedPlugin.WeakRefAssembly?.Target is not Assembly assembly) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 if (assembly.GetName() != assemblyName) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 LogUnrealSharpPlugins.Log($"Plugin {assemblyName} is already loaded."); | ||||
|                 return assembly; | ||||
|             } | ||||
|              | ||||
|             Plugin plugin = new Plugin(assemblyName, isCollectible, assemblyPath); | ||||
|             if (plugin.Load() && plugin.WeakRefAssembly != null && plugin.WeakRefAssembly.Target is Assembly loadedAssembly) | ||||
|             { | ||||
|                 LoadedPlugins.Add(plugin); | ||||
|                 LogUnrealSharpPlugins.Log($"Successfully loaded plugin: {assemblyName}"); | ||||
|                 return loadedAssembly; | ||||
|             } | ||||
|              | ||||
|             throw new InvalidOperationException($"Failed to load plugin: {assemblyName}"); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             LogUnrealSharpPlugins.LogError($"An error occurred while loading the plugin: {ex.Message}"); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|      | ||||
|     [MethodImpl(MethodImplOptions.NoInlining)] | ||||
|     private static WeakReference? RemovePlugin(string assemblyName) | ||||
|     { | ||||
|         foreach (Plugin loadedPlugin in LoadedPlugins) | ||||
|         { | ||||
|             // Trying to resolve the weakptr to the assembly here will cause unload issues, so we compare names instead | ||||
|             if (!loadedPlugin.IsLoadContextAlive || loadedPlugin.AssemblyName.Name != assemblyName) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|              | ||||
|             loadedPlugin.Unload(); | ||||
|             LoadedPlugins.Remove(loadedPlugin); | ||||
|             return loadedPlugin.WeakRefLoadContext; | ||||
|         } | ||||
|          | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     public static bool UnloadPlugin(string assemblyPath) | ||||
|     { | ||||
|         string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); | ||||
|         WeakReference? assemblyLoadContext = RemovePlugin(assemblyName); | ||||
|          | ||||
|         if (assemblyLoadContext == null) | ||||
|         { | ||||
|             LogUnrealSharpPlugins.Log($"Plugin {assemblyName} is not loaded or already unloaded."); | ||||
|             return true; | ||||
|         } | ||||
|          | ||||
|         try | ||||
|         { | ||||
|             LogUnrealSharpPlugins.Log($"Unloading plugin {assemblyName}..."); | ||||
|  | ||||
|             int startTimeMs = Environment.TickCount; | ||||
|             bool takingTooLong = false; | ||||
|  | ||||
|             while (assemblyLoadContext.IsAlive) | ||||
|             { | ||||
|                 GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); | ||||
|                 GC.WaitForPendingFinalizers(); | ||||
|  | ||||
|                 if (!assemblyLoadContext.IsAlive) | ||||
|                 { | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 int elapsedTimeMs = Environment.TickCount - startTimeMs; | ||||
|                  | ||||
|                 if (!takingTooLong && elapsedTimeMs >= 200) | ||||
|                 { | ||||
|                     takingTooLong = true; | ||||
|                     LogUnrealSharpPlugins.LogError($"Unloading {assemblyName} is taking longer than expected..."); | ||||
|                 } | ||||
|                 else if (elapsedTimeMs >= 1000) | ||||
|                 { | ||||
|                     throw new InvalidOperationException($"Failed to unload {assemblyName}. Possible causes: Strong GC handles, running threads, etc."); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             LogUnrealSharpPlugins.Log($"{assemblyName} unloaded successfully!"); | ||||
|             return true; | ||||
|         } | ||||
|         catch (Exception e) | ||||
|         { | ||||
|             LogUnrealSharpPlugins.LogError($"An error occurred while unloading the plugin: {e.Message}"); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public static Plugin? FindPluginByName(string assemblyName) | ||||
|     { | ||||
|         foreach (Plugin loadedPlugin in LoadedPlugins) | ||||
|         { | ||||
|             if (loadedPlugin.AssemblyName.Name == assemblyName) | ||||
|             { | ||||
|                 return loadedPlugin; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,41 @@ | ||||
| using System.Reflection; | ||||
| using System.Runtime.InteropServices; | ||||
| using UnrealSharp.Core; | ||||
|  | ||||
| namespace UnrealSharp.Plugins; | ||||
|  | ||||
| [StructLayout(LayoutKind.Sequential)] | ||||
| public unsafe struct PluginsCallbacks | ||||
| { | ||||
|     public delegate* unmanaged<char*, NativeBool, nint> LoadPlugin; | ||||
|     public delegate* unmanaged<char*, NativeBool> UnloadPlugin; | ||||
|      | ||||
|     [UnmanagedCallersOnly] | ||||
|     private static nint ManagedLoadPlugin(char* assemblyPath, NativeBool isCollectible) | ||||
|     { | ||||
|         Assembly? newPlugin = PluginLoader.LoadPlugin(new string(assemblyPath), isCollectible.ToManagedBool()); | ||||
|  | ||||
|         if (newPlugin == null) | ||||
|         { | ||||
|             return IntPtr.Zero; | ||||
|         }; | ||||
|  | ||||
|         return GCHandle.ToIntPtr(GCHandleUtilities.AllocateStrongPointer(newPlugin, newPlugin)); | ||||
|     } | ||||
|  | ||||
|     [UnmanagedCallersOnly] | ||||
|     private static NativeBool ManagedUnloadPlugin(char* assemblyPath) | ||||
|     { | ||||
|         string assemblyPathStr = new(assemblyPath); | ||||
|         return PluginLoader.UnloadPlugin(assemblyPathStr).ToNativeBool(); | ||||
|     } | ||||
|  | ||||
|     public static PluginsCallbacks Create() | ||||
|     { | ||||
|         return new PluginsCallbacks | ||||
|         { | ||||
|             LoadPlugin = &ManagedLoadPlugin, | ||||
|             UnloadPlugin = &ManagedUnloadPlugin, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,37 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
|         <OutputPath>../../../Binaries/Managed</OutputPath> | ||||
|         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> | ||||
| 	    <NoWarn>$(NoWarn);1570;0649;0169;0108;0109</NoWarn> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <DefineConstants>WITH_EDITOR</DefineConstants> | ||||
|         <DefineConstants Condition="'$(DisableWithEditor)' == 'true'">$(DefineConstants.Replace('WITH_EDITOR;', '').Replace('WITH_EDITOR', ''))</DefineConstants> | ||||
|         <DefineConstants Condition="'$(DefineAdditionalConstants)' != ''">$(DefineConstants);$(DefineAdditionalConstants)</DefineConstants> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\UnrealSharp.Core\UnrealSharp.Core.csproj" /> | ||||
|       <ProjectReference Include="..\UnrealSharp.Log\UnrealSharp.Log.csproj" /> | ||||
|          | ||||
|         <ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj"> | ||||
|             <OutputItemType>Analyzer</OutputItemType> | ||||
|             <ReferenceOutputAssembly>false</ReferenceOutputAssembly> | ||||
|         </ProjectReference> | ||||
|          | ||||
|         <ProjectReference Include="..\UnrealSharp\UnrealSharp.csproj" /> | ||||
|          | ||||
|         <PackageReference Include="Microsoft.Build.Locator" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <Compile Include="..\..\Shared\DotNetUtilities.cs" Link="..\..\Shared\DotNetUtilities.cs" /> | ||||
|     </ItemGroup> | ||||
|      | ||||
| </Project> | ||||
| @ -0,0 +1,50 @@ | ||||
| using System.Reflection; | ||||
| using System.Runtime.InteropServices; | ||||
|  | ||||
| namespace UnrealSharp; | ||||
|  | ||||
| public class UnrealSharpDllImportResolver(IntPtr internalHandle) | ||||
| { | ||||
|     public IntPtr OnResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) | ||||
|     { | ||||
|         if (libraryName != "__Internal") | ||||
|         { | ||||
|             return IntPtr.Zero; | ||||
|         } | ||||
|          | ||||
|         if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||||
|         { | ||||
|             return Win32.GetModuleHandle(IntPtr.Zero); | ||||
|         } | ||||
|  | ||||
|         if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) | ||||
|         { | ||||
|             return internalHandle; | ||||
|         } | ||||
|  | ||||
|         if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) | ||||
|         { | ||||
|             return MacOS.dlopen(IntPtr.Zero, MacOS.RTLD_LAZY); | ||||
|         } | ||||
|  | ||||
|         return IntPtr.Zero; | ||||
|     } | ||||
|      | ||||
|     private static class MacOS | ||||
|     { | ||||
|         private const string SystemLibrary = "/usr/lib/libSystem.dylib"; | ||||
|  | ||||
|         public const int RTLD_LAZY = 1; | ||||
|  | ||||
|         [DllImport(SystemLibrary)] | ||||
|         public static extern IntPtr dlopen(IntPtr path, int mode); | ||||
|     } | ||||
|  | ||||
|     private static class Win32 | ||||
|     { | ||||
|         private const string SystemLibrary = "Kernel32.dll"; | ||||
|  | ||||
|         [DllImport(SystemLibrary)] | ||||
|         public static extern IntPtr GetModuleHandle(IntPtr lpModuleName); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,3 @@ | ||||
| ; Shipped analyzer releases | ||||
| ; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md | ||||
|  | ||||
| @ -0,0 +1,21 @@ | ||||
| ; Unshipped analyzer release | ||||
| ; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md | ||||
|  | ||||
| ### New Rules | ||||
|  | ||||
| Rule ID | Category | Severity | Notes | ||||
| --------|----------|----------|------- | ||||
| PrefixAnalyzer | Naming | Error | UnrealTypeAnalyzer | ||||
| US0001 | UnrealSharp | Error | UStaticLambdaAnalyzer | ||||
| US0002 | Category | Error | UEnumAnalyzer | ||||
| US0003 | Category | Error | UInterfaceAnalyzer | ||||
| US0004 | Category | Error | UInterfaceAnalyzer | ||||
| US0006 | Category | Error | UnrealTypeAnalyzer | ||||
| US0007 | Category | Error | UObjectCreationAnalyzer | ||||
| US0008 | Category | Error | UObjectCreationAnalyzer | ||||
| US0009 | Category | Error | UObjectCreationAnalyzer | ||||
| US0010 | Category | Error | UObjectCreationAnalyzer | ||||
| US0011 | Category | Error | UObjectCreationAnalyzer | ||||
| US0012 | Usage | Error | UFunctionConflictAnalyzer | ||||
| US0013 | Category | Error | DefaultComponentAnalyzer | ||||
| US0014 | Category | Error | DefaultComponentAnalyzer | ||||
| @ -0,0 +1,182 @@ | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Operations; | ||||
| using System; | ||||
| using System.Collections.Immutable; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| public static class AnalyzerStatics | ||||
| { | ||||
|     public const string UStructAttribute = "UStructAttribute"; | ||||
|     public const string UEnumAttribute = "UEnumAttribute"; | ||||
|     public const string UClassAttribute = "UClassAttribute"; | ||||
|     public const string UInterfaceAttribute = "UInterfaceAttribute"; | ||||
|     public const string UMultiDelegateAttribute = "UMultiDelegateAttribute"; | ||||
|     public const string USingleDelegateAttribute = "USingleDelegateAttribute"; | ||||
|  | ||||
|     public const string GeneratedTypeAttribute = "GeneratedTypeAttribute"; | ||||
|      | ||||
|     public const string UPropertyAttribute = "UPropertyAttribute"; | ||||
|     public const string UFunctionAttribute = "UFunctionAttribute"; | ||||
|      | ||||
|     public const string BindingAttribute = "BindingAttribute"; | ||||
|      | ||||
|     public const string UObject = "UObject"; | ||||
|     public const string AActor = "AActor"; | ||||
|      | ||||
|     public const string DefaultComponent = "DefaultComponent"; | ||||
|     public const string New = "new"; | ||||
|     public const string UActorComponent = "UActorComponent"; | ||||
|     public const string USceneComponent = "USceneComponent"; | ||||
|     public const string UUserWidget = "UUserWidget"; | ||||
|      | ||||
|     private const string ContainerNamespace = "System.Collections.Generic"; | ||||
|     private static readonly string[] ContainerInterfaces = | ||||
|     { | ||||
|         "IList", | ||||
|         "IReadOnlyList", | ||||
|         "IDictionary", | ||||
|         "IReadOnlyDictionary", | ||||
|         "ISet", | ||||
|         "IReadOnlySet", | ||||
|     }; | ||||
|      | ||||
|     public static bool HasAttribute(ISymbol symbol, string attributeName) | ||||
|     { | ||||
|         foreach (var attribute in symbol.GetAttributes()) | ||||
|         { | ||||
|             if (attribute.AttributeClass?.Name == attributeName) | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     public static bool TryGetAttribute(ISymbol symbol, string attributeName, out AttributeData? attribute) | ||||
|     { | ||||
|         attribute = symbol.GetAttributes() | ||||
|             .FirstOrDefault(x => x.AttributeClass is not null && x.AttributeClass.Name == attributeName); | ||||
|          | ||||
|         return attribute is not null; | ||||
|     } | ||||
|      | ||||
|     public static bool HasAttribute(MemberDeclarationSyntax memberDecl, string attributeName) | ||||
|     { | ||||
|         foreach (var attrList in memberDecl.AttributeLists) | ||||
|         { | ||||
|             foreach (var attr in attrList.Attributes) | ||||
|             { | ||||
|                 if (attr.Name.ToString().Contains(attributeName)) | ||||
|                 { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     public static bool InheritsFrom(IPropertySymbol propertySymbol, string baseTypeName) | ||||
|     { | ||||
|         return propertySymbol.Type is INamedTypeSymbol namedTypeSymbol && InheritsFrom(namedTypeSymbol, baseTypeName); | ||||
|     } | ||||
|  | ||||
|     public static bool InheritsFrom(INamedTypeSymbol symbol, string baseTypeName) | ||||
|     { | ||||
|         INamedTypeSymbol? currentSymbol = symbol; | ||||
|  | ||||
|         while (currentSymbol != null) | ||||
|         { | ||||
|             if (currentSymbol.Name == baseTypeName) | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
|             currentSymbol = currentSymbol.BaseType; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     public static bool IsDefaultComponent(AttributeData? attributeData) | ||||
|     { | ||||
|         if (attributeData?.AttributeClass?.Name != UPropertyAttribute) return false; | ||||
|  | ||||
|         var argument = attributeData.NamedArguments.FirstOrDefault(x => x.Key == DefaultComponent); | ||||
|         if (string.IsNullOrWhiteSpace(argument.Key)) return false; | ||||
|  | ||||
|         return argument.Value.Value is true; | ||||
|     } | ||||
|  | ||||
|     public static bool IsNewKeywordInstancingOperation(IObjectCreationOperation operation, out Location? location) | ||||
|     { | ||||
|         location = null; | ||||
|         if (operation.Syntax is not ObjectCreationExpressionSyntax objectCreationExpression) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         location = objectCreationExpression.NewKeyword.GetLocation(); | ||||
|         return objectCreationExpression.NewKeyword.ValueText == New; | ||||
|     } | ||||
|  | ||||
|     public static bool IsContainerInterface(ITypeSymbol symbol) | ||||
|     { | ||||
|         var namespaceName = symbol.ContainingNamespace.ToString(); | ||||
|         return namespaceName.Equals(ContainerNamespace, StringComparison.InvariantCultureIgnoreCase) && | ||||
|                ContainerInterfaces.Contains(symbol.Name); | ||||
|     } | ||||
|      | ||||
|     public static string GenerateUniqueMethodName(ClassDeclarationSyntax containingClass, string suffix) | ||||
|     { | ||||
|         int counter = 1; | ||||
|         ImmutableHashSet<string> existingNames = containingClass.Members | ||||
|             .OfType<MethodDeclarationSyntax>() | ||||
|             .Select(m => m.Identifier.ValueText) | ||||
|             .ToImmutableHashSet(); | ||||
|  | ||||
|         string methodName; | ||||
|         do | ||||
|         { | ||||
|             methodName = $"Generated_{suffix}_{counter++}"; | ||||
|         }  | ||||
|         while (existingNames.Contains(methodName)); | ||||
|  | ||||
|         return methodName; | ||||
|     } | ||||
|  | ||||
|     public static string GetFullNamespace(this CSharpSyntaxNode declaration) | ||||
|     { | ||||
|         var namespaceNode = declaration.FirstAncestorOrSelf<BaseNamespaceDeclarationSyntax>(); | ||||
|         var namespaceBuilder = new StringBuilder(); | ||||
|         if (namespaceNode != null) | ||||
|         { | ||||
|             namespaceBuilder.Append(namespaceNode.Name.ToString()); | ||||
|             var currentNamespace = namespaceNode.Parent as BaseNamespaceDeclarationSyntax; | ||||
|             while (currentNamespace != null) | ||||
|             { | ||||
|                 namespaceBuilder.Insert(0, $"{currentNamespace.Name}."); | ||||
|                 currentNamespace = currentNamespace.Parent as BaseNamespaceDeclarationSyntax; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return namespaceBuilder.ToString(); | ||||
|     } | ||||
|  | ||||
|     public static string? GetAnnotatedTypeName(this TypeSyntax? type, SemanticModel model) | ||||
|     { | ||||
|         if (type is null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         var typeInfo = model.GetTypeInfo(type).Type; | ||||
|         return type is NullableTypeSyntax ? | ||||
|             typeInfo?.WithNullableAnnotation(NullableAnnotation.Annotated).ToString() : typeInfo?.ToString(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,379 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| internal readonly struct AsyncMethodInfo( | ||||
|     ClassDeclarationSyntax parentClass, | ||||
|     MethodDeclarationSyntax method, | ||||
|     string ns, | ||||
|     TypeSyntax? returnType, | ||||
|     IReadOnlyDictionary<string, string> metadata, | ||||
|     bool nullableAwareable, | ||||
|     bool returnsValueTask = false) | ||||
| { | ||||
|     public ClassDeclarationSyntax ParentClass { get; } = parentClass; | ||||
|     public MethodDeclarationSyntax Method { get; } = method; | ||||
|     public string Namespace { get; } = ns; | ||||
|     public TypeSyntax? ReturnType { get; } = returnType; | ||||
|     public IReadOnlyDictionary<string, string> Metadata { get; } = metadata; | ||||
|     public bool NullableAwareable { get; } = nullableAwareable; | ||||
|     public bool ReturnsValueTask { get; } = returnsValueTask; | ||||
| } | ||||
|  | ||||
| [Generator] | ||||
| public class AsyncWrapperGenerator : IIncrementalGenerator | ||||
| { | ||||
|     public void Initialize(IncrementalGeneratorInitializationContext context) | ||||
|     { | ||||
|         var asyncMethods = context.SyntaxProvider.CreateSyntaxProvider( | ||||
|                 static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax } m && m.AttributeLists.Count > 0, | ||||
|                 static (syntaxContext, _) => GetAsyncMethodInfo(syntaxContext)) | ||||
|             .Where(static m => m.HasValue) | ||||
|             .Select(static (m, _) => m!.Value); | ||||
|  | ||||
|         var asyncMethodsWithCompilation = asyncMethods.Combine(context.CompilationProvider); | ||||
|  | ||||
|         context.RegisterSourceOutput(asyncMethodsWithCompilation, static (spc, pair) => | ||||
|         { | ||||
|             var methodInfo = pair.Left; | ||||
|             var compilation = pair.Right; | ||||
|             var source = Generate(methodInfo, compilation); | ||||
|             if (!string.IsNullOrEmpty(source)) | ||||
|             { | ||||
|                 spc.AddSource($"{methodInfo.ParentClass.Identifier.Text}.{methodInfo.Method.Identifier.Text}.generated.cs", SourceText.From(source, Encoding.UTF8)); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private static string Generate(AsyncMethodInfo asyncMethodInfo, Compilation compilation) | ||||
|     { | ||||
|         var model = compilation.GetSemanticModel(asyncMethodInfo.Method.SyntaxTree); | ||||
|         var method = asyncMethodInfo.Method; | ||||
|  | ||||
|         var cancellationTokenType = compilation.GetTypeByMetadataName("System.Threading.CancellationToken"); | ||||
|         ParameterSyntax? cancellationTokenParameter = null; | ||||
|  | ||||
|         HashSet<string> namespaces = new() { "UnrealSharp", "UnrealSharp.Attributes", "UnrealSharp.UnrealSharpCore" }; | ||||
|         foreach (var parameter in method.ParameterList.Parameters) | ||||
|         { | ||||
|             if (parameter.Type == null) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|             var typeInfo = model.GetTypeInfo(parameter.Type); | ||||
|             var typeSymbol = typeInfo.Type; | ||||
|             if (SymbolEqualityComparer.Default.Equals(typeSymbol, cancellationTokenType)) | ||||
|             { | ||||
|                 cancellationTokenParameter = parameter; | ||||
|             } | ||||
|  | ||||
|             if (typeSymbol == null || typeSymbol.ContainingNamespace == null) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if (typeSymbol is INamedTypeSymbol nts && nts.IsGenericType) | ||||
|             { | ||||
|                 namespaces.UnionWith(nts.TypeArguments.Select(t => t.ContainingNamespace.ToDisplayString())); | ||||
|             } | ||||
|  | ||||
|             namespaces.Add(typeSymbol.ContainingNamespace.ToDisplayString()); | ||||
|                  | ||||
|             namespaces.UnionWith(parameter.AttributeLists.SelectMany(a => a.Attributes) | ||||
|                 .Select(a => model.GetTypeInfo(a).Type) | ||||
|                 .Where(type => type is not null) | ||||
|                 .Where(type => type!.ContainingNamespace is not null) | ||||
|                 .Select(type => type!.ContainingNamespace.ToDisplayString())); | ||||
|         } | ||||
|  | ||||
|         var returnTypeName = asyncMethodInfo.ReturnType.GetAnnotatedTypeName(model); | ||||
|         var actionClassName = $"{asyncMethodInfo.ParentClass.Identifier.Text}{method.Identifier.Text}Action"; | ||||
|         var actionBaseClassName = cancellationTokenParameter != null ? "UCSCancellableAsyncAction" : "UCSBlueprintAsyncActionBase"; | ||||
|         var delegateName = $"{actionClassName}Delegate"; | ||||
|         var taskTypeName = asyncMethodInfo.ReturnType != null ? $"Task<{returnTypeName}>" : "Task"; | ||||
|         var paramNameList = string.Join(", ", method.ParameterList.Parameters.Select(p => p == cancellationTokenParameter ? "cancellationToken" : p.Identifier.Text)); | ||||
|         var paramDeclListNoCancellationToken = string.Join(", ", method.ParameterList.Parameters.Where(p => p != cancellationTokenParameter)); | ||||
|  | ||||
|         var metadataAttributeList = string.Join(", ", asyncMethodInfo.Metadata.Select(static pair => $"UMetaData({pair.Key}, {pair.Value})")); | ||||
|         if (string.IsNullOrEmpty(metadataAttributeList)) | ||||
|         { | ||||
|             metadataAttributeList = "UMetaData(\"BlueprintInternalUseOnly\", \"true\")"; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             metadataAttributeList = $"UMetaData(\"BlueprintInternalUseOnly\", \"true\"), {metadataAttributeList}"; | ||||
|         } | ||||
|  | ||||
|         var isStatic = method.Modifiers.Any(static x => x.IsKind(SyntaxKind.StaticKeyword)); | ||||
|         if (!isStatic) | ||||
|         { | ||||
|             metadataAttributeList = $"UMetaData(\"DefaultToSelf\", \"Target\"), {metadataAttributeList}"; | ||||
|         } | ||||
|  | ||||
|         var sourceBuilder = new StringBuilder(); | ||||
|         var nullableAnnotation = "?"; | ||||
|         var nullableSuppression = "!"; | ||||
|         if (asyncMethodInfo.NullableAwareable) | ||||
|         { | ||||
|             sourceBuilder.AppendLine("#nullable enable"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine("#nullable disable"); | ||||
|             nullableAnnotation = ""; | ||||
|             nullableSuppression = ""; | ||||
|         } | ||||
|         sourceBuilder.AppendLine(); | ||||
|         foreach (var ns in namespaces) | ||||
|         { | ||||
|             if (!string.IsNullOrWhiteSpace(ns)) | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"using {ns};"); | ||||
|             } | ||||
|         } | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"namespace {asyncMethodInfo.Namespace};"); | ||||
|         sourceBuilder.AppendLine(); | ||||
|         if (asyncMethodInfo.ReturnType != null) | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"public delegate void {delegateName}({returnTypeName} Result, string{nullableAnnotation} Exception);"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"public delegate void {delegateName}(string{nullableAnnotation} Exception);"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"public class U{delegateName} : MulticastDelegate<{delegateName}>"); | ||||
|         sourceBuilder.AppendLine("{"); | ||||
|         if (asyncMethodInfo.ReturnType != null) | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"    protected void Invoker({returnTypeName} Result, string{nullableAnnotation} Exception)"); | ||||
|             sourceBuilder.AppendLine("    {"); | ||||
|             sourceBuilder.AppendLine("        ProcessDelegate(IntPtr.Zero);"); | ||||
|             sourceBuilder.AppendLine("    }"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"    protected void Invoker(string{nullableAnnotation} Exception)"); | ||||
|             sourceBuilder.AppendLine("    {"); | ||||
|             sourceBuilder.AppendLine("        ProcessDelegate(IntPtr.Zero);"); | ||||
|             sourceBuilder.AppendLine("    }"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"    protected override {delegateName} GetInvoker()"); | ||||
|         sourceBuilder.AppendLine("    {"); | ||||
|         sourceBuilder.AppendLine("        return Invoker;"); | ||||
|         sourceBuilder.AppendLine("    }"); | ||||
|         sourceBuilder.AppendLine("}"); | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine("[UClass]"); | ||||
|         sourceBuilder.AppendLine($"public class {actionClassName} : {actionBaseClassName}"); | ||||
|         sourceBuilder.AppendLine("{"); | ||||
|         sourceBuilder.AppendLine($"    private {taskTypeName}{nullableAnnotation} task;"); | ||||
|         if (cancellationTokenParameter != null) | ||||
|         { | ||||
|             sourceBuilder.AppendLine("    private readonly CancellationTokenSource cancellationTokenSource = new();"); | ||||
|             sourceBuilder.AppendLine($"    private Func<CancellationToken, {taskTypeName}>{nullableAnnotation} asyncDelegate;"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"    private Func<{taskTypeName}>{nullableAnnotation} asyncDelegate;"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"    [UProperty(PropertyFlags.BlueprintAssignable)]"); | ||||
|         sourceBuilder.AppendLine($"    public TMulticastDelegate<{delegateName}>{nullableAnnotation} Completed {{ get; set; }}"); | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"    [UProperty(PropertyFlags.BlueprintAssignable)]"); | ||||
|         sourceBuilder.AppendLine($"    public TMulticastDelegate<{delegateName}>{nullableAnnotation} Failed {{ get; set; }}"); | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"    [UFunction(FunctionFlags.BlueprintCallable), {metadataAttributeList}]"); | ||||
|         string conversion = asyncMethodInfo.ReturnsValueTask ? ".AsTask()" : ""; | ||||
|         if (isStatic) | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"    public static {actionClassName} {method.Identifier.Text}({paramDeclListNoCancellationToken})"); | ||||
|             sourceBuilder.AppendLine($"    {{"); | ||||
|             sourceBuilder.AppendLine($"        var action = NewObject<{actionClassName}>(GetTransientPackage());"); | ||||
|              | ||||
|             if (cancellationTokenParameter != null) | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"        action.asyncDelegate = (cancellationToken) => {asyncMethodInfo.ParentClass.Identifier.Text}.{method.Identifier.Text}({paramNameList}){conversion};"); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"        action.asyncDelegate = () => {asyncMethodInfo.ParentClass.Identifier.Text}.{method.Identifier.Text}({paramNameList}){conversion};"); | ||||
|             } | ||||
|             sourceBuilder.AppendLine($"        return action;"); | ||||
|             sourceBuilder.AppendLine($"    }}"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(paramDeclListNoCancellationToken)) | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"    public static {actionClassName} {method.Identifier.Text}({asyncMethodInfo.ParentClass.Identifier.Text} Target)"); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"    public static {actionClassName} {method.Identifier.Text}({asyncMethodInfo.ParentClass.Identifier.Text} Target, {paramDeclListNoCancellationToken})"); | ||||
|             } | ||||
|             sourceBuilder.AppendLine($"    {{"); | ||||
|             sourceBuilder.AppendLine($"        var action = NewObject<{actionClassName}>(Target);"); | ||||
|             if (cancellationTokenParameter != null) | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"        action.asyncDelegate = (cancellationToken) => Target.{method.Identifier.Text}({paramNameList}){conversion};"); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"        action.asyncDelegate = () => Target.{method.Identifier.Text}({paramNameList}){conversion};"); | ||||
|             } | ||||
|             sourceBuilder.AppendLine($"        return action;"); | ||||
|             sourceBuilder.AppendLine($"    }}"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"    protected override void Activate()"); | ||||
|         sourceBuilder.AppendLine($"    {{"); | ||||
|         sourceBuilder.AppendLine($"        if (asyncDelegate == null) {{ throw new InvalidOperationException(\"AsyncDelegate was null\"); }}"); | ||||
|         if (cancellationTokenParameter != null) | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"        task = asyncDelegate(cancellationTokenSource.Token);"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"        task = asyncDelegate();"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine($"        task.ContinueWith(OnTaskCompleted);"); | ||||
|         sourceBuilder.AppendLine($"    }}"); | ||||
|         if (cancellationTokenParameter != null) | ||||
|         { | ||||
|             sourceBuilder.AppendLine(); | ||||
|             sourceBuilder.AppendLine($"    protected override void Cancel()"); | ||||
|             sourceBuilder.AppendLine($"    {{"); | ||||
|             sourceBuilder.AppendLine($"        cancellationTokenSource.Cancel();"); | ||||
|             sourceBuilder.AppendLine($"        base.Cancel();"); | ||||
|             sourceBuilder.AppendLine($"    }}"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"    private void OnTaskCompleted({taskTypeName} t)"); | ||||
|         sourceBuilder.AppendLine($"    {{"); | ||||
|         // sourceBuilder.AppendLine($"        if (!IsDestroyed) {{ PrintString($\"OnTaskCompleted for {{this}} on {{UnrealSynchronizationContext.CurrentThread}}\"); }}"); | ||||
|         sourceBuilder.AppendLine($"        if (UnrealSynchronizationContext.CurrentThread != NamedThread.GameThread)"); | ||||
|         sourceBuilder.AppendLine($"        {{"); | ||||
|         sourceBuilder.AppendLine($"            new UnrealSynchronizationContext(NamedThread.GameThread, t).Post(_ => OnTaskCompleted(t), null);"); | ||||
|         sourceBuilder.AppendLine($"            return;"); | ||||
|         sourceBuilder.AppendLine($"        }}"); | ||||
|         if (cancellationTokenParameter != null) | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"        if (cancellationTokenSource.IsCancellationRequested || IsDestroyed) {{ return; }}"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"        if (IsDestroyed) {{ return; }}"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine($"        if (t.IsFaulted)"); | ||||
|         sourceBuilder.AppendLine($"        {{"); | ||||
|         if (asyncMethodInfo.ReturnType != null) | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"            Failed?.InnerDelegate.Invoke(default{nullableSuppression}, t.Exception?.ToString() ?? \"Faulted without exception\");"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"            Failed?.InnerDelegate.Invoke(t.Exception?.ToString() ?? \"Faulted without exception\");"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine($"        }}"); | ||||
|         sourceBuilder.AppendLine($"        else"); | ||||
|         sourceBuilder.AppendLine($"        {{"); | ||||
|         if (asyncMethodInfo.ReturnType != null) | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"            Completed?.InnerDelegate.Invoke(t.Result, null);"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine($"            Completed?.InnerDelegate.Invoke(null);"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine($"        }}"); | ||||
|         sourceBuilder.AppendLine($"    }}"); | ||||
|         sourceBuilder.AppendLine($"}}"); | ||||
|  | ||||
|         return sourceBuilder.ToString(); | ||||
|     } | ||||
|  | ||||
|     private static AsyncMethodInfo? GetAsyncMethodInfo(GeneratorSyntaxContext context) | ||||
|     { | ||||
|         if (context.Node is not MethodDeclarationSyntax methodDeclaration) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|         if (methodDeclaration.Parent is not ClassDeclarationSyntax classDeclaration) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         var hasUFunctionAttribute = methodDeclaration.AttributeLists | ||||
|             .SelectMany(a => a.Attributes) | ||||
|             .Any(a => a.Name.ToString() == "UFunction"); | ||||
|         if (!hasUFunctionAttribute) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         TypeSyntax? returnType; | ||||
|         bool returnsValueTask; | ||||
|         switch (methodDeclaration.ReturnType) | ||||
|         { | ||||
|             case IdentifierNameSyntax { Identifier.ValueText: "Task" }: | ||||
|                 // Method returns non-generic task, e.g. without return value | ||||
|                 returnType = null; | ||||
|                 returnsValueTask = false; | ||||
|                 break; | ||||
|             case GenericNameSyntax { Identifier.ValueText: "Task" } genericTask: | ||||
|                 // Method returns generic task, e.g. with return value | ||||
|                 returnType = genericTask.TypeArgumentList.Arguments.Single(); | ||||
|                 returnsValueTask = false; | ||||
|                 break; | ||||
|             case IdentifierNameSyntax { Identifier.ValueText: "ValueTask" }: | ||||
|                 // Method returns non-generic task, e.g. without return value | ||||
|                 returnType = null; | ||||
|                 returnsValueTask = true; | ||||
|                 break; | ||||
|             case GenericNameSyntax { Identifier.ValueText: "ValueTask" } genericValueTask: | ||||
|                 // Method returns generic task, e.g. with return value | ||||
|                 returnType = genericValueTask.TypeArgumentList.Arguments.Single(); | ||||
|                 returnsValueTask = true; | ||||
|                 break; | ||||
|             default: | ||||
|                 return null; | ||||
|         } | ||||
|  | ||||
|         string namespaceName = methodDeclaration.GetFullNamespace(); | ||||
|         if (string.IsNullOrEmpty(namespaceName)) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         var metadataAttributes = methodDeclaration.AttributeLists | ||||
|             .SelectMany(a => a.Attributes) | ||||
|             .Where(a => a.Name.ToString() == "UMetaData" || a.GetFullNamespace() == "UnrealSharp.Attributes.MetaData"); | ||||
|  | ||||
|         Dictionary<string, string> metadata = new(); | ||||
|         foreach (var metadataAttribute in metadataAttributes) | ||||
|         { | ||||
|             if (metadataAttribute.ArgumentList == null || metadataAttribute.ArgumentList.Arguments.Count == 0) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|             var key = metadataAttribute.ArgumentList.Arguments[0].Expression.ToString(); | ||||
|             var value = metadataAttribute.ArgumentList.Arguments.Count > 1 ? metadataAttribute.ArgumentList.Arguments[1].Expression.ToString() : ""; | ||||
|             metadata[key] = value; | ||||
|         } | ||||
|  | ||||
|         return new AsyncMethodInfo(classDeclaration, methodDeclaration, namespaceName, returnType, metadata,  | ||||
|             context.SemanticModel | ||||
|                 .GetNullableContext(context.Node.Span.Start) | ||||
|                 .HasFlag(NullableContext.AnnotationsEnabled), returnsValueTask); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,65 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeAnalyzers; | ||||
|  | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class DefaultComponentAnalyzer : DiagnosticAnalyzer | ||||
| { | ||||
|     public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( | ||||
|         DefaultComponentRule, | ||||
|         DefaultComponentSetterRule | ||||
|         ); | ||||
|      | ||||
|     public const string DefaultComponentAnalyzerId = "US0013"; | ||||
|     private static readonly LocalizableString DefaultComponentAnalyzerTitle = "UnrealSharp DefaultComponent Analyzer"; | ||||
|     private static readonly LocalizableString DefaultComponentAnalyzerMessageFormat = "{0} is a DefaultComponent, which is not inherit from UActorComponent"; | ||||
|     private static readonly LocalizableString DefaultComponentAnalyzerDescription = "Ensures property type marked as DefaultComponent inherits from UActorComponent."; | ||||
|     private static readonly DiagnosticDescriptor DefaultComponentRule = new(DefaultComponentAnalyzerId, DefaultComponentAnalyzerTitle, DefaultComponentAnalyzerMessageFormat, RuleCategory.Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DefaultComponentAnalyzerDescription); | ||||
|  | ||||
|     public const string DefaultComponentSetterAnalyzerId = "US0014"; | ||||
|     private static readonly LocalizableString DefaultComponentSetterAnalyzerTitle = "UnrealSharp DefaultComponent Setter Analyzer"; | ||||
|     private static readonly LocalizableString DefaultComponentSetterAnalyzerMessageFormat = "{0} is a DefaultComponent without setter"; | ||||
|     private static readonly LocalizableString DefaultComponentSetterAnalyzerDescription = "Ensures property marked as DefaultComponent has setter."; | ||||
|     private static readonly DiagnosticDescriptor DefaultComponentSetterRule = new(DefaultComponentSetterAnalyzerId, DefaultComponentSetterAnalyzerTitle, DefaultComponentSetterAnalyzerMessageFormat, RuleCategory.Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DefaultComponentSetterAnalyzerDescription); | ||||
|      | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         context.EnableConcurrentExecution(); | ||||
|         context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); | ||||
|         context.RegisterSymbolAction(AnalyzeClassProperties, SymbolKind.Property); | ||||
|     } | ||||
|  | ||||
|     private static void AnalyzeClassProperties(SymbolAnalysisContext context) | ||||
|     { | ||||
|         if (context.Symbol.ContainingType.TypeKind != TypeKind.Class) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         if (context.Symbol is not IPropertySymbol propertySymbol) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         if (!AnalyzerStatics.TryGetAttribute(propertySymbol, AnalyzerStatics.UPropertyAttribute, out var propertyAttribute)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         bool isDefaultComponent = AnalyzerStatics.IsDefaultComponent(propertyAttribute); | ||||
|         bool inheritFromActorComponent = AnalyzerStatics.InheritsFrom(propertySymbol, AnalyzerStatics.UActorComponent); | ||||
|          | ||||
|         if (isDefaultComponent && !inheritFromActorComponent) | ||||
|         { | ||||
|             context.ReportDiagnostic(Diagnostic.Create(DefaultComponentRule, propertySymbol.Locations[0], propertySymbol.Name)); | ||||
|         } | ||||
|  | ||||
|         if (isDefaultComponent && propertySymbol.SetMethod is null) | ||||
|         { | ||||
|             context.ReportDiagnostic(Diagnostic.Create(DefaultComponentSetterRule, propertySymbol.Locations[0], propertySymbol.Name)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -0,0 +1,75 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeAnalyzers; | ||||
|  | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class UFunctionConflictAnalyzer : DiagnosticAnalyzer | ||||
| { | ||||
|     private static readonly DiagnosticDescriptor Rule = new( | ||||
|         "US0012", | ||||
|         "Conflicting UFunction Attribute", | ||||
|         "Method '{0}' in class '{1}' should not have a UFunction attribute because it is already defined in the interface '{2}'", | ||||
|         "Usage", | ||||
|         DiagnosticSeverity.Error, | ||||
|         isEnabledByDefault: true); | ||||
|  | ||||
|     public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||||
|         context.EnableConcurrentExecution(); | ||||
|         context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); | ||||
|     } | ||||
|  | ||||
|     private static void AnalyzeMethod(SymbolAnalysisContext context) | ||||
|     { | ||||
|         IMethodSymbol methodSymbol = (IMethodSymbol) context.Symbol; | ||||
|          | ||||
|         foreach (var implementedMethod in methodSymbol.ExplicitInterfaceImplementations) | ||||
|         { | ||||
|             CheckForUFunctionConflict(context, methodSymbol, implementedMethod); | ||||
|         } | ||||
|          | ||||
|         foreach (INamedTypeSymbol? typeSymbol in methodSymbol.ContainingType.AllInterfaces) | ||||
|         { | ||||
|             foreach (IMethodSymbol? interfaceMethod in typeSymbol.GetMembers().OfType<IMethodSymbol>()) | ||||
|             { | ||||
|                 ISymbol? implementation = methodSymbol.ContainingType.FindImplementationForInterfaceMember(interfaceMethod); | ||||
|                 if (SymbolEqualityComparer.Default.Equals(implementation, methodSymbol)) | ||||
|                 { | ||||
|                     CheckForUFunctionConflict(context, methodSymbol, interfaceMethod); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static void CheckForUFunctionConflict( | ||||
|         SymbolAnalysisContext context, | ||||
|         IMethodSymbol implementationMethod, | ||||
|         IMethodSymbol interfaceMethod) | ||||
|     { | ||||
|         bool HasUFunction(IMethodSymbol method) | ||||
|         { | ||||
|             return method.GetAttributes().Any(attr => | ||||
|                 attr.AttributeClass?.Name == AnalyzerStatics.UFunctionAttribute); | ||||
|         } | ||||
|          | ||||
|         if (!HasUFunction(interfaceMethod) || !HasUFunction(implementationMethod)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         Diagnostic diagnostic = Diagnostic.Create( | ||||
|             Rule, | ||||
|             implementationMethod.Locations[0], | ||||
|             implementationMethod.Name, | ||||
|             implementationMethod.ContainingType.Name, | ||||
|             interfaceMethod.ContainingType.Name); | ||||
|  | ||||
|         context.ReportDiagnostic(diagnostic); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,158 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Composition; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CodeActions; | ||||
| using Microsoft.CodeAnalysis.CodeFixes; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Rename; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeAnalyzers; | ||||
|  | ||||
| [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UnrealTypeCodeFixProvider)), Shared] | ||||
| public class UnrealTypeCodeFixProvider : CodeFixProvider | ||||
| { | ||||
|     public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create( | ||||
|         UnrealTypeAnalyzer.StructAnalyzerId, | ||||
|         UnrealTypeAnalyzer.ClassAnalyzerId, | ||||
|         UnrealTypeAnalyzer.PrefixAnalyzerId); | ||||
|  | ||||
|     public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||||
|  | ||||
|     public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||||
|     { | ||||
|         var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||||
|  | ||||
|         foreach (var diagnostic in context.Diagnostics) | ||||
|         { | ||||
|             TextSpan diagnosticSpan = diagnostic.Location.SourceSpan; | ||||
|             SyntaxNode node = root.FindNode(diagnosticSpan); | ||||
|   | ||||
|             switch (diagnostic.Id) | ||||
|             { | ||||
|                 case UnrealTypeAnalyzer.StructAnalyzerId: | ||||
|                     if (node is PropertyDeclarationSyntax propertyNode) | ||||
|                     { | ||||
|                         string name = propertyNode.Identifier.Text; | ||||
|                         context.RegisterCodeFix( | ||||
|                             CodeAction.Create( | ||||
|                                 title: $"Convert '{name}' to field", | ||||
|                                 createChangedDocument: c => ConvertPropertyToFieldAsync(context.Document, propertyNode, c), | ||||
|                                 equivalenceKey: "ConvertToField"), | ||||
|                             diagnostic); | ||||
|                     } | ||||
|                     break; | ||||
|  | ||||
|                 case UnrealTypeAnalyzer.ClassAnalyzerId: | ||||
|                     if (node is VariableDeclaratorSyntax fieldNode) | ||||
|                     { | ||||
|                         string name = fieldNode.Identifier.Text; | ||||
|                         context.RegisterCodeFix( | ||||
|                             CodeAction.Create( | ||||
|                                 title: $"Convert '{name}' to property", | ||||
|                                 createChangedDocument: c => ConvertFieldToPropertyAsync(context.Document, fieldNode, c), | ||||
|                                 equivalenceKey: "ConvertToProperty"), | ||||
|                             diagnostic); | ||||
|                     } | ||||
|                     break; | ||||
|  | ||||
|                 case UnrealTypeAnalyzer.PrefixAnalyzerId: | ||||
|                     if (node is BaseTypeDeclarationSyntax declaration) | ||||
|                     { | ||||
|                         var prefix = diagnostic.Properties["Prefix"]; | ||||
|                         context.RegisterCodeFix( | ||||
|                             CodeAction.Create( | ||||
|                                 title: $"Add prefix '{prefix}' to '{declaration.Identifier.Text}'", | ||||
|                                 createChangedDocument: c => AddPrefixToDeclarationAsync(context.Document, declaration, prefix, c), | ||||
|                                 equivalenceKey: "AddPrefix"), | ||||
|                             diagnostic); | ||||
|                     } | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task<Document> ConvertPropertyToFieldAsync(Document document, PropertyDeclarationSyntax propertyDeclaration, CancellationToken cancellationToken) | ||||
|     { | ||||
|         VariableDeclarationSyntax variableDeclaration = SyntaxFactory.VariableDeclaration(propertyDeclaration.Type) | ||||
|             .AddVariables(SyntaxFactory.VariableDeclarator(propertyDeclaration.Identifier)); | ||||
|  | ||||
|         FieldDeclarationSyntax fieldDeclaration = SyntaxFactory.FieldDeclaration(variableDeclaration) | ||||
|             .WithModifiers(propertyDeclaration.Modifiers) | ||||
|             .WithAttributeLists(propertyDeclaration.AttributeLists) | ||||
|             .WithTriviaFrom(propertyDeclaration); | ||||
|          | ||||
|         SyntaxTriviaList leadingTrivia = propertyDeclaration.GetLeadingTrivia(); | ||||
|         SyntaxTriviaList trailingTrivia = propertyDeclaration.GetTrailingTrivia(); | ||||
|          | ||||
|         if (leadingTrivia.Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia))) | ||||
|         { | ||||
|             var newLeadingTrivia = leadingTrivia.Where(t => !t.IsKind(SyntaxKind.EndOfLineTrivia)); | ||||
|             fieldDeclaration = fieldDeclaration.WithLeadingTrivia(newLeadingTrivia); | ||||
|         } | ||||
|  | ||||
|         fieldDeclaration = fieldDeclaration.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia); | ||||
|  | ||||
|         SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); | ||||
|         SyntaxNode? newRoot = root.ReplaceNode(propertyDeclaration, fieldDeclaration); | ||||
|  | ||||
|         return document.WithSyntaxRoot(newRoot); | ||||
|     } | ||||
|      | ||||
|     private async Task<Document> ConvertFieldToPropertyAsync(Document document, VariableDeclaratorSyntax fieldDeclaration, CancellationToken cancellationToken) | ||||
|     { | ||||
|         if (fieldDeclaration.Parent is not VariableDeclarationSyntax parentFieldDeclaration) | ||||
|         { | ||||
|             return document; | ||||
|         } | ||||
|  | ||||
|         if (parentFieldDeclaration.Parent is not FieldDeclarationSyntax fieldDecl) | ||||
|         { | ||||
|             return document; | ||||
|         } | ||||
|  | ||||
|         PropertyDeclarationSyntax propertyDeclaration = SyntaxFactory.PropertyDeclaration(parentFieldDeclaration.Type, fieldDeclaration.Identifier) | ||||
|             .AddModifiers(fieldDecl.Modifiers.ToArray()) | ||||
|             .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List(new[] | ||||
|             { | ||||
|                 SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) | ||||
|                     .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), | ||||
|                 SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) | ||||
|                     .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) | ||||
|             }))) | ||||
|             .WithTriviaFrom(fieldDecl) | ||||
|             .WithAttributeLists(fieldDecl.AttributeLists); | ||||
|  | ||||
|         SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); | ||||
|         SyntaxNode? newRoot = root.ReplaceNode(fieldDecl, propertyDeclaration); | ||||
|  | ||||
|         return document.WithSyntaxRoot(newRoot); | ||||
|     } | ||||
|  | ||||
|     private async Task<Document> AddPrefixToDeclarationAsync(Document document, BaseTypeDeclarationSyntax declaration, string prefix, | ||||
|         CancellationToken cancellationToken) | ||||
|     { | ||||
|         SyntaxToken identifierToken = declaration.Identifier; | ||||
|         string newName = prefix + identifierToken.Text; | ||||
|         SyntaxToken newIdentifierToken = SyntaxFactory.Identifier(newName); | ||||
|          | ||||
|         MemberDeclarationSyntax newDeclaration = declaration.WithIdentifier(newIdentifierToken) | ||||
|             .WithTriviaFrom(declaration); | ||||
|          | ||||
|         SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); | ||||
|         SyntaxNode newRoot = root.ReplaceNode(declaration, newDeclaration); | ||||
|          | ||||
|         SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); | ||||
|         ISymbol symbol = semanticModel.GetDeclaredSymbol(declaration, cancellationToken); | ||||
|          | ||||
|         Solution solution = document.Project.Solution; | ||||
|         Solution newSolution = await Renamer | ||||
|             .RenameSymbolAsync(solution, symbol, newName, solution.Options, cancellationToken).ConfigureAwait(false); | ||||
|          | ||||
|         return newSolution.GetDocument(document.Id); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,55 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class UStaticLambdaAnalyzer : DiagnosticAnalyzer | ||||
| { | ||||
|     private static readonly DiagnosticDescriptor Rule = new( | ||||
|         id: "US0001", | ||||
|         title: "Invalid UFunction lambda", | ||||
|         messageFormat: "Static UFunction lambdas are not supported, since it has no backing UObject instance. Make this lambda an instance method or capture the instance by using instance fields/methods.", | ||||
|         category: "UnrealSharp", | ||||
|         DiagnosticSeverity.Error, | ||||
|         isEnabledByDefault: true); | ||||
|  | ||||
|     public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         context.EnableConcurrentExecution(); | ||||
|         context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||||
|         context.RegisterSyntaxNodeAction(AnalyzeLambda, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression); | ||||
|     } | ||||
|  | ||||
|     private void AnalyzeLambda(SyntaxNodeAnalysisContext context) | ||||
|     { | ||||
|         LambdaExpressionSyntax lambda = (LambdaExpressionSyntax) context.Node; | ||||
|          | ||||
|         if (lambda.AttributeLists.Count == 0) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         SemanticModel semanticModel = context.SemanticModel; | ||||
|          | ||||
|         bool hasUFunction = lambda.AttributeLists | ||||
|             .SelectMany(list => list.Attributes) | ||||
|             .Any(attr => semanticModel.GetTypeInfo(attr).Type?.Name == "UFunctionAttribute"); | ||||
|  | ||||
|         if (!hasUFunction) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         DataFlowAnalysis? dataFlow = semanticModel.AnalyzeDataFlow(lambda); | ||||
|         if (dataFlow != null && !dataFlow.Captured.Any()) | ||||
|         { | ||||
|             context.ReportDiagnostic(Diagnostic.Create(Rule, lambda.GetLocation())); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,94 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Composition; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CodeFixes; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Editing; | ||||
| using Microsoft.CodeAnalysis.Formatting; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeAnalyzers; | ||||
|  | ||||
| [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UFunctionLambdaCodeFixProvider)), Shared] | ||||
| public class UFunctionLambdaCodeFixProvider : CodeFixProvider | ||||
| { | ||||
|     public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create("US0001"); | ||||
|     public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||||
|  | ||||
|     public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||||
|     { | ||||
|         Diagnostic diagnostic = context.Diagnostics.First(); | ||||
|         SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||||
|  | ||||
|         if (root?.FindNode(diagnostic.Location.SourceSpan) is not LambdaExpressionSyntax lambda) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         context.RegisterCodeFix(Microsoft.CodeAnalysis.CodeActions.CodeAction.Create( | ||||
|             title: "Convert Static Lambda to Instance Method", | ||||
|             createChangedDocument: c => ConvertToInstanceMethod(context.Document, lambda, c), | ||||
|             equivalenceKey: "ConvertToInstanceMethod"), diagnostic); | ||||
|     } | ||||
|  | ||||
|     private async Task<Document> ConvertToInstanceMethod(Document document, LambdaExpressionSyntax lambda, CancellationToken cancellationToken) | ||||
|     { | ||||
|         SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken); | ||||
|         DocumentEditor? editor = await DocumentEditor.CreateAsync(document, cancellationToken); | ||||
|  | ||||
|         MethodDeclarationSyntax? containingMethod = lambda.FirstAncestorOrSelf<MethodDeclarationSyntax>(); | ||||
|         ClassDeclarationSyntax? containingClass = lambda.FirstAncestorOrSelf<ClassDeclarationSyntax>(); | ||||
|          | ||||
|         if (containingMethod == null || containingClass == null || semanticModel == null) | ||||
|         { | ||||
|             return document; | ||||
|         } | ||||
|          | ||||
|         BlockSyntax lambdaBody = lambda.Body as BlockSyntax | ||||
|                                  ?? SyntaxFactory.Block(SyntaxFactory.ExpressionStatement((ExpressionSyntax)lambda.Body)); | ||||
|          | ||||
|         SeparatedSyntaxList<ParameterSyntax> parameters = lambda switch | ||||
|         { | ||||
|             SimpleLambdaExpressionSyntax simple => SyntaxFactory.SingletonSeparatedList(WithInferredType(simple.Parameter, semanticModel, cancellationToken)), | ||||
|             ParenthesizedLambdaExpressionSyntax paren => SyntaxFactory.SeparatedList(paren.ParameterList.Parameters.Select(p => WithInferredType(p, semanticModel, cancellationToken))), | ||||
|             _ => default | ||||
|         }; | ||||
|  | ||||
|         string methodName = AnalyzerStatics.GenerateUniqueMethodName(containingClass, "InstanceMethod"); | ||||
|         editor.ReplaceNode(lambda, SyntaxFactory.IdentifierName(methodName)); | ||||
|  | ||||
|         MethodDeclarationSyntax methodDecl = SyntaxFactory.MethodDeclaration( | ||||
|                 SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), methodName) | ||||
|             .AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)) | ||||
|             .WithAttributeLists(lambda.AttributeLists) | ||||
|             .WithParameterList(SyntaxFactory.ParameterList(parameters)) | ||||
|             .WithBody(lambdaBody); | ||||
|  | ||||
|         editor.AddMember(containingClass, methodDecl); | ||||
|  | ||||
|         Document? changedDocument = editor.GetChangedDocument(); | ||||
|         return await Formatter.FormatAsync(changedDocument, cancellationToken: cancellationToken); | ||||
|     } | ||||
|  | ||||
|     private static ParameterSyntax WithInferredType(ParameterSyntax parameter, SemanticModel model, CancellationToken token) | ||||
|     { | ||||
|         IParameterSymbol? symbol = model.GetDeclaredSymbol(parameter, token); | ||||
|          | ||||
|         if (symbol == null) | ||||
|         { | ||||
|             return parameter.WithType(SyntaxFactory.IdentifierName("object")); | ||||
|         } | ||||
|  | ||||
|         SymbolDisplayFormat displayFormat = new SymbolDisplayFormat( | ||||
|             typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, | ||||
|             genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, | ||||
|             miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | ||||
|         ); | ||||
|  | ||||
|         string typeName = symbol.Type.ToDisplayString(displayFormat); | ||||
|         return parameter.WithType(SyntaxFactory.ParseTypeName(typeName)); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,50 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeAnalyzers; | ||||
|  | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class UEnumAnalyzer : DiagnosticAnalyzer | ||||
| { | ||||
|     public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( | ||||
|         UEnumIsByteEnumRule | ||||
|     ); | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor UEnumIsByteEnumRule = new( | ||||
|         id: "US0002",  | ||||
|         title: "UnrealSharp UEnumIsByteEnum Analyzer",  | ||||
|         messageFormat: "{0} is a UEnum, which should have a underlying type of byte",  | ||||
|         RuleCategory.Category,  | ||||
|         DiagnosticSeverity.Error,  | ||||
|         isEnabledByDefault: true,  | ||||
|         description: "Ensures UEnum underlying type is byte." | ||||
|     ); | ||||
|      | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         context.EnableConcurrentExecution(); | ||||
|         context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); | ||||
|         context.RegisterSymbolAction(Test, SymbolKind.NamedType); | ||||
|     } | ||||
|  | ||||
|     private static void Test(SymbolAnalysisContext context) | ||||
|     { | ||||
|         if (context.Symbol is not INamedTypeSymbol namedTypeSymbol) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var isUEnum = namedTypeSymbol.TypeKind == TypeKind.Enum && AnalyzerStatics.HasAttribute(namedTypeSymbol, AnalyzerStatics.UEnumAttribute); | ||||
|         if (isUEnum && !IsByteEnum(namedTypeSymbol) && !AnalyzerStatics.HasAttribute(namedTypeSymbol, AnalyzerStatics.GeneratedTypeAttribute)) | ||||
|         { | ||||
|             context.ReportDiagnostic(Diagnostic.Create(UEnumIsByteEnumRule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static bool IsByteEnum(INamedTypeSymbol symbol) | ||||
|     { | ||||
|         return symbol.EnumUnderlyingType?.SpecialType == SpecialType.System_Byte; | ||||
|     } | ||||
|      | ||||
| } | ||||
| @ -0,0 +1,100 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeAnalyzers; | ||||
|  | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class UInterfaceAnalyzer : DiagnosticAnalyzer | ||||
| { | ||||
|     public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( | ||||
|         UInterfacePropertyTypeRule, | ||||
|         UInterfaceFunctionParameterTypeRule | ||||
|     ); | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor UInterfacePropertyTypeRule = new( | ||||
|         id: "US0003",  | ||||
|         title: "UnrealSharp UInterface UProperty Analyzer",  | ||||
|         messageFormat: "{0} is a UProperty with Interface type, which should has UInterface attribute",  | ||||
|         RuleCategory.Category,  | ||||
|         DiagnosticSeverity.Error,  | ||||
|         isEnabledByDefault: true,  | ||||
|         description: "Ensures UProperty type has a UInterface attribute." | ||||
|     ); | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor UInterfaceFunctionParameterTypeRule = new( | ||||
|         id: "US0004",  | ||||
|         title: "UnrealSharp UInterface function parameter Analyzer",  | ||||
|         messageFormat: "{0} is UFunction parameter with Interface type, which should has UInterface attribute",  | ||||
|         RuleCategory.Category,  | ||||
|         DiagnosticSeverity.Error,  | ||||
|         isEnabledByDefault: true,  | ||||
|         description: "Ensures interface type has a UInterface attribute." | ||||
|     ); | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         context.EnableConcurrentExecution(); | ||||
|         context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); | ||||
|         context.RegisterSymbolAction(AnalyzeProperty, SymbolKind.Property); | ||||
|         context.RegisterSymbolAction(AnalyzeFunctionParameter, SymbolKind.Parameter); | ||||
|     } | ||||
|  | ||||
|     private static void AnalyzeProperty(SymbolAnalysisContext context) | ||||
|     { | ||||
|         if (context.Symbol is not IPropertySymbol propertySymbol) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!AnalyzerStatics.HasAttribute(propertySymbol, AnalyzerStatics.UPropertyAttribute)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         var isInterfaceType = propertySymbol.Type.TypeKind == TypeKind.Interface; | ||||
|         if (!isInterfaceType) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         var hasUInterfaceAttribute = AnalyzerStatics.HasAttribute(propertySymbol.Type, AnalyzerStatics.UInterfaceAttribute); | ||||
|  | ||||
|         if (!hasUInterfaceAttribute && !AnalyzerStatics.IsContainerInterface(propertySymbol.Type)) | ||||
|         { | ||||
|             context.ReportDiagnostic(Diagnostic.Create(UInterfacePropertyTypeRule, propertySymbol.Locations[0], propertySymbol.Name)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static void AnalyzeFunctionParameter(SymbolAnalysisContext context) | ||||
|     { | ||||
|         if (context.Symbol is not IParameterSymbol parameterSymbol) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var isMethodParameter = parameterSymbol.ContainingSymbol.Kind == SymbolKind.Method; | ||||
|         if (!isMethodParameter) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         var isUFunction = AnalyzerStatics.HasAttribute(context.Symbol.ContainingSymbol, AnalyzerStatics.UFunctionAttribute); | ||||
|         if (!isUFunction) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         var isInterfaceType = parameterSymbol.Type.TypeKind == TypeKind.Interface; | ||||
|         if (!isInterfaceType) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         var hasUInterfaceAttribute = AnalyzerStatics.HasAttribute(parameterSymbol.Type, AnalyzerStatics.UInterfaceAttribute); | ||||
|         if (!hasUInterfaceAttribute && !AnalyzerStatics.IsContainerInterface(parameterSymbol.Type)) | ||||
|         { | ||||
|             context.ReportDiagnostic(Diagnostic.Create(UInterfaceFunctionParameterTypeRule, parameterSymbol.Locations[0], parameterSymbol.Name)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,112 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
| using Microsoft.CodeAnalysis.Operations; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeAnalyzers; | ||||
|  | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class UObjectCreationAnalyzer : DiagnosticAnalyzer | ||||
| { | ||||
|     public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( | ||||
|         UObjectCreationRule, | ||||
|         AActorCreationRule, | ||||
|         UUserWidgetCreationRule, | ||||
|         UActorComponentCreationRule, | ||||
|         USceneComponentCreationRule | ||||
|     ); | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor UObjectCreationRule = new( | ||||
|         id: "US0011",  | ||||
|         title: "UnrealSharp UObject creation Analyzer",  | ||||
|         messageFormat: "{0} is a UObject, which should be created by calling the method NewObject<T>()",  | ||||
|         RuleCategory.Category,  | ||||
|         DiagnosticSeverity.Error,  | ||||
|         isEnabledByDefault: true,  | ||||
|         description: "Ensures UObject instantiated by using NewObject<T>() method." | ||||
|         ); | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor AActorCreationRule = new( | ||||
|         id: "US0010",  | ||||
|         title: "UnrealSharp AActor creation Analyzer",  | ||||
|         messageFormat: "{0} is a AActor, which should be created by calling the method SpawnActor<T>()",  | ||||
|         RuleCategory.Category,  | ||||
|         DiagnosticSeverity.Error,  | ||||
|         isEnabledByDefault: true,  | ||||
|         description: "Ensures AActor instantiated by using SpawnActor<T>() method." | ||||
|     ); | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor UUserWidgetCreationRule = new( | ||||
|         id: "US0009",  | ||||
|         title: "UnrealSharp UUserWidget creation Analyzer",  | ||||
|         messageFormat: "{0} is a UUserWidget, which should be created by calling the method CreateWidget<T>()",  | ||||
|         RuleCategory.Category,  | ||||
|         DiagnosticSeverity.Error,  | ||||
|         isEnabledByDefault: true,  | ||||
|         description: "Ensures UUserWidget instantiated by using CreateWidget<T>() method." | ||||
|     ); | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor UActorComponentCreationRule = new( | ||||
|         id: "US0008",  | ||||
|         title: "UnrealSharp UActorComponent creation Analyzer",  | ||||
|         messageFormat: "{0} is a UActorComponent, which should be created by calling the method AddComponentByClass<T>()",  | ||||
|         RuleCategory.Category,  | ||||
|         DiagnosticSeverity.Error,  | ||||
|         isEnabledByDefault: true,  | ||||
|         description: "Ensures UActorComponent instantiated by using AddComponentByClass<T>() method." | ||||
|     ); | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor USceneComponentCreationRule = new( | ||||
|         id: "US0007",  | ||||
|         title: "UnrealSharp USceneComponent creation Analyzer",  | ||||
|         messageFormat: "{0} is a USceneComponent, which should be created by calling the method AddComponentByClass<T>()",  | ||||
|         RuleCategory.Category,  | ||||
|         DiagnosticSeverity.Error,  | ||||
|         isEnabledByDefault: true,  | ||||
|         description: "Ensures USceneComponent instantiated by using AddComponentByClass<T>() method." | ||||
|     ); | ||||
|      | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         context.EnableConcurrentExecution(); | ||||
|         context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); | ||||
|         context.RegisterOperationAction(AnalyzeUObjectCreation, OperationKind.ObjectCreation); | ||||
|     } | ||||
|  | ||||
|     //check new <UObject> syntax | ||||
|     private static void AnalyzeUObjectCreation(OperationAnalysisContext context) | ||||
|     { | ||||
|         if (context.Operation is not IObjectCreationOperation creationOperation) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (creationOperation.Type is not INamedTypeSymbol type) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var isNewKeywordOperation = AnalyzerStatics.IsNewKeywordInstancingOperation(creationOperation, out var newKeywordLocation); | ||||
|         if (!isNewKeywordOperation) return; | ||||
|  | ||||
|         var rule = GetRule(type); | ||||
|         if (rule is null) return; | ||||
|          | ||||
|         context.ReportDiagnostic(Diagnostic.Create(rule, newKeywordLocation, type.Name)); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private static DiagnosticDescriptor? GetRule(INamedTypeSymbol type) | ||||
|     { | ||||
|         return type switch | ||||
|         { | ||||
|             _ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.USceneComponent) => USceneComponentCreationRule, | ||||
|             _ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.UActorComponent) => UActorComponentCreationRule, | ||||
|             _ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.UUserWidget) => UUserWidgetCreationRule, | ||||
|             _ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.AActor) => AActorCreationRule, | ||||
|             _ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.UObject) => UObjectCreationRule, | ||||
|             _ => null | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -0,0 +1,120 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeAnalyzers; | ||||
|  | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class UnrealTypeAnalyzer : DiagnosticAnalyzer | ||||
| { | ||||
|     public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( | ||||
|         PrefixRule,  | ||||
|         ClassRule | ||||
|         ); | ||||
|  | ||||
|     public override void Initialize(AnalysisContext context) | ||||
|     { | ||||
|         context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||||
|         context.EnableConcurrentExecution(); | ||||
|         context.RegisterSymbolAction(AnalyzeType, SymbolKind.NamedType); | ||||
|         context.RegisterSymbolAction(AnalyzeClassFields, SymbolKind.Field); | ||||
|     } | ||||
|      | ||||
|     public const string StructAnalyzerId = "US0005"; | ||||
|     public const string ClassAnalyzerId = "US0006"; | ||||
|     private static readonly LocalizableString StructAnalyzerTitle = "UnrealSharp Struct Field Analyzer"; | ||||
|     private static readonly LocalizableString ClassAnalyzerTitle = "UnrealSharp Class Field Analyzer"; | ||||
|     private static readonly LocalizableString ClassAnalyzerMessageFormat = "{0} is a UProperty and a field, which is not allowed in classes. UProperties in classes must be properties."; | ||||
|     private static readonly LocalizableString StructAnalyzerDescription = "Ensures UProperties in structs are fields."; | ||||
|     private static readonly LocalizableString ClassAnalyzerDescription = "Ensures UProperties in classes are properties."; | ||||
|      | ||||
|     private static readonly DiagnosticDescriptor ClassRule = new(ClassAnalyzerId, ClassAnalyzerTitle, ClassAnalyzerMessageFormat, RuleCategory.Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: ClassAnalyzerDescription); | ||||
|  | ||||
|     private static void AnalyzeFields(SymbolAnalysisContext context, TypeKind typeKind, string requiredAttribute, DiagnosticDescriptor rule) | ||||
|     { | ||||
|         ISymbol symbol = context.Symbol; | ||||
|         INamedTypeSymbol type = symbol.ContainingType; | ||||
|  | ||||
|         if (type.TypeKind != typeKind && !AnalyzerStatics.HasAttribute(type, requiredAttribute)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UPropertyAttribute)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var diagnostic = Diagnostic.Create(rule, symbol.Locations[0], symbol.Name); | ||||
|         context.ReportDiagnostic(diagnostic); | ||||
|     } | ||||
|  | ||||
|     private static void AnalyzeClassFields(SymbolAnalysisContext context) | ||||
|     { | ||||
|         if (context.Symbol is IFieldSymbol) | ||||
|         { | ||||
|             AnalyzeFields(context, TypeKind.Class, AnalyzerStatics.UClassAttribute, ClassRule); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public const string PrefixAnalyzerId = "PrefixAnalyzer"; | ||||
|     private static readonly LocalizableString PrefixAnalyzerTitle = "UnrealSharp Prefix Analyzer"; | ||||
|     private static readonly LocalizableString PrefixAnalyzerMessageFormat = "{0} '{1}' is exposed to Unreal Engine and should have prefix '{2}'"; | ||||
|     private static readonly LocalizableString PrefixAnalyzerDescription = "Ensures types have appropriate prefixes."; | ||||
|     private static readonly DiagnosticDescriptor PrefixRule = new(PrefixAnalyzerId, PrefixAnalyzerTitle, PrefixAnalyzerMessageFormat, RuleCategory.Naming, DiagnosticSeverity.Error, isEnabledByDefault: true, description: PrefixAnalyzerDescription); | ||||
|  | ||||
|     private static void AnalyzeType(SymbolAnalysisContext context) | ||||
|     { | ||||
|         INamedTypeSymbol symbol = (INamedTypeSymbol)context.Symbol; | ||||
|         string prefix = null; | ||||
|  | ||||
|         // These types are generated by the script generator, and already have the correct prefix | ||||
|         if (AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.GeneratedTypeAttribute)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (symbol.TypeKind == TypeKind.Struct && AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UStructAttribute)) | ||||
|         { | ||||
|             prefix = "F"; | ||||
|         } | ||||
|         else if (symbol.TypeKind == TypeKind.Enum && AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UEnumAttribute)) | ||||
|         { | ||||
|             prefix = "E"; | ||||
|         } | ||||
|         else if (symbol.TypeKind == TypeKind.Class) | ||||
|         { | ||||
|             if (!AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UClassAttribute)) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (AnalyzerStatics.InheritsFrom(symbol, AnalyzerStatics.AActor)) | ||||
|             { | ||||
|                 prefix = "A"; | ||||
|             } | ||||
|             else if (AnalyzerStatics.InheritsFrom(symbol, AnalyzerStatics.UObject)) | ||||
|             { | ||||
|                 prefix = "U"; | ||||
|             } | ||||
|         } | ||||
|         else if (symbol.TypeKind == TypeKind.Interface && AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UInterfaceAttribute)) | ||||
|         { | ||||
|             prefix = "I"; | ||||
|         } | ||||
|  | ||||
|         if (prefix == null || symbol.Name.StartsWith(prefix)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         Dictionary<string, string> properties = new Dictionary<string, string> | ||||
|         { | ||||
|             { "Prefix", prefix } | ||||
|         }; | ||||
|  | ||||
|         Diagnostic diagnostic = Diagnostic.Create(PrefixRule, symbol.Locations[0], properties.ToImmutableDictionary(), symbol.TypeKind.ToString(), symbol.Name, prefix); | ||||
|         context.ReportDiagnostic(diagnostic); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,54 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Threading; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.CodeSuppressors; | ||||
|  | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class UPropertyReferenceTypeSuppressor : DiagnosticSuppressor | ||||
| { | ||||
|     public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => [DefaultComponentNullableRule]; | ||||
|      | ||||
|     private static readonly SuppressionDescriptor DefaultComponentNullableRule = new( | ||||
|         "NullableUPropertySuppressorId", | ||||
|         "CS8618", | ||||
|         "UProperties on UClasses are automatically instantiated." | ||||
|     ); | ||||
|      | ||||
|     public override void ReportSuppressions(SuppressionAnalysisContext context) | ||||
|     { | ||||
|         foreach (var diagnostic in context.ReportedDiagnostics) | ||||
|         { | ||||
|             var location = diagnostic.Location; | ||||
|             var syntaxTree = location.SourceTree; | ||||
|             if (syntaxTree is null) continue; | ||||
|              | ||||
|             var root = syntaxTree.GetRoot(context.CancellationToken); | ||||
|             var textSpan = location.SourceSpan; | ||||
|             var node = root.FindNode(textSpan); | ||||
|              | ||||
|             if (node is not PropertyDeclarationSyntax propertyNode || propertyNode.AttributeLists.Count == 0) continue; | ||||
|  | ||||
|             var semanticModel = context.GetSemanticModel(syntaxTree); | ||||
|  | ||||
|             if (IsDefaultComponentProperty(semanticModel, propertyNode, context.CancellationToken)) | ||||
|             { | ||||
|                 context.ReportSuppression(Suppression.Create(DefaultComponentNullableRule, diagnostic)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static bool IsDefaultComponentProperty( | ||||
|         SemanticModel semanticModel,  | ||||
|         PropertyDeclarationSyntax propertyNode, | ||||
|         CancellationToken cancellationToken) | ||||
|     { | ||||
|         var symbol = semanticModel.GetDeclaredSymbol(propertyNode, cancellationToken); | ||||
|         if (symbol is not IPropertySymbol propertySymbol) return false; | ||||
|          | ||||
|         return AnalyzerStatics.TryGetAttribute(propertySymbol, AnalyzerStatics.UPropertyAttribute, out _) &&  | ||||
|                AnalyzerStatics.HasAttribute(propertySymbol.ContainingType, AnalyzerStatics.UClassAttribute); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,99 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| [Generator] | ||||
| public class CustomLogSourceGenerator : IIncrementalGenerator | ||||
| { | ||||
|     private readonly struct ClassLogInfo | ||||
|     { | ||||
|         public readonly INamedTypeSymbol ClassSymbol; | ||||
|         public readonly string LogVerbosity; | ||||
|         public ClassLogInfo(INamedTypeSymbol classSymbol, string logVerbosity) | ||||
|         { | ||||
|             ClassSymbol = classSymbol; | ||||
|             LogVerbosity = logVerbosity; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void Initialize(IncrementalGeneratorInitializationContext context) | ||||
|     { | ||||
|         var classLogInfos = context.SyntaxProvider.CreateSyntaxProvider( | ||||
|                 static (node, _) => node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0, | ||||
|                 static (syntaxContext, _) => GetClassLogInfos(syntaxContext)) | ||||
|             .SelectMany(static (infos, _) => infos) | ||||
|             .Where(static info => info.ClassSymbol is not null); | ||||
|  | ||||
|         context.RegisterSourceOutput(classLogInfos, static (spc, info) => | ||||
|         { | ||||
|             string source = GenerateLoggerClass(info.ClassSymbol, info.ClassSymbol.Name, info.LogVerbosity); | ||||
|             spc.AddSource($"{info.ClassSymbol.Name}_CustomLog.generated.cs", SourceText.From(source, Encoding.UTF8)); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private static IEnumerable<ClassLogInfo> GetClassLogInfos(GeneratorSyntaxContext context) | ||||
|     { | ||||
|         if (context.Node is not ClassDeclarationSyntax classDeclaration) | ||||
|         { | ||||
|             return Array.Empty<ClassLogInfo>(); | ||||
|         } | ||||
|  | ||||
|         if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol) | ||||
|         { | ||||
|             return Array.Empty<ClassLogInfo>(); | ||||
|         } | ||||
|  | ||||
|         List<ClassLogInfo> list = new(); | ||||
|  | ||||
|         foreach (var attributeList in classDeclaration.AttributeLists) | ||||
|         { | ||||
|             foreach (var attribute in attributeList.Attributes) | ||||
|             { | ||||
|                 var attributeName = attribute.Name.ToString(); | ||||
|                 if (attributeName is not ("CustomLog" or "CustomLogAttribute")) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 var firstArgument = attribute.ArgumentList?.Arguments.FirstOrDefault(); | ||||
|                 string logVerbosity = firstArgument != null ? firstArgument.Expression.ToString() : "ELogVerbosity.Display"; | ||||
|                 list.Add(new ClassLogInfo(classSymbol, logVerbosity)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return list; | ||||
|     } | ||||
|  | ||||
|     private static string GenerateLoggerClass(INamedTypeSymbol classSymbol, string logFieldName, string logVerbosity) | ||||
|     { | ||||
|         string namespaceName = classSymbol.ContainingNamespace.IsGlobalNamespace | ||||
|             ? string.Empty | ||||
|             : classSymbol.ContainingNamespace.ToDisplayString(); | ||||
|  | ||||
|         string className = classSymbol.Name; | ||||
|         StringBuilder builder = new StringBuilder(); | ||||
|  | ||||
|         builder.AppendLine("using UnrealSharp.Log;"); | ||||
|  | ||||
|         if (!string.IsNullOrEmpty(namespaceName)) | ||||
|         { | ||||
|             builder.AppendLine($"namespace {namespaceName};"); | ||||
|         } | ||||
|  | ||||
|         builder.AppendLine($"public partial class {className}"); | ||||
|         builder.AppendLine("{"); | ||||
|         builder.AppendLine($"    public static void Log(string message) => UnrealLogger.Log(\"{logFieldName}\", message, {logVerbosity});"); | ||||
|         builder.AppendLine($"    public static void LogWarning(string message) => UnrealLogger.LogWarning(\"{logFieldName}\", message);"); | ||||
|         builder.AppendLine($"    public static void LogError(string message) => UnrealLogger.LogError(\"{logFieldName}\", message);"); | ||||
|         builder.AppendLine($"    public static void LogFatal(string message) => UnrealLogger.LogFatal(\"{logFieldName}\", message);"); | ||||
|         builder.AppendLine("}"); | ||||
|  | ||||
|         return builder.ToString(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,45 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.DelegateGenerator; | ||||
|  | ||||
| public abstract class DelegateBuilder | ||||
| { | ||||
|     public abstract void StartBuilding(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className, bool generateInvoker); | ||||
|      | ||||
|     protected void GenerateGetInvoker(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol) | ||||
|     { | ||||
|         stringBuilder.AppendLine($"    protected override {delegateSymbol} GetInvoker()"); | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("        return Invoker;"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|     } | ||||
|      | ||||
|     protected void GenerateInvoke(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol) | ||||
|     { | ||||
|         if (delegateSymbol.DelegateInvokeMethod == null) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         if (delegateSymbol.DelegateInvokeMethod.Parameters.IsEmpty) | ||||
|         { | ||||
|             stringBuilder.AppendLine($"    protected void Invoker()"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             stringBuilder.Append($"    protected void Invoker("); | ||||
|             stringBuilder.Append(string.Join(", ", delegateSymbol.DelegateInvokeMethod.Parameters.Select(x => $"{DelegateWrapperGenerator.GetRefKindKeyword(x)}{x.Type} {x.Name}"))); | ||||
|             stringBuilder.Append(")"); | ||||
|             stringBuilder.AppendLine(); | ||||
|         } | ||||
|          | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("       ProcessDelegate(IntPtr.Zero);"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| public enum DelegateType | ||||
| { | ||||
|     Multicast, | ||||
|     Single, | ||||
| } | ||||
| @ -0,0 +1,203 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.DelegateGenerator; | ||||
|  | ||||
| [Generator] | ||||
| public class DelegateWrapperGenerator : IIncrementalGenerator | ||||
| { | ||||
|     private sealed class DelegateGenerationInfo | ||||
|     { | ||||
|         public string NamespaceName { get; } | ||||
|         public string DelegateName { get; } | ||||
|         public INamedTypeSymbol DelegateSymbol { get; } | ||||
|         public bool GenerateInvoker { get; } | ||||
|         public DelegateType DelegateType { get; } | ||||
|         public string BaseTypeName { get; } | ||||
|         public bool NullableAwareable { get; } | ||||
|         public DelegateGenerationInfo(string namespaceName, string delegateName, INamedTypeSymbol delegateSymbol, bool generateInvoker, DelegateType delegateType, string baseTypeName, bool nullableAwareable) | ||||
|         { | ||||
|             NamespaceName = namespaceName; | ||||
|             DelegateName = delegateName; | ||||
|             DelegateSymbol = delegateSymbol; | ||||
|             GenerateInvoker = generateInvoker; | ||||
|             DelegateType = delegateType; | ||||
|             BaseTypeName = baseTypeName; | ||||
|             NullableAwareable = nullableAwareable; | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void Initialize(IncrementalGeneratorInitializationContext context) | ||||
|     { | ||||
|         var candidates = context.SyntaxProvider.CreateSyntaxProvider( | ||||
|                 static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { BaseList: not null } || syntaxNode is DelegateDeclarationSyntax, | ||||
|                 static (syntaxContext, _) => GetInfoOrNull(syntaxContext)) | ||||
|             .Where(static info => info is not null) | ||||
|             .Select(static (info, _) => info!); | ||||
|  | ||||
|         context.RegisterSourceOutput(candidates, static (spc, info) => Generate(spc, info)); | ||||
|     } | ||||
|  | ||||
|     private static DelegateGenerationInfo? GetInfoOrNull(GeneratorSyntaxContext context) | ||||
|     { | ||||
|         // Exclude members with [Binding] | ||||
|         if (context.Node is MemberDeclarationSyntax m && AnalyzerStatics.HasAttribute(m, "Binding")) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         INamedTypeSymbol? symbol; | ||||
|         INamedTypeSymbol? delegateSymbol; | ||||
|         string delegateName; | ||||
|         bool generateInvoker = true; | ||||
|         DelegateType delegateType; | ||||
|         string baseTypeName; | ||||
|  | ||||
|         if (context.Node is ClassDeclarationSyntax classDecl) | ||||
|         { | ||||
|             // Must derive from *Delegate | ||||
|             if (classDecl.BaseList == null || !classDecl.BaseList.Types.Any(bt => bt.Type.ToString().Contains("MulticastDelegate") || bt.Type.ToString().Contains("Delegate"))) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             symbol = context.SemanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol; | ||||
|             if (symbol == null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             if (symbol.IsGenericType || AnalyzerStatics.HasAttribute(symbol, "UnmanagedFunctionPointerAttribute")) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             delegateName = classDecl.Identifier.ValueText; | ||||
|             delegateSymbol = symbol.BaseType?.TypeArguments.FirstOrDefault() as INamedTypeSymbol; | ||||
|             if (delegateSymbol == null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             generateInvoker = !symbol.GetMembers().Any(x => x.Name == "Invoker"); | ||||
|         } | ||||
|         else if (context.Node is DelegateDeclarationSyntax delegateDecl) | ||||
|         { | ||||
|             if (AnalyzerStatics.HasAttribute(delegateDecl, "GeneratedType")) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             symbol = context.SemanticModel.GetDeclaredSymbol(delegateDecl) as INamedTypeSymbol; | ||||
|             if (symbol == null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             if (symbol.IsGenericType || AnalyzerStatics.HasAttribute(symbol, "UnmanagedFunctionPointerAttribute")) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             delegateName = "U" + delegateDecl.Identifier.ValueText; | ||||
|             delegateSymbol = symbol; // Underlying delegate is the symbol itself | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         if (AnalyzerStatics.HasAttribute(delegateSymbol, AnalyzerStatics.USingleDelegateAttribute)) | ||||
|         { | ||||
|             baseTypeName = "Delegate"; | ||||
|             delegateType = DelegateType.Single; | ||||
|         } | ||||
|         else if (AnalyzerStatics.HasAttribute(delegateSymbol, AnalyzerStatics.UMultiDelegateAttribute)) | ||||
|         { | ||||
|             baseTypeName = "MulticastDelegate"; | ||||
|             delegateType = DelegateType.Multicast; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             return null; // Not a recognized Unreal delegate wrapper | ||||
|         } | ||||
|  | ||||
|         string namespaceName = symbol.ContainingNamespace?.ToDisplayString() ?? "Global"; | ||||
|  | ||||
|         return new DelegateGenerationInfo(namespaceName, delegateName, delegateSymbol, generateInvoker, delegateType, baseTypeName, | ||||
|             context.SemanticModel.GetNullableContext(context.Node.Span.Start).HasFlag(NullableContext.AnnotationsEnabled)); | ||||
|     } | ||||
|  | ||||
|     private static void Generate(SourceProductionContext context, DelegateGenerationInfo info) | ||||
|     { | ||||
|         StringBuilder stringBuilder = new StringBuilder(); | ||||
|         if (info.NullableAwareable) | ||||
|         { | ||||
|             stringBuilder.AppendLine("#nullable enable"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             stringBuilder.AppendLine("#nullable disable"); | ||||
|         } | ||||
|         stringBuilder.AppendLine(); | ||||
|         stringBuilder.AppendLine("using UnrealSharp;"); | ||||
|         stringBuilder.AppendLine("using UnrealSharp.Interop;"); | ||||
|         stringBuilder.AppendLine(); | ||||
|         stringBuilder.AppendLine($"namespace {info.NamespaceName};"); | ||||
|         stringBuilder.AppendLine(); | ||||
|  | ||||
|         DelegateBuilder builder = info.DelegateType == DelegateType.Multicast | ||||
|             ? new MulticastDelegateBuilder() | ||||
|             : new SingleDelegateBuilder(); | ||||
|  | ||||
|         stringBuilder.AppendLine($"public partial class {info.DelegateName} : {info.BaseTypeName}<{info.DelegateSymbol}>"); | ||||
|         stringBuilder.AppendLine("{"); | ||||
|         builder.StartBuilding(stringBuilder, info.DelegateSymbol, info.DelegateName, info.GenerateInvoker); | ||||
|         stringBuilder.AppendLine("}"); | ||||
|         stringBuilder.AppendLine(); | ||||
|         GenerateDelegateExtensionsClass(stringBuilder, info.DelegateSymbol, info.DelegateName, info.DelegateType); | ||||
|  | ||||
|         context.AddSource($"{info.NamespaceName}.{info.DelegateName}.generated.cs", SourceText.From(stringBuilder.ToString(), Encoding.UTF8)); | ||||
|     } | ||||
|  | ||||
|     private static void GenerateDelegateExtensionsClass(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string delegateName, DelegateType delegateType) | ||||
|     { | ||||
|         stringBuilder.AppendLine($"public static class {delegateName}Extensions"); | ||||
|         stringBuilder.AppendLine("{"); | ||||
|  | ||||
|         var parametersList = delegateSymbol.DelegateInvokeMethod!.Parameters.ToList(); | ||||
|  | ||||
|         string args = parametersList.Any() | ||||
|             ? string.Join(", ", parametersList.Select(x => $"{GetRefKindKeyword(x)}{x.Type} {x.Name}")) | ||||
|             : string.Empty; | ||||
|  | ||||
|         string parameters = parametersList.Any() | ||||
|             ? string.Join(", ", parametersList.Select(x => $"{GetRefKindKeyword(x)}{x.Name}")) | ||||
|             : string.Empty; | ||||
|  | ||||
|         string delegateTypeString = delegateType == DelegateType.Multicast ? "TMulticastDelegate" : "TDelegate"; | ||||
|  | ||||
|         stringBuilder.AppendLine($"     public static void Invoke(this {delegateTypeString}<{delegateSymbol}> @delegate{(args.Any() ? $", {args}" : string.Empty)})"); | ||||
|         stringBuilder.AppendLine("     {"); | ||||
|         stringBuilder.AppendLine($"         @delegate.InnerDelegate.Invoke({parameters});"); | ||||
|         stringBuilder.AppendLine("     }"); | ||||
|         stringBuilder.AppendLine("}"); | ||||
|     } | ||||
|  | ||||
|     internal static string GetRefKindKeyword(IParameterSymbol x) | ||||
|     { | ||||
|         return x.RefKind switch | ||||
|         { | ||||
|             RefKind.RefReadOnlyParameter => "in ", | ||||
|             RefKind.In => "in ", | ||||
|             RefKind.Ref => "ref ", | ||||
|             RefKind.Out => "out ", | ||||
|             _ => string.Empty | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,42 @@ | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using UnrealSharp.SourceGenerators.DelegateGenerator; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| public class MulticastDelegateBuilder : DelegateBuilder | ||||
| { | ||||
|     public override void StartBuilding(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className, bool generateInvoker) | ||||
|     { | ||||
|         GenerateAddOperator(stringBuilder, delegateSymbol, className); | ||||
|         GenerateGetInvoker(stringBuilder, delegateSymbol); | ||||
|         GenerateRemoveOperator(stringBuilder, delegateSymbol, className); | ||||
|          | ||||
|         //Check if the class has an Invoker method already | ||||
|         if (generateInvoker) | ||||
|         { | ||||
|             GenerateInvoke(stringBuilder, delegateSymbol); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     void GenerateAddOperator(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className) | ||||
|     { | ||||
|         stringBuilder.AppendLine($"    public static {className} operator +({className} thisDelegate, {delegateSymbol.Name} handler)"); | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("        thisDelegate.Add(handler);"); | ||||
|         stringBuilder.AppendLine("        return thisDelegate;"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|     } | ||||
|      | ||||
|     void GenerateRemoveOperator(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className) | ||||
|     { | ||||
|         stringBuilder.AppendLine($"    public static {className} operator -({className} thisDelegate, {delegateSymbol.Name} handler)"); | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("        thisDelegate.Remove(handler);"); | ||||
|         stringBuilder.AppendLine("        return thisDelegate;"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,65 @@ | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using UnrealSharp.SourceGenerators.DelegateGenerator; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| public class SingleDelegateBuilder : DelegateBuilder | ||||
| { | ||||
|     public override void StartBuilding(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className, bool generateInvoker) | ||||
|     { | ||||
|         GenerateAddOperator(stringBuilder, delegateSymbol, className); | ||||
|  | ||||
|         GenerateGetInvoker(stringBuilder, delegateSymbol); | ||||
|  | ||||
|         GenerateRemoveOperator(stringBuilder, delegateSymbol, className); | ||||
|  | ||||
|         GenerateConstructors(stringBuilder, className); | ||||
|  | ||||
|         //Check if the class has an Invoker method already | ||||
|         if (generateInvoker) | ||||
|         { | ||||
|             GenerateInvoke(stringBuilder, delegateSymbol); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     void GenerateConstructors(StringBuilder stringBuilder, string className) | ||||
|     { | ||||
|         stringBuilder.AppendLine($"    public {className}() : base()"); | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|  | ||||
|         stringBuilder.AppendLine($"    public {className}(DelegateData data) : base(data)"); | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|          | ||||
|         stringBuilder.AppendLine($"    public {className}(UnrealSharp.CoreUObject.UObject targetObject, FName functionName) : base(targetObject, functionName)"); | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|     } | ||||
|  | ||||
|     void GenerateAddOperator(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className) | ||||
|     { | ||||
|         stringBuilder.AppendLine($"    public static {className} operator +({className} thisDelegate, {delegateSymbol.Name} handler)"); | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("        thisDelegate.Add(handler);"); | ||||
|         stringBuilder.AppendLine("        return thisDelegate;"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|     } | ||||
|  | ||||
|     void GenerateRemoveOperator(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className) | ||||
|     { | ||||
|         stringBuilder.AppendLine($"    public static {className} operator -({className} thisDelegate, {delegateSymbol.Name} handler)"); | ||||
|         stringBuilder.AppendLine("    {"); | ||||
|         stringBuilder.AppendLine("        thisDelegate.Remove(handler);"); | ||||
|         stringBuilder.AppendLine("        return thisDelegate;"); | ||||
|         stringBuilder.AppendLine("    }"); | ||||
|         stringBuilder.AppendLine(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -0,0 +1,336 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| public struct ParameterInfo | ||||
| { | ||||
|     public DelegateParameterInfo Parameter { get; set; } | ||||
| } | ||||
|  | ||||
| [Generator] | ||||
| public class NativeCallbacksWrapperGenerator : IIncrementalGenerator | ||||
| { | ||||
|     public void Initialize(IncrementalGeneratorInitializationContext context) | ||||
|     { | ||||
|         var classDeclarations = context.SyntaxProvider.CreateSyntaxProvider( | ||||
|                 static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0, | ||||
|                 static (syntaxContext, _) => GetClassInfoOrNull(syntaxContext)); | ||||
|  | ||||
|         var classAndCompilation = classDeclarations.Combine(context.CompilationProvider); | ||||
|  | ||||
|         context.RegisterSourceOutput(classAndCompilation, (spc, pair) => | ||||
|         { | ||||
|             var maybeClassInfo = pair.Left; // ClassInfo? | ||||
|             var compilation = pair.Right; | ||||
|             if (!maybeClassInfo.HasValue) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
|             GenerateForClass(spc, compilation, maybeClassInfo.Value); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private static void GenerateForClass(SourceProductionContext context, Compilation compilation, ClassInfo classInfo) | ||||
|     { | ||||
|         var model = compilation.GetSemanticModel(classInfo.ClassDeclaration.SyntaxTree); | ||||
|         var sourceBuilder = new StringBuilder(); | ||||
|  | ||||
|         HashSet<string> namespaces = []; | ||||
|         foreach (DelegateInfo delegateInfo in classInfo.Delegates) | ||||
|         { | ||||
|             foreach (var parameter in delegateInfo.ParametersAndReturnValue) | ||||
|             { | ||||
|                 var typeInfo = model.GetTypeInfo(parameter.Type); | ||||
|                 var typeSymbol = typeInfo.Type; | ||||
|  | ||||
|                 if (typeSymbol == null || typeSymbol.ContainingNamespace == null) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 if (typeSymbol is INamedTypeSymbol nts && nts.IsGenericType) | ||||
|                 { | ||||
|                     namespaces.UnionWith(nts.TypeArguments.Where(t => t.ContainingNamespace != null).Select(t => t.ContainingNamespace!.ToDisplayString())); | ||||
|                 } | ||||
|  | ||||
|                 namespaces.Add(typeSymbol.ContainingNamespace.ToDisplayString()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (classInfo.NullableAwareable) | ||||
|         { | ||||
|             sourceBuilder.AppendLine("#nullable enable"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             sourceBuilder.AppendLine("#nullable disable"); | ||||
|         } | ||||
|         sourceBuilder.AppendLine("#pragma warning disable CS8500, CS0414"); | ||||
|         sourceBuilder.AppendLine(); | ||||
|  | ||||
|         foreach (string? ns in namespaces) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(ns)) continue; | ||||
|             sourceBuilder.AppendLine($"using {ns};"); | ||||
|         } | ||||
|  | ||||
|         sourceBuilder.AppendLine(); | ||||
|         sourceBuilder.AppendLine($"namespace {classInfo.Namespace}"); | ||||
|         sourceBuilder.AppendLine("{"); | ||||
|         sourceBuilder.AppendLine($"    public static unsafe partial class {classInfo.Name}"); | ||||
|         sourceBuilder.AppendLine("    {"); | ||||
|  | ||||
|         sourceBuilder.AppendLine("        static " + classInfo.Name + "()"); | ||||
|         sourceBuilder.AppendLine("        {"); | ||||
|  | ||||
|         foreach (DelegateInfo delegateInfo in classInfo.Delegates) | ||||
|         { | ||||
|             string delegateName = delegateInfo.Name; | ||||
|  | ||||
|             string totalSizeDelegateName = delegateName + "TotalSize"; | ||||
|             if (!delegateInfo.HasReturnValue && delegateInfo.Parameters.Count == 0) | ||||
|             { | ||||
|                 sourceBuilder.AppendLine($"             int {totalSizeDelegateName} = 0;"); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 sourceBuilder.Append($"             int {totalSizeDelegateName} = "); | ||||
|  | ||||
|                 void AppendSizeOf(DelegateParameterInfo param) | ||||
|                 { | ||||
|                     string typeFullName = param.Type.GetAnnotatedTypeName(model) ?? param.Type.ToString(); | ||||
|  | ||||
|                     if (param.IsOutParameter || param.IsRefParameter) | ||||
|                     { | ||||
|                         sourceBuilder.Append($"IntPtr.Size"); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         sourceBuilder.Append($"sizeof({typeFullName})"); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 List<DelegateParameterInfo> parameters = delegateInfo.ParametersAndReturnValue; | ||||
|  | ||||
|                 for (int i = 0; i < parameters.Count; i++) | ||||
|                 { | ||||
|                     AppendSizeOf(parameters[i]); | ||||
|  | ||||
|                     if (i != parameters.Count - 1) | ||||
|                     { | ||||
|                         sourceBuilder.Append(" + "); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 sourceBuilder.AppendLine(";"); | ||||
|             } | ||||
|  | ||||
|             string funcPtrName = delegateName + "FuncPtr"; | ||||
|             sourceBuilder.AppendLine($"             IntPtr {funcPtrName} = UnrealSharp.Binds.NativeBinds.TryGetBoundFunction(\"{classInfo.Name}\", \"{delegateInfo.Name}\", {totalSizeDelegateName});"); | ||||
|             sourceBuilder.Append($"             {delegateName} = (delegate* unmanaged<"); | ||||
|             sourceBuilder.Append(string.Join(", ", delegateInfo.Parameters.Select(p => | ||||
|             { | ||||
|                 string prefix = p.IsOutParameter ? "out " : p.IsRefParameter ? "ref " : string.Empty; | ||||
|                 return prefix + (p.Type.GetAnnotatedTypeName(model) ?? p.Type.ToString()); | ||||
|             }))); | ||||
|  | ||||
|             if (delegateInfo.Parameters.Count > 0) | ||||
|             { | ||||
|                 sourceBuilder.Append(", "); | ||||
|             } | ||||
|  | ||||
|             sourceBuilder.Append(delegateInfo.ReturnValue.Type.GetAnnotatedTypeName(model) ?? delegateInfo.ReturnValue.Type.ToString()); | ||||
|  | ||||
|             sourceBuilder.Append($">){funcPtrName};"); | ||||
|             sourceBuilder.AppendLine(); | ||||
|         } | ||||
|  | ||||
|         sourceBuilder.AppendLine("        }"); | ||||
|  | ||||
|         foreach (DelegateInfo delegateInfo in classInfo.Delegates) | ||||
|         { | ||||
|             string returnTypeFullName = delegateInfo.ReturnValue.Type.GetAnnotatedTypeName(model) ?? delegateInfo.ReturnValue.Type.ToString(); | ||||
|             sourceBuilder.AppendLine($"        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]"); | ||||
|             sourceBuilder.Append($"        public static {returnTypeFullName} Call{delegateInfo.Name}("); | ||||
|  | ||||
|             bool firstParameter = true; | ||||
|             foreach (DelegateParameterInfo parameter in delegateInfo.Parameters) | ||||
|             { | ||||
|                 if (!firstParameter) | ||||
|                 { | ||||
|                     sourceBuilder.Append(", "); | ||||
|                 } | ||||
|  | ||||
|                 firstParameter = false; | ||||
|  | ||||
|                 if (parameter.IsOutParameter) | ||||
|                 { | ||||
|                     sourceBuilder.Append("out "); | ||||
|                 } | ||||
|  | ||||
|                 if (parameter.IsRefParameter) | ||||
|                 { | ||||
|                     sourceBuilder.Append("ref "); | ||||
|                 } | ||||
|  | ||||
|                 string typeFullName = parameter.Type.GetAnnotatedTypeName(model) ?? parameter.Type.ToString(); | ||||
|                 sourceBuilder.Append($"{typeFullName} {parameter.Name}"); | ||||
|             } | ||||
|  | ||||
|             sourceBuilder.AppendLine(")"); | ||||
|             sourceBuilder.AppendLine("        {"); | ||||
|  | ||||
|             string delegateName = delegateInfo.Name; | ||||
|  | ||||
|             if (delegateInfo.ReturnValue.Type.ToString() != "void") | ||||
|             { | ||||
|                 sourceBuilder.Append($"            return {delegateName}("); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 sourceBuilder.Append($"            {delegateName}("); | ||||
|             } | ||||
|  | ||||
|             sourceBuilder.Append(string.Join(", ", delegateInfo.Parameters.Select(p => | ||||
|             { | ||||
|                 string prefix = p.IsOutParameter ? "out " : p.IsRefParameter ? "ref " : string.Empty; | ||||
|                 return prefix + p.Name; | ||||
|             }))); | ||||
|  | ||||
|             sourceBuilder.AppendLine(");"); | ||||
|             sourceBuilder.AppendLine("        }"); | ||||
|         } | ||||
|  | ||||
|         // End class definition | ||||
|         sourceBuilder.AppendLine("    }"); | ||||
|         sourceBuilder.AppendLine("}"); | ||||
|  | ||||
|         context.AddSource($"{classInfo.Name}.generated.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); | ||||
|     } | ||||
|  | ||||
|     private static ClassInfo? GetClassInfoOrNull(GeneratorSyntaxContext context) | ||||
|     { | ||||
|         if (context.Node is not ClassDeclarationSyntax classDeclaration) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Check attribute (support both NativeCallbacks and NativeCallbacksAttribute) | ||||
|         bool hasNativeCallbacksAttribute = classDeclaration.AttributeLists | ||||
|             .SelectMany(a => a.Attributes) | ||||
|             .Any(a => a.Name.ToString() is "NativeCallbacks" or "NativeCallbacksAttribute"); | ||||
|  | ||||
|         if (!hasNativeCallbacksAttribute) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         string namespaceName = classDeclaration.GetFullNamespace(); | ||||
|  | ||||
|         if (string.IsNullOrEmpty(namespaceName)) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         var classInfo = new ClassInfo | ||||
|         { | ||||
|             ClassDeclaration = classDeclaration, | ||||
|             Name = classDeclaration.Identifier.ValueText, | ||||
|             Namespace = namespaceName, | ||||
|             Delegates = new List<DelegateInfo>(), | ||||
|             NullableAwareable = context.SemanticModel.GetNullableContext(context.Node.Span.Start).HasFlag(NullableContext.AnnotationsEnabled) | ||||
|         }; | ||||
|  | ||||
|         foreach (MemberDeclarationSyntax member in classDeclaration.Members) | ||||
|         { | ||||
|             if (member is not FieldDeclarationSyntax fieldDeclaration || | ||||
|                 fieldDeclaration.Declaration.Type is not FunctionPointerTypeSyntax functionPointerTypeSyntax) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var delegateInfo = new DelegateInfo | ||||
|             { | ||||
|                 Name = fieldDeclaration.Declaration.Variables.First().Identifier.ValueText, | ||||
|                 Parameters = new List<DelegateParameterInfo>() | ||||
|             }; | ||||
|  | ||||
|             char paramName = 'a'; | ||||
|  | ||||
|             for (int i = 0; i < functionPointerTypeSyntax.ParameterList.Parameters.Count; i++) | ||||
|             { | ||||
|                 FunctionPointerParameterSyntax param = functionPointerTypeSyntax.ParameterList.Parameters[i]; | ||||
|  | ||||
|                 DelegateParameterInfo parameter = new DelegateParameterInfo | ||||
|                 { | ||||
|                     Name = paramName.ToString(), | ||||
|                     IsOutParameter = param.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.OutKeyword)), | ||||
|                     IsRefParameter = param.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.RefKeyword)), | ||||
|                     Type = param.Type, | ||||
|                 }; | ||||
|  | ||||
|                 bool isReturnParameter = i == functionPointerTypeSyntax.ParameterList.Parameters.Count - 1; | ||||
|                 if (isReturnParameter) | ||||
|                 { | ||||
|                     delegateInfo.ReturnValue = parameter; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     delegateInfo.Parameters.Add(parameter); | ||||
|                 } | ||||
|  | ||||
|                 paramName++; | ||||
|             } | ||||
|  | ||||
|             classInfo.Delegates.Add(delegateInfo); | ||||
|         } | ||||
|  | ||||
|         return classInfo; | ||||
|     } | ||||
| } | ||||
|  | ||||
| internal struct ClassInfo | ||||
| { | ||||
|     public ClassDeclarationSyntax ClassDeclaration; | ||||
|     public string Name; | ||||
|     public string Namespace; | ||||
|     public List<DelegateInfo> Delegates; | ||||
|     public bool NullableAwareable; | ||||
| } | ||||
|  | ||||
| internal struct DelegateInfo | ||||
| { | ||||
|     public string Name; | ||||
|     public List<DelegateParameterInfo> Parameters; | ||||
|     public List<DelegateParameterInfo> ParametersAndReturnValue | ||||
|     { | ||||
|         get | ||||
|         { | ||||
|             List<DelegateParameterInfo> allParameters = new List<DelegateParameterInfo>(Parameters); | ||||
|  | ||||
|             if (ReturnValue.Type.ToString() != "void") | ||||
|             { | ||||
|                 allParameters.Add(ReturnValue); | ||||
|             } | ||||
|  | ||||
|             return allParameters; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public bool HasReturnValue => ReturnValue.Type.ToString() != "void"; | ||||
|     public DelegateParameterInfo ReturnValue; | ||||
| } | ||||
|  | ||||
| public struct DelegateParameterInfo | ||||
| { | ||||
|     public string Name; | ||||
|     public TypeSyntax Type; | ||||
|     public bool IsOutParameter; | ||||
|     public bool IsRefParameter; | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| { | ||||
|   "$schema": "http://json.schemastore.org/launchsettings.json", | ||||
|   "profiles": { | ||||
|     "Generators": { | ||||
|       "commandName": "DebugRoslynComponent", | ||||
|       "targetProject": "../UnrealSharp/UnrealSharp.csproj" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| internal static class RuleCategory | ||||
| { | ||||
|     public const string Naming = nameof(Naming); | ||||
|     public const string Category = nameof(Category); | ||||
| } | ||||
| @ -0,0 +1,76 @@ | ||||
| using System; | ||||
| using System.CodeDom.Compiler; | ||||
| using System.IO; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators; | ||||
|  | ||||
| public class SourceBuilder : IDisposable | ||||
| { | ||||
|      | ||||
|     private readonly StringBuilder _stringBuilder; | ||||
|     private readonly IndentedTextWriter _indentedTextWriter; | ||||
|  | ||||
|     public SourceBuilder() | ||||
|     { | ||||
|         _stringBuilder = new StringBuilder(); | ||||
|         _indentedTextWriter = new IndentedTextWriter(new StringWriter(_stringBuilder), "    "); | ||||
|     } | ||||
|  | ||||
|     public int Indent | ||||
|     { | ||||
|         get => _indentedTextWriter.Indent; | ||||
|         set => _indentedTextWriter.Indent = value; | ||||
|     } | ||||
|  | ||||
|     public Scope OpenBlock() | ||||
|     { | ||||
|         return new Scope(this); | ||||
|     } | ||||
|  | ||||
|     public SourceBuilder Append(string line) | ||||
|     { | ||||
|         _indentedTextWriter.Write(line); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     public SourceBuilder AppendLine(string line) | ||||
|     { | ||||
|         _indentedTextWriter.WriteLine(line); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     public SourceBuilder AppendLine() | ||||
|     { | ||||
|         return AppendLine(string.Empty); | ||||
|     } | ||||
|  | ||||
|     public override string ToString() | ||||
|     { | ||||
|         return _stringBuilder.ToString(); | ||||
|     } | ||||
|  | ||||
|     public void Dispose() | ||||
|     { | ||||
|         _indentedTextWriter.Dispose(); | ||||
|     } | ||||
|      | ||||
|     public readonly struct Scope : IDisposable | ||||
|     { | ||||
|         private readonly SourceBuilder _sourceBuilder; | ||||
|  | ||||
|         internal Scope(SourceBuilder sourceBuilder) | ||||
|         { | ||||
|             _sourceBuilder = sourceBuilder; | ||||
|             _sourceBuilder.AppendLine("{"); | ||||
|             _sourceBuilder.Indent++; | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             _sourceBuilder.Indent--; | ||||
|             _sourceBuilder.AppendLine("}"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,75 @@ | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
|  | ||||
| namespace UnrealSharp.SourceGenerators.StructGenerator; | ||||
|  | ||||
| [Generator] | ||||
| public class MarshalledStructGenerator : IIncrementalGenerator | ||||
| { | ||||
|     public void Initialize(IncrementalGeneratorInitializationContext context) | ||||
|     { | ||||
|         var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider((n, _) => n is StructDeclarationSyntax or RecordDeclarationSyntax, | ||||
|                 (ctx, _) => | ||||
|                 { | ||||
|                     var structDeclaration = ctx.Node; | ||||
|                     if (ctx.SemanticModel.GetDeclaredSymbol(structDeclaration) is not INamedTypeSymbol { TypeKind: TypeKind.Struct } structSymbol) | ||||
|                     { | ||||
|                         return null; | ||||
|                     } | ||||
|  | ||||
|                     return AnalyzerStatics.HasAttribute(structSymbol, AnalyzerStatics.UStructAttribute) | ||||
|                            && !structSymbol.Interfaces.Any(i => i.MetadataName == "MarshalledStruct`1") | ||||
|                            && structSymbol.DeclaringSyntaxReferences | ||||
|                                .Select(r => r.GetSyntax()) | ||||
|                                .OfType<TypeDeclarationSyntax>() | ||||
|                                .SelectMany(s => s.Modifiers) | ||||
|                                .Any(m => m.IsKind(SyntaxKind.PartialKeyword)) | ||||
|                         ? structSymbol | ||||
|                         : null; | ||||
|                 }) | ||||
|             .Where(sym => sym is not null); | ||||
|  | ||||
|         context.RegisterSourceOutput(syntaxProvider, GenerateStruct!); | ||||
|     } | ||||
|  | ||||
|     private static void GenerateStruct(SourceProductionContext context, INamedTypeSymbol structSymbol) | ||||
|     { | ||||
|         using var builder = new SourceBuilder(); | ||||
|  | ||||
|         WriteStructCode(builder, structSymbol); | ||||
|  | ||||
|         context.AddSource($"{structSymbol.Name}.generated.cs", builder.ToString()); | ||||
|     } | ||||
|  | ||||
|     private static void WriteStructCode(SourceBuilder builder, INamedTypeSymbol structSymbol) | ||||
|     { | ||||
|         builder.AppendLine("using UnrealSharp;"); | ||||
|         builder.AppendLine("using UnrealSharp.Attributes;"); | ||||
|         builder.AppendLine("using UnrealSharp.Core.Marshallers;"); | ||||
|         builder.AppendLine("using UnrealSharp.Interop;"); | ||||
|         builder.AppendLine(); | ||||
|         builder.AppendLine($"namespace {structSymbol.ContainingNamespace.ToDisplayString()};"); | ||||
|         builder.AppendLine(); | ||||
|  | ||||
|         builder.AppendLine( | ||||
|             $"partial {(structSymbol.IsRecord ? "record " : "")}struct {structSymbol.Name} : MarshalledStruct<{structSymbol.Name}>"); | ||||
|  | ||||
|         using var structScope = builder.OpenBlock(); | ||||
|  | ||||
|         builder.AppendLine("[WeaverGenerated]"); | ||||
|         builder.AppendLine("public static extern IntPtr GetNativeClassPtr();"); | ||||
|         builder.AppendLine(); | ||||
|         builder.AppendLine("[WeaverGenerated]"); | ||||
|         builder.AppendLine("public static extern int GetNativeDataSize();"); | ||||
|         builder.AppendLine(); | ||||
|         builder.AppendLine("[WeaverGenerated]"); | ||||
|         builder.AppendLine($"public static extern {structSymbol.Name} FromNative(IntPtr InNativeStruct);"); | ||||
|         builder.AppendLine(); | ||||
|         builder.AppendLine("[WeaverGenerated]"); | ||||
|         builder.AppendLine("public extern void ToNative(IntPtr buffer);"); | ||||
|     } | ||||
| } | ||||
|  | ||||
|      | ||||
| @ -0,0 +1,18 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|      | ||||
|     <PropertyGroup> | ||||
|         <LangVersion>latest</LangVersion> | ||||
|         <TargetFramework>netstandard2.0</TargetFramework> | ||||
|         <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||||
|         <IsRoslynComponent>true</IsRoslynComponent> | ||||
|         <NoWarn>$(NoWarn);1570;0649;0169;0108;0109;RS1038</NoWarn> | ||||
|         <Nullable>enable</Nullable> | ||||
|     </PropertyGroup> | ||||
|      | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" /> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" /> | ||||
|         <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" PrivateAssets="all" /> | ||||
|     </ItemGroup> | ||||
|      | ||||
| </Project> | ||||
| @ -0,0 +1,34 @@ | ||||
| using System.Diagnostics; | ||||
| using System.Runtime.Loader; | ||||
|  | ||||
| namespace UnrealSharp.StaticVars; | ||||
|  | ||||
| public class FBaseStaticVar<T> | ||||
| { | ||||
|     [DebuggerBrowsable(DebuggerBrowsableState.Never)] | ||||
|     private T? _value; | ||||
|      | ||||
|     public virtual T? Value | ||||
|     { | ||||
|         get => _value; | ||||
|         set => _value = value; | ||||
|     } | ||||
|      | ||||
| #if WITH_EDITOR | ||||
|     public FBaseStaticVar() | ||||
|     { | ||||
|         AssemblyLoadContext alc = AssemblyLoadContext.GetLoadContext(GetType().Assembly)!; | ||||
|         alc.Unloading += OnAlcUnloading; | ||||
|     } | ||||
|  | ||||
|     protected virtual void OnAlcUnloading(AssemblyLoadContext alc) | ||||
|     { | ||||
|         alc.Unloading -= OnAlcUnloading; | ||||
|     } | ||||
| #endif | ||||
|      | ||||
|     public override string ToString() | ||||
|     { | ||||
|         return Value.ToString(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,77 @@ | ||||
| using System.Diagnostics; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Runtime.Loader; | ||||
| using UnrealSharp.Core; | ||||
| using UnrealSharp.StaticVars.Interop; | ||||
|  | ||||
| namespace UnrealSharp.StaticVars; | ||||
|  | ||||
| /// <summary> | ||||
| /// A static variable which will be alive during the whole game session. | ||||
| /// In editor the value will reset on Play In Editor start/end and on hot reload. | ||||
| /// </summary> | ||||
| /// <typeparam name="T">The type of the static variable</typeparam> | ||||
| public sealed class FGameStaticVar<T> : FBaseStaticVar<T> | ||||
| { | ||||
| #if WITH_EDITOR | ||||
|     [DebuggerBrowsable(DebuggerBrowsableState.Never)] | ||||
|     private readonly FDelegateHandle _onPieStartEndHandle; | ||||
|      | ||||
|     [DebuggerBrowsable(DebuggerBrowsableState.Never)] | ||||
|     private readonly FDelegateHandle _onPieEndHandle; | ||||
|      | ||||
|     private readonly FEditorDelegates.FOnPIEEvent _onPIEndDelegate; | ||||
|  | ||||
|     public FGameStaticVar() | ||||
|     { | ||||
|         _onPIEndDelegate = OnPIEStartEnd; | ||||
|         IntPtr onPIEStartEndFuncPtr = Marshal.GetFunctionPointerForDelegate(_onPIEndDelegate); | ||||
|         FEditorDelegatesExporter.CallBindEndPIE(onPIEStartEndFuncPtr, out _onPieEndHandle); | ||||
|         FEditorDelegatesExporter.CallBindStartPIE(onPIEStartEndFuncPtr, out _onPieStartEndHandle); | ||||
|     } | ||||
|      | ||||
|     public FGameStaticVar(T value) : this() | ||||
|     { | ||||
|         Value = value; | ||||
|     } | ||||
|      | ||||
|     ~FGameStaticVar() | ||||
|     { | ||||
|         Cleanup(); | ||||
|     } | ||||
|  | ||||
|     protected override void OnAlcUnloading(AssemblyLoadContext alc) | ||||
|     { | ||||
|         base.OnAlcUnloading(alc); | ||||
|         Cleanup(); | ||||
|     } | ||||
|  | ||||
|     private void OnPIEStartEnd(NativeBool simulating) | ||||
|     { | ||||
|         ResetToDefault(); | ||||
|     } | ||||
|      | ||||
|     void Cleanup() | ||||
|     { | ||||
|         ResetToDefault(); | ||||
|         FEditorDelegatesExporter.CallUnbindStartPIE(_onPieStartEndHandle); | ||||
|         FEditorDelegatesExporter.CallUnbindEndPIE(_onPieEndHandle); | ||||
|     } | ||||
|      | ||||
|     void ResetToDefault() | ||||
|     { | ||||
|         Value = default; | ||||
|     } | ||||
|      | ||||
| #else | ||||
|     public FGameStaticVar(T value) | ||||
|     { | ||||
|         Value = value; | ||||
|     } | ||||
| #endif | ||||
|      | ||||
|     public static implicit operator T(FGameStaticVar<T> value) | ||||
|     { | ||||
|         return value.Value; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,20 @@ | ||||
| using System.Runtime.InteropServices; | ||||
| using UnrealSharp.Binds; | ||||
| using UnrealSharp.Core; | ||||
|  | ||||
| namespace UnrealSharp.StaticVars.Interop; | ||||
|  | ||||
| public struct FEditorDelegates | ||||
| { | ||||
|     [UnmanagedFunctionPointer(CallingConvention.Cdecl)] | ||||
|     public delegate void FOnPIEEvent(NativeBool sessionEnded); | ||||
| } | ||||
|  | ||||
| [NativeCallbacks] | ||||
| public static unsafe partial class FEditorDelegatesExporter | ||||
| { | ||||
|     public static delegate* unmanaged<IntPtr, out FDelegateHandle, void> BindStartPIE; | ||||
|     public static delegate* unmanaged<IntPtr, out FDelegateHandle, void> BindEndPIE; | ||||
|     public static delegate* unmanaged<FDelegateHandle, void> UnbindStartPIE; | ||||
|     public static delegate* unmanaged<FDelegateHandle, void> UnbindEndPIE; | ||||
| } | ||||
| @ -0,0 +1,18 @@ | ||||
| using System.Runtime.InteropServices; | ||||
| using UnrealSharp.Binds; | ||||
| using UnrealSharp.Core; | ||||
|  | ||||
| namespace UnrealSharp.Interop; | ||||
|  | ||||
| public struct FWorldDelegates | ||||
| { | ||||
|     [UnmanagedFunctionPointer(CallingConvention.Cdecl)] | ||||
|     public delegate void FWorldCleanupEvent(IntPtr world, NativeBool sessionEnded, NativeBool cleanupResources); | ||||
| } | ||||
|  | ||||
| [NativeCallbacks] | ||||
| public static unsafe partial class FWorldDelegatesExporter | ||||
| { | ||||
|     public static delegate* unmanaged<IntPtr, out FDelegateHandle, void> BindOnWorldCleanup; | ||||
|     public static delegate* unmanaged<FDelegateHandle, void> UnbindOnWorldCleanup; | ||||
| } | ||||
| @ -0,0 +1,24 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <DefineConstants>WITH_EDITOR</DefineConstants> | ||||
|         <DefineConstants Condition="'$(DisableWithEditor)' == 'true'">$(DefineConstants.Replace('WITH_EDITOR;', '').Replace('WITH_EDITOR', ''))</DefineConstants> | ||||
|         <DefineConstants Condition="'$(DefineAdditionalConstants)' != ''">$(DefineConstants);$(DefineAdditionalConstants)</DefineConstants> | ||||
|     </PropertyGroup> | ||||
|      | ||||
|     <ItemGroup> | ||||
|         <ProjectReference Include="..\UnrealSharp.Core\UnrealSharp.Core.csproj" /> | ||||
|         <ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj"> | ||||
|             <OutputItemType>Analyzer</OutputItemType> | ||||
|             <ReferenceOutputAssembly>false</ReferenceOutputAssembly> | ||||
|         </ProjectReference> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @ -0,0 +1,73 @@ | ||||
| using System.Diagnostics; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Runtime.Loader; | ||||
| using UnrealSharp.Core; | ||||
| using UnrealSharp.Interop; | ||||
|  | ||||
| namespace UnrealSharp.StaticVars; | ||||
|  | ||||
| /// <summary> | ||||
| /// A static variable that has the lifetime of a UWorld. When the world is destroyed, the value is destroyed. | ||||
| /// For example when traveling between levels, the value is destroyed. | ||||
| /// </summary> | ||||
| public sealed class FWorldStaticVar<T> : FBaseStaticVar<T> | ||||
| { | ||||
|     [DebuggerBrowsable(DebuggerBrowsableState.Never)] | ||||
|     private readonly Dictionary<IntPtr, T> _worldToValue = new Dictionary<IntPtr, T>(); | ||||
|      | ||||
|     [DebuggerBrowsable(DebuggerBrowsableState.Never)] | ||||
|     private readonly FDelegateHandle _onWorldCleanupHandle; | ||||
|      | ||||
|     public FWorldStaticVar() | ||||
|     { | ||||
|         FWorldDelegates.FWorldCleanupEvent onWorldCleanupDelegate = OnWorldCleanup; | ||||
|         IntPtr onWorldCleanup = Marshal.GetFunctionPointerForDelegate(onWorldCleanupDelegate); | ||||
|         FWorldDelegatesExporter.CallBindOnWorldCleanup(onWorldCleanup, out _onWorldCleanupHandle); | ||||
|     } | ||||
|      | ||||
|     public FWorldStaticVar(T value) : this() | ||||
|     { | ||||
|         Value = value; | ||||
|     } | ||||
|      | ||||
|     ~FWorldStaticVar() | ||||
|     { | ||||
|         FWorldDelegatesExporter.CallUnbindOnWorldCleanup(_onWorldCleanupHandle); | ||||
|     } | ||||
|      | ||||
|     public override T? Value | ||||
|     { | ||||
|         get => GetWorldValue(); | ||||
|         set => SetWorldValue(value!); | ||||
|     } | ||||
|      | ||||
|     private T? GetWorldValue() | ||||
|     { | ||||
|         IntPtr worldPtr = FCSManagerExporter.CallGetCurrentWorldPtr(); | ||||
|         return _worldToValue.GetValueOrDefault(worldPtr); | ||||
|     } | ||||
|      | ||||
|     private void SetWorldValue(T value) | ||||
|     { | ||||
|         IntPtr worldPtr = FCSManagerExporter.CallGetCurrentWorldPtr(); | ||||
|         if (_worldToValue.TryAdd(worldPtr, value)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         _worldToValue[worldPtr] = value; | ||||
|     } | ||||
|      | ||||
|     private void OnWorldCleanup(IntPtr world, NativeBool sessionEnded, NativeBool cleanupResources) | ||||
|     { | ||||
|         _worldToValue.Remove(world); | ||||
|     } | ||||
|  | ||||
| #if WITH_EDITOR | ||||
|     protected override void OnAlcUnloading(AssemblyLoadContext alc) | ||||
|     { | ||||
|         base.OnAlcUnloading(alc); | ||||
|         FWorldDelegatesExporter.CallUnbindOnWorldCleanup(_onWorldCleanupHandle); | ||||
|     } | ||||
| #endif | ||||
| } | ||||
							
								
								
									
										202
									
								
								Plugins/UnrealSharp/Managed/UnrealSharp/UnrealSharp/Array.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								Plugins/UnrealSharp/Managed/UnrealSharp/UnrealSharp/Array.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,202 @@ | ||||
| using UnrealSharp.Attributes; | ||||
| using UnrealSharp.Core; | ||||
| using UnrealSharp.Core.Attributes; | ||||
| using UnrealSharp.Core.Marshallers; | ||||
| using UnrealSharp.Interop; | ||||
|  | ||||
| namespace UnrealSharp; | ||||
|  | ||||
| /// <summary> | ||||
| /// An array that can be used to interact with Unreal Engine arrays. | ||||
| /// </summary> | ||||
| /// <typeparam name="T"> The type of elements in the array. </typeparam> | ||||
| [Binding] | ||||
| public class TArray<T> : UnrealArrayBase<T>, IList<T>, IReadOnlyList<T> | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public bool IsReadOnly => false; | ||||
|  | ||||
|     public TArray(IntPtr nativeUnrealProperty, IntPtr nativeBuffer, MarshallingDelegates<T>.ToNative toNative, MarshallingDelegates<T>.FromNative fromNative) | ||||
|         : base(nativeUnrealProperty, nativeBuffer, toNative, fromNative) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc /> | ||||
|     public T this[int index] | ||||
|     { | ||||
|         get  | ||||
|         { | ||||
|             if (index < 0 || index >= Count) | ||||
|             { | ||||
|                 throw new IndexOutOfRangeException($"Index {index} is out of bounds. Array size is {Count}."); | ||||
|             } | ||||
|             return Get(index); | ||||
|         } | ||||
|         set | ||||
|         { | ||||
|             if (index < 0 || index >= Count) | ||||
|             { | ||||
|                 throw new IndexOutOfRangeException($"Index {index} is out of bounds. Array size is {Count}."); | ||||
|             } | ||||
|             ToNative(NativeArrayBuffer, index, value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|      | ||||
|     /// <summary> | ||||
|     /// Adds an element to the end of the array. | ||||
|     /// </summary> | ||||
|     /// <param name="item"> The element to add. </param> | ||||
|     public void Add(T item) | ||||
|     { | ||||
|         int newIndex = Count; | ||||
|         AddInternal(); | ||||
|         this[newIndex] = item; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Removes all elements from the array. | ||||
|     /// </summary> | ||||
|     public void Clear() | ||||
|     { | ||||
|         ClearInternal(); | ||||
|     } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// Resizes the array to the specified size. | ||||
|     /// If the new size is smaller than the current size, elements will be removed. If the new size is larger, elements will be added. | ||||
|     /// </summary> | ||||
|     /// <param name="newSize"> The new size of the array. </param> | ||||
|     public void Resize(int newSize) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             FArrayPropertyExporter.CallResizeArray(NativeProperty, NativeBuffer, newSize); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// Swaps the elements at the specified indices. | ||||
|     /// </summary> | ||||
|     /// <param name="indexA"> The index of the first element to swap. </param> | ||||
|     /// <param name="indexB"> The index of the second element to swap. </param> | ||||
|     public void Swap(int indexA, int indexB) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             FArrayPropertyExporter.CallSwapValues(NativeProperty, NativeBuffer, indexA, indexB); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Copy the elements of the array to an array starting at the specified index. | ||||
|     /// </summary> | ||||
|     /// <param name="array"> The array to copy the elements to. </param> | ||||
|     /// <param name="arrayIndex"> The index in the array to start copying to. </param> | ||||
|     public void CopyTo(T[] array, int arrayIndex) | ||||
|     { | ||||
|         int numElements = Count; | ||||
|         for (int i = 0; i < numElements; ++i) | ||||
|         { | ||||
|             array[i + arrayIndex] = this[i]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Removes the first occurrence of a specific object from the array. | ||||
|     /// </summary> | ||||
|     /// <param name="item"> The object to remove. </param> | ||||
|     /// <returns> True if the object was successfully removed; otherwise, false. This method also returns false if the object is not found in the array. </returns> | ||||
|     public bool Remove(T item) | ||||
|     { | ||||
|         int index = IndexOf(item); | ||||
|         if (index != -1) | ||||
|         { | ||||
|             RemoveAt(index); | ||||
|         } | ||||
|         return index != -1; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Gets the index of the specified element in the array. | ||||
|     /// </summary> | ||||
|     /// <param name="item"> The element to find. </param> | ||||
|     /// <returns> The index of the element in the array, or -1 if the element is not in the array. </returns> | ||||
|     public int IndexOf(T item) | ||||
|     { | ||||
|         int numElements = Count; | ||||
|         for (int i = 0; i < numElements; ++i) | ||||
|         { | ||||
|             if (this[i].Equals(item)) | ||||
|             { | ||||
|                 return i; | ||||
|             } | ||||
|         } | ||||
|         return -1; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Inserts an element into the array at the specified index. | ||||
|     /// </summary> | ||||
|     /// <param name="index"> The index to insert the element at. </param> | ||||
|     /// <param name="item"> The element to insert. </param> | ||||
|     public void Insert(int index, T item) | ||||
|     { | ||||
|         InsertInternal(index); | ||||
|         this[index] = item; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Removes the element at the specified index. | ||||
|     /// </summary> | ||||
|     /// <param name="index"> The index of the element to remove. </param> | ||||
|     public void RemoveAt(int index) | ||||
|     { | ||||
|         RemoveAtInternal(index); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class ArrayMarshaller<T>(IntPtr nativeProperty, MarshallingDelegates<T>.ToNative toNative, MarshallingDelegates<T>.FromNative fromNative) | ||||
| { | ||||
|     private TArray<T>? _arrayWrapper; | ||||
|  | ||||
|     public void ToNative(IntPtr nativeBuffer, int arrayIndex, IList<T> obj) | ||||
|     { | ||||
|         ToNative(nativeBuffer, obj); | ||||
|     } | ||||
|  | ||||
|     public void ToNative(IntPtr nativeBuffer, IList<T> obj) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             UnmanagedArray* mirror = (UnmanagedArray*)nativeBuffer; | ||||
|             if (mirror->ArrayNum == obj.Count) | ||||
|             { | ||||
|                 for (int i = 0; i < obj.Count; ++i) | ||||
|                 { | ||||
|                     toNative(mirror->Data, i, obj[i]); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 FArrayPropertyExporter.CallResizeArray(nativeProperty, mirror, obj.Count); | ||||
|                 for (int i = 0; i < obj.Count; ++i) | ||||
|                 { | ||||
|                     toNative(mirror->Data, i, obj[i]); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public TArray<T> FromNative(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         if (_arrayWrapper == null) | ||||
|         { | ||||
|             unsafe | ||||
|             { | ||||
|                 _arrayWrapper = new TArray<T>(nativeProperty, nativeBuffer + arrayIndex * sizeof(UnmanagedArray), toNative, fromNative); | ||||
|             } | ||||
|         } | ||||
|         return _arrayWrapper; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,56 @@ | ||||
| using UnrealSharp.Core; | ||||
| using UnrealSharp.Core.Marshallers; | ||||
| using UnrealSharp.Interop; | ||||
|  | ||||
| namespace UnrealSharp; | ||||
|  | ||||
| public class TArrayReadOnly<T> : UnrealArrayBase<T>, IReadOnlyList<T> | ||||
| { | ||||
|     public TArrayReadOnly(IntPtr nativeUnrealProperty, IntPtr nativeBuffer, MarshallingDelegates<T>.ToNative toNative, MarshallingDelegates<T>.FromNative fromNative) | ||||
|         : base(nativeUnrealProperty, nativeBuffer, toNative, fromNative) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc /> | ||||
|     public T this[int index] => Get(index); | ||||
| } | ||||
|  | ||||
| public class ArrayReadOnlyMarshaller<T>(IntPtr nativeProperty, MarshallingDelegates<T>.ToNative toNative, MarshallingDelegates<T>.FromNative fromNative) | ||||
| { | ||||
|     private TArrayReadOnly<T>? _readOnlyWrapper; | ||||
|  | ||||
|     public void ToNative(IntPtr nativeBuffer, IReadOnlyList<T> obj) | ||||
|     { | ||||
|         unsafe | ||||
|         { | ||||
|             UnmanagedArray* mirror = (UnmanagedArray*)nativeBuffer; | ||||
|             if (mirror->ArrayNum == obj.Count) | ||||
|             { | ||||
|                 for (int i = 0; i < obj.Count; ++i) | ||||
|                 { | ||||
|                     toNative(mirror->Data, i, obj[i]); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 FArrayPropertyExporter.CallResizeArray(nativeProperty, mirror, obj.Count); | ||||
|                 for (int i = 0; i < obj.Count; ++i) | ||||
|                 { | ||||
|                     toNative(mirror->Data, i, obj[i]); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public TArrayReadOnly<T> FromNative(IntPtr nativeBuffer, int arrayIndex) | ||||
|     { | ||||
|         if (_readOnlyWrapper == null) | ||||
|         { | ||||
|             unsafe | ||||
|             { | ||||
|                 _readOnlyWrapper = new TArrayReadOnly<T>(nativeProperty, nativeBuffer + arrayIndex * sizeof(UnmanagedArray), toNative, fromNative); | ||||
|             } | ||||
|         } | ||||
|         return _readOnlyWrapper; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,14 @@ | ||||
| namespace UnrealSharp.Attributes; | ||||
|  | ||||
| public class BaseUAttribute : Attribute | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The display name of the type, used in the editor. | ||||
|     /// </summary> | ||||
|     public string DisplayName = ""; | ||||
|      | ||||
|     /// <summary> | ||||
|     /// The category of the type, used in the editor. | ||||
|     /// </summary>  | ||||
|     public string Category = ""; | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user