Files
LCE-Revelations/Minecraft.World/LiquidTileDynamic.cpp
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

369 lines
8.9 KiB
C++

#include "stdafx.h"
#include "net.minecraft.world.level.h"
#include "LiquidTileDynamic.h"
#include "net.minecraft.world.level.dimension.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\FourKitBridge.h"
#include "Dimension.h"
#endif
LiquidTileDynamic::LiquidTileDynamic(int id, Material *material) : LiquidTile(id, material)
{
maxCount = 0;
result = new bool[4];
dist = new int[4];
m_iterativeInstatick = false;
}
LiquidTileDynamic::~LiquidTileDynamic()
{
delete[] result;
delete[] dist;
}
void LiquidTileDynamic::setStatic(Level *level, int x, int y, int z)
{
int d = level->getData(x, y, z);
level->setTileAndData(x, y, z, id + 1, d, Tile::UPDATE_CLIENTS);
}
bool LiquidTileDynamic::isPathfindable(LevelSource *level, int x, int y, int z)
{
return material != Material::lava;
}
void LiquidTileDynamic::iterativeTick(Level *level, int x, int y, int z, Random *random)
{
m_tilesToTick.push_back(LiquidTickData(level, x,y,z,random));
int failsafe = 100;
while((m_tilesToTick.size() > 0) && ( failsafe > 0 ) )
{
LiquidTickData tickData = m_tilesToTick.front();
m_tilesToTick.pop_front();
mainTick(tickData.level, tickData.x, tickData.y, tickData.z, tickData.random);
failsafe--;
}
m_tilesToTick.clear();
}
void LiquidTileDynamic::tick(Level *level, int x, int y, int z, Random *random)
{
if(!m_iterativeInstatick && level->getInstaTick() )
{
m_iterativeInstatick = true;
iterativeTick(level, x, y, z, random);
m_iterativeInstatick = false;
}
else if(m_iterativeInstatick && level->getInstaTick())
{
m_tilesToTick.push_back(LiquidTickData(level, x,y,z,random));
}
else
{
mainTick(level, x, y, z, random);
}
}
// 4J Stu - Split off what was the tick function to be able to change between recursive and iterative ticking
// This is to fix the stack overflow that occurs sometimes when instaticking on level gen.
void LiquidTileDynamic::mainTick(Level *level, int x, int y, int z, Random *random)
{
int depth = getDepth(level, x, y, z);
int dropOff = 1;
if (material == Material::lava && !level->dimension->ultraWarm) dropOff = 2;
bool becomeStatic = true;
int tickDelay = getTickDelay(level);
if (depth > 0)
{
int highest = -100;
maxCount = 0;
highest = getHighest(level, x - 1, y, z, highest);
highest = getHighest(level, x + 1, y, z, highest);
highest = getHighest(level, x, y, z - 1, highest);
highest = getHighest(level, x, y, z + 1, highest);
int newDepth = highest + dropOff;
if (newDepth >= 8 || highest < 0)
{
newDepth = -1;
}
if (getDepth(level, x, y + 1, z) >= 0)
{
int above = getDepth(level, x, y + 1, z);
if (above >= 8) newDepth = above;
else newDepth = above + 8;
}
if (maxCount >= 2 && material == Material::water)
{
// Only spread spring if it's on top of an existing spring, or
// on top of solid ground.
if (level->getMaterial(x, y - 1, z)->isSolid())
{
newDepth = 0;
}
else if (level->getMaterial(x, y - 1, z) == material && level->getData(x, y - 1, z) == 0)
{
newDepth = 0;
}
}
if (material == Material::lava)
{
if (depth < 8 && newDepth < 8)
{
if (newDepth > depth)
{
if (random->nextInt(4) != 0)
{
tickDelay = tickDelay * 4;
}
}
}
}
if (newDepth == depth)
{
if (becomeStatic)
{
setStatic(level, x, y, z);
}
}
else
{
depth = newDepth;
if (depth < 0)
{
level->removeTile(x, y, z);
}
else
{
level->setData(x, y, z, depth, Tile::UPDATE_CLIENTS);
level->addToTickNextTick(x, y, z, id, tickDelay);
level->updateNeighborsAt(x, y, z, id);
}
}
}
else
{
setStatic(level, x, y, z);
}
if (canSpreadTo(level, x, y - 1, z))
{
if (material == Material::lava)
{
if (level->getMaterial(x, y - 1, z) == Material::water)
{
level->setTileAndUpdate(x, y - 1, z, Tile::stone_Id);
fizz(level, x, y - 1, z);
return;
}
}
if (depth >= 8) trySpreadTo(level, x, y - 1, z, x, y, z, depth);
else trySpreadTo(level, x, y - 1, z, x, y, z, depth + 8);
}
else if (depth >= 0 && (depth == 0 || isWaterBlocking(level, x, y - 1, z)))
{
bool *spreads = getSpread(level, x, y, z);
int neighbor = depth + dropOff;
if (depth >= 8)
{
neighbor = 1;
}
if (neighbor >= 8) return;
if (spreads[0]) trySpreadTo(level, x - 1, y, z, x, y, z, neighbor);
if (spreads[1]) trySpreadTo(level, x + 1, y, z, x, y, z, neighbor);
if (spreads[2]) trySpreadTo(level, x, y, z - 1, x, y, z, neighbor);
if (spreads[3]) trySpreadTo(level, x, y, z + 1, x, y, z, neighbor);
}
}
void LiquidTileDynamic::trySpreadTo(Level *level, int x, int y, int z, int fromX, int fromY, int fromZ, int neighbor)
{
if (canSpreadTo(level, x, y, z))
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
int face = 6; // SELF
int dx = x - fromX, dy = y - fromY, dz = z - fromZ;
if (dy < 0) face = 0; // DOWN
else if (dy > 0) face = 1; // UP
else if (dz < 0) face = 2; // NORTH
else if (dz > 0) face = 3; // SOUTH
else if (dx < 0) face = 4; // WEST
else if (dx > 0) face = 5; // EAST
if (FourKitBridge::FireBlockFromTo(level->dimension->id, fromX, fromY, fromZ, x, y, z, face))
return;
}
#endif
{
int old = level->getTile(x, y, z);
if (old > 0)
{
if (material == Material::lava)
{
fizz(level, x, y, z);
}
else
{
Tile::tiles[old]->spawnResources(level, x, y, z, level->getData(x, y, z), 0);
}
}
}
level->setTileAndData(x, y, z, id, neighbor, Tile::UPDATE_ALL);
}
}
int LiquidTileDynamic::getSlopeDistance(Level *level, int x, int y, int z, int pass, int from)
{
int lowest = 1000;
for (int d = 0; d < 4; d++)
{
if (d == 0 && from == 1) continue;
if (d == 1 && from == 0) continue;
if (d == 2 && from == 3) continue;
if (d == 3 && from == 2) continue;
int xx = x;
int yy = y;
int zz = z;
if (d == 0) xx--;
if (d == 1) xx++;
if (d == 2) zz--;
if (d == 3) zz++;
if (isWaterBlocking(level, xx, yy, zz))
{
continue;
} else if (level->getMaterial(xx, yy, zz) == material && level->getData(xx, yy, zz) == 0)
{
continue;
}
else
{
if (isWaterBlocking(level, xx, yy - 1, zz))
{
if (pass < 4)
{
int v = getSlopeDistance(level, xx, yy, zz, pass + 1, d);
if (v < lowest) lowest = v;
}
}
else
{
return pass;
}
}
}
return lowest;
}
bool *LiquidTileDynamic::getSpread(Level *level, int x, int y, int z)
{
for (int d = 0; d < 4; d++)
{
dist[d] = 1000;
int xx = x;
int yy = y;
int zz = z;
if (d == 0) xx--;
if (d == 1) xx++;
if (d == 2) zz--;
if (d == 3) zz++;
if (isWaterBlocking(level, xx, yy, zz))
{
continue;
}
else if (level->getMaterial(xx, yy, zz) == material && level->getData(xx, yy, zz) == 0)
{
continue;
}
{
if (isWaterBlocking(level, xx, yy - 1, zz))
{
dist[d] = getSlopeDistance(level, xx, yy, zz, 1, d);
}
else
{
dist[d] = 0;
}
}
}
int lowest = dist[0];
for (int d = 1; d < 4; d++)
{
if (dist[d] < lowest) lowest = dist[d];
}
for (int d = 0; d < 4; d++)
{
result[d] = (dist[d] == lowest);
}
return result;
}
bool LiquidTileDynamic::isWaterBlocking(Level *level, int x, int y, int z)
{
int t = level->getTile(x, y, z);
if (t == Tile::door_wood_Id || t == Tile::door_iron_Id || t == Tile::sign_Id || t == Tile::ladder_Id || t == Tile::reeds_Id)
{
return true;
}
if (t == 0) return false;
Material *m = Tile::tiles[t]->material;
if (m == Material::portal) return true;
if (m->blocksMotion()) return true;
return false;
}
int LiquidTileDynamic::getHighest(Level *level, int x, int y, int z, int current)
{
int d = getDepth(level, x, y, z);
if (d < 0) return current;
if (d == 0) maxCount++;
if (d >= 8)
{
d = 0;
}
return current < 0 || d < current ? d : current;
}
bool LiquidTileDynamic::canSpreadTo(Level *level, int x, int y, int z)
{
// 4J added - don't try and spread out of our restricted map. If we don't do this check then tiles at the edge of the world will try and spread outside as the outside tiles report that they contain
// only air. The fact that this successfully spreads then updates the neighbours of the tile outside of the map, one of which is the original tile just inside the map, which gets set back to being
// dynamic, and added to the pending ticks array.
int xc = x >> 4;
int zc = z >> 4;
int ix = xc + (level->chunkSourceXZSize/2);
int iz = zc + (level->chunkSourceXZSize/2);
if( ( ix < 0 ) || ( ix >= level->chunkSourceXZSize ) ) return false;
if( ( iz < 0 ) || ( iz >= level->chunkSourceXZSize ) ) return false;
Material *target = level->getMaterial(x, y, z);
if (target == material) return false;
if (target == Material::lava) return false;
return !isWaterBlocking(level, x, y, z);
}
void LiquidTileDynamic::onPlace(Level *level, int x, int y, int z)
{
LiquidTile::onPlace(level, x, y, z);
if (level->getTile(x, y, z) == id)
{
level->addToTickNextTick(x, y, z, id, getTickDelay(level));
}
}
bool LiquidTileDynamic::canInstantlyTick()
{
return true;
}