mirror of
https://git.revela.dev/itsRevela/LCE-Revelations.git
synced 2026-05-21 19:24:55 +00:00
Adds the FourKit .NET 10 plugin host as a second dedicated server
build flavour alongside the existing vanilla server. Both flavours
build from the same source tree, with FourKit gated by the
MINECRAFT_SERVER_FOURKIT_BUILD preprocessor define.
Build layout:
Minecraft.Server vanilla, no plugin support, no .NET dep
Minecraft.Server.FourKit FourKit-enabled, ships with bundled
.NET 10 self-contained runtime in runtime/
and an empty plugins/ folder
Both produce a Minecraft.Server.exe in their own per-target output
dir. The variant identity lives in the directory name, not the
binary name, so either flavour can be shipped as a drop-in.
Native bridge (Minecraft.Server/FourKit*.{cpp,h}):
* FourKitRuntime: hosts CoreCLR via hostfxr's command-line init API
(the runtime-config API does not support self-contained components)
* FourKitBridge: ~50 Fire* event entry points, with inline no-op
stubs for the standalone build so gameplay code can call them
unconditionally
* FourKitNatives: ~80 native callbacks the managed side invokes
for player/world/inventory mutations
* FourKitMappers: type and enum mapping helpers
Managed plugin host (Minecraft.Server.FourKit/):
* Bukkit-style API: Player, World, Block, Inventory, Command,
Listener, EventHandler attribute, ~54 event classes
* PluginLoader with per-plugin AssemblyLoadContext
* FourKitHost as the [UnmanagedCallersOnly] entry point table
* Runtime resolves plugins relative to the host process so they
always live next to Minecraft.Server.exe regardless of where the
managed assembly itself is loaded from
Engine hooks (Minecraft.Client/, Minecraft.World/):
* Player lifecycle (PreLogin, Login, Join, Quit, Kick, Move,
Teleport, Portal, Death) wired into PendingConnection and
PlayerConnection without disturbing the cipher handshake or
identity-token security flow
* Inventory open/click/drop hooks across every container menu type
* Block place/break/grow/burn/spread/from-to hooks across the
full tile family
* Bed enter/leave, sign change, entity damage/death, ender pearl
teleport hooks
Regression fixes preserved while applying donor diffs:
* ServerPlayer::die() retains the LCE-Revelations hardcore branch
(setGameMode(ADVENTURE) + banPlayerForHardcoreDeath) in both the
FourKit and non-FourKit code paths
* ServerLevel::entityAdded() retains the sub-entity ID reassignment
loop required by the client's handleAddMob offset, fixing Ender
Dragon and Wither boss multi-part hit detection
* LivingEntity::travel() retains the raw Player* cast and the
cached frictionTile, both Revelations perf wins that the donor
silently reverted
* ServerLogger.cpp keeps the file-logging code donor stripped
* PlayerList.cpp end portal transition fix and UIScene_EndPoem
bounds-check are intact
Build system:
* Top-level CMakeLists.txt adds the Minecraft.Server.FourKit
subdirectory and pulls in the new shared cmake/ServerTarget.cmake
helper
* Minecraft.Server/cmake/sources/Common.cmake is now location
independent (uses CMAKE_CURRENT_LIST_DIR) so the source list
can be consumed from either server target's CMakeLists.txt
* The seven FourKit*.cpp/h files live in their own
_MINECRAFT_SERVER_COMMON_SERVER_FOURKIT variable so the
standalone target omits them
* configure-time .NET 10 SDK check fails fast with a clear
download link if the SDK is missing
* global.json pins the SDK to 10.0.100 with latestFeature
rollforward
Sample plugin (samples/HelloPlugin/) demonstrates the loader and
the PlayerJoinEvent listener pattern.
CI:
* nightly.yml builds both server flavours, ships
LCE-Revelations-Server-Win64.zip and
LCE-Revelations-Server-Win64-FourKit.zip, attests both, and
updates release notes for the dual-flavour layout
* pull-request.yml pulls in actions/setup-dotnet so the FourKit
publish step works in PR validation
* All zip artifacts and the client zip are renamed from
LCREWindows64 to LCE-Revelations-{Client,Server}-Win64
Documentation:
* COMPILE.md gets a VS 2022 quick start, .NET 10 prereq section,
server flavours explanation, and a troubleshooting section
* docs/FOURKIT_PORT_RECON.md captures the file-by-file recon that
drove the port
* docs/FOURKIT_PARITY.md is the canonical reference for which
events FourKit fires
Docker:
* docker-compose.dedicated-server.yml MC_RUNTIME_DIR default points
at the vanilla CMake output. The FourKit Docker image is
intentionally NOT shipped yet because hosting .NET 10 self
contained inside Wine has not been smoke-tested
279 lines
9.0 KiB
C++
279 lines
9.0 KiB
C++
#include "FourKitRuntime.h"
|
|
#include "ServerLogger.h"
|
|
#include "stdafx.h"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <windows.h>
|
|
|
|
using ServerRuntime::LogError;
|
|
|
|
typedef void *hostfxr_handle;
|
|
|
|
typedef int(__cdecl *hostfxr_initialize_for_runtime_config_fn)(
|
|
const wchar_t *runtime_config_path,
|
|
const void *parameters,
|
|
hostfxr_handle *host_context_handle);
|
|
|
|
// Self-contained component loading must use the command-line init API.
|
|
// hostfxr_initialize_for_runtime_config returns 0x80008093 ("Initialization
|
|
// for self-contained components is not supported") for self-contained
|
|
// publishes. We never call hostfxr_run_app, so the assembly's Main is
|
|
// never invoked; we just use this entry point to start the runtime and
|
|
// then ask for the load_assembly_and_get_function_pointer delegate.
|
|
typedef int(__cdecl *hostfxr_initialize_for_dotnet_command_line_fn)(
|
|
int argc,
|
|
const wchar_t **argv,
|
|
const void *parameters,
|
|
hostfxr_handle *host_context_handle);
|
|
|
|
enum hostfxr_delegate_type
|
|
{
|
|
hdt_com_activation = 0,
|
|
hdt_load_in_memory_assembly = 1,
|
|
hdt_winrt_activation = 2,
|
|
hdt_com_register = 3,
|
|
hdt_com_unregister = 4,
|
|
hdt_load_assembly_and_get_function_pointer = 5,
|
|
hdt_get_function_pointer = 6,
|
|
};
|
|
|
|
typedef int(__cdecl *hostfxr_get_runtime_delegate_fn)(
|
|
const hostfxr_handle host_context_handle,
|
|
hostfxr_delegate_type type,
|
|
void **delegate);
|
|
|
|
typedef int(__cdecl *hostfxr_close_fn)(const hostfxr_handle host_context_handle);
|
|
|
|
struct hostfxr_initialize_parameters
|
|
{
|
|
size_t size;
|
|
const wchar_t *host_path;
|
|
const wchar_t *dotnet_root;
|
|
};
|
|
|
|
namespace
|
|
{
|
|
static hostfxr_initialize_for_runtime_config_fn s_initConfigFn = nullptr;
|
|
static hostfxr_initialize_for_dotnet_command_line_fn s_initCmdLineFn = nullptr;
|
|
static hostfxr_get_runtime_delegate_fn s_getDelegateFn = nullptr;
|
|
static hostfxr_close_fn s_closeFn = nullptr;
|
|
static std::wstring s_dotnetRoot;
|
|
|
|
static std::wstring FindNet10SystemRoot()
|
|
{
|
|
std::vector<std::wstring> candidates;
|
|
wchar_t envRoot[MAX_PATH] = {};
|
|
DWORD len = GetEnvironmentVariableW(L"DOTNET_ROOT", envRoot, MAX_PATH);
|
|
if (len > 0 && len < MAX_PATH)
|
|
{
|
|
candidates.push_back(std::wstring(envRoot));
|
|
}
|
|
candidates.push_back(L"C:\\Program Files\\dotnet");
|
|
|
|
for (const auto &root : candidates)
|
|
{
|
|
std::wstring fxrDir = root + L"\\host\\fxr";
|
|
WIN32_FIND_DATAW fd;
|
|
HANDLE h = FindFirstFileW((fxrDir + L"\\*").c_str(), &fd);
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
{
|
|
continue;
|
|
}
|
|
bool has10 = false;
|
|
do
|
|
{
|
|
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && fd.cFileName[0] != L'.')
|
|
{
|
|
if (std::wstring(fd.cFileName).substr(0, 3) == L"10.")
|
|
{
|
|
has10 = true;
|
|
}
|
|
}
|
|
} while (!has10 && FindNextFileW(h, &fd));
|
|
FindClose(h);
|
|
if (has10)
|
|
{
|
|
return root;
|
|
}
|
|
}
|
|
|
|
return L"C:\\Program Files\\dotnet";
|
|
}
|
|
|
|
static bool TryLoadHostfxrFromPath(const std::wstring &path)
|
|
{
|
|
HMODULE lib = LoadLibraryW(path.c_str());
|
|
if (!lib)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
s_initConfigFn = (hostfxr_initialize_for_runtime_config_fn)GetProcAddress(lib, "hostfxr_initialize_for_runtime_config");
|
|
s_initCmdLineFn = (hostfxr_initialize_for_dotnet_command_line_fn)GetProcAddress(lib, "hostfxr_initialize_for_dotnet_command_line");
|
|
s_getDelegateFn = (hostfxr_get_runtime_delegate_fn)GetProcAddress(lib, "hostfxr_get_runtime_delegate");
|
|
s_closeFn = (hostfxr_close_fn)GetProcAddress(lib, "hostfxr_close");
|
|
|
|
// We require the command-line init function for self-contained loading.
|
|
// The runtime-config init function is optional (kept for diagnostics).
|
|
if (s_initCmdLineFn && s_getDelegateFn && s_closeFn)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
s_initConfigFn = nullptr;
|
|
s_initCmdLineFn = nullptr;
|
|
s_getDelegateFn = nullptr;
|
|
s_closeFn = nullptr;
|
|
FreeLibrary(lib);
|
|
return false;
|
|
}
|
|
|
|
static bool LoadHostfxr()
|
|
{
|
|
wchar_t exePath[MAX_PATH] = {};
|
|
GetModuleFileNameW(NULL, exePath, MAX_PATH);
|
|
std::wstring exeDir(exePath);
|
|
size_t lastSlash = exeDir.find_last_of(L"\\/");
|
|
if (lastSlash != std::wstring::npos)
|
|
{
|
|
exeDir = exeDir.substr(0, lastSlash);
|
|
}
|
|
|
|
// Preferred layout: self-contained publish output is staged in "<exeDir>\runtime\"
|
|
// by the CMake POST_BUILD step so the server root stays clean. hostfxr.dll
|
|
// discovers the rest of the runtime relative to its own location.
|
|
if (TryLoadHostfxrFromPath(exeDir + L"\\runtime\\hostfxr.dll"))
|
|
{
|
|
s_dotnetRoot = exeDir + L"\\runtime";
|
|
return true;
|
|
}
|
|
|
|
// Legacy fallback: hostfxr.dll dropped directly next to the exe.
|
|
if (TryLoadHostfxrFromPath(exeDir + L"\\hostfxr.dll"))
|
|
{
|
|
s_dotnetRoot = FindNet10SystemRoot();
|
|
return true;
|
|
}
|
|
|
|
wchar_t dotnetRoot[MAX_PATH] = {};
|
|
DWORD len = GetEnvironmentVariableW(L"DOTNET_ROOT", dotnetRoot, MAX_PATH);
|
|
if (len == 0 || len >= MAX_PATH)
|
|
{
|
|
wcscpy_s(dotnetRoot, L"C:\\Program Files\\dotnet");
|
|
}
|
|
|
|
std::wstring hostfxrDir = std::wstring(dotnetRoot) + L"\\host\\fxr";
|
|
|
|
WIN32_FIND_DATAW fd;
|
|
HANDLE hFind = FindFirstFileW((hostfxrDir + L"\\*").c_str(), &fd);
|
|
if (hFind != INVALID_HANDLE_VALUE)
|
|
{
|
|
std::wstring bestVersion;
|
|
do
|
|
{
|
|
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && fd.cFileName[0] != L'.')
|
|
{
|
|
std::wstring ver(fd.cFileName);
|
|
if (ver.substr(0, 3) == L"10." && ver > bestVersion)
|
|
{
|
|
bestVersion = ver;
|
|
}
|
|
}
|
|
} while (FindNextFileW(hFind, &fd));
|
|
FindClose(hFind);
|
|
|
|
if (!bestVersion.empty())
|
|
{
|
|
if (TryLoadHostfxrFromPath(hostfxrDir + L"\\" + bestVersion + L"\\hostfxr.dll"))
|
|
{
|
|
s_dotnetRoot = std::wstring(dotnetRoot);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
LogError("fourkit", "hostfxr.dll not found. Install the .NET 10 x64 runtime (https://aka.ms/dotnet/download) or copy hostfxr.dll from C:\\Program Files\\dotnet\\host\\fxr\\10.x.x\\ next to the server executable.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
namespace FourKitBridge
|
|
{
|
|
bool LoadManagedRuntime(const wchar_t *assemblyPath,
|
|
const wchar_t *hostPath,
|
|
load_assembly_fn *outLoadAssembly)
|
|
{
|
|
if (!LoadHostfxr())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
hostfxr_initialize_parameters initParams = {};
|
|
initParams.size = sizeof(hostfxr_initialize_parameters);
|
|
initParams.host_path = hostPath;
|
|
initParams.dotnet_root = s_dotnetRoot.c_str();
|
|
|
|
// Use the dotnet_command_line API for self-contained component loading.
|
|
// We pass the FourKit assembly path as argv[0]; hostfxr starts the
|
|
// self-contained runtime that ships in the same directory. We never
|
|
// call hostfxr_run_app, so the assembly's Main entry point is never
|
|
// executed -- we only need the load_assembly_and_get_function_pointer
|
|
// delegate to invoke individual [UnmanagedCallersOnly] methods.
|
|
const wchar_t *argv[1] = { assemblyPath };
|
|
|
|
hostfxr_handle ctx = nullptr;
|
|
int rc = s_initCmdLineFn(1, argv, &initParams, &ctx);
|
|
// hostfxr returns Success_HostAlreadyInitialized (0x00000001) when the
|
|
// runtime is already up; both 0 and 1 are success for our purposes.
|
|
if ((rc != 0 && rc != 1) || ctx == nullptr)
|
|
{
|
|
char msg[256];
|
|
sprintf_s(msg, "hostfxr_initialize_for_dotnet_command_line failed (0x%08X). Check the FourKit assembly path.", rc);
|
|
LogError("fourkit", msg);
|
|
if (ctx)
|
|
{
|
|
s_closeFn(ctx);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
load_assembly_fn loadAssembly = nullptr;
|
|
rc = s_getDelegateFn(ctx, hdt_load_assembly_and_get_function_pointer, (void **)&loadAssembly);
|
|
s_closeFn(ctx);
|
|
|
|
if (rc != 0 || loadAssembly == nullptr)
|
|
{
|
|
LogError("fourkit", "Failed to get load_assembly_and_get_function_pointer delegate.");
|
|
return false;
|
|
}
|
|
|
|
*outLoadAssembly = loadAssembly;
|
|
return true;
|
|
}
|
|
|
|
bool GetManagedEntryPoint(load_assembly_fn loadAssembly,
|
|
const wchar_t *assemblyPath,
|
|
const wchar_t *typeName,
|
|
const wchar_t *methodName,
|
|
void **outFnPtr)
|
|
{
|
|
int rc = loadAssembly(
|
|
assemblyPath,
|
|
typeName,
|
|
methodName,
|
|
UNMANAGEDCALLERSONLY_METHOD,
|
|
nullptr,
|
|
outFnPtr);
|
|
|
|
if (rc != 0 || *outFnPtr == nullptr)
|
|
{
|
|
char methodNarrow[256];
|
|
sprintf_s(methodNarrow, "%S::%S", typeName, methodName);
|
|
LogError("fourkit", (std::string("Failed to resolve managed entry point: ") + methodNarrow).c_str());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|