@ -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 = "";
|
||||
}
|
||||
@ -0,0 +1,721 @@
|
||||
#pragma warning disable CS9113 // Suppress 'Parameter is unread' on this file as these parameters are used via reflection
|
||||
namespace UnrealSharp.Attributes.MetaTags;
|
||||
|
||||
//====================================================
|
||||
// MetaData Tags
|
||||
// See: https://dev.epicgames.com/documentation/en-us/unreal-engine/metadata-specifiers-in-unreal-engine
|
||||
//
|
||||
// Note: If a MetaData Tag is not listed, you can use [UMetaData("key","value")] to specify the key/value directly
|
||||
//====================================================
|
||||
|
||||
|
||||
//----------------------------------------------------
|
||||
// Shared Metadata Specifiers
|
||||
//----------------------------------------------------
|
||||
#region Shared
|
||||
/// <summary>
|
||||
/// [DisplayName]
|
||||
/// The name to display for this property, instead of the code-generated name.
|
||||
/// </summary>
|
||||
/// <param name="DisplayName">Property Name</param>
|
||||
[AttributeUsage(AttributeTargets.All)]
|
||||
public sealed class DisplayNameAttribute(string DisplayName) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ToolTip]
|
||||
/// Overrides the automatically generated tooltip from code comments.
|
||||
/// </summary>
|
||||
/// <param name="tooltip">Hand-written tooltip</param>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Field | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property)]
|
||||
public class ToolTipAttribute(string tooltip) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ShortToolTip]
|
||||
/// A short tooltip that is used in some contexts where the full tooltip might be overwhelming, such as the Parent Class Picker dialog.
|
||||
/// </summary>
|
||||
/// <param name="ShortToolTip">Short tooltip</param>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method)]
|
||||
public sealed class ShortToolTipAttribute(string ShortToolTip) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ScriptName]
|
||||
/// The name to use for this clas, property, or function when exporting it to a scripting language. You may include deprecated names as additional semi-colon-separated entries.
|
||||
/// </summary>
|
||||
/// <param name="ScriptName">DisplayName</param>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method)]
|
||||
public sealed class ScriptNameAttribute(string ScriptName) : Attribute { }
|
||||
|
||||
#endregion
|
||||
|
||||
//----------------------------------------------------
|
||||
// Class Metadata Specifiers
|
||||
// Classes can use the following Metatag Specifiers:
|
||||
//----------------------------------------------------
|
||||
#region Class
|
||||
/// <summary>
|
||||
/// [BlueprintSpawnableComponent]
|
||||
/// If present, the component Class can be spawned by a Blueprint.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class BlueprintSpawnableComponentAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BlueprintThreadSafe]
|
||||
/// Only valid on Blueprint function libraries. This specifier marks the functions in this class as callable on non-game threads in animation Blueprints.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class BlueprintThreadSafeAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ChildCannotTick]
|
||||
/// Used for Actor and Component classes. If the native class cannot tick, Blueprint-generated classes based on this Actor or Component can never tick, even if bCanBlueprintsTickByDefault is true.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ChildCannotTickAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ChildCanTick]
|
||||
/// Used for Actor and Component classes. If the native class cannot tick, Blueprint-generated classes based on this Actor or Component can have the bCanEverTick flag overridden, even if bCanBlueprintsTickByDefault is false.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ChildCanTickAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [DeprecatedNode]
|
||||
/// For behavior tree nodes, indicates that the class is deprecated and will display a warning when compiled.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class DeprecatedNodeAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [DeprecationMessage]
|
||||
/// Deprecated classes with this metadata will include this text with the standard deprecation warning that Blueprint Scripts generate during compilation.
|
||||
/// </summary>
|
||||
/// <param name="DeprecationMessage">Message Text</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class DeprecationMessageAttribute(string DeprecationMessage) : Attribute { }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [DontUseGenericSpawnObject]
|
||||
/// Do not spawn an Object of the class using Generic Create Object node in Blueprint Scripts; this specifier applies only to Blueprint-type classes that are neither Actors nor Actor Components.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class DontUseGenericSpawnObjectAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ExposedAsyncProxy]
|
||||
/// Expose a proxy Object of this class in Async Task nodes.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ExposedAsyncProxyAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [IgnoreCategoryKeywordsInSubclasses]
|
||||
/// Used to make the first subclass of a class ignore all inherited ShowCategories and HideCategories Specifiers.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class IgnoreCategoryKeywordsInSubclassesAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [IsBlueprintBase]
|
||||
/// States that this class is (or is not) an acceptable base class for creating Blueprints, similar to the Blueprintable or NotBlueprintable Specifiers.
|
||||
/// </summary>
|
||||
/// <param name="IsBlueprintBase">true/false</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class IsBlueprintBaseAttribute(string IsBlueprintBase) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [KismetHideOverrides]
|
||||
/// List of Blueprint events that are not allowed to be overridden.
|
||||
/// </summary>
|
||||
/// <param name="KismetHideOverrides">Event1, Event2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class KismetHideOverridesAttribute(string KismetHideOverrides) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ProhibitedInterfaces]
|
||||
/// Lists Interfaces that are not compatible with the class.
|
||||
/// </summary>
|
||||
/// <param name="ProhibitedInterfaces">Interface1, Interface2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ProhibitedInterfacesAttribute(string ProhibitedInterfaces) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [RestrictedToClasses]
|
||||
/// Blueprint function library classes can use this to restrict usage to the classes named in the list.
|
||||
/// </summary>
|
||||
/// <param name="RestrictedToClasses">Class1, Class2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class RestrictedToClassesAttribute(string RestrictedToClasses) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ShowWorldContextPin]
|
||||
/// Indicates that Blueprint nodes placed in graphs owned by this class must show their World context pins, even if they are normally hidden, because Objects of this class cannot be used as World context.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ShowWorldContextPinAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [UsesHierarchy]
|
||||
/// Indicates the class uses hierarchical data. Used to instantiate hierarchical editing features in Details panels.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class UsesHierarchyAttribute : Attribute { }
|
||||
|
||||
#endregion
|
||||
|
||||
//----------------------------------------------------
|
||||
// Enum Metadata Specifiers
|
||||
// Enumerated types can use the following Metadata Specifiers:
|
||||
//----------------------------------------------------
|
||||
#region Enum
|
||||
/// <summary>
|
||||
/// [Bitflags]
|
||||
/// Indicates that this enumerated type can be used as flags by integer UPROPERTY variables that are set up with the Bitmask Metadata Specifier.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Enum)]
|
||||
public sealed class BitflagsAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [Experimental]
|
||||
/// Labels this type as experimental and unsupported.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Enum)]
|
||||
public sealed class ExperimentalAttribute : Attribute { }
|
||||
#endregion
|
||||
|
||||
//----------------------------------------------------
|
||||
// Interface Metadata Specifiers
|
||||
//----------------------------------------------------
|
||||
#region Interface
|
||||
/// <summary>
|
||||
/// [CannotImplementInterfaceInBlueprint]
|
||||
/// This interface may not contain BlueprintImplementableEvent or BlueprintNativeEvent functions, other than internal-only functions. If it contains Blueprint-callable functions that are not blueprint-defined, those functions must be implemented in native code.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Interface)]
|
||||
public sealed class CannotImplementInterfaceInBlueprintAttribute : Attribute { }
|
||||
#endregion
|
||||
|
||||
|
||||
//----------------------------------------------------
|
||||
// Struct Metadata Specifiers
|
||||
//----------------------------------------------------
|
||||
#region Struct
|
||||
/// <summary>
|
||||
/// [HasNativeBreak]
|
||||
/// Indicates that this struct has a custom Break Struct node. The module, class, and function name must be provided.
|
||||
/// </summary>
|
||||
/// <param name="HasNativeBreak">Module.Class.Function</param>
|
||||
[AttributeUsage(AttributeTargets.Struct)]
|
||||
public sealed class HasNativeBreakAttribute(string HasNativeBreak) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [HasNativeMake]
|
||||
/// Indicates that this struct has a custom Make Struct node. The module, class, and function name must be provided.
|
||||
/// </summary>
|
||||
/// <param name="HasNativeMake">Module.Class.Function</param>
|
||||
[AttributeUsage(AttributeTargets.Struct)]
|
||||
public sealed class HasNativeMakeAttribute(string HasNativeMake) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [HiddenByDefault]
|
||||
/// Pins in Make Struct and Break Struct nodes are hidden by default.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Struct)]
|
||||
public sealed class HiddenByDefaultAttribute : Attribute { }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
//----------------------------------------------------
|
||||
// Function Metadata Specifiers
|
||||
//----------------------------------------------------
|
||||
#region Function (Method)
|
||||
/// <summary>
|
||||
/// [AdvancedDisplay]
|
||||
/// The comma-separated list of parameters will show up as advanced pins (requiring UI expansion).
|
||||
/// or
|
||||
/// Replace N with a number, and all parameters after the Nth will show up as advanced pins (requiring UI expansion). For example, 'AdvancedDisplay=2' will mark all but the first two parameters as advanced).
|
||||
/// </summary>
|
||||
/// <param name="AdvancedDisplay">Parameter1, Parameter2, .. or N</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class AdvancedDisplayAttribute(string AdvancedDisplay) : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// [ArrayParm]
|
||||
/// Indicates that a BlueprintCallable function should use a Call Array Function node and that the listed parameters should be treated as wild card array properties.
|
||||
/// </summary>
|
||||
/// <param name="ArrayParm">Parameter1, Parameter2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class ArrayParmAttribute(string ArrayParm) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ArrayTypeDependentParams]
|
||||
/// When ArrayParm is used, this specifier indicates one parameter which will determine the types of all parameters in the ArrayParm list.
|
||||
/// </summary>
|
||||
/// <param name="ArrayTypeDependentParams">Parameter</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class ArrayTypeDependentParamsAttribute(string ArrayTypeDependentParams) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [AutoCreateRefTerm]
|
||||
/// The listed parameters, although passed by reference, will have an automatically created default if their pins are left disconnected. This is a convenience feature for Blueprints, often used on array pins.
|
||||
/// </summary>
|
||||
/// <param name="AutoCreateRefTerm">Parameter1, Parameter2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class AutoCreateRefTermAttribute(string AutoCreateRefTerm) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BlueprintAutocast]
|
||||
/// Used only by static BlueprintPure functions from a Blueprint function library. A cast node will be automatically added for the return type and the type of the first parameter of the function.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class BlueprintAutocastAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BlueprintInternalUseOnly]
|
||||
/// This function is an internal implementation detail, used to implement another function or node. It is never directly exposed in a Blueprint graph.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class BlueprintInternalUseOnlyAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BlueprintProtected]
|
||||
/// This function can only be called on the owning Object in a Blueprint. It cannot be called on another instance.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class BlueprintProtectedAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [CallableWithoutWorldContext]
|
||||
/// Used for BlueprintCallable functions that have a WorldContext pin to indicate that the function can be called even if its Class does not implement the GetWorld function.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class CallableWithoutWorldContextAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [CommutativeAssociativeBinaryOperator]
|
||||
/// Indicates that a BlueprintCallable function should use the Commutative Associative Binary node. This node lacks pin names, but features an Add Pin button that creates additional input pins.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class CommutativeAssociativeBinaryOperatorAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [CompactNodeTitle]
|
||||
/// Indicates that a BlueprintCallable function should display in the compact display mode, and provides the name to display in that mode.
|
||||
/// </summary>
|
||||
/// <param name="CompactNodeTitle">Name</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class CompactNodeTitleAttribute(string CompactNodeTitle) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [CustomStructureParam]
|
||||
/// The listed parameters are all treated as wildcards. This specifier requires the UFUNCTION-level specifier, CustomThunk, which will require the user to provide a custom exec function. In this function, the parameter types can be checked and the appropriate function calls can be made based on those parameter types. The base UFUNCTION should never be called, and should assert or log an error if it is. To declare a custom exec function, use the syntax DECLARE_FUNCTION(execMyFunctionName) where MyFunctionName is the name of the original function.
|
||||
/// </summary>
|
||||
/// <param name="CustomStructureParam">Parameter1, Parameter2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class CustomStructureParamAttribute(string CustomStructureParam) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [DefaultToSelf]
|
||||
/// For BlueprintCallable functions, this indicates that the Object property's named default value should be the self context of the node.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class DefaultToSelfAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [DeprecatedFunction]
|
||||
/// Any Blueprint references to this function will cause compilation warnings telling the user that the function is deprecated. You can add to the deprecation warning message (for example, to provide instructions on replacing the deprecated function) using the DeprecationMessage metadata specifier.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class DeprecatedFunctionAttribute : Attribute { }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [DeterminesOutputType]
|
||||
/// The return type of the function will dynamically change to match the input that is connected to the named parameter pin. The parameter should be a templated type like TSubClassOf<X> or TSoftObjectPtr<X>, where the function's original return type is X* or a container with X* as the value type, such as TArray<X*>.
|
||||
/// </summary>
|
||||
/// <param name="DeterminesOutputType">Parameter</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class DeterminesOutputTypeAttribute(string DeterminesOutputType) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [DevelopmentOnly]
|
||||
/// Functions marked as DevelopmentOnly will only run in Development mode. This is useful for functionality like debug output, which is expected not to exist in shipped products.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class DevelopmentOnlyAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ExpandEnumAsExecs("enum")]
|
||||
/// For BlueprintCallable functions, this indicates that one input execution pin should be created for each entry in the enum used by the parameter.
|
||||
/// The parameter must be of an enumerated type that has the UENUM tag and be set as an out parameter on the function
|
||||
/// </summary>
|
||||
/// <param name="ExpandEnumAsExecs">name of out enum parameter to use</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class ExpandEnumAsExecsAttribute(string ExpandEnumAsExecs) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ForceAsFunction]
|
||||
/// Change a BlueprintImplementableEvent with no return value from an event into a function.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class ForceAsFunctionAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [HidePin]
|
||||
/// For BlueprintCallable functions, this indicates that the parameter pin should be hidden from the user's view. Only one pin per function can be hidden in this manner.
|
||||
/// </summary>
|
||||
/// <param name="HidePin">Parameter</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class HidePinAttribute(string HidePin) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [HideSelfPin]
|
||||
/// Hides the "self" pin, which indicates the object on which the function is being called. The "self" pin is automatically hidden on BlueprintPure functions that are compatible with the calling Blueprint's Class. Functions that use the HideSelfPin Meta Tag frequently also use the DefaultToSelf Specifier.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class HideSelfPinAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [InternalUseParam]
|
||||
/// Similar to HidePin, this hides the named parameter's pin from the user's view, and can only be used for one parameter per function.
|
||||
/// </summary>
|
||||
/// <param name="InternalUseParam">Parameter</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class InternalUseParamAttribute(string InternalUseParam) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [KeyWords]
|
||||
/// Specifies a set of keywords that can be used when searching for this function, such as when placing a node to call the function in a Blueprint Graph.
|
||||
/// </summary>
|
||||
/// <param name="KeyWords">Set Of Keywords</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class KeyWordsAttribute(string KeyWords) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [Latent]
|
||||
/// Indicates a latent action. Latent actions have one parameter of type FLatentActionInfo, and this parameter is named by the LatentInfo specifier.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class LatentAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [LatentInfo]
|
||||
/// For Latent BlueprintCallable functions, indicates which parameter is the LatentInfo parameter.
|
||||
/// </summary>
|
||||
/// <param name="LatentInfo">Parameter</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class LatentInfoAttribute(string LatentInfo) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [MaterialParameterCollectionFunction]
|
||||
/// For BlueprintCallable functions, indicates that the material override node should be used.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class MaterialParameterCollectionFunctionAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [NativeBreakFunc]
|
||||
/// For BlueprintCallable functions, indicates that the function should be displayed the same way as a standard Break Struct node.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class NativeBreakFuncAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [NotBlueprintThreadSafe]
|
||||
/// Only valid in Blueprint function libraries. This function will be treated as an exception to the owning Class's general BlueprintThreadSafe metadata.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class NotBlueprintThreadSafeAttribute : Attribute { }
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [UnsafeDuringActorConstruction]
|
||||
/// This function is not safe to call during Actor construction.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class UnsafeDuringActorConstructionAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [WorldContext]
|
||||
/// Used by BlueprintCallable functions to indicate which parameter determines the World in which the operation takes place.
|
||||
/// </summary>
|
||||
/// <param name="WorldContext">Parameter</param>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class WorldContextAttribute(string WorldContext) : Attribute { }
|
||||
#endregion
|
||||
|
||||
|
||||
//----------------------------------------------------
|
||||
// Property Metadata Specifiers
|
||||
//----------------------------------------------------
|
||||
#region Property
|
||||
/// <summary>
|
||||
/// [AllowAbstract]
|
||||
/// Used for Subclass and SoftClass properties. Indicates whether abstract Class types should be shown in the Class picker.
|
||||
/// </summary>
|
||||
/// <param name="AllowAbstract">true/false</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
|
||||
public sealed class AllowAbstractAttribute(string AllowAbstract) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [AllowedClasses]
|
||||
/// Used for FSoftObjectPath properties. Comma delimited list that indicates the Class type(s) of assets to be displayed in the Asset picker.
|
||||
/// </summary>
|
||||
/// <param name="AllowedClasses">Class1, Class2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
|
||||
public sealed class AllowedClassesAttribute(string AllowedClasses) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [MustImplement]
|
||||
/// Limits the classes selectable in blueprint from TSoftClassPtr or TSubclassOf to only those that implement the named
|
||||
/// interfaces
|
||||
/// </summary>
|
||||
/// <param name="RequiredInterface">Interface1, Interface2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
|
||||
public sealed class MustImplementAttribute(string RequiredInterface) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [AllowPreserveRatio]
|
||||
/// Used for FVector properties. It causes a ratio lock to be added when displaying this property in details panels.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class AllowPreserveRatioAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ArrayClamp]
|
||||
/// Used for integer properties. Clamps the valid values that can be entered in the UI to be between 0 and the length of the array property named.
|
||||
/// </summary>
|
||||
/// <param name="ArrayClamp">ArrayProperty</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class ArrayClampAttribute(string ArrayClamp) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [AssetBundles]
|
||||
/// Used for SoftObjectPtr or SoftObjectPath properties. List of Bundle names used inside Primary Data Assets to specify which Bundles this reference is part of.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class AssetBundlesAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BlueprintBaseOnly]
|
||||
/// Used for Subclass and SoftClass properties. Indicates whether only Blueprint Classes should be shown in the Class picker.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class BlueprintBaseOnlyAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BlueprintCompilerGeneratedDefaults]
|
||||
/// Property defaults are generated by the Blueprint compiler and will not be copied when the CopyPropertiesForUnrelatedObjects function is called post-compile.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class BlueprintCompilerGeneratedDefaultsAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ClampMin]
|
||||
/// Used for float and integer properties. Specifies the minimum value N that may be entered for the property.
|
||||
/// </summary>
|
||||
/// <param name="ClampMin">N</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class ClampMinAttribute(string ClampMin) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ClampMax]
|
||||
/// Used for float and integer properties. Specifies the maximum value N that may be entered for the property.
|
||||
/// </summary>
|
||||
/// <param name="ClampMax">N</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class ClampMaxAttribute(string ClampMax) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [UIMin]
|
||||
/// Used for float and integer properties. Specifies the lowest that the value slider should represent.
|
||||
/// </summary>
|
||||
/// <param name="ClampMin">N</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class UIMinAttribute(string UIMin) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [UIMax]
|
||||
/// Used for float and integer properties. Specifies the highest that the value slider should represent.
|
||||
/// </summary>
|
||||
/// <param name="ClampMax">N</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class UIMaxAttribute(string UIMax) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ConfigHierarchyEditable]
|
||||
/// This property is serialized to a config (.ini) file, and can be set anywhere in the config hierarchy.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class ConfigHierarchyEditableAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ContentDir]
|
||||
/// Used by FDirectoryPath properties. Indicates that the path will be picked using the Slate-style directory picker inside the Content folder.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class ContentDirAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [DisplayAfter]
|
||||
/// This property will show up in the Blueprint Editor immediately after the property named PropertyName, regardless of its order in source code, as long as both properties are in the same category. If multiple properties have the same DisplayAfter value and the same DisplayPriority value, they will appear after the named property in the order in which they are declared in the header file.
|
||||
/// </summary>
|
||||
/// <param name="DisplayAfter">PropertyName</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class DisplayAfterAttribute(string DisplayAfter) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [DisplayPriority]
|
||||
/// If two properties feature the same DisplayAfter value, or are in the same category and do not have the DisplayAfter Meta Tag, this property will determine their sorting order. The highest-priority value is 1, meaning that a property with a DisplayPriority value of 1 will appear above a property with a DisplayPriority value of 2. If multiple properties have the same DisplayAfter value, they will appear in the order in which they are declared in the header file.
|
||||
/// </summary>
|
||||
/// <param name="DisplayPriority">N</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class DisplayPriorityAttribute(string DisplayPriority) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [DisplayThumbnail]
|
||||
/// Indicates that the property is an Asset type and it should display the thumbnail of the selected Asset.
|
||||
/// </summary>
|
||||
/// <param name="DisplayThumbnail">true</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class DisplayThumbnailAttribute(string DisplayThumbnail) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [EditCondition]
|
||||
/// Names a boolean property that is used to indicate whether editing of this property is disabled. Putting "!" before the property name inverts the test. The EditCondition meta tag is no longer limited to a single boolean property. It is now evaluated using a full-fledged expression parser, meaning you can include a full C++ expression.
|
||||
/// </summary>
|
||||
/// <param name="EditCondition">BooleanPropertyName</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class EditConditionAttribute(string EditCondition) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [EditConditionHides]
|
||||
/// Paired with EditCondition to hide a property from the view when the condition is false.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class EditConditionHidesAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [EditFixedOrder]
|
||||
/// Keeps the elements of an array from being reordered by dragging.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class EditFixedOrderAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ExactClass]
|
||||
/// Used for FSoftObjectPath properties in conjunction with AllowedClasses. Indicates whether only the exact Classes specified in AllowedClasses can be used, or if subclasses are also valid.
|
||||
/// </summary>
|
||||
/// <param name="ExactClass">true</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class ExactClassAttribute(string ExactClass) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ExposeFunctionCategories]
|
||||
/// Specifies a list of categories whose functions should be exposed when building a function list in the Blueprint Editor.
|
||||
/// </summary>
|
||||
/// <param name="ExposeFunctionCategories">Category1, Category2, ..</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class ExposeFunctionCategoriesAttribute(string ExposeFunctionCategories) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [ExposeOnSpawn]
|
||||
/// Specifies whether the property should be exposed on a Spawn Actor node for this Class type.
|
||||
/// </summary>
|
||||
/// <param name="ExposeOnSpawn">true</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class ExposeOnSpawnAttribute(string ExposeOnSpawn) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [FilePathFilter]
|
||||
/// Used by FFilePath properties. Indicates the path filter to display in the file picker. Common values include "uasset" and "umap", but these are not the only possible values.
|
||||
/// </summary>
|
||||
/// <param name="FilePathFilter">FileType</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class FilePathFilterAttribute(string FilePathFilter) : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [GetByRef]
|
||||
/// Makes the "Get" Blueprint Node for this property return a const reference to the property instead of a copy of its value. Only usable with Sparse Class Data, and only when NoGetter is not present.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class GetByRefAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [HideAlphaChannel]
|
||||
/// Used for FColor and FLinearColor properties. Indicates that the Alpha property should be hidden when displaying the property widget in the details.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class HideAlphaChannelAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [HideViewOptions]
|
||||
/// Used for Subclass and SoftClass properties. Hides the ability to change view options in the Class picker.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class HideViewOptionsAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [InlineEditConditionToggle]
|
||||
/// Signifies that the boolean property is only displayed inline as an edit condition toggle in other properties, and should not be shown on its own row.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class InlineEditConditionToggleAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [LongPackageName]
|
||||
/// Used by FDirectoryPath properties. Converts the path to a long package name.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class LongPackageNameAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [MakeEditWidget]
|
||||
/// Used for Transform or Rotator properties, or Arrays of Transforms or Rotators. Indicates that the property should be exposed in the viewport as a movable widget.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class MakeEditWidgetAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [NoGetter]
|
||||
/// Causes Blueprint generation not to generate a "get" Node for this property. Only usable with Sparse Class Data.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class NoGetterAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BindWidget]
|
||||
/// Used for UWidget properties. Indicates that the property should be bound to a widget in the Blueprint Editor.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class BindWidgetAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BindWidgetOptional]
|
||||
/// Used for UWidget properties. Indicates that the property should be bound to a widget in the Blueprint Editor.
|
||||
// Will not cause an error if not found.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class BindWidgetOptionalAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [BindWidgetAnim]
|
||||
/// Used for binding widget animations to a property
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public sealed class BindWidgetAnimAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// [CategoriesPermalink]
|
||||
/// This allows you to limit which gameplay tags are allowed to be chosen for a FGameplayTag property. Multiple tags
|
||||
/// can be specified with commas separating them.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
|
||||
public sealed class CategoriesAttribute(string Categories) : Attribute { }
|
||||
|
||||
#endregion
|
||||
@ -0,0 +1,126 @@
|
||||
namespace UnrealSharp.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class)]
|
||||
class ClassFlagsMapAttribute(NativeClassFlags flags = NativeClassFlags.None) : Attribute
|
||||
{
|
||||
public NativeClassFlags Flags = flags;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ClassFlags : ulong
|
||||
{
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.None)]
|
||||
None = 0x00000000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Abstract)]
|
||||
Abstract = 0x00000001u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.DefaultConfig)]
|
||||
DefaultConfig = 0x00000002u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Config)]
|
||||
Config = 0x00000004u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Transient)]
|
||||
Transient = 0x00000008u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Optional)]
|
||||
Optional = 0x00000010u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.MatchedSerializers)]
|
||||
MatchedSerializers = 0x00000020u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.ProjectUserConfig)]
|
||||
ProjectUserConfig = 0x00000040u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Native)]
|
||||
Native = 0x00000080u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.NoExport)]
|
||||
NoExport = 0x00000100u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.NotPlaceable)]
|
||||
NotPlaceable = 0x00000200u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.PerObjectConfig)]
|
||||
PerObjectConfig = 0x00000400u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.ReplicationDataIsSetUp)]
|
||||
ReplicationDataIsSetUp = 0x00000800u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.EditInlineNew)]
|
||||
EditInlineNew = 0x00001000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.CollapseCategories)]
|
||||
CollapseCategories = 0x00002000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Interface)]
|
||||
Interface = 0x00004000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.CustomConstructor)]
|
||||
CustomConstructor = 0x00008000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Const)]
|
||||
Const = 0x00010000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.NeedsDeferredDependencyLoading)]
|
||||
NeedsDeferredDependencyLoading = 0x00020000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.CompiledFromBlueprint)]
|
||||
CompiledFromBlueprint = 0x00040000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.MinimalAPI)]
|
||||
MinimalAPI = 0x00080000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.RequiredAPI)]
|
||||
RequiredAPI = 0x00100000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.DefaultToInstanced)]
|
||||
DefaultToInstanced = 0x00200000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.TokenStreamAssembled)]
|
||||
TokenStreamAssembled = 0x00400000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.HasInstancedReference)]
|
||||
HasInstancedReference= 0x00800000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Hidden)]
|
||||
Hidden = 0x01000000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Deprecated)]
|
||||
Deprecated = 0x02000000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.HideDropDown)]
|
||||
HideDropDown = 0x04000000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.GlobalUserConfig)]
|
||||
GlobalUserConfig = 0x08000000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Intrinsic)]
|
||||
Intrinsic = 0x10000000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.Constructed)]
|
||||
Constructed = 0x20000000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.ConfigDoNotCheckDefaults)]
|
||||
ConfigDoNotCheckDefaults = 0x40000000u,
|
||||
|
||||
[ClassFlagsMapAttribute(NativeClassFlags.NewerVersionExists)]
|
||||
NewerVersionExists = 0x80000000u,
|
||||
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
[ClassFlagsMap]
|
||||
public sealed class UClassAttribute(ClassFlags flags = ClassFlags.None) : BaseUAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The flags of the class.
|
||||
/// </summary>
|
||||
public ClassFlags Flags = flags;
|
||||
|
||||
/// <summary>
|
||||
/// The category of the config file to use for this class.
|
||||
/// </summary>
|
||||
public string ConfigCategory;
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace UnrealSharp.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Enum)]
|
||||
public sealed class UEnumAttribute : BaseUAttribute;
|
||||
@ -0,0 +1,59 @@
|
||||
namespace UnrealSharp.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Property)]
|
||||
class FunctionFlagsMapAttribute(NativeFunctionFlags flags = NativeFunctionFlags.None) : Attribute
|
||||
{
|
||||
public NativeFunctionFlags Flags = flags;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum FunctionFlags : ulong
|
||||
{
|
||||
[FunctionFlagsMap]
|
||||
None = 0x00000000,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.BlueprintCallable)]
|
||||
BlueprintCallable = NativeFunctionFlags.BlueprintCallable,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.BlueprintAuthorityOnly)]
|
||||
BlueprintAuthorityOnly = NativeFunctionFlags.BlueprintAuthorityOnly,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.BlueprintCosmetic)]
|
||||
BlueprintCosmetic = NativeFunctionFlags.BlueprintCosmetic,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.BlueprintPure)]
|
||||
BlueprintPure = NativeFunctionFlags.BlueprintPure,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.BlueprintNativeEvent)]
|
||||
BlueprintEvent = NativeFunctionFlags.BlueprintNativeEvent,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.NetMulticast)]
|
||||
Multicast = NativeFunctionFlags.NetMulticast,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.NetServer)]
|
||||
RunOnServer = NativeFunctionFlags.NetServer,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.NetClient)]
|
||||
RunOnClient = NativeFunctionFlags.NetClient,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.NetReliable)]
|
||||
Reliable = NativeFunctionFlags.NetReliable,
|
||||
|
||||
[FunctionFlagsMap(NativeFunctionFlags.Exec)]
|
||||
Exec = NativeFunctionFlags.Exec,
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
[FunctionFlagsMap(NativeFunctionFlags.Native)]
|
||||
public sealed class UFunctionAttribute(FunctionFlags flags = FunctionFlags.None) : BaseUAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The flags of the function.
|
||||
/// </summary>
|
||||
public FunctionFlags Flags = flags;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the function can be called from an instance of the class in the editor.
|
||||
/// </summary>
|
||||
public bool CallInEditor = false;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
namespace UnrealSharp.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Interface)]
|
||||
public sealed class UInterfaceAttribute : BaseUAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, the interface cannot be implemented in a blueprint.
|
||||
/// </summary>
|
||||
public bool CannotImplementInterfaceInBlueprint = false;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
namespace UnrealSharp.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// [UMetaData("key", "value")]
|
||||
/// Directly set the key and value for your MetaData
|
||||
/// Note: There are specific MetaTags available (e.g. [HideSelfPin]) to avoid setting via magic strings
|
||||
/// but this allows full control to add any new or missing key
|
||||
/// https://dev.epicgames.com/documentation/en-us/unreal-engine/metadata-specifiers-in-unreal-engine
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple=true)]
|
||||
public sealed class UMetaDataAttribute : Attribute
|
||||
{
|
||||
public UMetaDataAttribute(string key, string value = "")
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace UnrealSharp.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Delegate)]
|
||||
public class UMultiDelegateAttribute : Attribute;
|
||||
@ -0,0 +1,131 @@
|
||||
namespace UnrealSharp.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class)]
|
||||
class PropertyFlagsMapAttribute(NativePropertyFlags flags = NativePropertyFlags.None) : Attribute
|
||||
{
|
||||
public NativePropertyFlags Flags = flags;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum PropertyFlags : ulong
|
||||
{
|
||||
[PropertyFlagsMap]
|
||||
None = 0,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.Config)]
|
||||
Config = NativePropertyFlags.Config,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.InstancedReference)]
|
||||
Instanced = NativePropertyFlags.PersistentInstance,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.ExportObject)]
|
||||
Export = NativePropertyFlags.ExportObject,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.NoClear)]
|
||||
NoClear = NativePropertyFlags.NoClear,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.EditFixedSize)]
|
||||
EditFixedSize = NativePropertyFlags.EditFixedSize,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.SaveGame)]
|
||||
SaveGame = NativePropertyFlags.SaveGame,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.BlueprintReadOnly)]
|
||||
BlueprintReadOnly = NativePropertyFlags.BlueprintReadOnly | NativePropertyFlags.BlueprintVisible,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.BlueprintReadWrite)]
|
||||
BlueprintReadWrite = NativePropertyFlags.BlueprintReadWrite,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.Net)]
|
||||
Replicated = NativePropertyFlags.Net,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.EditDefaultsOnly)]
|
||||
EditDefaultsOnly = NativePropertyFlags.EditDefaultsOnly,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.EditInstanceOnly)]
|
||||
EditInstanceOnly = NativePropertyFlags.EditInstanceOnly,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.EditAnywhere)]
|
||||
EditAnywhere = NativePropertyFlags.EditAnywhere,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.BlueprintAssignable)]
|
||||
BlueprintAssignable = NativePropertyFlags.BlueprintAssignable | BlueprintReadOnly,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.BlueprintCallable)]
|
||||
BlueprintCallable = NativePropertyFlags.BlueprintCallable,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.VisibleAnywhere)]
|
||||
VisibleAnywhere = NativePropertyFlags.VisibleAnywhere,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.VisibleDefaultsOnly)]
|
||||
VisibleDefaultsOnly = NativePropertyFlags.VisibleDefaultsOnly,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.VisibleInstanceOnly)]
|
||||
VisibleInstanceOnly = NativePropertyFlags.VisibleInstanceOnly,
|
||||
|
||||
[PropertyFlagsMap(NativePropertyFlags.Transient)]
|
||||
Transient = NativePropertyFlags.Transient,
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
[PropertyFlagsMap]
|
||||
public sealed class UPropertyAttribute(PropertyFlags flags = PropertyFlags.None) : BaseUAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// These flags determine how the property is handled in the engine.
|
||||
/// </summary>
|
||||
public PropertyFlags Flags = flags;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this property is a component that should be automatically initialized as a default subobject of the Actor.
|
||||
/// Works only on properties of type ActorComponent.
|
||||
/// </summary>
|
||||
public bool DefaultComponent = false;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether this component is the root component of the Actor. If multiple components are marked as root, only the first
|
||||
/// one encountered in the order of declaration will be used as the root.
|
||||
/// </summary>
|
||||
public bool RootComponent = false;
|
||||
|
||||
/// <summary>
|
||||
/// The name of another component to which this component should be attached. Specify the variable name of the
|
||||
/// target component as a string.
|
||||
/// </summary>
|
||||
public string AttachmentComponent = "";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the socket on the parent component to which this component should be attached.
|
||||
/// </summary>
|
||||
public string AttachmentSocket = "";
|
||||
|
||||
/// <summary>
|
||||
/// The callback function used when the property value changes on the server. Declaring this function
|
||||
/// automatically enables replication for this property, so it's not necessary to explicitly mark it as replicated.
|
||||
/// To use this, assign the name of the callback function to 'ReplicatedUsing'. For example:
|
||||
/// ReplicatedUsing = "OnRep_PropertyName";
|
||||
/// where "OnRep_PropertyName" is the method that will be called whenever the property is updated on clients.
|
||||
///
|
||||
/// Acceptable method signatures:
|
||||
/// void OnRep_PropertyName() {}
|
||||
/// void OnRep_PropertyName(int oldValue) {}
|
||||
/// </summary>
|
||||
public string ReplicatedUsing = "";
|
||||
|
||||
/// <summary>
|
||||
/// The condition for the lifetime of the property.
|
||||
/// </summary>
|
||||
public LifetimeCondition LifetimeCondition = LifetimeCondition.None;
|
||||
|
||||
/// <summary>
|
||||
/// The function to call when the property is changed.
|
||||
/// </summary>
|
||||
public string BlueprintSetter = "";
|
||||
|
||||
/// <summary>
|
||||
/// The function to call when the property is getting accessed.
|
||||
/// </summary>
|
||||
public string BlueprintGetter = "";
|
||||
|
||||
public int ArrayDim = 1;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user