Compare commits

...

9 Commits

Author SHA1 Message Date
60596f2772 Merge pull request 'CSharpMigrate' (#15) from CSharpMigrate into main
Reviewed-on: #15
2025-10-26 21:51:42 +08:00
648386cd73 Lua向C#逻辑迁移 一期 #13
将整个插件代码上传
2025-10-26 21:48:39 +08:00
56994b3927 Lua向C#逻辑迁移 一期 #13
瓦片地图实现修改
2025-10-26 19:01:10 +08:00
8c0623b397 Lua向C#逻辑迁移 一期 #13
狐狸移动,技能迁移和完善
2025-10-26 15:46:04 +08:00
dab9990b44 Lua向C#逻辑迁移 一期
创建了若干C#文件,替代对应lua文件
2025-10-25 05:53:20 +08:00
fc1078f20c 成功编译C#插件 #12
添加了C#二进制相关文件的忽略
2025-10-25 05:22:01 +08:00
580e7cf72b 移除slua插件,升级UE5.6 #11
额外移除emmylua的配置
2025-10-25 04:21:55 +08:00
367ac0bab7 移除slua插件,升级UE5.6 #11 2025-10-25 04:15:58 +08:00
4ae803b4f2 将直接继承于slua的蓝图父类修改为UE对应的蓝图父类 #10 2025-10-25 03:21:43 +08:00
1133 changed files with 54772 additions and 169169 deletions

View File

@ -1,93 +0,0 @@
{
"completion": {
"enable": true,
"autoRequire": true,
"autoRequireFunction": "require",
"autoRequireNamingConvention": "keep",
"autoRequireSeparator": ".",
"callSnippet": false,
"postfix": "@",
"baseFunctionIncludesName": true
},
"diagnostics": {
"disable": [
"unnecessary-if"
],
"enable": true,
"globals": [],
"globalsRegex": [],
"severity": {},
"enables": [],
"diagnosticInterval": 500
},
"signature": {
"detailSignatureHelper": true
},
"hint": {
"enable": true,
"paramHint": true,
"indexHint": true,
"localHint": true,
"overrideHint": true,
"metaCallHint": true
},
"runtime": {
"version": "LuaLatest",
"requireLikeFunction": [],
"frameworkVersions": [],
"extensions": [],
"requirePattern": [],
"classDefaultCall": {
"functionName": "",
"forceNonColon": false,
"forceReturnSelf": false
}
},
"workspace": {
"ignoreDir": [],
"ignoreGlobs": [],
"library": [],
"workspaceRoots": [],
"preloadFileSize": 0,
"encoding": "utf-8",
"moduleMap": [],
"reindexDuration": 5000,
"enableReindex": false
},
"resource": {
"paths": []
},
"codeLens": {
"enable": true
},
"strict": {
"requirePath": false,
"typeCall": false,
"arrayIndex": true,
"metaOverrideFileDefine": true,
"docBaseConstMatchBaseType": true
},
"semanticTokens": {
"enable": true
},
"references": {
"enable": true,
"fuzzySearch": true,
"shortStringSearch": false
},
"hover": {
"enable": true
},
"documentColor": {
"enable": true
},
"codeAction": {
"insertSpace": false
},
"inlineValues": {
"enable": true
},
"doc": {
"privateName": []
}
}

12
.gitignore vendored
View File

@ -77,3 +77,15 @@ Plugins/**/Intermediate/*
DerivedDataCache/*
BusyRabbit.uproject.DotSettings.user
Plugins/SpinePlugin/Script/SpinePlugin.Glue
Script/ManagedBusyRabbit/bin
Script/ManagedBusyRabbit/obj
Script/ManagedBusyRabbit/Properties
Script/BusyRabbit.Glue/bin
Script/BusyRabbit.Glue/BusyRabbit
Script/BusyRabbit.Glue/obj
Script/BusyRabbit.Glue/Properties
Script/BusyRabbit.Glue/AssetIds.cs
Script/BusyRabbit.Glue/GameplayTags.cs
Script/BusyRabbit.Glue/TraceChannel.cs

View File

@ -1,6 +1,6 @@
{
"FileVersion": 3,
"EngineAssociation": "5.4",
"EngineAssociation": "5.6",
"Category": "",
"Description": "",
"Modules": [
@ -9,7 +9,6 @@
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"slua_unreal",
"Engine",
"CoreUObject",
"GameplayAbilities",

View File

@ -5,13 +5,24 @@ ClearInvalidTags=False
AllowEditorTagUnloading=True
AllowGameTagUnloading=False
FastReplication=False
bDynamicReplication=False
InvalidTagCharacters="\"\',"
NumBitsForContainerSize=6
NetIndexFirstBitSegment=16
+GameplayTagList=(Tag="Ability.Block.UltimatePlaying",DevComment="大招正在释放中")
+GameplayTagList=(Tag="Ability.Common",DevComment="通用技能")
+GameplayTagList=(Tag="Ability.Common.Move",DevComment="移动技能")
+GameplayTagList=(Tag="Ability.Flags.Cooldown",DevComment="技能冷却")
+GameplayTagList=(Tag="Ability.Flags.Recast",DevComment="可以再次释放技能的标记")
+GameplayTagList=(Tag="Ability.Fox",DevComment="狐狸技能标签")
+GameplayTagList=(Tag="Ability.Fox.CastTag",DevComment="可以释放技能的标签")
+GameplayTagList=(Tag="Ability.Fox.CastTag.Ultimate1",DevComment="可以使用大招第一阶段")
+GameplayTagList=(Tag="Ability.Fox.CastTag.Ultimate2",DevComment="可以使用二阶大招")
+GameplayTagList=(Tag="Ability.Fox.CastTag.Ultimate3",DevComment="可以使用第三阶段大招")
+GameplayTagList=(Tag="Ability.Fox.StateTag",DevComment="状态标签")
+GameplayTagList=(Tag="Ability.Fox.StateTag.Ultimate1",DevComment="正在使用第一阶段大招")
+GameplayTagList=(Tag="Ability.Fox.StateTag.Ultimate2",DevComment="正在使用第二阶段大招")
+GameplayTagList=(Tag="Ability.Fox.StateTag.Ultimate3",DevComment="正在使用第三阶段大招")
+GameplayTagList=(Tag="Ability.Fox.UltimateStage1",DevComment="一阶大招")
+GameplayTagList=(Tag="Ability.Fox.UltimateStage2",DevComment="二阶大招就绪标签")
+GameplayTagList=(Tag="Ability.Fox.UltimateStage3",DevComment="三阶大招就绪")

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,38 +0,0 @@
---@enum EBusyRoleState
local EBusyRoleState = {
BonfireIdle = 0,
Searching = 1,
Picking = 2,
PickFinished = 3,
BackBonfire = 4
}
---@enum ERoleMoveDirection
local ERoleMoveDirection = {
Move_Right = 0,
Move_Left = 1,
Move_All_Cnt = 2
};
---@enum EBusyItemEffectType
local EBusyItemEffectType = {
Health = 0,
Hunger = 1,
Speed = 2,
};
---@enum EBusyAnimationPhase
local EBusyAnimationPhase = {
PrepareCast = 0,
Casting = 1,
PostCast = 2,
};
---@enum EWidgetLayoutType
local EWidgetLayoutType = {
MainLayer = 0,
PopupLayer = 1,
FloatLayer = 2,
TopLayer = 3,
LayerTypeMax = 4
};

View File

@ -1,4 +0,0 @@
--- @class BusyGameplayLibrary
--- @field K2_GetWorld fun(obj:table):table
--- @field RequestGameplayTag fun(TagName:string):GameplayTag
local BusyGameplayLibrary = {}

View File

@ -1,42 +0,0 @@
-- 这个返回值固定返回的是string我希望当我使用import("ERoleState")时它返回的是ERoleState的注解或者是ERoleState.lua这个文件返回值的注解
-- ---自定义导入函数功能类似require但支持额外特性
-- ---@generic T
-- ---@param modulePath T 模块路径或预加载的模块
-- ---@param hotReload? boolean 是否启用热重载
-- ---@return T 返回加载的模块
-- function import(modulePath, hotReload) end
---自定义模块导入函数,支持类型感知的模块加载
---@generic T : string -- 限定modulePath为字符串类型
---@param modulePath `T` 模块路径(如"ERoleState"
---@param hotReload? boolean 是否启用热重载
---@return T 返回对应模块的类型
---@error 当模块加载失败时抛出错误
function import(modulePath, hotReload) end
function Class(a, b, c) end
---@class slua
slua = {
createDelegate = function(func) end
}
--- @class KismetSystemLibrary
KismetSystemLibrary = {
K2_ClearTimerHandle = function() end
}
--- @class GameplayTag
local GameplayTag
--- @class GameplayStatics
--- @field GetPlayerController fun(uobj:table,idx:number):table
--- @field GetGameState fun(uobj:table):table
local GameplayStatics
--- @class AbilitySystemBlueprintLibrary
--- @field AssignTagSetByCallerMagnitude fun(SpecHandle:table, DataTag:GameplayTag, Magnitude:number)
local AbilitySystemBlueprintLibrary

View File

@ -1,8 +0,0 @@
---@enum ESlateVisibility
local ESlateVisibility = {
Visible = 0,
Collapsed = 1,
Hidden = 2,
HitTestInvisible = 3,
SelfHitTestInvisible = 4
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,222 +0,0 @@
#ifndef LIBPDEBUG_H
#define LIBPDEBUG_H
//1.使用源码编译要打开宏USE_SOURCE_CODE. win下要设置LUA_INTEGER和lua版本号
#define LUA_DEBUGGER_NAME "LuaPanda" //debugger's name in LuaDebug.lua
#define HOOK_LIB_VERSION "3.2.0" //lib version
//#define USE_SOURCE_CODE //using source code to build
#if !defined(USE_SOURCE_CODE) && defined(_WIN32)
#define LUA_INTEGER long long //set LUA_INTEGER. In 501 is ptrdiff_t. 503 can set longlong(64bit) or int(32bit)
#define LUA_VERSION_NUM 503 //lua version used by WIN32 build lib. eg. 501,503
#endif
//setting end
#if !defined(USE_SOURCE_CODE) && defined(_WIN32)
#include <Windows.h>
#include <Tlhelp32.h>
#else
//2.如果lua源码是C++形式注释掉下面extern "C"
extern "C"{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
#include "luaconf.h"
}
#endif
//3.如果lua代码在命名空间中要设置用户命名空间. 防止找不到lua方法
//using namespace slua;
#ifdef USE_SOURCE_CODE
extern "C" void pdebug_init(lua_State* L);
#endif
#if !defined(USE_SOURCE_CODE) && defined(_WIN32)
/*
** Lua - An Extensible Extension Language
** Lua.org, PUC-Rio, Brazil (http://www.lua.org)
** See Copyright Notice at the end of this file
*/
#if LUA_VERSION_NUM == 501
#define lua_getglobal(L,s) lua_getfield(L, LUA_GLOBALSINDEX, (s))
#endif
#define LUA_TNONE (-1)
#define LUA_TNIL 0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8
#define LUA_NUMBER double
#define LUA_REGISTRYINDEX (-10000)
#define LUA_ENVIRONINDEX (-10001)
#define LUA_GLOBALSINDEX (-10002)
#define lua_upvalueindex(i) (LUA_GLOBALSINDEX-(i))
#define LUA_IDSIZE 60
#define LUA_HOOKCALL 0
#define LUA_HOOKRET 1
#define LUA_HOOKLINE 2
#define LUA_HOOKCOUNT 3
#define LUA_HOOKTAILRET 4
#define LUA_MASKCALL (1 << LUA_HOOKCALL)
#define LUA_MASKRET (1 << LUA_HOOKRET)
#define LUA_MASKLINE (1 << LUA_HOOKLINE)
#define LUA_MASKCOUNT (1 << LUA_HOOKCOUNT)
#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL))
#define lua_tostring(L,i) lua_tolstring(L, (i), NULL)
#define lua_istable(L,n) (lua_type(L, (n)) == LUA_TTABLE)
#define lua_isfunction(L,n) (lua_type(L, (n)) == LUA_TFUNCTION)
#define lua_pop(L,n) lua_settop(L, -(n)-1)
#define lua_newtable(L) lua_createtable(L, 0, 0)
struct lua_State;
struct lua_Debug {
int event;
const char *name; /* (n) */
const char *namewhat; /* (n) `global', `local', `field', `method' */
const char *what; /* (S) `Lua', `C', `main', `tail' */
const char *source; /* (S) */
int currentline; /* (l) */
int nups; /* (u) number of upvalues */
int linedefined; /* (S) */
int lastlinedefined; /* (S) */
char short_src[LUA_IDSIZE]; /* (S) */
/* private part */
int i_ci; /* active function */
};
typedef LUA_INTEGER lua_Integer;
typedef LUA_NUMBER lua_Number;
typedef int (*lua_CFunction) (lua_State *L);
typedef struct luaL_Reg {
const char *name;
lua_CFunction func;
} luaL_Reg;
#define LUA_KCONTEXT ptrdiff_t
typedef LUA_KCONTEXT lua_KContext;
//lua function
typedef lua_Integer(*luaDLL_checkinteger) (lua_State *L, int numArg);
typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);
typedef int (*lua_KFunction) (lua_State *L, int status, lua_KContext ctx);
typedef const lua_Number *(*luaDLL_version)(lua_State *L);
typedef void (*luaLDLL_register)(lua_State *L, const char *libname, const luaL_Reg *l);
typedef int (*luaDLL_gettop)(lua_State *L);
typedef const char *(*luaDLL_pushstring)(lua_State *L, const char *s);
typedef int (*luaDLL_settop)(lua_State *L, int idx);
typedef int (*luaDLL_tointeger)(lua_State *L, int idx);
typedef int (*luaDLL_next)(lua_State *L, int idx);
typedef int (*luaDLL_pcall)(lua_State *L, int nargs, int nresults, int errfunc);
typedef void (*luaDLL_pushnil)(lua_State *L);
typedef void (*luaDLL_getfield)(lua_State *L, int idx, const char *k);
typedef int (*luaDLL_getinfo)(lua_State *L, const char *what, void *ar);
typedef void (*luaDLL_pushinteger) (lua_State *L, lua_Integer n);
#if LUA_VERSION_NUM == 501
typedef int(*luaDLL_sethook)(lua_State *L, void* func, int mask, int count);
#else
typedef void (*luaDLL_sethook)(lua_State *L, lua_Hook f, int mask, int count);
#endif
typedef void (*luaDLL_pushnumber)(lua_State *L, lua_Number n);
typedef lua_Number (*luaDLL_checknumber)(lua_State *L, int narg);
typedef const char *(*luaDLL_checklstring)(lua_State *L, int narg, size_t *len);
typedef const char *(*luaDLL_tolstring)(lua_State *L, int idx, size_t *len);
typedef int (*luaDLL_type)(lua_State *L, int idx);
//5.3
typedef void (*luaDLL_createtable)(lua_State *L, int narray, int nrec);
typedef void (*luaDLL_setfuncs)(lua_State *L, const luaL_Reg *l, int nup);
typedef lua_Integer(*luaDLL_tointegerx)(lua_State *L, int idx, int *pisnum);
typedef int (*luaDLL_getglobal)(lua_State *L, const char *name);
typedef int (*luaDLL_pcallk)(lua_State *L, int nargs, int nresults, int msgh, lua_KContext ctx, lua_KFunction k);
typedef int (*luaDLL_toboolean)(lua_State *L, int index);
luaDLL_checkinteger luaL_checkinteger;
luaDLL_version lua_version;
luaDLL_gettop lua_gettop;
luaDLL_pushstring lua_pushstring;
luaLDLL_register luaL_register;
luaDLL_settop lua_settop;
luaDLL_pcall lua_pcall;
luaDLL_pushnumber lua_pushnumber;
luaDLL_checklstring luaL_checklstring;
luaDLL_tointeger lua_tointeger;
luaDLL_pushnil lua_pushnil;
luaDLL_getfield lua_getfield;
luaDLL_next lua_next;
luaDLL_getinfo lua_getinfo;
luaDLL_sethook lua_sethook;
luaDLL_checknumber luaL_checknumber;
luaDLL_type lua_type;
luaDLL_tolstring lua_tolstring;
luaDLL_pushinteger lua_pushinteger;
luaDLL_toboolean lua_toboolean;
//
HMODULE hInstLibrary;
//slua-ue header
#if LUA_VERSION_NUM > 501
//5.3
luaDLL_createtable lua_createtable;
luaDLL_setfuncs luaL_setfuncs;
luaDLL_tointegerx lua_tointegerx;
luaDLL_getglobal lua_getglobal;
luaDLL_pcallk lua_pcallk;
#define lua_pcall(L,n,r,f) lua_pcallk(L, (n), (r), (f), 0, NULL)
#define lua_tointeger(L,i) lua_tointegerx(L,(i),NULL);
#define PURE_API =0
namespace slua {
struct LuaInterface {
virtual const lua_Number *lua_version(lua_State *L) PURE_API;
virtual const char *lua_pushstring(lua_State *L, const char *s) PURE_API;
virtual int lua_gettop(lua_State *L) PURE_API;
virtual void lua_settop(lua_State *L, int index) PURE_API;
virtual int lua_pcallk(lua_State *L, int nargs, int nresults, int msgh, lua_KContext ctx, lua_KFunction k) PURE_API;
virtual void lua_pushnumber(lua_State *L, lua_Number n) PURE_API;
virtual const char *luaL_checklstring(lua_State *L, int arg, size_t *l) PURE_API;
virtual const char *lua_tolstring(lua_State *L, int index, size_t *len) PURE_API;
virtual int lua_type(lua_State *L, int index) PURE_API;
virtual lua_Integer lua_tointegerx(lua_State *L, int index, int *isnum) PURE_API;
virtual void lua_pushnil(lua_State *L) PURE_API;
virtual int lua_getfield(lua_State *L, int index, const char *k) PURE_API;
virtual int lua_next(lua_State *L, int index) PURE_API;
virtual int lua_getinfo(lua_State *L, const char *what, lua_Debug *ar) PURE_API;
virtual void lua_sethook(lua_State *L, lua_Hook f, int mask, int count) PURE_API;
virtual lua_Number luaL_checknumber(lua_State *L, int arg) PURE_API;
virtual void lua_createtable(lua_State *L, int narr, int nrec) PURE_API;
virtual void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup) PURE_API;
virtual int lua_getglobal(lua_State *L, const char *name) PURE_API;
virtual int lua_toboolean(lua_State *L, int index) PURE_API;
};
}
typedef slua::LuaInterface* (*dll_GetLuaInterface)();
dll_GetLuaInterface getInter;
#endif //LUA_VERSION_NUM > 501
#endif //_WIN32
#endif //LIBPDEBUG_H
/******************************************************************************
* Copyright (C) 1994-2008 Lua.org, PUC-Rio. All rights reserved.
*
* 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

@ -1,4 +0,0 @@
local MoveAbility = {}
return Class(nil, nil, MoveAbility)

View File

@ -1,196 +0,0 @@
local GameplayStatics = import("GameplayStatics")
local AbilitySystemBlueprintLibrary = import("AbilitySystemBlueprintLibrary")
local BusyGameplayLibrary = import("BusyGameplayLibrary")
--- @class FoxUltimate
--- @field RecastWindow number
local FoxUltimate = {}
function FoxUltimate:ctor()
self.ultimate_phase = 1 -- 大招阶段
self.active_recast_handle = nil
self.active_accelerate_handle = nil
self.overlap_delegate_handle = nil
self.tag_add_or_remove_delegate = nil
end
function FoxUltimate:K2_ActivateAbilityFromEvent(EventData)
print("FoxUltimate:K2_ActivateAbilityFromEvent", self.ultimate_phase)
if not self:K2_CommitAbilityCost(false) then
return self:K2_EndAbility()
end
local owner = self:GetOwningActorFromActorInfo()
self.movement = owner.MovementComponent
self.animation = owner.SpineAnimationComponent
self.asc = AbilitySystemBlueprintLibrary.GetAbilitySystemComponent(owner)
self.recast_tag = BusyGameplayLibrary.RequestGameplayTag("Ability.Flags.Recast")
self.owner = owner
local asc = self.asc
if self.tag_add_or_remove_delegate == nil then
self.tag_add_or_remove_delegate = slua.createDelegate(function(tag, is_add)
if is_add == 0 and not self.bIsActive then
self.ultimate_phase = 1
self:K2_CommitAbilityCooldown(false, false)
end
end)
owner:BindGameplayTagAddOrRemove(self.recast_tag, self.tag_add_or_remove_delegate)
end
if self.ultimate_phase == 1 or asc:HasMatchingGameplayTag(self.recast_tag) then
self:TriggerUltimate(asc)
else
self:K2_EndAbility()
end
end
function FoxUltimate:TriggerUltimate(asc)
if self.ultimate_phase == 1 then
-- 第一次释放大招添加一个可以再次释放的Tag
local _, recast_effect = self:GetAbilityEffectSpecHandle("Recast", asc, 1, nil)
self.active_recast_handle = asc:BP_ApplyGameplayEffectSpecToSelf(recast_effect)
elseif self.ultimate_phase == 2 then
-- 第二次激活移除可重复释放的tag
self.asc:RemoveActiveGameplayEffect(self.active_recast_handle, -1)
elseif self.ultimate_phase == 3 then
self.asc:RemoveActiveGameplayEffect(self.active_recast_handle, -1)
end
-- 播放动画,并监听动画完成的事件
self:PlayAnimation()
end
function FoxUltimate:GetAnimationName(direction)
local animation_name_prefix
if direction.X >= 0 then
animation_name_prefix = "Ultimate/Right/"
else
animation_name_prefix = "Ultimate/Left/"
end
if self.ultimate_phase == 1 then
return animation_name_prefix .. "UltimateStage1"
elseif self.ultimate_phase == 2 then
return animation_name_prefix .. "UltimateStage2"
elseif self.ultimate_phase == 3 then
return animation_name_prefix .. "UltimateStage3"
end
return nil
end
function FoxUltimate:K2_OnEndAbility(bWasCancelled)
print("FoxUltimate:K2_OnEndAbility")
end
function FoxUltimate:GetSprintSpeedFactor()
if self.ultimate_phase == 1 then
return 2.5
elseif self.ultimate_phase == 2 then
return 2.8
elseif self.ultimate_phase == 3 then
return 3.0
end
end
function FoxUltimate:PlayAnimation()
local animation = self.animation
local PC = GameplayStatics.GetPlayerController(self, 0)
local result, ability_direction = PC:GetCursorDirection(nil) -- 获取技能的朝向
if not result then
print("FoxUltimate:TriggerPrimaryPhase can't find direction")
return
end
local anim_name = self:GetAnimationName(ability_direction) -- 获取技能动画名称
if not anim_name then
print("FoxUltimate:TriggerPrimaryPhase can't get animation", self.ultimate_phase)
return
end
local anim_entry = animation:SetAnimation(0, anim_name, false)
anim_entry.AnimationComplete:Add(function(entry) self:OnAnimationComplete(entry) end)
anim_entry.AnimationEvent:Add(function(entry, event) self:OnAnimationEvent(entry, event) end)
local anim_total_time = anim_entry:GetAnimationEnd() -- 获取技能动画时长
-- 附加加速buff
self:MakeAccelerate(self:GetSprintSpeedFactor(), anim_total_time)
self.movement:ActivateSprint(ability_direction, anim_total_time)
end
function FoxUltimate:MakeAccelerate(speed_factor, total_time)
local _, accelerate_effect = self:GetAbilityEffectSpecHandle("Accelerate", self.asc, 1, nil)
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
accelerate_effect,
BusyGameplayLibrary.RequestGameplayTag("Effect.Factor"),
speed_factor
)
AbilitySystemBlueprintLibrary.AssignTagSetByCallerMagnitude(
accelerate_effect,
BusyGameplayLibrary.RequestGameplayTag("Effect.Duration"),
total_time
)
return self.asc:BP_ApplyGameplayEffectSpecToSelf(accelerate_effect)
end
function FoxUltimate:OnAnimationComplete(entry)
local new_phase = self.ultimate_phase + 1
if new_phase > 3 then
self.ultimate_phase = 1
else
self.ultimate_phase = new_phase
end
print("FoxUltimate:OnAnimationComplete", self.ultimate_phase)
if not self.asc:HasMatchingGameplayTag(self.recast_tag) then
self.ultimate_phase = 1
self:K2_CommitAbilityCooldown(false, false)
end
entry.AnimationComplete:Clear()
entry.AnimationEvent:Clear()
self:K2_EndAbility()
end
function FoxUltimate:OnAnimationEvent(entry, event)
if event.Name == "OnSpeedChange" then
self.active_accelerate_handle = self:MakeAccelerate(8.0, 0.5)
elseif event.Name == "OnSpeedReset" then
self.asc:RemoveActiveGameplayEffect(self.active_accelerate_handle, -1)
elseif event.Name == "OnDamageBegin" then
local collision = self.owner["TailCollision"]
collision:SetCollisionEnabled(1)
self.overlap_delegate_handle = collision.OnComponentBeginOverlap:Add(function()
if not self.asc:HasMatchingGameplayTag(self.recast_tag) then
local _, recast_effect = self:GetAbilityEffectSpecHandle("Recast", self.asc, 1, nil)
self.active_recast_handle = self.asc:BP_ApplyGameplayEffectSpecToSelf(recast_effect)
end
end)
elseif event.Name == "OnDamageEnd" then
self.owner["TailCollision"]:SetCollisionEnabled(0)
if self.overlap_delegate_handle ~= nil then
self.owner["TailCollision"].OnComponentBeginOverlap:Remove(self.overlap_delegate_handle)
self.overlap_delegate_handle = nil
end
end
end
return Class(nil, nil, FoxUltimate)

View File

@ -1,4 +0,0 @@
local RabbitUltimate = {}
return Class(nil, nil, RabbitUltimate)

View File

@ -1,54 +0,0 @@
local LevelFoxRole = {}
local Vector2D = require("Utils.Vector2D")
function LevelFoxRole:ctor()
end
function LevelFoxRole:ReceiveBeginPlay()
self["SpineAnimationComponent"]:SetAnimation(0, "Idle/Front", true)
self.last_animation = "Idle/Front"
self["SpineBoneFollower"].Target = self
self["SpineBoneFollower"].BoneName = "tail"
self["SpineBoneFollower"].UseComponentTransform = true
self["SpineBoneFollower"].UseScale = true
self["TailCollision"]:SetCollisionEnabled(0)
end
function LevelFoxRole:OnMoveDirectionChanged(InDirection)
-- 运动组件更新方向响应函数
local cur_animation
if Vector2D.Equals(InDirection, Vector2D.Zero) then -- Idle的情况
local forward_direction = self["MovementComponent"]:GetForwardDirection()
if(forward_direction.Y >= 0) then
cur_animation = "Idle/Front"
else
cur_animation = "Idle/Back"
end
else
if(InDirection.Y >= 0) then
cur_animation = "Move/Front"
else
cur_animation = "Move/Back"
end
end
if cur_animation ~= self.last_animation then
self["SpineAnimationComponent"]:SetAnimation(0, cur_animation, true)
self.last_animation = cur_animation
end
self["SpineAnimationComponent"]:SetTimeScale(1.0)
end
function LevelFoxRole:OnMove(location)
-- 控制器移动相应函数
self["MovementComponent"]:MoveTo(location)
end
return Class(nil, nil, LevelFoxRole)

View File

@ -1,36 +0,0 @@
local LevelRabbitRole = {}
function LevelRabbitRole:ctor()
self.last_animation = "back/move"
end
function LevelRabbitRole:ReceiveBeginPlay()
self["SpineAnimationComponent"]:SetSkin("back/move")
self["SpineAnimationComponent"]:SetAnimation(0, "animation", true)
end
function LevelRabbitRole:OnMove(location)
-- 控制器移动相应函数
self["MovementComponent"]:MoveTo(location)
end
function LevelRabbitRole:OnMoveDirectionChanged(InDirection)
-- 运动组件更新方向响应函数
local cur_animation
if(InDirection.Y >= 0) then
cur_animation = "front/move"
else
cur_animation = "back/move"
end
print("LevelRabbitRole:OnMoveDirectionChanged", cur_animation)
if cur_animation ~= self.last_animation then
self["SpineAnimationComponent"]:SetSkin(cur_animation)
self["SpineAnimationComponent"]:SetSlotsToSetupPose()
self.last_animation = cur_animation
end
end
return Class(nil, nil, LevelRabbitRole)

View File

@ -1,4 +0,0 @@
local Campsite = {}
return Class(nil, nil, Campsite)

View File

@ -1,45 +0,0 @@
local Vector = import("Vector")
local GameplayStatics = import("GameplayStatics")
local BusyGameplayLibrary = import("BusyGameplayLibrary")
--[[****************************私有函数区域******************************]]--
--- 生成资源点
local function GenerateResourcePoint()
end
--- 保留到以后做联机内容时拓展
--- @class LevelGameMode
--- @field GameMapActorClass table
local LevelGameMode = {}
function LevelGameMode:ctor()
self.map_actor = nil
end
function LevelGameMode:ReceiveBeginPlay()
print("LevelGameMode:ReceiveBeginPlay", self.GameMapActorClass)
local world = BusyGameplayLibrary.K2_GetWorld(self)
local game_state = GameplayStatics.GetGameState(self) --- @type LevelGameState
if self.GameMapActorClass then
local map_actor = world:SpawnActor(self.GameMapActorClass, Vector(), nil, nil)
game_state:SetGameMapActor(map_actor)
end
end
-- function LevelGameMode:K2_PostLogin(new_player_controller)
-- local new_player_state = new_player_controller.PlayerState
-- local role = new_player_state:CreateRoleRoster(new_player_controller)
-- local new_pos = FVector()
-- new_pos.X = 500
-- new_pos.Y = 500
-- new_pos.Z = 50
-- role:K2_SetActorLocation(new_pos, true, nil, false)
-- new_player_controller:Possess(role)
-- end
return Class(nil, nil, LevelGameMode)

View File

@ -1,9 +0,0 @@
--- @class LevelGameState
local LevelGameState = {}
function LevelGameState:SetGameMapActor(game_map_actor)
self.GameMapActor = game_map_actor
end
return Class(nil, nil, LevelGameState)

View File

@ -1,12 +0,0 @@
local GameplayStatics = import("GameplayStatics")
local LevelPlayerState = {}
function LevelPlayerState:ReceiveBeginPlay()
local pc = GameplayStatics.GetPlayerController(self, 0)
local role = self:CreateRoleRoster(pc)
print("LevelPlayerState:ReceiveBeginPlay", role, self:HasAuthority())
pc:Possess(role)
end
return Class(nil, nil, LevelPlayerState)

Binary file not shown.

Binary file not shown.

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

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