Files
itsRevela 42a582fb9f feat: add FourKit plugin host with dual server build
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
2026-04-08 03:02:48 -05:00

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