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