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
This commit is contained in:
itsRevela
2026-04-08 03:02:48 -05:00
parent 5d56f5080f
commit 42a582fb9f
197 changed files with 20946 additions and 788 deletions
+7 -76
View File
@@ -1,86 +1,17 @@
# Note: A lot of this file is the same as the client due to the shared code
# Vanilla dedicated server (no plugin support).
#
# The FourKit-enabled flavour is built by Minecraft.Server.FourKit/CMakeLists.txt
# from the same shared source list. Both targets use configure_lce_server_target()
# from cmake/ServerTarget.cmake to apply identical compile/link/asset settings.
include("${CMAKE_CURRENT_LIST_DIR}/cmake/sources/Common.cmake")
include("${CMAKE_SOURCE_DIR}/cmake/CommonSources.cmake")
include("${CMAKE_SOURCE_DIR}/cmake/ServerTarget.cmake")
# Combine all source files into a single variable for the target
# We cant use CMAKE_CONFIGURE_PRESET here as VS doesn't set it, so just rely on the folder
set(MINECRAFT_SERVER_SOURCES
${MINECRAFT_SERVER_COMMON}
${SOURCES_COMMON}
)
add_executable(Minecraft.Server ${MINECRAFT_SERVER_SOURCES})
target_include_directories(Minecraft.Server PRIVATE
"${CMAKE_BINARY_DIR}/generated/" # This is for the generated BuildVer.h
"${CMAKE_SOURCE_DIR}/Minecraft.Client/"
"${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/Iggy/include"
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/include/"
)
target_compile_definitions(Minecraft.Server PRIVATE
${MINECRAFT_SHARED_DEFINES}
MINECRAFT_SERVER_BUILD
)
target_precompile_headers(Minecraft.Server PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:stdafx.h>")
set_source_files_properties("${CMAKE_SOURCE_DIR}/Minecraft.Client/compat_shims.cpp" PROPERTIES SKIP_PRECOMPILE_HEADERS ON) # This redefines internal MSVC CRT symbols which will cause an issue with PCH
configure_compiler_target(Minecraft.Server)
set_target_properties(Minecraft.Server PROPERTIES
OUTPUT_NAME "Minecraft.Server"
VS_DEBUGGER_WORKING_DIRECTORY "$<TARGET_FILE_DIR:Minecraft.Server>"
VS_DEBUGGER_COMMAND_ARGUMENTS "-port 25565 -bind 0.0.0.0 -name DedicatedServer"
)
target_link_libraries(Minecraft.Server PRIVATE
Minecraft.World
d3d11
dxgi
d3dcompiler
XInput9_1_0
wsock32
legacy_stdio_definitions
$<$<CONFIG:Debug>: # Debug 4J libraries
"${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Input_d.lib"
"${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Storage_d.lib"
"${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Render_PC_d.lib"
>
$<$<NOT:$<CONFIG:Debug>>: # Release 4J libraries
"${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Input.lib"
"${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Storage.lib"
"${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Render_PC.lib"
>
)
# Iggy libs
foreach(lib IN LISTS IGGY_LIBS)
target_link_libraries(Minecraft.Server PRIVATE "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/Iggy/lib/${lib}")
endforeach()
# ---
# Asset / redist copy
# ---
include("${CMAKE_SOURCE_DIR}/cmake/CopyAssets.cmake")
# Copy res
set(ASSET_FOLDER_PAIRS
"${CMAKE_SOURCE_DIR}/Minecraft.Client/Common/res" "Common/res"
)
setup_asset_folder_copy(Minecraft.Server "${ASSET_FOLDER_PAIRS}")
# Copy arc media
set(ASSET_FILES_PAIRS
"${CMAKE_SOURCE_DIR}/Minecraft.Client/Common/Media/MediaWindows64.arc" "Common/Media/"
)
setup_asset_file_copy(Minecraft.Server "${ASSET_FILES_PAIRS}")
# Copy redist files
add_copyredist_target(Minecraft.Server)
# Make sure GameHDD exists on Windows
if(PLATFORM_NAME STREQUAL "Windows64")
add_gamehdd_target(Minecraft.Server)
endif()
configure_lce_server_target(Minecraft.Server)
@@ -27,6 +27,7 @@
#include "..\Common\StringUtils.h"
#include "..\ServerShutdown.h"
#include "..\ServerLogger.h"
#include "..\FourKitBridge.h"
#include "..\..\Minecraft.Client\MinecraftServer.h"
#include "..\..\Minecraft.Client\PlayerList.h"
#include "..\..\Minecraft.Client\ServerPlayer.h"
@@ -163,6 +164,10 @@ namespace ServerRuntime
IServerCliCommand *command = m_registry->FindMutable(parsed.tokens[0]);
if (command == NULL)
{
if (FourKitBridge::HandleConsoleCommand(normalizedLine))
{
return true;
}
LogWarn("Unknown command: " + parsed.tokens[0]);
return false;
}
@@ -209,6 +214,38 @@ namespace ServerRuntime
}
m_registry->SuggestCommandNames(prefix, linePrefix, out);
char buf[4096];
int len = 0;
int count = FourKitBridge::GetPluginCommandHelp(buf, sizeof(buf), &len);
if (count > 0 && len > 0)
{
const char *ptr = buf;
const char *end = buf + len;
for (int i = 0; i < count && ptr < end; ++i)
{
std::string usage(ptr);
ptr += usage.size() + 1;
if (ptr >= end)
{
break;
}
std::string description(ptr);
ptr += description.size() + 1;
std::string name = usage;
if (!name.empty() && name[0] == '/')
{
name = name.substr(1);
}
if (name.size() >= prefix.size() &&
name.compare(0, prefix.size(), prefix) == 0)
{
out->push_back(linePrefix + name);
}
}
}
}
else
{
@@ -4,6 +4,9 @@
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliRegistry.h"
#include "..\..\..\FourKitBridge.h"
#include <cstring>
namespace ServerRuntime
{
@@ -40,6 +43,34 @@ namespace ServerRuntime
row += commands[i]->Description();
engine->LogInfo(row);
}
char buf[4096];
int len = 0;
int count = FourKitBridge::GetPluginCommandHelp(buf, sizeof(buf), &len);
if (count > 0 && len > 0)
{
engine->LogInfo("Plugin commands:");
const char *ptr = buf;
const char *end = buf + len;
for (int i = 0; i < count && ptr < end; ++i)
{
std::string usage(ptr);
ptr += usage.size() + 1;
if (ptr >= end) break;
std::string description(ptr);
ptr += description.size() + 1;
std::string row = " ";
row += usage;
if (!description.empty())
{
row += " - ";
row += description;
}
engine->LogInfo(row);
}
}
return true;
}
}
File diff suppressed because it is too large Load Diff
+215
View File
@@ -0,0 +1,215 @@
#pragma once
#include <string>
// FourKit plugin host bridge.
//
// In the FourKit-enabled server build, MINECRAFT_SERVER_FOURKIT_BUILD is
// defined and the real implementations live in FourKitBridge.cpp /
// FourKitNatives.cpp / FourKitRuntime.cpp / FourKitMappers.cpp.
//
// In the standalone server build (no plugin support), the macro is undefined
// and every entry point becomes an inline no-op stub. This lets gameplay code
// in Minecraft.Server / Minecraft.Client / Minecraft.World call into the
// bridge unconditionally without per-call-site #ifdefs. Cancellable hooks
// return false (do not cancel) so vanilla behaviour is preserved.
namespace FourKitBridge
{
#ifdef MINECRAFT_SERVER_FOURKIT_BUILD
void Initialize();
void Shutdown();
void FireWorldSave();
bool FirePlayerPreLogin(const std::wstring& name, const std::string& ip, int port);
bool FirePlayerLogin(const std::wstring& name, const std::string& ip, int port, int type, unsigned long long* onlineXUID, unsigned long long* offlineXUID);
void FirePlayerJoin(int entityId, const std::wstring& name, const std::wstring& uuid, unsigned long long onlineXUID, unsigned long long offlineXUID);
bool FirePlayerQuit(int entityId);
bool FirePlayerKick(int entityId, int disconnectReason,
std::wstring &outLeaveMessage);
bool FirePlayerMove(int entityId,
double fromX, double fromY, double fromZ,
double toX, double toY, double toZ,
double *outToX, double *outToY, double *outToZ);
bool FireStructureGrow(int dimId, int x, int y, int z, int treeType, bool wasBonemeal = false, int entityId = -1);
bool FirePlayerChat(int entityId,
const std::wstring &message,
std::wstring &outFormatted);
void UpdatePlayerEntityId(int oldEntityId, int newEntityId);
int GetTileId(int dimId, int x, int y, int z);
int GetTileData(int dimId, int x, int y, int z);
void SetTile(int dimId, int x, int y, int z, int tileId, int data);
void SetTileData(int dimId, int x, int y, int z, int data);
int BreakBlock(int dimId, int x, int y, int z);
int GetHighestBlockY(int dimId, int x, int z);
bool FireBlockPlace(int entityId, int dimId,
int placedX, int placedY, int placedZ,
int againstX, int againstY, int againstZ,
int itemId, int itemCount, bool canBuild);
int FireBlockBreak(int entityId, int dimId,
int x, int y, int z, int tileId, int data, int exp);
bool FireEntityDamage(int entityId, int entityTypeId, int dimId,
double x, double y, double z, int causeId, double damage,
double *outDamage,
int damagerEntityId, int damagerEntityTypeId,
double damagerX, double damagerY, double damagerZ);
bool FireSignChange(int entityId, int dimId,
int x, int y, int z,
const std::wstring &line0, const std::wstring &line1,
const std::wstring &line2, const std::wstring &line3,
std::wstring outLines[4]);
int FireEntityDeath(int entityId, int entityTypeId, int dimId,
double x, double y, double z, int exp);
int FirePlayerDeath(int entityId, const std::wstring &deathMessage, int exp,
std::wstring &outDeathMessage, int *outKeepInventory,
int *outNewExp, int *outNewLevel, int *outKeepLevel);
int MapEntityType(int nativeType);
int MapDamageCause(void *source);
bool FirePlayerDropItem(int entityId, int itemId, int itemCount, int itemAux,
int *outItemId, int *outItemCount, int *outItemAux);
bool FirePlayerInteract(int entityId, int action,
int itemId, int itemCount, int itemAux,
int clickedX, int clickedY, int clickedZ,
int blockFace, int dimId,
int *outUseItemInHand);
bool FirePlayerInteractEntity(int playerEntityId,
int targetEntityId, int targetEntityTypeId,
int dimId, double targetX, double targetY, double targetZ,
float targetHealth, float targetMaxHealth, float targetEyeHeight);
bool FirePlayerPickupItem(int playerEntityId,
int itemEntityId, int dimId, double itemX, double itemY, double itemZ,
int itemId, int itemCount, int itemAux, int remaining,
int *outItemId, int *outItemCount, int *outItemAux);
bool FireInventoryOpen(int entityId, int nativeContainerType,
const std::wstring &title, int containerSize);
bool HandlePlayerCommand(int entityId, const std::wstring &commandLine);
bool HandleConsoleCommand(const std::string &commandLine);
int GetPluginCommandHelp(char *outBuf, int outBufSize, int *outLen);
bool FirePlayerTeleport(int entityId,
double fromX, double fromY, double fromZ, int fromDimId,
double toX, double toY, double toZ, int toDimId,
int cause,
double *outToX, double *outToY, double *outToZ);
bool FirePlayerPortal(int entityId,
double fromX, double fromY, double fromZ, int fromDimId,
double toX, double toY, double toZ, int toDimId,
int cause,
double *outToX, double *outToY, double *outToZ);
int FireInventoryClick(int entityId, int slot, int button, int clickType);
bool FireBedEnter(int entityId, int dimId, int bedX, int bedY, int bedZ);
void FireBedLeave(int entityId, int dimId, int bedX, int bedY, int bedZ);
bool FireBlockGrow(int dimId, int x, int y, int z, int newTileId, int newTileData);
bool FireBlockForm(int dimId, int x, int y, int z, int newTileId, int newTileData);
bool FireBlockBurn(int dimId, int x, int y, int z);
bool FireBlockSpread(int dimId, int x, int y, int z, int srcX, int srcY, int srcZ, int newTileId, int newTileData);
bool FirePistonExtend(int dimId, int x, int y, int z, int direction, int length);
bool FirePistonRetract(int dimId, int x, int y, int z, int direction);
bool FireCommandPreprocess(int entityId, const std::wstring &commandLine, std::wstring &outCommand);
bool FireBlockFromTo(int dimId, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, int face);
#else
// Standalone build: every hook is an inline no-op. Cancellable hooks
// return false so vanilla code paths run unmodified, AND every out
// parameter is defaulted to its corresponding input value (or to a
// benign default like 0 / empty string) so callers do not read
// uninitialized stack memory. Reading uninitialized out-params would
// pass garbage into anti-cheat / damage / inventory checks and break
// the vanilla server build.
inline void Initialize() {}
inline void Shutdown() {}
inline void FireWorldSave() {}
inline bool FirePlayerPreLogin(const std::wstring&, const std::string&, int) { return false; }
inline bool FirePlayerLogin(const std::wstring&, const std::string&, int, int, unsigned long long*, unsigned long long*) { return false; }
inline void FirePlayerJoin(int, const std::wstring&, const std::wstring&, unsigned long long, unsigned long long) {}
inline bool FirePlayerQuit(int) { return false; }
inline bool FirePlayerKick(int, int, std::wstring &outLeaveMessage) { outLeaveMessage.clear(); return false; }
inline bool FirePlayerMove(int, double, double, double, double toX, double toY, double toZ, double *outToX, double *outToY, double *outToZ)
{
if (outToX) *outToX = toX;
if (outToY) *outToY = toY;
if (outToZ) *outToZ = toZ;
return false;
}
inline bool FireStructureGrow(int, int, int, int, int, bool = false, int = -1) { return false; }
inline bool FirePlayerChat(int, const std::wstring&, std::wstring &outFormatted) { outFormatted.clear(); return false; }
inline void UpdatePlayerEntityId(int, int) {}
inline int GetTileId(int, int, int, int) { return 0; }
inline int GetTileData(int, int, int, int) { return 0; }
inline void SetTile(int, int, int, int, int, int) {}
inline void SetTileData(int, int, int, int, int) {}
inline int BreakBlock(int, int, int, int) { return 0; }
inline int GetHighestBlockY(int, int, int) { return 0; }
inline bool FireBlockPlace(int, int, int, int, int, int, int, int, int, int, bool) { return false; }
inline int FireBlockBreak(int, int, int, int, int, int, int, int exp) { return exp; }
inline bool FireEntityDamage(int, int, int, double, double, double, int, double damage, double *outDamage, int, int, double, double, double)
{
if (outDamage) *outDamage = damage;
return false;
}
inline bool FireSignChange(int, int, int, int, int, const std::wstring &line0, const std::wstring &line1, const std::wstring &line2, const std::wstring &line3, std::wstring outLines[4])
{
if (outLines) { outLines[0] = line0; outLines[1] = line1; outLines[2] = line2; outLines[3] = line3; }
return false;
}
inline int FireEntityDeath(int, int, int, double, double, double, int exp) { return exp; }
inline int FirePlayerDeath(int, const std::wstring&, int exp, std::wstring &outDeathMessage, int *outKeepInventory, int *outNewExp, int *outNewLevel, int *outKeepLevel)
{
outDeathMessage.clear();
if (outKeepInventory) *outKeepInventory = 0;
if (outNewExp) *outNewExp = 0;
if (outNewLevel) *outNewLevel = 0;
if (outKeepLevel) *outKeepLevel = 0;
return exp;
}
inline int MapEntityType(int nativeType) { return nativeType; }
inline int MapDamageCause(void*) { return 0; }
inline bool FirePlayerDropItem(int, int itemId, int itemCount, int itemAux, int *outItemId, int *outItemCount, int *outItemAux)
{
if (outItemId) *outItemId = itemId;
if (outItemCount) *outItemCount = itemCount;
if (outItemAux) *outItemAux = itemAux;
return false;
}
inline bool FirePlayerInteract(int, int, int, int, int, int, int, int, int, int, int *outUseItemInHand)
{
if (outUseItemInHand) *outUseItemInHand = 1;
return false;
}
inline bool FirePlayerInteractEntity(int, int, int, int, double, double, double, float, float, float) { return false; }
inline bool FirePlayerPickupItem(int, int, int, double, double, double, int itemId, int itemCount, int itemAux, int, int *outItemId, int *outItemCount, int *outItemAux)
{
if (outItemId) *outItemId = itemId;
if (outItemCount) *outItemCount = itemCount;
if (outItemAux) *outItemAux = itemAux;
return false;
}
inline bool FireInventoryOpen(int, int, const std::wstring&, int) { return false; }
inline bool HandlePlayerCommand(int, const std::wstring&) { return false; }
inline bool HandleConsoleCommand(const std::string&) { return false; }
inline int GetPluginCommandHelp(char*, int, int *outLen) { if (outLen) *outLen = 0; return 0; }
inline bool FirePlayerTeleport(int, double, double, double, int, double toX, double toY, double toZ, int, int, double *outToX, double *outToY, double *outToZ)
{
if (outToX) *outToX = toX;
if (outToY) *outToY = toY;
if (outToZ) *outToZ = toZ;
return false;
}
inline bool FirePlayerPortal(int, double, double, double, int, double toX, double toY, double toZ, int, int, double *outToX, double *outToY, double *outToZ)
{
if (outToX) *outToX = toX;
if (outToY) *outToY = toY;
if (outToZ) *outToZ = toZ;
return false;
}
inline int FireInventoryClick(int, int, int, int) { return 0; }
inline bool FireBedEnter(int, int, int, int, int) { return false; }
inline void FireBedLeave(int, int, int, int, int) {}
inline bool FireBlockGrow(int, int, int, int, int, int) { return false; }
inline bool FireBlockForm(int, int, int, int, int, int) { return false; }
inline bool FireBlockBurn(int, int, int, int) { return false; }
inline bool FireBlockSpread(int, int, int, int, int, int, int, int, int) { return false; }
inline bool FirePistonExtend(int, int, int, int, int, int) { return false; }
inline bool FirePistonRetract(int, int, int, int, int) { return false; }
inline bool FireCommandPreprocess(int, const std::wstring &commandLine, std::wstring &outCommand) { outCommand = commandLine; return false; }
inline bool FireBlockFromTo(int, int, int, int, int, int, int, int) { return false; }
#endif
}
+251
View File
@@ -0,0 +1,251 @@
#include "FourKitBridge.h"
#include "stdafx.h"
#include "..\Minecraft.World\DamageSource.h"
#include "..\Minecraft.World\EntityDamageSource.h"
namespace FourKitBridge
{
int MapEntityType(int nativeType)
{
eINSTANCEOF type = (eINSTANCEOF)nativeType;
const int ARROW = 0, BAT = 1, BLAZE = 2, BOAT = 3, CAVE_SPIDER = 4;
const int CHICKEN = 5, COW = 7, CREEPER = 8, DROPPED_ITEM = 9;
const int EGG = 10, ENDER_CRYSTAL = 11, ENDER_DRAGON = 12;
const int ENDER_PEARL = 13, ENDER_SIGNAL = 14, ENDERMAN = 15;
const int EXPERIENCE_ORB = 16, FALLING_BLOCK = 17, FIREBALL = 18;
const int FIREWORK = 19, FISHING_HOOK = 20, GHAST = 21, GIANT = 22;
const int HORSE = 23, IRON_GOLEM = 24, ITEM_FRAME = 25;
const int LEASH_HITCH = 26, LIGHTNING = 27, MAGMA_CUBE = 28;
const int MINECART = 29, MINECART_CHEST = 30, MINECART_FURNACE = 32;
const int MINECART_HOPPER = 33, MINECART_MOB_SPAWNER = 34;
const int MINECART_TNT = 35, MUSHROOM_COW = 36, OCELOT = 37;
const int PAINTING = 38, PIG = 39, PIG_ZOMBIE = 40, PLAYER = 41;
const int PRIMED_TNT = 42, SHEEP = 43, SILVERFISH = 44;
const int SKELETON = 45, SLIME = 46, SMALL_FIREBALL = 47;
const int SNOWBALL = 48, SNOWMAN = 49, SPIDER = 50;
const int SPLASH_POTION = 51, SQUID = 52, THROWN_EXP_BOTTLE = 53;
const int UNKNOWN = 54, VILLAGER = 55, WITCH = 57;
const int WITHER = 58, WITHER_SKULL = 59, WOLF = 60, ZOMBIE = 61;
switch (type)
{
case eTYPE_ARROW:
return ARROW;
case eTYPE_BAT:
return BAT;
case eTYPE_BLAZE:
return BLAZE;
case eTYPE_BOAT:
return BOAT;
case eTYPE_CAVESPIDER:
return CAVE_SPIDER;
case eTYPE_CHICKEN:
return CHICKEN;
case eTYPE_COW:
return COW;
case eTYPE_CREEPER:
return CREEPER;
case eTYPE_ITEMENTITY:
return DROPPED_ITEM;
case eTYPE_THROWNEGG:
return EGG;
case eTYPE_NETHER_SPHERE:
return ENDER_CRYSTAL;
case eTYPE_ENDERDRAGON:
return ENDER_DRAGON;
case eTYPE_THROWNENDERPEARL:
return ENDER_PEARL;
case eTYPE_EYEOFENDERSIGNAL:
return ENDER_SIGNAL;
case eTYPE_ENDERMAN:
return ENDERMAN;
case eTYPE_EXPERIENCEORB:
return EXPERIENCE_ORB;
case eTYPE_FALLINGTILE:
return FALLING_BLOCK;
case eTYPE_LARGE_FIREBALL:
return FIREBALL;
case eTYPE_FIREWORKS_ROCKET:
return FIREWORK;
case eTYPE_FISHINGHOOK:
return FISHING_HOOK;
case eTYPE_GHAST:
return GHAST;
case eTYPE_GIANT:
return GIANT;
case eTYPE_HORSE:
return HORSE;
case eTYPE_VILLAGERGOLEM:
return IRON_GOLEM;
case eTYPE_ITEM_FRAME:
return ITEM_FRAME;
case eTYPE_LEASHFENCEKNOT:
return LEASH_HITCH;
case eTYPE_LIGHTNINGBOLT:
return LIGHTNING;
case eTYPE_LAVASLIME:
return MAGMA_CUBE;
case eTYPE_MINECART_RIDEABLE:
return MINECART;
case eTYPE_MINECART_CHEST:
return MINECART_CHEST;
case eTYPE_MINECART_FURNACE:
return MINECART_FURNACE;
case eTYPE_MINECART_HOPPER:
return MINECART_HOPPER;
case eTYPE_MINECART_SPAWNER:
return MINECART_MOB_SPAWNER;
case eTYPE_MINECART_TNT:
return MINECART_TNT;
case eTYPE_MUSHROOMCOW:
return MUSHROOM_COW;
case eTYPE_OCELOT:
return OCELOT;
case eTYPE_PAINTING:
return PAINTING;
case eTYPE_PIG:
return PIG;
case eTYPE_PIGZOMBIE:
return PIG_ZOMBIE;
case eTYPE_PLAYER:
return PLAYER;
case eTYPE_SERVERPLAYER:
return PLAYER;
case eTYPE_REMOTEPLAYER:
return PLAYER;
case eTYPE_LOCALPLAYER:
return PLAYER;
case eTYPE_PRIMEDTNT:
return PRIMED_TNT;
case eTYPE_SHEEP:
return SHEEP;
case eTYPE_SILVERFISH:
return SILVERFISH;
case eTYPE_SKELETON:
return SKELETON;
case eTYPE_SLIME:
return SLIME;
case eTYPE_SMALL_FIREBALL:
return SMALL_FIREBALL;
case eTYPE_SNOWBALL:
return SNOWBALL;
case eTYPE_SNOWMAN:
return SNOWMAN;
case eTYPE_SPIDER:
return SPIDER;
case eTYPE_THROWNPOTION:
return SPLASH_POTION;
case eTYPE_SQUID:
return SQUID;
case eTYPE_THROWNEXPBOTTLE:
return THROWN_EXP_BOTTLE;
case eTYPE_VILLAGER:
return VILLAGER;
case eTYPE_WITCH:
return WITCH;
case eTYPE_WITHERBOSS:
return WITHER;
case eTYPE_WITHER_SKULL:
return WITHER_SKULL;
case eTYPE_WOLF:
return WOLF;
case eTYPE_ZOMBIE:
return ZOMBIE;
default:
return UNKNOWN;
}
}
int MapDamageCause(void *sourcePtr)
{
DamageSource *source = (DamageSource *)sourcePtr;
const int CONTACT = 1, CUSTOM = 2, DROWNING = 3;
const int ENTITY_ATTACK = 4, ENTITY_EXPLOSION = 5;
const int FALL = 6, FALLING_BLOCK = 7, FIRE = 8, FIRE_TICK = 9;
const int LAVA = 10, MAGIC = 12;
const int PROJECTILE = 15, STARVATION = 16, SUFFOCATION = 17;
const int CAUSE_VOID = 20, CAUSE_WITHER = 21;
if (source == nullptr)
{
return CUSTOM;
}
if (source == DamageSource::inFire)
{
return FIRE;
}
if (source == DamageSource::onFire)
{
return FIRE_TICK;
}
if (source == DamageSource::lava)
{
return LAVA;
}
if (source == DamageSource::inWall)
{
return SUFFOCATION;
}
if (source == DamageSource::drown)
{
return DROWNING;
}
if (source == DamageSource::starve)
{
return STARVATION;
}
if (source == DamageSource::cactus)
{
return CONTACT;
}
if (source == DamageSource::fall)
{
return FALL;
}
if (source == DamageSource::outOfWorld)
{
return CAUSE_VOID;
}
if (source == DamageSource::genericSource)
{
return CUSTOM;
}
if (source == DamageSource::magic)
{
return MAGIC;
}
if (source == DamageSource::wither)
{
return CAUSE_WITHER;
}
if (source == DamageSource::anvil)
{
return FALLING_BLOCK;
}
if (source == DamageSource::fallingBlock)
{
return FALLING_BLOCK;
}
if (source->isExplosion())
{
return ENTITY_EXPLOSION;
}
if (source->isProjectile())
{
return PROJECTILE;
}
if (source->isMagic())
{
return MAGIC;
}
if (dynamic_cast<EntityDamageSource *>(source) != nullptr)
{
return ENTITY_ATTACK;
}
return CUSTOM;
}
}
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
#pragma once
namespace FourKitBridge
{
// core
void __cdecl NativeDamagePlayer(int entityId, float amount);
void __cdecl NativeSetPlayerHealth(int entityId, float health);
void __cdecl NativeTeleportPlayer(int entityId, double x, double y, double z);
void __cdecl NativeSetPlayerGameMode(int entityId, int gameMode);
void __cdecl NativeBroadcastMessage(const char *utf8, int len);
void __cdecl NativeSetFallDistance(int entityId, float distance);
void __cdecl NativeGetPlayerSnapshot(int entityId, double *outData);
void __cdecl NativeSendMessage(int entityId, const char *utf8, int len);
void __cdecl NativeSetWalkSpeed(int entityId, float speed);
void __cdecl NativeTeleportEntity(int entityId, int dimId, double x, double y, double z);
// World
int __cdecl NativeGetTileId(int dimId, int x, int y, int z);
int __cdecl NativeGetTileData(int dimId, int x, int y, int z);
void __cdecl NativeSetTile(int dimId, int x, int y, int z, int tileId, int data);
void __cdecl NativeSetTileData(int dimId, int x, int y, int z, int data);
int __cdecl NativeBreakBlock(int dimId, int x, int y, int z);
int __cdecl NativeGetHighestBlockY(int dimId, int x, int z);
void __cdecl NativeGetWorldInfo(int dimId, double *outBuf);
void __cdecl NativeSetWorldTime(int dimId, int64_t time);
void __cdecl NativeSetWeather(int dimId, int storm, int thundering, int thunderDuration);
int __cdecl NativeCreateExplosion(int dimId, double x, double y, double z, float power, int setFire, int breakBlocks);
int __cdecl NativeStrikeLightning(int dimId, double x, double y, double z, int effectOnly);
int __cdecl NativeSetSpawnLocation(int dimId, int x, int y, int z);
void __cdecl NativeDropItem(int dimId, double x, double y, double z, int itemId, int count, int auxValue, int naturally);
// plr
void __cdecl NativeKickPlayer(int entityId, int reason);
int __cdecl NativeBanPlayer(int entityId, const char *reasonUtf8, int reasonByteLen);
int __cdecl NativeBanPlayerIp(int entityId, const char *reasonUtf8, int reasonByteLen);
int __cdecl NativeGetPlayerAddress(int entityId, char* outIpBuf, int outIpBufSize, int* outPort);
int __cdecl NativeGetPlayerLatency(int entityId);
//plr connection
int __cdecl NativeSendRaw(int entityId, unsigned char* dataBuf, int dataBufSize);
// inv
void __cdecl NativeGetPlayerInventory(int entityId, int *outData);
void __cdecl NativeSetPlayerInventorySlot(int entityId, int slot, int itemId, int count, int aux);
void __cdecl NativeGetContainerContents(int entityId, int *outData, int maxSlots);
void __cdecl NativeSetContainerSlot(int entityId, int slot, int itemId, int count, int aux);
void __cdecl NativeGetContainerViewerEntityIds(int entityId, int *outIds, int maxCount, int *outCount);
void __cdecl NativeCloseContainer(int entityId);
void __cdecl NativeOpenVirtualContainer(int entityId, int nativeType, const char *titleUtf8, int titleByteLen, int slotCount, int *itemsBuf);
int __cdecl NativeGetItemMeta(int entityId, int slot, char *outBuf, int bufSize);
void __cdecl NativeSetItemMeta(int entityId, int slot, const char *inBuf, int bufSize);
void __cdecl NativeSetHeldItemSlot(int entityId, int slot);
// ent
void __cdecl NativeSetSneaking(int entityId, int sneak);
void __cdecl NativeSetVelocity(int entityId, double x, double y, double z);
void __cdecl NativeSetAllowFlight(int entityId, int allowFlight);
void __cdecl NativePlaySound(int entityId, int soundId, double x, double y, double z, float volume, float pitch);
void __cdecl NativeSetSleepingIgnored(int entityId, int ignored);
// x[p&food
void __cdecl NativeSetLevel(int entityId, int level);
void __cdecl NativeSetExp(int entityId, float exp);
void __cdecl NativeGiveExp(int entityId, int amount);
void __cdecl NativeGiveExpLevels(int entityId, int amount);
void __cdecl NativeSetFoodLevel(int entityId, int foodLevel);
void __cdecl NativeSetSaturation(int entityId, float saturation);
void __cdecl NativeSetExhaustion(int entityId, float exhaustion);
// particle
void __cdecl NativeSpawnParticle(int entityId, int particleId, float x, float y, float z, float offsetX, float offsetY, float offsetZ, float speed, int count);
// vehicle
int __cdecl NativeSetPassenger(int entityId, int passengerEntityId);
int __cdecl NativeLeaveVehicle(int entityId);
int __cdecl NativeEject(int entityId);
int __cdecl NativeGetVehicleId(int entityId);
int __cdecl NativeGetPassengerId(int entityId);
void __cdecl NativeGetEntityInfo(int entityId, double *outData);
}
+278
View File
@@ -0,0 +1,278 @@
#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;
}
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#define UNMANAGEDCALLERSONLY_METHOD ((const wchar_t *)-1)
namespace FourKitBridge
{
typedef int(__stdcall *load_assembly_fn)(
const wchar_t *assembly_path,
const wchar_t *type_name,
const wchar_t *method_name,
const wchar_t *delegate_type_name,
void *reserved,
void **delegate);
// Loads the .NET runtime via hostfxr's command-line init API and returns a
// delegate that can resolve individual [UnmanagedCallersOnly] entry points
// from the FourKit assembly. The command-line API is required because
// self-contained components are not supported by hostfxr_initialize_for_runtime_config.
bool LoadManagedRuntime(const wchar_t *assemblyPath,
const wchar_t *hostPath,
load_assembly_fn *outLoadAssembly);
bool GetManagedEntryPoint(load_assembly_fn loadAssembly,
const wchar_t *assemblyPath,
const wchar_t *typeName,
const wchar_t *methodName,
void **outFnPtr);
}
@@ -14,6 +14,7 @@
#include "..\Security\SecurityConfig.h"
#include "..\Security\RateLimiter.h"
#include "..\Security\IdentityTokenManager.h"
#include "..\FourKitBridge.h"
#include "..\WorldManager.h"
#include "..\Console\ServerCli.h"
#include "Tesselator.h"
@@ -681,6 +682,8 @@ int main(int argc, char **argv)
LogStartupStep("server startup complete");
LogInfof("startup", "Dedicated server listening on %s:%d", g_Win64MultiplayerIP, g_Win64MultiplayerPort);
FourKitBridge::Initialize();
if (worldBootstrap.status == eWorldBootstrap_CreatedNew && !IsShutdownRequested() && !app.m_bShutdown)
{
// Windows64 suppresses saveToDisc right after new world creation
@@ -737,6 +740,7 @@ int main(int argc, char **argv)
{
LogWorldIO("requesting autosave");
app.SetXuiServerAction(kServerActionPad, eXuiServerAction_AutoSaveGame);
FourKitBridge::FireWorldSave();
autosaveRequested = true;
}
nextAutosaveTick = now + autosaveIntervalMs;
@@ -747,6 +751,8 @@ int main(int argc, char **argv)
serverCli.Stop();
app.m_bShutdown = true;
FourKitBridge::Shutdown(); //close out the translation layer early for plugin shutdown
LogInfof("shutdown", "Dedicated server stopped");
MinecraftServer *server = MinecraftServer::getInstance();
if (server != NULL)
File diff suppressed because it is too large Load Diff