Lua向C#逻辑迁移 一期 #13

将整个插件代码上传
This commit is contained in:
2025-10-26 21:48:39 +08:00
parent 56994b3927
commit 648386cd73
785 changed files with 53683 additions and 2 deletions

Submodule Plugins/UnrealSharp deleted from a5e501bc43

2
Plugins/UnrealSharp/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

84
Plugins/UnrealSharp/.gitignore vendored Normal file
View File

@ -0,0 +1,84 @@
# Visual Studio 2015 user specific files
.vs/
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
*.ipa
# These project files can be generated by the engine
*.xcodeproj
*.xcworkspace
*.suo
*.opensdf
*.sdf
*.VC.db
*.VC.opendb
# Precompiled Assets
SourceArt/**/*.png
SourceArt/**/*.tga
# Binary Files
Binaries/*
Plugins/*/Binaries/*
# Builds
Build/*
# Whitelist PakBlacklist-<BuildConfiguration>.txt files
!Build/*/
Build/*/**
!Build/*/PakBlacklist*.txt
# Don't ignore icon files in Build
!Build/**/*.ico
# Built data for maps
*_BuiltData.uasset
# Configuration files generated by the Editor
Saved/*
# Compiled source files for the engine to use
Intermediate/*
Plugins/*/Intermediate/*
# Cache files for the editor to use
DerivedDataCache/*
*.user
.idea/
bin/
Generated/
obj/
*.props
!Directory.Packages.props

View File

@ -0,0 +1,117 @@
{
"Structs": {
"CustomTypes": [
"FInstancedStruct"
],
"BlittableTypes": [
{
"Name": "EStreamingSourcePriority"
},
{
"Name": "ETriggerEvent"
},
{
"Name": "FVector",
"ManagedType": "UnrealSharp.CoreUObject.FVector"
},
{
"Name": "FVector2D",
"ManagedType": "UnrealSharp.CoreUObject.FVector2D"
},
{
"Name": "FVector_NetQuantize",
"ManagedType": "UnrealSharp.CoreUObject.FVector"
},
{
"Name": "FVector_NetQuantize10",
"ManagedType": "UnrealSharp.CoreUObject.FVector"
},
{
"Name": "FVector_NetQuantize100",
"ManagedType": "UnrealSharp.CoreUObject.FVector"
},
{
"Name": "FVector_NetQuantizeNormal",
"ManagedType": "UnrealSharp.CoreUObject.FVector"
},
{
"Name": "FVector2f",
"ManagedType": "UnrealSharp.CoreUObject.FVector2f"
},
{
"Name": "FVector3f",
"ManagedType": "UnrealSharp.CoreUObject.FVector3f"
},
{
"Name": "FVector4f",
"ManagedType": "UnrealSharp.CoreUObject.FVector4f"
},
{
"Name": "FQuatf4",
"ManagedType": "UnrealSharp.CoreUObject.FVector4f"
},
{
"Name": "FRotator",
"ManagedType": "UnrealSharp.CoreUObject.FRotator"
},
{
"Name": "FMatrix44f",
"ManagedType": "UnrealSharp.CoreUObject.FMatrix44f"
},
{
"Name": "FTransform",
"ManagedType": "UnrealSharp.CoreUObject.FTransform"
},
{
"Name": "FTimerHandle",
"ManagedType": "UnrealSharp.Engine.FTimerHandle"
},
{
"Name": "FInputActionValue",
"ManagedType": "UnrealSharp.EnhancedInput.FInputActionValue"
},
{
"Name": "FRandomStream",
"ManagedType": "UnrealSharp.CoreUObject.FRandomStream"
},
{
"Name": "FSubsystemCollectionBaseRef",
"ManagedType": "UnrealSharp.UnrealSharpCore.FSubsystemCollectionBaseRef"
}
],
"NativelyTranslatableTypes": [
{
"Name": "FMoverDataCollection",
"HasDestructor": false
},
{
"Name": "FPaintContext",
"HasDestructor": false
},
{
"Name": "FGeometry",
"HasDestructor": false
},
{
"Name": "FGameplayEffectSpec",
"HasDestructor": true
},
{
"Name": "FGameplayEffectSpecHandle",
"HasDestructor": true
},
{
"Name": "FKeyEvent",
"HasDestructor": false
},
{
"Name": "FInputEvent",
"HasDestructor": false
},
{
"Name": "FKey",
"HasDestructor": false
}
]
}
}

View File

@ -0,0 +1,5 @@
[CoreRedirects]
+ClassRedirects=(OldName="/Script/CSharpForUE.CSClass",NewName="/Script/UnrealSharpCore.CSClass")
+StructRedirects=(OldName="/Script/CSharpForUE.CSScriptStruct",NewName="/Script/UnrealSharpCore.CSScriptStruct")
+EnumRedirects=(OldName="/Script/CSharpForUE.CSEnum",NewName="/Script/UnrealSharpCore.CSEnum")
+ClassRedirects=(OldName="/Script/UnrealSharpCore.CSDeveloperSettings",NewName="/Script/UnrealSharpCore.CSUnrealSharpSettings")

View File

@ -0,0 +1,8 @@
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll

View File

@ -0,0 +1,18 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="LanguageExt.Core" Version="4.4.9" />
<PackageVersion Include="Mono.Cecil" Version="0.11.6" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.13.9" />
<PackageVersion Include="Microsoft.Build" Version="17.13.9" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.13.0" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.Build.Locator" Version="1.9.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 UnrealSharp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -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__

View 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__

View 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__

View 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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,4 @@
namespace UnrealSharp.Binds;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class NativeCallbacksAttribute : Attribute;

View File

@ -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>

View File

@ -0,0 +1,3 @@
namespace UnrealSharp.Core.Attributes;
public class BindingAttribute : Attribute;

View File

@ -0,0 +1,4 @@
namespace UnrealSharp.Core.Attributes;
[AttributeUsage(AttributeTargets.Struct)]
public class BlittableTypeAttribute : Attribute;

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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)!;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
using UnrealSharp.Binds;
namespace UnrealSharp.Interop;
[NativeCallbacks]
public unsafe partial class FStringExporter
{
public static delegate* unmanaged<IntPtr, char*, void> MarshalToNativeString;
}

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
using UnrealSharp.Log;
namespace UnrealSharp.Core;
[CustomLog]
public static partial class LogUnrealSharpCore;

View File

@ -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,
};
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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)!;
}
}

View File

@ -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();
}
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,7 @@
namespace UnrealSharp.Engine.Core.Modules;
public interface IModuleInterface
{
public void StartupModule();
public void ShutdownModule();
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
using UnrealSharp.Log;
namespace UnrealSharp.Editor;
[CustomLog]
public static partial class LogUnrealSharpEditor;

View File

@ -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>

View File

@ -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()
{
}
}

View File

@ -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));
});
}
}

View File

@ -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>

View File

@ -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(" }");
}
}

View File

@ -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(" }");
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}

View File

@ -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>

View File

@ -0,0 +1,7 @@
namespace UnrealSharp.Log;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CustomLog(ELogVerbosity verbosity = ELogVerbosity.Display) : Attribute
{
private ELogVerbosity _verbosity = verbosity;
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
using UnrealSharp.Log;
namespace UnrealSharp.Plugins;
[CustomLog]
public static partial class LogUnrealSharpPlugins;

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,
};
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

View File

@ -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

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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()));
}
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}

View File

@ -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
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
namespace UnrealSharp.SourceGenerators;
public enum DelegateType
{
Multicast,
Single,
}

View File

@ -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
};
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Generators": {
"commandName": "DebugRoslynComponent",
"targetProject": "../UnrealSharp/UnrealSharp.csproj"
}
}
}

View File

@ -0,0 +1,7 @@
namespace UnrealSharp.SourceGenerators;
internal static class RuleCategory
{
public const string Naming = nameof(Naming);
public const string Category = nameof(Category);
}

View File

@ -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("}");
}
}
}

View File

@ -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);");
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,14 @@
namespace UnrealSharp.Attributes;
public class BaseUAttribute : Attribute
{
/// <summary>
/// The display name of the type, used in the editor.
/// </summary>
public string DisplayName = "";
/// <summary>
/// The category of the type, used in the editor.
/// </summary>
public string Category = "";
}

Some files were not shown because too many files have changed in this diff Show More