finish rewrite; port to cmake, loads of other changes

Theres documentation at https://sylvessa.zip/fourkit/ now. And a bunch of other changes. Check the discord server for a more comprehensive list
This commit is contained in:
sylvessa 2026-03-21 13:52:26 -05:00
parent ecb3f00bd6
commit f5f9aa1cf5
107 changed files with 14289 additions and 40 deletions

BIN
.github/doxygen-api-logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -5,6 +5,7 @@ on:
push:
branches:
- 'main'
- 'feature/plugin-api'
paths-ignore:
- '.gitignore'
- '*.md'
@ -34,6 +35,15 @@ jobs:
- name: Set platform lowercase
run: echo "MATRIX_PLATFORM=$('${{ matrix.platform }}'.ToLower())" >> $env:GITHUB_ENV
- name: Create temporary global.json
run: |
echo '{"sdk":{"version": "10.0.201"}}' > ./global.json
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.201'
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
@ -60,7 +70,10 @@ jobs:
- name: Stage exe and pdb
if: matrix.platform == 'Windows64'
run: |
Copy-Item ./build/${{ env.MATRIX_PLATFORM }}/Minecraft.Server/Release/Minecraft.Server.exe staging/
Copy-Item @(
"./build/${{ env.MATRIX_PLATFORM }}/Minecraft.Server/Release/Minecraft.Server.exe",
"./build/${{ env.MATRIX_PLATFORM }}/Minecraft.Server/Release/Minecraft.Server.Fourkit.dll"
) staging/
- name: Upload artifacts
uses: actions/upload-artifact@v6

3
.gitignore vendored
View file

@ -414,3 +414,6 @@ build/
tmp*/
_server_asset_probe/
server-data/
doxygen/

View file

@ -80,6 +80,7 @@ add_subdirectory(Minecraft.World)
add_subdirectory(Minecraft.Client)
if(PLATFORM_NAME STREQUAL "Windows64") # Server is only supported on Windows for now
add_subdirectory(Minecraft.Server)
add_subdirectory(Minecraft.Server.FourKit)
endif()
# ---
@ -100,6 +101,7 @@ add_dependencies(Minecraft.World GenerateBuildVer)
add_dependencies(Minecraft.Client GenerateBuildVer)
if(PLATFORM_NAME STREQUAL "Windows64")
add_dependencies(Minecraft.Server GenerateBuildVer)
# add_dependencies(Minecraft.Server.FourKit GenerateBuildVer)
endif()
# ---

3037
Doxyfile Normal file

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,7 @@
#include "Options.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogManager.h"
#include "..\Minecraft.Server\FourKitBridge.h"
#endif
namespace
@ -62,6 +63,7 @@ PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connecti
aboveGroundTickCount = 0;
xLastOk = yLastOk = zLastOk = 0;
synched = true;
hasDoneFirstTickFourKit = false;
didTick = false;
lastKeepAliveId = 0;
lastKeepAliveTime = 0;
@ -113,6 +115,14 @@ unsigned char PlayerConnection::getLogSmallId()
void PlayerConnection::tick()
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (!hasDoneFirstTickFourKit)
{
FourKitBridge::FirePlayerJoin(player->entityId, player->name, player->getUUID());
hasDoneFirstTickFourKit = true;
}
#endif
if( done ) return;
if( m_bCloseOnTick )
@ -153,12 +163,27 @@ void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason)
return;
}
std::wstring kickLeaveMessage;
bool fourKitHandledQuit = false;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (reason != DisconnectPacket::eDisconnect_Closed &&
reason != DisconnectPacket::eDisconnect_ConnectionCreationFailed &&
reason != DisconnectPacket::eDisconnect_Quitting)
{
if (FourKitBridge::FirePlayerKick(player->entityId, (int)reason, kickLeaveMessage))
{
m_bWasKicked = false;
LeaveCriticalSection(&done_cs);
return;
}
}
ServerRuntime::ServerLogManager::OnPlayerDisconnected(
getLogSmallId(),
(player != NULL) ? player->name : std::wstring(),
reason,
true);
fourKitHandledQuit = FourKitBridge::FirePlayerQuit(player->entityId);
#endif
app.DebugPrintf("PlayerConnection disconect reason: %d\n", reason );
player->disconnect();
@ -169,13 +194,20 @@ void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason)
connection->sendAndQuit();
// 4J-PB - removed, since it needs to be localised in the language the client is in
//server->players->broadcastAll( shared_ptr<ChatPacket>( new ChatPacket(L"<22>e" + player->name + L" left the game.") ) );
if(getWasKicked())
if (!kickLeaveMessage.empty())
{
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(player->name, ChatPacket::e_ChatPlayerKickedFromGame));
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(kickLeaveMessage));
}
else
else if (!fourKitHandledQuit)
{
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(player->name, ChatPacket::e_ChatPlayerLeftGame));
if(getWasKicked())
{
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(player->name, ChatPacket::e_ChatPlayerKickedFromGame));
}
else
{
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(player->name, ChatPacket::e_ChatPlayerLeftGame));
}
}
server->getPlayers()->remove(player);
@ -264,6 +296,8 @@ void PlayerConnection::handleMovePlayer(shared_ptr<MovePlayerPacket> packet)
float yRotT = player->yRot;
float xRotT = player->xRot;
const float yRotOld = yRotT;
const float xRotOld = xRotT;
if (packet->hasPos && packet->y == -999 && packet->yView == -999)
{
@ -327,6 +361,31 @@ void PlayerConnection::handleMovePlayer(shared_ptr<MovePlayerPacket> packet)
return;
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (xLastOk != xt || yLastOk != yt || zLastOk != zt || yRotT != yRotOld || xRotT != xRotOld)
{
double moveToX, moveToY, moveToZ;
bool cancelled = FourKitBridge::FirePlayerMove(player->entityId,
xLastOk, yLastOk, zLastOk,
xt, yt, zt,
&moveToX, &moveToY, &moveToZ);
if (cancelled)
{
teleport(xLastOk, yLastOk, zLastOk, yRotT, xRotT);
return;
}
if (moveToX != xt || moveToY != yt || moveToZ != zt)
{
xt = moveToX;
yt = moveToY;
zt = moveToZ;
xDist = xt - player->x;
yDist = yt - player->y;
zDist = zt - player->z;
}
}
#endif
float r = 1 / 16.0f;
bool oldOk = level->getCubes(player, player->bb->copy()->shrink(r, r, r))->empty();
@ -433,11 +492,45 @@ void PlayerConnection::handlePlayerAction(shared_ptr<PlayerActionPacket> packet)
if (packet->action == PlayerActionPacket::DROP_ITEM)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
shared_ptr<ItemInstance> selected = player->inventory->getSelected();
if (selected != nullptr && selected->count > 0)
{
int outId = selected->id, outCount = 1, outAux = selected->getAuxValue();
bool cancelled = FourKitBridge::FirePlayerDropItem(
player->entityId, selected->id, 1, selected->getAuxValue(),
&outId, &outCount, &outAux);
if (cancelled)
return;
player->inventory->removeItem(player->inventory->selected, 1);
player->drop(std::make_shared<ItemInstance>(outId, outCount, outAux));
return;
}
}
#endif
player->drop(false);
return;
}
else if (packet->action == PlayerActionPacket::DROP_ALL_ITEMS)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
shared_ptr<ItemInstance> selected = player->inventory->getSelected();
if (selected != nullptr && selected->count > 0)
{
int outId = selected->id, outCount = selected->count, outAux = selected->getAuxValue();
bool cancelled = FourKitBridge::FirePlayerDropItem(
player->entityId, selected->id, selected->count, selected->getAuxValue(),
&outId, &outCount, &outAux);
if (cancelled)
return;
player->inventory->removeItem(player->inventory->selected, selected->count);
player->drop(std::make_shared<ItemInstance>(outId, outCount, outAux));
return;
}
}
#endif
player->drop(true);
return;
}
@ -475,6 +568,23 @@ void PlayerConnection::handlePlayerAction(shared_ptr<PlayerActionPacket> packet)
if (packet->action == PlayerActionPacket::START_DESTROY_BLOCK)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
lastLeftClickTick = tickCount;
{
shared_ptr<ItemInstance> heldItem = player->inventory->getSelected();
int iId = heldItem ? heldItem->id : 0;
int iCount = heldItem ? heldItem->count : 0;
int iAux = heldItem ? heldItem->getAuxValue() : 0;
int useItemInHand = 1;
bool cancelled = FourKitBridge::FirePlayerInteract(
player->entityId, 1 /* LEFT_CLICK_BLOCK */,
iId, iCount, iAux,
x, y, z, packet->face, player->dimension,
&useItemInHand);
if (cancelled)
return;
}
#endif
// Anti-cheat: validate spawn protection on the server for mining start.
if (!server->isUnderSpawnProtection(level, x, y, z, player)) player->gameMode->startDestroyBlock(x, y, z, packet->face);
else player->connection->send(std::make_shared<TileUpdatePacket>(x, y, z, level));
@ -506,20 +616,129 @@ void PlayerConnection::handleUseItem(shared_ptr<UseItemPacket> packet)
if (packet->getFace() == 255)
{
if (item == nullptr) return;
if (item == nullptr) return;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
int iId = item->id;
int iCount = item->count;
int iAux = item->getAuxValue();
int useItemInHand = 1;
bool cancelled = FourKitBridge::FirePlayerInteract(
player->entityId, 2,
iId, iCount, iAux,
0, 0, 0, 6, player->dimension,
&useItemInHand);
if (cancelled || !useItemInHand)
return;
}
#endif
player->gameMode->useItem(player, level, item);
}
else if ((packet->getY() < server->getMaxBuildHeight() - 1) || (packet->getFace() != Facing::UP && packet->getY() < server->getMaxBuildHeight()))
{
if (synched && player->distanceToSqr(x + 0.5, y + 0.5, z + 0.5) < 8 * 8)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
int iId = item ? item->id : 0;
int iCount = item ? item->count : 0;
int iAux = item ? item->getAuxValue() : 0;
int useItemInHand = 1;
bool cancelled = FourKitBridge::FirePlayerInteract(
player->entityId, 3 /* RIGHT_CLICK_BLOCK */,
iId, iCount, iAux,
x, y, z, face, player->dimension,
&useItemInHand);
if (cancelled || !useItemInHand)
{
informClient = true;
goto skipUseItemOn;
}
}
#endif
// Anti-cheat: block placement/use must pass server-side spawn protection.
if (!server->isUnderSpawnProtection(level, x, y, z, player))
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
int placeX = x, placeY = y, placeZ = z;
bool validFace = true;
if (face == 0) placeY--;
else if (face == 1) placeY++;
else if (face == 2) placeZ--;
else if (face == 3) placeZ++;
else if (face == 4) placeX--;
else if (face == 5) placeX++;
else validFace = false;
int oldTileId = validFace ? level->getTile(placeX, placeY, placeZ) : 0;
int oldTileData = validFace ? level->getData(placeX, placeY, placeZ) : 0;
int oldClickedId = level->getTile(x, y, z);
int oldClickedData = level->getData(x, y, z);
int savedItemId = item ? item->id : 0;
int savedItemCount = item ? item->count : 0;
#endif
player->gameMode->useItemOn(player, level, item, x, y, z, face, packet->getClickX(), packet->getClickY(), packet->getClickZ());
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (validFace)
{
int newTileId = level->getTile(placeX, placeY, placeZ);
int newClickedId = level->getTile(x, y, z);
int fireX = placeX, fireY = placeY, fireZ = placeZ;
int againstX = x, againstY = y, againstZ = z;
int revertId = oldTileId, revertData = oldTileData;
bool shouldFire = false;
if (newTileId != 0 && newTileId != oldTileId)
{
shouldFire = true;
}
else if (newClickedId != 0 && newClickedId != oldClickedId)
{
shouldFire = true;
fireX = x; fireY = y; fireZ = z;
revertId = oldClickedId; revertData = oldClickedData;
againstX = x; againstY = y; againstZ = z;
if (face == 0) againstY++;
else if (face == 1) againstY--;
else if (face == 2) againstZ++;
else if (face == 3) againstZ--;
else if (face == 4) againstX++;
else if (face == 5) againstX--;
}
if (shouldFire)
{
bool cancelled = FourKitBridge::FireBlockPlace(
player->entityId, player->dimension,
fireX, fireY, fireZ,
againstX, againstY, againstZ,
savedItemId, savedItemCount, true);
if (cancelled)
{
level->setTileAndData(fireX, fireY, fireZ, revertId, revertData, Tile::UPDATE_ALL);
auto &slot = player->inventory->items[player->inventory->selected];
if (slot != nullptr)
{
slot->count = savedItemCount;
}
else if (savedItemId > 0 && savedItemId < 256 && Tile::tiles[savedItemId] != nullptr && savedItemCount > 0)
{
slot = std::make_shared<ItemInstance>(Tile::tiles[savedItemId], savedItemCount);
}
informClient = true;
}
}
}
#endif
}
}
skipUseItemOn:
informClient = true;
}
else
@ -588,23 +807,28 @@ void PlayerConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason,
LeaveCriticalSection(&done_cs);
return;
}
bool fourKitHandledQuit = false;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnPlayerDisconnected(
getLogSmallId(),
(player != NULL) ? player->name : std::wstring(),
reason,
false);
fourKitHandledQuit = FourKitBridge::FirePlayerQuit(player->entityId);
#endif
// logger.info(player.name + " lost connection: " + reason);
// 4J-PB - removed, since it needs to be localised in the language the client is in
//server->players->broadcastAll( shared_ptr<ChatPacket>( new ChatPacket(L"<22>e" + player->name + L" left the game.") ) );
if(getWasKicked())
if (!fourKitHandledQuit)
{
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(player->name, ChatPacket::e_ChatPlayerKickedFromGame));
}
else
{
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(player->name, ChatPacket::e_ChatPlayerLeftGame));
if(getWasKicked())
{
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(player->name, ChatPacket::e_ChatPlayerKickedFromGame));
}
else
{
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(player->name, ChatPacket::e_ChatPlayerLeftGame));
}
}
server->getPlayers()->remove(player);
done = true;
@ -678,8 +902,20 @@ void PlayerConnection::handleChat(shared_ptr<ChatPacket> packet)
handleCommand(message);
return;
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
std::wstring formatted;
if (!FourKitBridge::FirePlayerChat(player->entityId, message, formatted))
{
if (formatted.empty())
formatted = L"<" + player->name + L"> " + message;
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(formatted));
}
}
#else
wstring formatted = L"<" + player->name + L"> " + message;
server->getPlayers()->broadcastAll(shared_ptr<ChatPacket>(new ChatPacket(formatted)));
#endif
chatSpamTickCount += SharedConstants::TICKS_PER_SECOND;
if (chatSpamTickCount > SharedConstants::TICKS_PER_SECOND * 10)
{
@ -689,6 +925,10 @@ void PlayerConnection::handleChat(shared_ptr<ChatPacket> packet)
void PlayerConnection::handleCommand(const wstring& message)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::HandlePlayerCommand(player->entityId, message))
return;
#endif
// 4J - TODO
#if 0
server.getCommandDispatcher().performCommand(player, message);
@ -700,6 +940,23 @@ void PlayerConnection::handleAnimate(shared_ptr<AnimatePacket> packet)
player->resetLastActionTime();
if (packet->action == AnimatePacket::SWING)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (lastLeftClickTick != tickCount)
{
int useItemInHand = 1;
auto item = player->inventory->getSelected();
int iId = item ? item->id : 0;
int iCount = item ? item->count : 0;
int iAux = item ? item->getAuxValue() : 0;
bool cancelled = FourKitBridge::FirePlayerInteract(
player->entityId, 0,
iId, iCount, iAux,
0, 0, 0, 6, player->dimension,
&useItemInHand);
if (cancelled)
return;
}
#endif
player->swing();
}
}
@ -810,10 +1067,31 @@ void PlayerConnection::handleInteract(shared_ptr<InteractPacket> packet)
if (packet->action == InteractPacket::INTERACT)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
int mappedType = FourKitBridge::MapEntityType((int)target->GetType());
float targetHealth = 0, targetMaxHealth = 0, targetEyeHeight = 0;
auto living = dynamic_pointer_cast<LivingEntity>(target);
if (living)
{
targetHealth = living->getHealth();
targetMaxHealth = living->getMaxHealth();
targetEyeHeight = living->getHeadHeight();
}
if (FourKitBridge::FirePlayerInteractEntity(
player->entityId, target->entityId, mappedType,
player->dimension, target->x, target->y, target->z,
targetHealth, targetMaxHealth, targetEyeHeight))
return;
}
#endif
player->interact(target);
}
else if (packet->action == InteractPacket::ATTACK)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
lastLeftClickTick = tickCount;
#endif
if ((target->GetType() == eTYPE_ITEMENTITY) || (target->GetType() == eTYPE_EXPERIENCEORB) || (target->GetType() == eTYPE_ARROW) || target == player)
{
//disconnect("Attempting to attack an invalid entity");
@ -1107,7 +1385,13 @@ void PlayerConnection::handleClientCommand(shared_ptr<ClientCommandPacket> packe
{
if (player->wonGame)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
int oldEntityId = player->entityId;
#endif
player = server->getPlayers()->respawn(player, player->m_enteredEndExitPortal?0:player->dimension, true);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
FourKitBridge::UpdatePlayerEntityId(oldEntityId, player->entityId);
#endif
}
//else if (player.getLevel().getLevelData().isHardcore())
//{
@ -1128,7 +1412,13 @@ void PlayerConnection::handleClientCommand(shared_ptr<ClientCommandPacket> packe
else
{
if (player->getHealth() > 0) return;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
int oldEntityId = player->entityId;
#endif
player = server->getPlayers()->respawn(player, 0, false);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
FourKitBridge::UpdatePlayerEntityId(oldEntityId, player->entityId);
#endif
}
}
}
@ -1188,6 +1478,20 @@ void PlayerConnection::handleContainerClick(shared_ptr<ContainerClickPacket> pac
player->resetLastActionTime();
if (player->containerMenu->containerId == packet->containerId && player->containerMenu->isSynched(player))
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
int fourKitClickResult = FourKitBridge::FireInventoryClick(player->entityId, packet->slotNum, packet->buttonNum, packet->clickType);
if (fourKitClickResult == 1)
{
expectedAcks[player->containerMenu->containerId] = packet->uid;
player->connection->send(std::make_shared<ContainerAckPacket>(packet->containerId, packet->uid, false));
player->containerMenu->setSynched(player, false);
vector<shared_ptr<ItemInstance>> items;
for (unsigned int i = 0; i < player->containerMenu->slots.size(); i++)
items.push_back(player->containerMenu->slots.at(i)->getItem());
player->refreshContainer(player->containerMenu, &items);
return;
}
#endif
shared_ptr<ItemInstance> clicked = player->containerMenu->clicked(packet->slotNum, packet->buttonNum, packet->clickType, player);
if (ItemInstance::matches(packet->item, clicked))
@ -1215,6 +1519,15 @@ void PlayerConnection::handleContainerClick(shared_ptr<ContainerClickPacket> pac
// player.containerMenu.broadcastChanges();
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (fourKitClickResult == 2)
{
vector<shared_ptr<ItemInstance>> refreshItems;
for (unsigned int i = 0; i < player->containerMenu->slots.size(); i++)
refreshItems.push_back(player->containerMenu->slots.at(i)->getItem());
player->refreshContainer(player->containerMenu, &refreshItems);
}
#endif
}
}
@ -1352,6 +1665,28 @@ void PlayerConnection::handleSignUpdate(shared_ptr<SignUpdatePacket> packet)
int y = packet->y;
int z = packet->z;
shared_ptr<SignTileEntity> ste = dynamic_pointer_cast<SignTileEntity>(te);
wstring lines[4];
for (int i = 0; i < 4; i++)
{
lines[i] = packet->lines[i].substr(0,15);
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
std::wstring outLines[4];
bool cancelled = FourKitBridge::FireSignChange(
player->entityId, player->dimension,
x, y, z,
lines[0], lines[1], lines[2], lines[3],
outLines);
if (cancelled)
return;
for (int i = 0; i < 4; i++)
lines[i] = outLines[i];
}
#endif
for (int i = 0; i < 4; i++)
{
wstring lineText = packet->lines[i].substr(0,15);

View file

@ -30,11 +30,13 @@ private:
bool didTick;
int lastKeepAliveId;
bool hasDoneFirstTickFourKit;
int64_t lastKeepAliveTime;
static Random random;
int64_t lastKeepAliveTick;
int chatSpamTickCount;
int dropSpamTickCount;
int lastLeftClickTick = 0;
bool m_bHasClientTickedOnce;
unsigned char m_logSmallId;

View file

@ -33,6 +33,70 @@
#include "..\Minecraft.World\LevelChunk.h"
#include "LevelRenderer.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\FourKitBridge.h"
#include "..\Minecraft.World\ChatPacket.h"
// todo: rework this to use string localization funcs instead of hardcoded
static std::wstring FormatDeathMessage(const shared_ptr<ChatPacket>& packet)
{
if (!packet) return L"";
const std::wstring& player = packet->m_stringArgs.size() > 0 ? packet->m_stringArgs[0] : L"";
const std::wstring& killer = packet->m_stringArgs.size() > 1 ? packet->m_stringArgs[1] : L"";
const std::wstring& item = packet->m_stringArgs.size() > 2 ? packet->m_stringArgs[2] : L"";
switch (packet->m_messageType)
{
case ChatPacket::e_ChatCustom: return player;
case ChatPacket::e_ChatDeathInFire: return player + L" went up in flames";
case ChatPacket::e_ChatDeathOnFire: return player + L" burned to death";
case ChatPacket::e_ChatDeathLava: return player + L" tried to swim in lava";
case ChatPacket::e_ChatDeathInWall: return player + L" suffocated in a wall";
case ChatPacket::e_ChatDeathDrown: return player + L" drowned";
case ChatPacket::e_ChatDeathStarve: return player + L" starved to death";
case ChatPacket::e_ChatDeathCactus: return player + L" was pricked to death";
case ChatPacket::e_ChatDeathFall: return player + L" hit the ground too hard";
case ChatPacket::e_ChatDeathOutOfWorld: return player + L" fell out of the world";
case ChatPacket::e_ChatDeathGeneric: return player + L" died";
case ChatPacket::e_ChatDeathExplosion: return player + L" blew up";
case ChatPacket::e_ChatDeathMagic: return player + L" was killed by magic";
case ChatPacket::e_ChatDeathWither: return player + L" withered away";
case ChatPacket::e_ChatDeathDragonBreath: return player + L" was killed by Ender Dragon breath";
case ChatPacket::e_ChatDeathAnvil: return player + L" was squashed by a falling Anvil.";
case ChatPacket::e_ChatDeathFallingBlock: return player + L" was squashed by a falling block.";
case ChatPacket::e_ChatDeathFellAccidentLadder: return player + L" fell off a ladder";
case ChatPacket::e_ChatDeathFellAccidentVines: return player + L" fell off some vines";
case ChatPacket::e_ChatDeathFellAccidentWater: return player + L" fell out of the water";
case ChatPacket::e_ChatDeathFellAccidentGeneric:return player + L" fell from a high place";
case ChatPacket::e_ChatDeathFellKiller: return player + L" was doomed to fall";
case ChatPacket::e_ChatDeathMob:
case ChatPacket::e_ChatDeathPlayer: return killer.empty() ? player + L" was slain" : player + L" was slain by " + killer;
case ChatPacket::e_ChatDeathArrow: return killer.empty() ? player + L" was shot" : player + L" was shot by " + killer;
case ChatPacket::e_ChatDeathFireball: return killer.empty() ? player + L" was fireballed" : player + L" was fireballed by " + killer;
case ChatPacket::e_ChatDeathThrown: return killer.empty() ? player + L" was pummeled" : player + L" was pummeled by " + killer;
case ChatPacket::e_ChatDeathIndirectMagic: return killer.empty() ? player + L" was killed by magic" : player + L" was killed by " + killer + L" using magic";
case ChatPacket::e_ChatDeathThorns: return killer.empty() ? player + L" died" : player + L" was killed trying to hurt " + killer;
case ChatPacket::e_ChatDeathExplosionPlayer: return killer.empty() ? player + L" blew up" : player + L" was blown up by " + killer;
case ChatPacket::e_ChatDeathInFirePlayer: return killer.empty() ? player + L" went up in flames" : player + L" walked into fire whilst fighting " + killer;
case ChatPacket::e_ChatDeathOnFirePlayer: return killer.empty() ? player + L" burned to death" : player + L" was burnt to a crisp whilst fighting " + killer;
case ChatPacket::e_ChatDeathLavaPlayer: return killer.empty() ? player + L" tried to swim in lava" : player + L" tried to swim in lava to escape " + killer;
case ChatPacket::e_ChatDeathDrownPlayer: return killer.empty() ? player + L" drowned" : player + L" drowned whilst trying to escape " + killer;
case ChatPacket::e_ChatDeathCactusPlayer: return killer.empty() ? player + L" was pricked to death" : player + L" walked into a cactus whilst trying to escape " + killer;
case ChatPacket::e_ChatDeathFellAssist: return killer.empty() ? player + L" was doomed to fall" : player + L" was doomed to fall by " + killer;
case ChatPacket::e_ChatDeathFellFinish: return killer.empty() ? player + L" fell too far" : player + L" fell too far and was finished by " + killer;
case ChatPacket::e_ChatDeathPlayerItem: return (killer.empty() ? player + L" was slain" : player + L" was slain by " + killer) + (item.empty() ? L"" : L" using " + item);
case ChatPacket::e_ChatDeathArrowItem: return (killer.empty() ? player + L" was shot" : player + L" was shot by " + killer) + (item.empty() ? L"" : L" with " + item);
case ChatPacket::e_ChatDeathFireballItem: return (killer.empty() ? player + L" was fireballed" : player + L" was fireballed by " + killer) + (item.empty() ? L"" : L" with " + item);
case ChatPacket::e_ChatDeathThrownItem: return (killer.empty() ? player + L" was pummeled" : player + L" was pummeled by " + killer) + (item.empty() ? L"" : L" using " + item);
case ChatPacket::e_ChatDeathIndirectMagicItem: return (killer.empty() ? player + L" was killed by magic" : player + L" was killed by " + killer) + (item.empty() ? L"" : L" using " + item);
case ChatPacket::e_ChatDeathFellAssistItem: return (killer.empty() ? player + L" was doomed to fall" : player + L" was doomed to fall by " + killer) + (item.empty() ? L"" : L" using " + item);
case ChatPacket::e_ChatDeathFellFinishItem: return (killer.empty() ? player + L" fell too far" : player + L" fell too far and was finished by " + killer) + (item.empty() ? L"" : L" using " + item);
default: return player + L" died";
}
}
#endif
ServerPlayer::ServerPlayer(MinecraftServer *server, Level *level, const wstring& name, ServerPlayerGameMode *gameMode) : Player(level, name)
{
@ -567,12 +631,38 @@ shared_ptr<ItemInstance> ServerPlayer::getCarried(int slot)
void ServerPlayer::die(DamageSource *source)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
shared_ptr<ChatPacket> deathPacket = getCombatTracker()->getDeathMessagePacket();
std::wstring deathMsg = FormatDeathMessage(deathPacket);
int exp = getExperienceReward(nullptr);
std::wstring outDeathMsg;
int keepInventory = 0;
int outNewExp = 0, outNewLevel = 0, outKeepLevel = 0;
int outExp = FourKitBridge::FirePlayerDeath(entityId, deathMsg, exp, outDeathMsg, &keepInventory,
&outNewExp, &outNewLevel, &outKeepLevel);
fk_hasDeathState = true;
fk_deathKeepInventory = (keepInventory != 0);
fk_deathKeepLevel = (outKeepLevel != 0);
fk_deathNewExp = outNewExp;
fk_deathNewLevel = outNewLevel;
if (!outDeathMsg.empty())
server->getPlayers()->broadcastAll(std::make_shared<ChatPacket>(outDeathMsg));
if (keepInventory == 0 && !level->getGameRules()->getBoolean(GameRules::RULE_KEEPINVENTORY))
{
inventory->dropAll();
}
#else
server->getPlayers()->broadcastAll(getCombatTracker()->getDeathMessagePacket());
if (!level->getGameRules()->getBoolean(GameRules::RULE_KEEPINVENTORY))
{
inventory->dropAll();
}
#endif
vector<Objective *> *objectives = level->getScoreboard()->findObjectiveFor(ObjectiveCriteria::DEATH_COUNT);
if(objectives)
@ -765,6 +855,10 @@ void ServerPlayer::changeDimension(int i)
}
else
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
bool portalDestModified = false;
double portalOutX = 0, portalOutY = 0, portalOutZ = 0;
#endif
if (dimension == 0 && i == 1)
{
awardStat(GenericStats::theEnd(), GenericStats::param_theEnd());
@ -772,7 +866,24 @@ void ServerPlayer::changeDimension(int i)
Pos *pos = server->getLevel(i)->getDimensionSpecificSpawn();
if (pos != nullptr)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
double outX, outY, outZ;
bool cancelled = FourKitBridge::FirePlayerPortal(entityId,
x, y, z, dimension,
pos->x, pos->y, pos->z, i,
4,
&outX, &outY, &outZ);
if (cancelled)
{
delete pos;
return;
}
connection->teleport(outX, outY, outZ, 0, 0);
}
#else
connection->teleport(pos->x, pos->y, pos->z, 0, 0);
#endif
delete pos;
}
@ -780,10 +891,48 @@ void ServerPlayer::changeDimension(int i)
}
else
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
double scale = server->getLevel(i)->getLevelData()->getHellScale();
double toX = x, toY = y, toZ = z;
if (i == -1)
{
toX = x / scale;
toZ = z / scale;
}
else if (dimension == -1 && i == 0)
{
toX = x * scale;
toZ = z * scale;
}
double outX, outY, outZ;
bool cancelled = FourKitBridge::FirePlayerPortal(entityId,
x, y, z, dimension,
toX, toY, toZ, i,
3,
&outX, &outY, &outZ);
if (cancelled)
return;
if (outX != toX || outY != toY || outZ != toZ)
{
portalDestModified = true;
portalOutX = outX;
portalOutY = outY;
portalOutZ = outZ;
}
}
#endif
// 4J: Removed on the advice of the mighty King of Achievments (JV)
// awardStat(GenericStats::portal(), GenericStats::param_portal());
}
server->getPlayers()->toggleDimension( dynamic_pointer_cast<ServerPlayer>(shared_from_this()), i);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (portalDestModified)
{
connection->teleport(portalOutX, portalOutY, portalOutZ, yRot, xRot);
}
#endif
lastSentExp = -1;
lastSentHealth = -1;
lastSentFood = -1;
@ -874,10 +1023,18 @@ bool ServerPlayer::startCrafting(int x, int y, int z)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::WORKBENCH, L"", 9, false));
containerMenu = new CraftingMenu(inventory, level, x, y, z);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::WORKBENCH, L"", 9))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::WORKBENCH, L"", 9, false));
refreshContainer(containerMenu);
}
else
{
@ -892,20 +1049,36 @@ bool ServerPlayer::openFireworks(int x, int y, int z)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::FIREWORKS, L"", 9, false));
containerMenu = new FireworksMenu(inventory, level, x, y, z);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::FIREWORKS, L"", 9))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::FIREWORKS, L"", 9, false));
refreshContainer(containerMenu);
}
else if(dynamic_cast<CraftingMenu *>(containerMenu) != nullptr)
{
closeContainer();
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::FIREWORKS, L"", 9, false));
containerMenu = new FireworksMenu(inventory, level, x, y, z);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::FIREWORKS, L"", 9))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::FIREWORKS, L"", 9, false));
refreshContainer(containerMenu);
}
else
{
@ -920,10 +1093,18 @@ bool ServerPlayer::startEnchanting(int x, int y, int z, const wstring &name)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::ENCHANTMENT, name.empty() ? L"" : name, 9, !name.empty()));
containerMenu = new EnchantmentMenu(inventory, level, x, y, z);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::ENCHANTMENT, name.empty() ? L"" : name, 9))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::ENCHANTMENT, name.empty() ? L"" : name, 9, !name.empty()));
refreshContainer(containerMenu);
}
else
{
@ -938,10 +1119,18 @@ bool ServerPlayer::startRepairing(int x, int y, int z)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::REPAIR_TABLE, L"", 9, false));
containerMenu = new AnvilMenu(inventory, level, x, y, z, dynamic_pointer_cast<Player>(shared_from_this()));
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::REPAIR_TABLE, L"", 9))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::REPAIR_TABLE, L"", 9, false));
refreshContainer(containerMenu);
}
else
{
@ -961,11 +1150,18 @@ bool ServerPlayer::openContainer(shared_ptr<Container> container)
int containerType = container->getContainerType();
assert(containerType >= 0);
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, containerType, container->getCustomName(), container->getContainerSize(), container->hasCustomName()));
containerMenu = new ContainerMenu(inventory, container);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, containerType, container->getCustomName(), container->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, containerType, container->getCustomName(), container->getContainerSize(), container->hasCustomName()));
refreshContainer(containerMenu);
}
else
{
@ -980,10 +1176,18 @@ bool ServerPlayer::openHopper(shared_ptr<HopperTileEntity> container)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::HOPPER, container->getCustomName(), container->getContainerSize(), container->hasCustomName()));
containerMenu = new HopperMenu(inventory, container);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::HOPPER, container->getCustomName(), container->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::HOPPER, container->getCustomName(), container->getContainerSize(), container->hasCustomName()));
refreshContainer(containerMenu);
}
else
{
@ -998,10 +1202,18 @@ bool ServerPlayer::openHopper(shared_ptr<MinecartHopper> container)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::HOPPER, container->getCustomName(), container->getContainerSize(), container->hasCustomName()));
containerMenu = new HopperMenu(inventory, container);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::HOPPER, container->getCustomName(), container->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::HOPPER, container->getCustomName(), container->getContainerSize(), container->hasCustomName()));
refreshContainer(containerMenu);
}
else
{
@ -1016,10 +1228,18 @@ bool ServerPlayer::openFurnace(shared_ptr<FurnaceTileEntity> furnace)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::FURNACE, furnace->getCustomName(), furnace->getContainerSize(), furnace->hasCustomName()));
containerMenu = new FurnaceMenu(inventory, furnace);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::FURNACE, furnace->getCustomName(), furnace->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::FURNACE, furnace->getCustomName(), furnace->getContainerSize(), furnace->hasCustomName()));
refreshContainer(containerMenu);
}
else
{
@ -1034,10 +1254,18 @@ bool ServerPlayer::openTrap(shared_ptr<DispenserTileEntity> trap)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, trap->GetType() == eTYPE_DROPPERTILEENTITY ? ContainerOpenPacket::DROPPER : ContainerOpenPacket::TRAP, trap->getCustomName(), trap->getContainerSize(), trap->hasCustomName()));
containerMenu = new TrapMenu(inventory, trap);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, trap->GetType() == eTYPE_DROPPERTILEENTITY ? ContainerOpenPacket::DROPPER : ContainerOpenPacket::TRAP, trap->getCustomName(), trap->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, trap->GetType() == eTYPE_DROPPERTILEENTITY ? ContainerOpenPacket::DROPPER : ContainerOpenPacket::TRAP, trap->getCustomName(), trap->getContainerSize(), trap->hasCustomName()));
refreshContainer(containerMenu);
}
else
{
@ -1052,10 +1280,18 @@ bool ServerPlayer::openBrewingStand(shared_ptr<BrewingStandTileEntity> brewingSt
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::BREWING_STAND, brewingStand->getCustomName(), brewingStand->getContainerSize(), brewingStand->hasCustomName()));
containerMenu = new BrewingStandMenu(inventory, brewingStand);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::BREWING_STAND, brewingStand->getCustomName(), brewingStand->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::BREWING_STAND, brewingStand->getCustomName(), brewingStand->getContainerSize(), brewingStand->hasCustomName()));
refreshContainer(containerMenu);
}
else
{
@ -1070,10 +1306,18 @@ bool ServerPlayer::openBeacon(shared_ptr<BeaconTileEntity> beacon)
if(containerMenu == inventoryMenu)
{
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::BEACON, beacon->getCustomName(), beacon->getContainerSize(), beacon->hasCustomName()));
containerMenu = new BeaconMenu(inventory, beacon);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::BEACON, beacon->getCustomName(), beacon->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::BEACON, beacon->getCustomName(), beacon->getContainerSize(), beacon->hasCustomName()));
refreshContainer(containerMenu);
}
else
{
@ -1093,7 +1337,15 @@ bool ServerPlayer::openTrading(shared_ptr<Merchant> traderTarget, const wstring
containerMenu->addSlotListener(this);
shared_ptr<Container> container = static_cast<MerchantMenu *>(containerMenu)->getTradeContainer();
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::TRADER_NPC, name.empty() ? L"" : name, container->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::TRADER_NPC, name.empty() ? L"" : name, container->getContainerSize(), !name.empty()));
refreshContainer(containerMenu);
MerchantRecipeList *offers = traderTarget->getOffers(dynamic_pointer_cast<Player>(shared_from_this()));
if (offers != nullptr)
@ -1123,10 +1375,18 @@ bool ServerPlayer::openHorseInventory(shared_ptr<EntityHorse> horse, shared_ptr<
closeContainer();
}
nextContainerCounter();
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::HORSE, horse->getCustomName(), container->getContainerSize(), container->hasCustomName(), horse->entityId));
containerMenu = new HorseInventoryMenu(inventory, container, horse);
containerMenu->containerId = containerCounter;
containerMenu->addSlotListener(this);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::HORSE, horse->getCustomName(), container->getContainerSize()))
{
doCloseContainer();
return true;
}
#endif
connection->send(std::make_shared<ContainerOpenPacket>(containerCounter, ContainerOpenPacket::HORSE, horse->getCustomName(), container->getContainerSize(), container->hasCustomName(), horse->entityId));
refreshContainer(containerMenu);
return true;
}

View file

@ -10,9 +10,16 @@
#include "..\Minecraft.World\net.minecraft.world.level.h"
#include "..\Minecraft.World\net.minecraft.world.level.chunk.h"
#include "..\Minecraft.World\net.minecraft.world.level.dimension.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.World\EnchantmentHelper.h"
#include "..\Minecraft.World\ExperienceOrb.h"
#include "..\Minecraft.Server\FourKitBridge.h"
#endif
#include "MultiPlayerLevel.h"
#include "LevelRenderer.h"
extern bool g_suppressExpDrops;
ServerPlayerGameMode::ServerPlayerGameMode(Level *level)
{
// 4J - added initialisers
@ -247,7 +254,44 @@ bool ServerPlayerGameMode::destroyBlock(int x, int y, int z)
int t = level->getTile(x, y, z);
int data = level->getData(x, y, z);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
int eventExp = 0;
if (!isCreative() && !gameModeForPlayer->isAdventureRestricted())
{
Tile *tile = Tile::tiles[t];
if (tile != nullptr && player->canDestroy(tile))
{
if (!EnchantmentHelper::hasSilkTouch(player))
{
// todo: shouldnt we get these values from the actual blocks?
if (t == Tile::coalOre_Id)
eventExp = Mth::nextInt(level->random, 0, 2);
else if (t == Tile::diamondOre_Id)
eventExp = Mth::nextInt(level->random, 3, 7);
else if (t == Tile::emeraldOre_Id)
eventExp = Mth::nextInt(level->random, 3, 7);
else if (t == Tile::lapisOre_Id)
eventExp = Mth::nextInt(level->random, 2, 5);
else if (t == Tile::netherQuartz_Id)
eventExp = Mth::nextInt(level->random, 2, 5);
else if (t == Tile::redStoneOre_Id || t == Tile::redStoneOre_lit_Id)
eventExp = 1 + level->random->nextInt(5);
else if (t == Tile::mobSpawner_Id)
eventExp = 15 + level->random->nextInt(15) + level->random->nextInt(15);
}
}
}
int dimId = level->dimension ? level->dimension->id : 0;
int breakResult = FourKitBridge::FireBlockBreak(player->entityId, dimId, x, y, z, t, data, eventExp);
if (breakResult < 0)
{
// Cancelled: send block correction to client
player->connection->send(std::make_shared<TileUpdatePacket>(x, y, z, level));
return false;
}
int finalExp = breakResult;
#endif
level->levelEvent(player, LevelEvent::PARTICLES_DESTROY_BLOCK, x, y, z, t + (level->getData(x, y, z) << Tile::TILE_NUM_SHIFT));
// 4J - In creative mode, the point where we need to tell the renderer that we are about to destroy a tile via destroyingTileAt is quite complicated.
@ -305,8 +349,25 @@ bool ServerPlayerGameMode::destroyBlock(int x, int y, int z)
}
}
if (changed && canDestroy)
{
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
g_suppressExpDrops = true;
#endif
Tile::tiles[t]->playerDestroy(level, player, x, y, z, data);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
g_suppressExpDrops = false;
if (finalExp > 0)
{
while (finalExp > 0)
{
int xpDrop = ExperienceOrb::getExperienceValue(finalExp);
finalExp -= xpDrop;
level->addEntity(std::make_shared<ExperienceOrb>(level, x + .5, y + .5, z + .5, xpDrop));
}
}
#endif
}
}
return changed;

View file

@ -8,6 +8,9 @@
#include "..\Minecraft.World\net.minecraft.world.level.h"
#include "..\Minecraft.World\net.minecraft.world.level.dimension.h"
#include "TeleportCommand.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\FourKitBridge.h"
#endif
EGameCommand TeleportCommand::getId()
{
@ -32,7 +35,21 @@ void TeleportCommand::execute(shared_ptr<CommandSender> source, byteArray comman
if(subject != nullptr && destination != nullptr && subject->level->dimension->id == destination->level->dimension->id && subject->isAlive() )
{
subject->ride(nullptr);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
double outX, outY, outZ;
bool cancelled = FourKitBridge::FirePlayerTeleport(subject->entityId,
subject->x, subject->y, subject->z, subject->dimension,
destination->x, destination->y, destination->z, destination->dimension,
1 /* COMMAND */,
&outX, &outY, &outZ);
if (cancelled)
return;
subject->connection->teleport(outX, outY, outZ, destination->yRot, destination->xRot);
}
#else
subject->connection->teleport(destination->x, destination->y, destination->z, destination->yRot, destination->xRot);
#endif
//logAdminAction(source, "commands.tp.success", subject->getAName(), destination->getAName());
logAdminAction(source, ChatPacket::e_ChatCommandTeleportSuccess, subject->getName(), eTYPE_SERVERPLAYER, destination->getName());

View file

@ -0,0 +1,26 @@
namespace Minecraft.Server.FourKit.Block;
/// <summary>
/// Represents the action type for a player interaction.
/// </summary>
public enum Action
{
/// <summary>Left-clicking the air.</summary>
LEFT_CLICK_AIR = 0,
/// <summary>Left-clicking a block.</summary>
LEFT_CLICK_BLOCK = 1,
/// <summary>Right-clicking the air.</summary>
RIGHT_CLICK_AIR = 2,
/// <summary>Right-clicking a block.</summary>
RIGHT_CLICK_BLOCK = 3,
/// <summary>
/// Stepping onto or into a block (Ass-pressure).
/// Examples: Jumping on soil, Standing on pressure plate,
/// Triggering redstone ore, Triggering tripwire.
/// </summary>
PHYSICAL = 4
}

View file

@ -0,0 +1,115 @@
namespace Minecraft.Server.FourKit.Block;
/// <summary>
/// Represents a block. This is a live object, and only one Block may exist for
/// any given location in a world.
/// </summary>
public class Block
{
private readonly World _world;
private readonly int _x;
private readonly int _y;
private readonly int _z;
internal Block(World world, int x, int y, int z)
{
_world = world;
_x = x;
_y = y;
_z = z;
}
/// <summary>
/// Gets the Location of the block.
/// </summary>
/// <returns>Location of the block.</returns>
public Location getLocation()
{
return new Location(_world, _x, _y, _z, 0f, 0f);
}
/// <summary>
/// Gets the type of this block.
/// </summary>
/// <returns>Block type.</returns>
public Material getType()
{
int id = getTypeId();
return Enum.IsDefined(typeof(Material), id) ? (Material)id : Material.AIR;
}
/// <summary>
/// Gets the type ID of this block.
/// </summary>
/// <returns>Block type ID.</returns>
public int getTypeId()
{
if (NativeBridge.GetTileId != null)
return NativeBridge.GetTileId(_world.getDimensionId(), _x, _y, _z);
return 0;
}
/// <summary>
/// Gets the world which contains this Block.
/// </summary>
/// <returns>World containing this block.</returns>
public World getWorld() => _world;
/// <summary>
/// Gets the x-coordinate of this block.
/// </summary>
/// <returns>X-coordinate.</returns>
public int getX() => _x;
/// <summary>
/// Gets the y-coordinate of this block.
/// </summary>
/// <returns>Y-coordinate.</returns>
public int getY() => _y;
/// <summary>
/// Gets the z-coordinate of this block.
/// </summary>
/// <returns>Z-coordinate.</returns>
public int getZ() => _z;
/// <summary>
/// Sets the type of this block.
/// </summary>
/// <param name="type">Material to change this block to.</param>
public void setType(Material type)
{
setTypeId((int)type);
}
/// <summary>
/// Sets the type ID of this block.
/// </summary>
/// <param name="type">Type ID to change this block to.</param>
/// <returns>Whether the change was successful.</returns>
public bool setTypeId(int type)
{
NativeBridge.SetTile?.Invoke(_world.getDimensionId(), _x, _y, _z, type, 0);
return true;
}
/// <summary>
/// Sets the metadata value for this block.
/// </summary>
/// <param name="data">New block specific metadata.</param>
public void setData(byte data)
{
NativeBridge.SetTileData?.Invoke(_world.getDimensionId(), _x, _y, _z, data);
}
/// <summary>
/// Breaks the block and spawns items as if a player had digged it.
/// </summary>
/// <returns>true if the block was destroyed.</returns>
public bool breakNaturally()
{
if (NativeBridge.BreakBlock != null)
return NativeBridge.BreakBlock(_world.getDimensionId(), _x, _y, _z) != 0;
return false;
}
}

View file

@ -0,0 +1,118 @@
namespace Minecraft.Server.FourKit.Block;
/// <summary>
/// Represents the face of a block.
/// </summary>
public enum BlockFace
{
DOWN = 0,
UP = 1,
NORTH = 2,
SOUTH = 3,
WEST = 4,
EAST = 5,
SELF = 6,
NORTH_EAST,
NORTH_WEST,
SOUTH_EAST,
SOUTH_WEST,
WEST_NORTH_WEST,
NORTH_NORTH_WEST,
NORTH_NORTH_EAST,
EAST_NORTH_EAST,
EAST_SOUTH_EAST,
SOUTH_SOUTH_EAST,
SOUTH_SOUTH_WEST,
WEST_SOUTH_WEST
}
public static class BlockFaceExtensions
{
/// <summary>
/// Get the amount of X-coordinates to modify to get the represented block.
/// </summary>
/// <param name="face">The block face.</param>
/// <returns>Amount of X-coordinates to modify.</returns>
public static int getModX(this BlockFace face) => face switch
{
BlockFace.EAST => 1,
BlockFace.WEST => -1,
BlockFace.NORTH_EAST => 1,
BlockFace.NORTH_WEST => -1,
BlockFace.SOUTH_EAST => 1,
BlockFace.SOUTH_WEST => -1,
BlockFace.EAST_NORTH_EAST => 1,
BlockFace.EAST_SOUTH_EAST => 1,
BlockFace.NORTH_NORTH_EAST => 1,
BlockFace.SOUTH_SOUTH_EAST => 1,
BlockFace.WEST_NORTH_WEST => -1,
BlockFace.WEST_SOUTH_WEST => -1,
BlockFace.NORTH_NORTH_WEST => -1,
BlockFace.SOUTH_SOUTH_WEST => -1,
_ => 0
};
/// <summary>
/// Get the amount of Y-coordinates to modify to get the represented block.
/// </summary>
/// <param name="face">The block face.</param>
/// <returns>Amount of Y-coordinates to modify.</returns>
public static int getModY(this BlockFace face) => face switch
{
BlockFace.UP => 1,
BlockFace.DOWN => -1,
_ => 0
};
/// <summary>
/// Get the amount of Z-coordinates to modify to get the represented block.
/// </summary>
/// <param name="face">The block face.</param>
/// <returns>Amount of Z-coordinates to modify.</returns>
public static int getModZ(this BlockFace face) => face switch
{
BlockFace.NORTH => -1,
BlockFace.SOUTH => 1,
BlockFace.NORTH_EAST => -1,
BlockFace.NORTH_WEST => -1,
BlockFace.SOUTH_EAST => 1,
BlockFace.SOUTH_WEST => 1,
BlockFace.NORTH_NORTH_EAST => -1,
BlockFace.NORTH_NORTH_WEST => -1,
BlockFace.EAST_NORTH_EAST => -1,
BlockFace.WEST_NORTH_WEST => -1,
BlockFace.EAST_SOUTH_EAST => 1,
BlockFace.WEST_SOUTH_WEST => 1,
BlockFace.SOUTH_SOUTH_EAST => 1,
BlockFace.SOUTH_SOUTH_WEST => 1,
_ => 0
};
/// <summary>
/// Gets the opposite face of this block face.
/// </summary>
/// <param name="face">The block face.</param>
/// <returns>The opposite block face.</returns>
public static BlockFace getOppositeFace(this BlockFace face) => face switch
{
BlockFace.NORTH => BlockFace.SOUTH,
BlockFace.SOUTH => BlockFace.NORTH,
BlockFace.EAST => BlockFace.WEST,
BlockFace.WEST => BlockFace.EAST,
BlockFace.UP => BlockFace.DOWN,
BlockFace.DOWN => BlockFace.UP,
BlockFace.NORTH_EAST => BlockFace.SOUTH_WEST,
BlockFace.NORTH_WEST => BlockFace.SOUTH_EAST,
BlockFace.SOUTH_EAST => BlockFace.NORTH_WEST,
BlockFace.SOUTH_WEST => BlockFace.NORTH_EAST,
BlockFace.WEST_NORTH_WEST => BlockFace.EAST_SOUTH_EAST,
BlockFace.NORTH_NORTH_WEST => BlockFace.SOUTH_SOUTH_EAST,
BlockFace.NORTH_NORTH_EAST => BlockFace.SOUTH_SOUTH_WEST,
BlockFace.EAST_NORTH_EAST => BlockFace.WEST_SOUTH_WEST,
BlockFace.EAST_SOUTH_EAST => BlockFace.WEST_NORTH_WEST,
BlockFace.SOUTH_SOUTH_EAST => BlockFace.NORTH_NORTH_WEST,
BlockFace.SOUTH_SOUTH_WEST => BlockFace.NORTH_NORTH_EAST,
BlockFace.WEST_SOUTH_WEST => BlockFace.EAST_NORTH_EAST,
_ => BlockFace.SELF
};
}

View file

@ -0,0 +1,32 @@
set(FOURKIT_PROJECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(FOURKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/bin/$<CONFIG>")
set(FOURKIT_CSPROJ "${FOURKIT_PROJECT_DIR}/Minecraft.Server.FourKit.csproj")
file(GLOB_RECURSE FOURKIT_SOURCES RELATIVE "${FOURKIT_PROJECT_DIR}" "${FOURKIT_PROJECT_DIR}/*.cs")
list(FILTER FOURKIT_SOURCES EXCLUDE REGEX "([/\\](obj|bin)[/\\])|^(obj|bin)[/\\]")
set(DOTNET_CONFIG "$<IF:$<CONFIG:Debug>,Debug,Release>")
foreach(src_file IN LISTS FOURKIT_SOURCES)
get_filename_component(src_path "${src_file}" PATH)
if(src_path)
string(REPLACE "/" "\\" group_path "${src_path}")
source_group("${group_path}" FILES "${FOURKIT_PROJECT_DIR}/${src_file}")
endif()
endforeach()
list(TRANSFORM FOURKIT_SOURCES PREPEND "${FOURKIT_PROJECT_DIR}/")
add_custom_target(Minecraft.Server.FourKit ALL
COMMAND dotnet build "${FOURKIT_CSPROJ}"
--configuration "${DOTNET_CONFIG}"
--output "${FOURKIT_OUTPUT_DIR}"
WORKING_DIRECTORY "${FOURKIT_PROJECT_DIR}"
SOURCES ${FOURKIT_SOURCES}
COMMENT "dotnet build Minecraft.Server.FourKit"
VERBATIM
)
set_target_properties(Minecraft.Server.FourKit PROPERTIES
OUTPUT_NAME "Minecraft.Server.FourKit"
)

View file

@ -0,0 +1,110 @@
namespace Minecraft.Server.FourKit.Command;
/// <summary>
/// Represents a Command, which executes various tasks upon user input.
/// </summary>
public abstract class Command
{
private string _name;
private string _description;
private string _usage;
private List<string> _aliases;
/// <summary>
/// Creates a new command with the given name and no aliases.
/// </summary>
/// <param name="name">Name of this command.</param>
protected Command(string name)
{
_name = name;
_description = string.Empty;
_usage = "/" + name;
_aliases = new List<string>();
}
/// <summary>
/// Creates a new command with the given name, description, and aliases.
/// </summary>
/// <param name="name">Name of this command.</param>
/// <param name="description">A brief description of this command.</param>
/// <param name="aliases">A list of aliases for this command.</param>
protected Command(string name, string description, List<string> aliases)
{
_name = name;
_description = description ?? string.Empty;
_usage = "/" + name;
_aliases = aliases ?? new List<string>();
}
/// <summary>
/// Executes the command, returning its success.
/// </summary>
/// <param name="sender">Source of the command.</param>
/// <param name="commandLabel">Alias of the command which was used.</param>
/// <param name="args">Passed command arguments.</param>
/// <returns><c>true</c> if the command was successful, otherwise <c>false</c>.</returns>
public abstract bool execute(CommandSender sender, string commandLabel, string[] args);
/// <summary>
/// Returns a list of active aliases of this command.
/// </summary>
/// <returns>List of aliases.</returns>
public List<string> getAliases() => new(_aliases);
/// <summary>
/// Gets a brief description of this command.
/// </summary>
/// <returns>Description of this command.</returns>
public string getDescription() => _description;
/// <summary>
/// Returns the current label for this command.
/// </summary>
/// <returns>Current label.</returns>
public string getLabel() => _name;
/// <summary>
/// Returns the name of this command.
/// </summary>
/// <returns>Name of this command.</returns>
public string getName() => _name;
/// <summary>
/// Gets an example usage of this command.
/// </summary>
/// <returns>Usage string.</returns>
public string getUsage() => _usage;
/// <summary>
/// Sets the list of aliases to request on registration for this command.
/// </summary>
/// <param name="aliases">Aliases to register.</param>
/// <returns>This command.</returns>
public Command setAliases(List<string> aliases)
{
_aliases = aliases ?? new List<string>();
return this;
}
/// <summary>
/// Sets a brief description of this command.
/// </summary>
/// <param name="description">New command description.</param>
/// <returns>This command.</returns>
public Command setDescription(string description)
{
_description = description ?? string.Empty;
return this;
}
/// <summary>
/// Sets the example usage of this command.
/// </summary>
/// <param name="usage">New example usage.</param>
/// <returns>This command.</returns>
public Command setUsage(string usage)
{
_usage = usage ?? string.Empty;
return this;
}
}

View file

@ -0,0 +1,17 @@
namespace Minecraft.Server.FourKit.Command;
/// <summary>
/// Represents a class which contains a single method for executing commands.
/// </summary>
public interface CommandExecutor
{
/// <summary>
/// Executes the given command, returning its success.
/// </summary>
/// <param name="sender">Source of the command.</param>
/// <param name="command">Command which was executed.</param>
/// <param name="label">Alias of the command which was used.</param>
/// <param name="args">Passed command arguments.</param>
/// <returns><c>true</c> if a valid command, otherwise <c>false</c>.</returns>
bool onCommand(CommandSender sender, Command command, string label, string[] args);
}

View file

@ -0,0 +1,25 @@
namespace Minecraft.Server.FourKit.Command;
/// <summary>
/// Represents something that can send commands and receive messages.
/// </summary>
public interface CommandSender
{
/// <summary>
/// Sends this sender a message.
/// </summary>
/// <param name="message">Message to be displayed.</param>
void sendMessage(string message);
/// <summary>
/// Sends this sender multiple messages.
/// </summary>
/// <param name="messages">An array of messages to be displayed.</param>
void sendMessage(string[] messages);
/// <summary>
/// Gets the name of this command sender.
/// </summary>
/// <returns>Name of the sender.</returns>
string getName();
}

View file

@ -0,0 +1,27 @@
namespace Minecraft.Server.FourKit.Command;
/// <summary>
/// Represents the server console as a command sender.
/// </summary>
public class ConsoleCommandSender : CommandSender
{
internal static readonly ConsoleCommandSender Instance = new();
private ConsoleCommandSender() { }
/// <inheritdoc/>
public void sendMessage(string message)
{
ServerLog.Info("console", message);
}
/// <inheritdoc/>
public void sendMessage(string[] messages)
{
foreach (var msg in messages)
sendMessage(msg);
}
/// <inheritdoc/>
public string getName() => "CONSOLE";
}

View file

@ -0,0 +1,44 @@
namespace Minecraft.Server.FourKit.Command;
/// <summary>
/// Represents a <see cref="Command"/> belonging to a plugin.
/// </summary>
public class PluginCommand : Command
{
private CommandExecutor? _executor;
// should this remain internal?
/// <summary>
/// Creates a new plugin command with the given name.
/// Use <see cref="FourKit.getCommand"/> to obtain instances.
/// </summary>
/// <param name="name">Name of this command.</param>
internal PluginCommand(string name) : base(name)
{
}
/// <inheritdoc/>
public override bool execute(CommandSender sender, string commandLabel, string[] args)
{
if (_executor != null)
return _executor.onCommand(sender, this, commandLabel, args);
return false;
}
/// <summary>
/// Gets the <see cref="CommandExecutor"/> associated with this command.
/// </summary>
/// <returns>The command executor, or <c>null</c>.</returns>
public CommandExecutor? getExecutor() => _executor;
/// <summary>
/// Sets the <see cref="CommandExecutor"/> to run when the command is dispatched.
/// </summary>
/// <param name="executor">New executor to set.</param>
/// <returns><c>true</c> if the executor was set.</returns>
public bool setExecutor(CommandExecutor executor)
{
_executor = executor;
return true;
}
}

View file

@ -0,0 +1,79 @@
namespace Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents an <see cref="Entity"/> that can take damage and has health.
/// </summary>
public class Damageable : Entity
{
private double _health = 20.0;
private double _maxHealth = 20.0;
private readonly double _originalMaxHealth = 20.0;
/// <summary>
/// Deals the given amount of damage to this entity.
/// This calls into the native server to apply real damage.
/// </summary>
/// <param name="amount">Amount of damage to deal.</param>
public void damage(double amount)
{
NativeBridge.DamagePlayer?.Invoke(getEntityId(), (float)amount);
}
/// <summary>
/// Gets the entity's health from 0 to <see cref="getMaxHealth"/>, where 0 is dead.
/// </summary>
/// <returns>The current health.</returns>
public double getHealth() => _health;
/// <summary>
/// Gets the maximum health this entity has.
/// </summary>
/// <returns>The maximum health.</returns>
public double getMaxHealth() => _maxHealth;
/// <summary>
/// Resets the max health to the original amount.
/// </summary>
public void resetMaxHealth()
{
_maxHealth = _originalMaxHealth;
if (_health > _maxHealth)
_health = _maxHealth;
}
/// <summary>
/// Sets the entity's health from 0 to <see cref="getMaxHealth"/>, where 0 is dead.
/// This calls into the native server to apply the health change.
/// </summary>
/// <param name="health">New health value.</param>
public void setHealth(double health)
{
NativeBridge.SetPlayerHealth?.Invoke(getEntityId(), (float)Math.Clamp(health, 0.0, _maxHealth));
}
/// <summary>
/// Sets the maximum health this entity can have.
/// If the entity's current health exceeds the new maximum, it is clamped.
/// </summary>
/// <param name="health">New maximum health value.</param>
public void setMaxHealth(double health)
{
_maxHealth = health;
if (_health > _maxHealth)
_health = _maxHealth;
}
// --- Internal setter used by the bridge ---
/// <summary>
/// Updates health directly. Called internally by the bridge.
/// </summary>
/// <param name="health">The new health value.</param>
internal void SetHealthInternal(double health) => _health = health;
/// <summary>
/// Updates max health directly. Called internally by the bridge.
/// </summary>
/// <param name="maxHealth">The new max health value.</param>
internal void SetMaxHealthInternal(double maxHealth) => _maxHealth = maxHealth;
}

View file

@ -0,0 +1,66 @@
namespace Minecraft.Server.FourKit.Entity;
// eh
/// <summary>
/// Enum representing the reason a player was disconnected from the server.
/// mirrored from <c>DisconnectPacket::eDisconnectReason</c>.
/// </summary>
public enum DisconnectReason
{
/// <summary>No specific reason.</summary>
NONE = 0,
/// <summary>The player quit voluntarily.</summary>
QUITTING = 1,
/// <summary>The connection was closed.</summary>
CLOSED = 2,
/// <summary>The login took too long.</summary>
LOGIN_TOO_LONG = 3,
/// <summary>The player had an illegal stance.</summary>
ILLEGAL_STANCE = 4,
/// <summary>The player had an illegal position.</summary>
ILLEGAL_POSITION = 5,
/// <summary>The player moved too quickly.</summary>
MOVED_TOO_QUICKLY = 6,
/// <summary>The player was flying when not allowed.</summary>
NO_FLYING = 7,
/// <summary>The player was kicked by an operator or plugin.</summary>
KICKED = 8,
/// <summary>The connection timed out.</summary>
TIME_OUT = 9,
/// <summary>Packet overflow.</summary>
OVERFLOW = 10,
/// <summary>End of stream reached unexpectedly.</summary>
END_OF_STREAM = 11,
/// <summary>The server is full.</summary>
SERVER_FULL = 12,
/// <summary>The server is outdated.</summary>
OUTDATED_SERVER = 13,
/// <summary>The client is outdated.</summary>
OUTDATED_CLIENT = 14,
/// <summary>An unexpected packet was received.</summary>
UNEXPECTED_PACKET = 15,
/// <summary>Connection creation failed.</summary>
CONNECTION_CREATION_FAILED = 16,
/// <summary>The host does not have multiplayer privileges.</summary>
NO_MULTIPLAYER_PRIVILEGES_HOST = 17,
/// <summary>The joining player does not have multiplayer privileges.</summary>
NO_MULTIPLAYER_PRIVILEGES_JOIN = 18,
/// <summary>All local players lack UGC permissions.</summary>
NO_UGC_ALL_LOCAL = 19,
/// <summary>A single local player lacks UGC permissions.</summary>
NO_UGC_SINGLE_LOCAL = 20,
/// <summary>All local players have content restrictions.</summary>
CONTENT_RESTRICTED_ALL_LOCAL = 21,
/// <summary>A single local player has content restrictions.</summary>
CONTENT_RESTRICTED_SINGLE_LOCAL = 22,
/// <summary>A remote player lacks UGC permissions.</summary>
NO_UGC_REMOTE = 23,
/// <summary>No friends in the game.</summary>
NO_FRIENDS_IN_GAME = 24,
/// <summary>The player was banned.</summary>
BANNED = 25,
/// <summary>The player is not friends with the host.</summary>
NOT_FRIENDS_WITH_HOST = 26,
/// <summary>NAT type mismatch.</summary>
NAT_MISMATCH = 27,
}

View file

@ -0,0 +1,91 @@
namespace Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents a base entity in the world
/// </summary>
public abstract class Entity
{
private Location _location = new();
private Guid _uniqueId = Guid.NewGuid();
private float _fallDistance;
private int _dimensionId;
private int _entityId;
private EntityType _entityType = EntityType.UNKNOWN;
/// <summary>
/// Gets the entity's current position.
/// </summary>
/// <returns>a new copy of <see cref="Location"/> containing the position of this entity</returns>
public Location getLocation() => _location;
/// <summary>
/// Returns a unique id for this entity
/// </summary>
/// <returns>Entity id</returns>
public virtual int getEntityId() => _entityId;
/// <summary>
/// Get the type of the entity.
/// </summary>
/// <returns>The <see cref="EntityType"/> of this entity.</returns>
public new virtual EntityType getType() => _entityType;
public new virtual EntityType GetType() => _entityType;
/// <summary>
/// Returns a unique and persistent id for this entity. Note that this is not the standard UUID for players.
/// </summary>
/// <returns>A <see cref="Guid"/> unique to this entity.</returns>
public Guid getUniqueId() => _uniqueId;
/// <summary>
/// Teleports this entity to the given location.
/// This calls into the native server to perform the actual teleport.
/// </summary>
/// <param name="location">The destination location.</param>
/// <returns><c>true</c> if the teleport was successful.</returns>
public virtual bool teleport(Location location)
{
NativeBridge.TeleportEntity?.Invoke(getEntityId(), _dimensionId, location.getX(), location.getY(), location.getZ());
SetLocation(location);
return true;
}
/// <summary>
/// Sets the fall distance for this entity.
/// </summary>
/// <param name="distance">The fall distance value.</param>
public void setFallDistance(float distance)
{
_fallDistance = distance;
NativeBridge.SetFallDistance?.Invoke(getEntityId(), distance);
}
/// <summary>
/// Returns the distance this entity has fallen.
/// </summary>
/// <returns>The current fall distance.</returns>
public float getFallDistance() => _fallDistance;
/// <summary>
/// Gets the current world this entity resides in.
/// </summary>
/// <returns>World containing this entity.</returns>
public World getWorld() => FourKit.getWorld(_dimensionId);
// INTERNAL
internal void SetLocation(Location location)
{
_location = location;
}
internal void SetFallDistanceInternal(float distance) => _fallDistance = distance;
internal void SetUniqueId(Guid id)
{
_uniqueId = id;
}
internal void SetDimensionInternal(int dimensionId) => _dimensionId = dimensionId;
internal void SetEntityIdInternal(int entityId) => _entityId = entityId;
internal void SetEntityTypeInternal(EntityType entityType) => _entityType = entityType;
}

View file

@ -0,0 +1,132 @@
namespace Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents the type of an <see cref="Entity"/>.
/// </summary>
public enum EntityType
{
/// <summary>An arrow projectile; may get stuck in the ground.</summary>
ARROW,
/// <summary>A bat.</summary>
BAT,
/// <summary>A blaze.</summary>
BLAZE,
/// <summary>A placed boat.</summary>
BOAT,
/// <summary>A cave spider.</summary>
CAVE_SPIDER,
/// <summary>A chicken.</summary>
CHICKEN,
/// <summary>A complex entity part.</summary>
COMPLEX_PART,
/// <summary>A cow.</summary>
COW,
/// <summary>A creeper.</summary>
CREEPER,
/// <summary>An item resting on the ground.</summary>
DROPPED_ITEM,
/// <summary>A flying chicken egg.</summary>
EGG,
/// <summary>An ender crystal.</summary>
ENDER_CRYSTAL,
/// <summary>An ender dragon.</summary>
ENDER_DRAGON,
/// <summary>A flying ender pearl.</summary>
ENDER_PEARL,
/// <summary>An ender eye signal.</summary>
ENDER_SIGNAL,
/// <summary>An enderman.</summary>
ENDERMAN,
/// <summary>An experience orb.</summary>
EXPERIENCE_ORB,
/// <summary>A block that is going to or is about to fall.</summary>
FALLING_BLOCK,
/// <summary>A flying large fireball, as thrown by a Ghast for example.</summary>
FIREBALL,
/// <summary>A firework rocket.</summary>
FIREWORK,
/// <summary>A fishing line and bobber.</summary>
FISHING_HOOK,
/// <summary>A ghast.</summary>
GHAST,
/// <summary>A giant.</summary>
GIANT,
/// <summary>A horse.</summary>
HORSE,
/// <summary>An iron golem.</summary>
IRON_GOLEM,
/// <summary>An item frame on a wall.</summary>
ITEM_FRAME,
/// <summary>A leash attached to a fencepost.</summary>
LEASH_HITCH,
/// <summary>A bolt of lightning.</summary>
LIGHTNING,
/// <summary>A magma cube.</summary>
MAGMA_CUBE,
/// <summary>A minecart.</summary>
MINECART,
/// <summary>A minecart with a chest.</summary>
MINECART_CHEST,
/// <summary>A minecart with a command block.</summary>
MINECART_COMMAND,
/// <summary>A minecart with a furnace.</summary>
MINECART_FURNACE,
/// <summary>A minecart with a hopper.</summary>
MINECART_HOPPER,
/// <summary>A minecart with a mob spawner.</summary>
MINECART_MOB_SPAWNER,
/// <summary>A minecart with TNT.</summary>
MINECART_TNT,
/// <summary>A mooshroom.</summary>
MUSHROOM_COW,
/// <summary>An ocelot.</summary>
OCELOT,
/// <summary>A painting on a wall.</summary>
PAINTING,
/// <summary>A pig.</summary>
PIG,
/// <summary>A zombie pigman.</summary>
PIG_ZOMBIE,
/// <summary>A player.</summary>
PLAYER,
/// <summary>Primed TNT that is about to explode.</summary>
PRIMED_TNT,
/// <summary>A sheep.</summary>
SHEEP,
/// <summary>A silverfish.</summary>
SILVERFISH,
/// <summary>A skeleton.</summary>
SKELETON,
/// <summary>A slime.</summary>
SLIME,
/// <summary>A flying small fireball, such as thrown by a Blaze or player.</summary>
SMALL_FIREBALL,
/// <summary>A flying snowball.</summary>
SNOWBALL,
/// <summary>A snowman.</summary>
SNOWMAN,
/// <summary>A spider.</summary>
SPIDER,
/// <summary>A flying splash potion.</summary>
SPLASH_POTION,
/// <summary>A squid.</summary>
SQUID,
/// <summary>A flying experience bottle.</summary>
THROWN_EXP_BOTTLE,
/// <summary>An unknown entity without an Entity Class.</summary>
UNKNOWN,
/// <summary>A villager.</summary>
VILLAGER,
/// <summary>A weather entity.</summary>
WEATHER,
/// <summary>A witch.</summary>
WITCH,
/// <summary>A wither.</summary>
WITHER,
/// <summary>A flying wither skull projectile.</summary>
WITHER_SKULL,
/// <summary>A wolf.</summary>
WOLF,
/// <summary>A zombie.</summary>
ZOMBIE,
}

View file

@ -0,0 +1,157 @@
namespace Minecraft.Server.FourKit.Entity;
using System.Runtime.InteropServices;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents a human entity in the world (e.g. a player).
/// </summary>
public abstract class HumanEntity : LivingEntity, InventoryHolder
{
private GameMode _gameMode = GameMode.SURVIVAL;
private string _name = string.Empty;
internal PlayerInventory _playerInventory = new();
internal Inventory _enderChestInventory = new("Ender Chest", InventoryType.ENDER_CHEST, 27);
private ItemStack? _cursorItem;
/// <summary>
/// Gets this human's current <see cref="GameMode"/>.
/// </summary>
/// <returns>The current game mode.</returns>
public GameMode getGameMode() => _gameMode;
/// <summary>
/// Returns the name of this player.
/// </summary>
/// <returns>The display name.</returns>
public string getName() => _name;
/// <summary>
/// Sets this human's current <see cref="GameMode"/>.
/// </summary>
/// <param name="mode">The new game mode.</param>
public void setGameMode(GameMode mode)
{
NativeBridge.SetPlayerGameMode?.Invoke(getEntityId(), (int)mode);
}
/// <summary>
/// Get the player's inventory.
/// </summary>
/// <returns>The inventory of the player, this also contains the armor slots.</returns>
Inventory InventoryHolder.getInventory() => getInventory();
/// <summary>
/// Get the player's inventory.
/// This also contains the armor slots.
/// </summary>
/// <returns>The player's inventory.</returns>
public PlayerInventory getInventory()
{
return _playerInventory;
}
/// <summary>
/// Get the player's EnderChest inventory.
/// </summary>
/// <returns>The EnderChest of the player.</returns>
public Inventory getEnderChest()
{
return _enderChestInventory;
}
/// <summary>
/// Returns the ItemStack currently in your hand, can be empty.
/// </summary>
/// <returns>The ItemStack of the item you are currently holding.</returns>
public ItemStack? getItemInHand()
{
return _playerInventory.getItemInHand();
}
/// <summary>
/// Sets the item to the given ItemStack, this will replace whatever the
/// user was holding.
/// </summary>
/// <param name="item">The ItemStack which will end up in the hand.</param>
public void setItemInHand(ItemStack? item)
{
_playerInventory.setItemInHand(item);
}
/// <summary>
/// Returns the ItemStack currently on your cursor, can be empty.
/// Will always be empty if the player currently has no open window.
/// </summary>
/// <returns>The ItemStack of the item you are currently moving around.</returns>
public ItemStack? getItemOnCursor() => _cursorItem;
/// <summary>
/// Sets the item to the given ItemStack, this will replace whatever the
/// user was moving. Will always be empty if the player currently has no open window.
/// </summary>
/// <param name="item">The ItemStack which will end up in the hand.</param>
public void setItemOnCursor(ItemStack? item) => _cursorItem = item;
/// <summary>
/// If the player currently has an inventory window open, this method will
/// close it on both the server and client side.
/// </summary>
public void closeInventory()
{
NativeBridge.CloseContainer?.Invoke(getEntityId());
}
/// <summary>
/// Opens an inventory window with the specified inventory on the top.
/// </summary>
/// <param name="inventory">The inventory to open.</param>
/// <returns>The newly opened InventoryView, or null if it could not be opened.</returns>
public InventoryView? openInventory(Inventory inventory)
{
if (NativeBridge.OpenVirtualContainer == null)
return null;
closeInventory();
int nativeType = inventory.getType() switch
{
InventoryType.CHEST => 0,
InventoryType.DISPENSER => 3,
InventoryType.DROPPER => 10,
InventoryType.HOPPER => 5,
_ => 0,
};
int size = inventory.getSize();
int[] buf = new int[size * 3];
for (int i = 0; i < size; i++)
{
var item = inventory._items[i];
buf[i * 3] = item?.getTypeId() ?? 0;
buf[i * 3 + 1] = item?.getAmount() ?? 0;
buf[i * 3 + 2] = item?.getDurability() ?? 0;
}
string title = inventory.getName();
int titleByteLen = System.Text.Encoding.UTF8.GetByteCount(title);
IntPtr titlePtr = Marshal.StringToCoTaskMemUTF8(title);
var gh = GCHandle.Alloc(buf, GCHandleType.Pinned);
try
{
NativeBridge.OpenVirtualContainer(getEntityId(), nativeType, titlePtr, titleByteLen, size, gh.AddrOfPinnedObject());
}
finally
{
gh.Free();
Marshal.FreeCoTaskMem(titlePtr);
}
var view = new InventoryView(inventory, getInventory(), this, inventory.getType());
return view;
}
internal void SetGameModeInternal(GameMode mode) => _gameMode = mode;
internal void SetNameInternal(string name) => _name = name;
}

View file

@ -0,0 +1,32 @@
namespace Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents a dropped item on the ground.
/// </summary>
public class Item : Entity
{
private ItemStack _itemStack;
internal Item(int entityId, int dimId, double x, double y, double z, ItemStack itemStack)
{
SetEntityIdInternal(entityId);
SetEntityTypeInternal(EntityType.DROPPED_ITEM);
SetDimensionInternal(dimId);
SetLocation(new Location(FourKit.getWorld(dimId), x, y, z));
_itemStack = itemStack;
}
/// <summary>
/// Gets the item stack associated with this item.
/// </summary>
/// <returns>An item stack.</returns>
public ItemStack getItemStack() => _itemStack;
/// <summary>
/// Sets the item stack of this item.
/// </summary>
/// <param name="stack">The new item stack.</param>
public void setItemStack(ItemStack stack) => _itemStack = stack;
}

View file

@ -0,0 +1,51 @@
namespace Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents a living entity in the world that has health and can take damage.
/// </summary>
public class LivingEntity : Damageable
{
private double _eyeHeight = 1.62;
internal LivingEntity() { }
internal LivingEntity(int entityId, EntityType entityType, int dimId, double x, double y, double z,
float health = 20f, float maxHealth = 20f)
{
SetEntityIdInternal(entityId);
SetEntityTypeInternal(entityType);
SetDimensionInternal(dimId);
SetLocation(new Location(FourKit.getWorld(dimId), x, y, z));
if (maxHealth > 0)
SetMaxHealthInternal(maxHealth);
SetHealthInternal(health);
}
/// <summary>
/// Gets the height of the living entity's eyes above its <see cref="Location"/>.
/// </summary>
/// <returns>The eye height.</returns>
public double getEyeHeight() => _eyeHeight;
/// <summary>
/// Gets the height of the living entity's eyes above its <see cref="Location"/>.
/// </summary>
/// <param name="ignoreSneaking">If <c>true</c>, returns the standing eye height regardless of sneak state.</param>
/// <returns>The eye height.</returns>
public double getEyeHeight(bool ignoreSneaking)
{
if (ignoreSneaking)
return _eyeHeight;
// When sneaking the eye height is slightly lower
return _eyeHeight - 0.08;
}
// --- Internal setter used by the bridge ---
/// <summary>
/// Updates the eye height. Called internally by the bridge.
/// </summary>
/// <param name="eyeHeight">The new eye height.</param>
internal void SetEyeHeightInternal(double eyeHeight) => _eyeHeight = eyeHeight;
}

View file

@ -0,0 +1,26 @@
namespace Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents a player identity that may or may not currently be online.
/// </summary>
public interface OfflinePlayer
{
/// <summary>Returns the name of this player.</summary>
/// <returns>The player's name.</returns>
string getName();
/// <summary>Gets a Player object that this represents, if there is one.</summary>
/// <returns>A <see cref="Player"/> instance if the player is online; otherwise <c>null</c>.</returns>
Player? getPlayer();
/// <summary>
/// Returns the UUID that uniquely identifies this player across sessions.
/// This is the player-specific UUID, not the entity UUID.
/// </summary>
/// <returns>The player's unique identifier.</returns>
Guid getUniqueId();
/// <summary>Checks if this player is currently online.</summary>
/// <returns><c>true</c> if the player is online; otherwise <c>false</c>.</returns>
bool isOnline();
}

View file

@ -0,0 +1,209 @@
namespace Minecraft.Server.FourKit.Entity;
using System.Runtime.InteropServices;
using Minecraft.Server.FourKit.Command;
using Minecraft.Server.FourKit.Inventory;
using Minecraft.Server.FourKit.Net;
/// <summary>
/// Represents a player connected to the server.
/// </summary>
public class Player : HumanEntity, OfflinePlayer, CommandSender
{
private float _saturation = 5.0f;
private float _walkSpeed = 0.2f;
private Guid _playerUniqueId;
private string? _displayName;
internal bool IsOnline { get; set; }
internal Player(int entityId, string name)
{
SetEntityIdInternal(entityId);
SetEntityTypeInternal(EntityType.PLAYER);
SetNameInternal(name);
IsOnline = true;
_playerInventory._holder = this;
}
/// <inheritdoc/>
public override EntityType getType() => EntityType.PLAYER;
/// <inheritdoc/>
public override EntityType GetType() => EntityType.PLAYER;
/// <inheritdoc/>
public override bool teleport(Location location)
{
NativeBridge.TeleportPlayer?.Invoke(getEntityId(), location.X, location.Y, location.Z);
SetLocation(location);
return true;
}
/// <inheritdoc/>
public Player? getPlayer() => IsOnline ? this : null;
/// <summary>
/// Gets the "friendly" name to display of this player.
/// This may include color. If no custom display name has been set,
/// this returns the player's <see cref="HumanEntity.getName"/>.
/// </summary>
/// <returns>The display name.</returns>
public string getDisplayName() => _displayName ?? getName();
/// <summary>
/// Sets the "friendly" name to display of this player.
/// </summary>
/// <param name="name">The display name, or <c>null</c> to reset to <see cref="HumanEntity.getName"/>.</param>
public void setDisplayName(string? name)
{
_displayName = name;
}
/// <inheritdoc/>
public bool isOnline() => IsOnline;
/// <summary>
/// Returns the UUID that uniquely identifies this player across sessions.
/// This is the player-specific UUID, not the entity UUID.
/// </summary>
/// <returns>The player's unique identifier.</returns>
public new Guid getUniqueId() => _playerUniqueId;
/// <summary>
/// Gets the player's current saturation level.
/// Saturation acts as a buffer before hunger begins to deplete.
/// </summary>
/// <returns>The current saturation level.</returns>
public float getSaturation() => _saturation;
/// <summary>
/// Gets the current allowed speed that a client can walk.
/// The default value is 0.2.
/// </summary>
/// <returns>The current walk speed.</returns>
public float getWalkSpeed() => _walkSpeed;
/// <summary>
/// Sets the speed at which a client will walk.
/// This calls into the native server to apply the change.
/// </summary>
/// <param name="value">The new walk speed.</param>
public void setWalkSpeed(float value)
{
_walkSpeed = value;
NativeBridge.SetWalkSpeed?.Invoke(getEntityId(), value);
}
/// <inheritdoc/>
public void sendMessage(string message)
{
if (string.IsNullOrEmpty(message) || NativeBridge.SendMessage == null)
return;
if (message.Length > FourKit.MAX_CHAT_LENGTH)
message = message[..FourKit.MAX_CHAT_LENGTH];
IntPtr ptr = Marshal.StringToCoTaskMemUTF8(message);
try
{
NativeBridge.SendMessage(getEntityId(), ptr, System.Text.Encoding.UTF8.GetByteCount(message));
}
finally
{
Marshal.FreeCoTaskMem(ptr);
}
}
/// <inheritdoc/>
public void sendMessage(string[] messages)
{
foreach (var msg in messages)
sendMessage(msg);
}
/// <summary>
/// Kicks player with the default <see cref="DisconnectReason.KICKED"/> reason.
/// </summary>
public void kickPlayer()
{
NativeBridge.KickPlayer?.Invoke(getEntityId(), (int)DisconnectReason.KICKED);
}
/// <summary>
/// Bans the player by UID with the specified reason and disconnects them.
/// </summary>
/// <param name="reason">The ban reason.</param>
/// <returns><c>true</c> if the ban was applied successfully.</returns>
public bool banPlayer(string reason)
{
if (NativeBridge.BanPlayer == null) return false;
IntPtr ptr = Marshal.StringToCoTaskMemUTF8(reason ?? string.Empty);
try
{
int byteLen = System.Text.Encoding.UTF8.GetByteCount(reason ?? string.Empty);
return NativeBridge.BanPlayer(getEntityId(), ptr, byteLen) != 0;
}
finally
{
Marshal.FreeCoTaskMem(ptr);
}
}
/// <summary>
/// Bans the player's IP address with the specified reason.
/// </summary>
/// <param name="reason">The ban reason.</param>
/// <returns><c>true</c> if the IP ban was applied successfully.</returns>
public bool banPlayerIp(string reason)
{
if (NativeBridge.BanPlayerIp == null) return false;
IntPtr ptr = Marshal.StringToCoTaskMemUTF8(reason ?? string.Empty);
try
{
int byteLen = System.Text.Encoding.UTF8.GetByteCount(reason ?? string.Empty);
return NativeBridge.BanPlayerIp(getEntityId(), ptr, byteLen) != 0;
}
finally
{
Marshal.FreeCoTaskMem(ptr);
}
}
/// <summary>
/// Gets the socket address of this player.
/// </summary>
/// <returns>The player's socket address, or <c>null</c> if the address could not be determined.</returns>
public InetSocketAddress? getAddress()
{
if (NativeBridge.GetPlayerAddress == null)
return null;
const int ipBufSize = 64;
IntPtr ipBuf = Marshal.AllocCoTaskMem(ipBufSize);
IntPtr portBuf = Marshal.AllocCoTaskMem(sizeof(int));
try
{
int result = NativeBridge.GetPlayerAddress(getEntityId(), ipBuf, ipBufSize, portBuf);
if (result == 0)
return null;
string? ip = Marshal.PtrToStringAnsi(ipBuf);
int port = Marshal.ReadInt32(portBuf);
if (string.IsNullOrEmpty(ip))
return null;
return new InetSocketAddress(new InetAddress(ip), port);
}
finally
{
Marshal.FreeCoTaskMem(ipBuf);
Marshal.FreeCoTaskMem(portBuf);
}
}
// INTERNAL
internal void SetSaturationInternal(float saturation) => _saturation = saturation;
internal void SetWalkSpeedInternal(float walkSpeed) => _walkSpeed = walkSpeed;
internal void SetPlayerUniqueIdInternal(Guid id) => _playerUniqueId = id;
}

View file

@ -0,0 +1,57 @@
namespace Minecraft.Server.FourKit.Event.Block;
using Minecraft.Server.FourKit.Block;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Called when a block is broken by a player.
///
/// If you wish to have the block drop experience, you must set the experience
/// value above 0. By default, experience will be set in the event if:
/// <list type="bullet">
/// <item><description>The player is not in creative or adventure mode</description></item>
/// <item><description>The player can loot the block (ie: does not destroy it completely, by using the correct tool)</description></item>
/// <item><description>The player does not have silk touch</description></item>
/// <item><description>The block drops experience in vanilla Minecraft</description></item>
/// </list>
///
/// Note: Plugins wanting to simulate a traditional block drop should set the
/// block to air and utilize their own methods for determining what the default
/// drop for the block being broken is and what to do about it, if anything.
///
/// If a Block Break event is cancelled, the block will not break and experience
/// will not drop.
/// </summary>
public class BlockBreakEvent : BlockExpEvent, Cancellable
{
private readonly Player _player;
private bool _cancel;
internal BlockBreakEvent(Block block, Player player, int exp)
: base(block, exp)
{
_player = player;
_cancel = false;
}
/// <summary>
/// Gets the Player that is breaking the block involved in this event.
/// </summary>
/// <returns>The Player that is breaking the block involved in this event.</returns>
public Player getPlayer() => _player;
/// <summary>
/// Gets the cancellation state of this event.
/// </summary>
/// <returns><c>true</c> if this event is cancelled.</returns>
public bool isCancelled() => _cancel;
/// <summary>
/// Sets the cancellation state of this event. A cancelled event will not be
/// executed in the server, but will still pass to other plugins.
/// </summary>
/// <param name="cancel"><c>true</c> if you wish to cancel this event.</param>
public void setCancelled(bool cancel)
{
_cancel = cancel;
}
}

View file

@ -0,0 +1,22 @@
namespace Minecraft.Server.FourKit.Event.Block;
using Minecraft.Server.FourKit.Block;
/// <summary>
/// Represents a Block-related event.
/// </summary>
public abstract class BlockEvent : Event
{
private readonly Block _block;
internal protected BlockEvent(Block block)
{
_block = block;
}
/// <summary>
/// Gets the block involved in this event.
/// </summary>
/// <returns>The Block which is involved in this event.</returns>
public Block getBlock() => _block;
}

View file

@ -0,0 +1,31 @@
namespace Minecraft.Server.FourKit.Event.Block;
using Minecraft.Server.FourKit.Block;
/// <summary>
/// An event that is called when a block yields experience.
/// </summary>
public class BlockExpEvent : BlockEvent
{
private int _exp;
internal BlockExpEvent(Block block, int exp)
: base(block)
{
_exp = exp;
}
/// <summary>
/// Get the experience dropped by the block after the event has processed.
/// </summary>
/// <returns>The experience to drop.</returns>
public int getExpToDrop() => _exp;
/// <summary>
/// Set the amount of experience dropped by the block after the event has processed.
/// </summary>
/// <param name="exp">1 or higher to drop experience, else nothing will drop.</param>
public void setExpToDrop(int exp)
{
_exp = exp;
}
}

View file

@ -0,0 +1,58 @@
namespace Minecraft.Server.FourKit.Event.Block;
using Minecraft.Server.FourKit.Block;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Called when a block is placed by a player.
/// </summary>
public class BlockPlaceEvent : BlockEvent, Cancellable
{
protected Block placedAgainst;
protected ItemStack itemInHand;
protected Player player;
protected bool canBuild;
protected bool cancel;
internal BlockPlaceEvent(Block placedBlock, Block placedAgainst, ItemStack itemInHand, Player thePlayer, bool canBuild)
: base(placedBlock)
{
this.placedAgainst = placedAgainst;
this.itemInHand = itemInHand;
this.player = thePlayer;
this.canBuild = canBuild;
this.cancel = false;
}
/// <summary>
/// Gets the player who placed the block involved in this event.
/// </summary>
/// <returns>The Player who placed the block involved in this event.</returns>
public Player getPlayer() => player;
/// <summary>
/// Clarity method for getting the placed block. Not really needed except
/// for reasons of clarity.
/// </summary>
/// <returns>The Block that was placed.</returns>
public Block getBlockPlaced() => getBlock();
/// <summary>
/// Gets the block that this block was placed against.
/// </summary>
/// <returns>Block the block that the new block was placed against.</returns>
public Block getBlockAgainst() => placedAgainst;
/// <summary>
/// Gets the item in the player's hand when they placed the block.
/// </summary>
/// <returns>The ItemStack for the item in the player's hand when they placed the block.</returns>
public ItemStack getItemInHand() => itemInHand;
/// <inheritdoc />
public bool isCancelled() => cancel;
/// <inheritdoc />
public void setCancelled(bool cancel) => this.cancel = cancel;
}

View file

@ -0,0 +1,65 @@
namespace Minecraft.Server.FourKit.Event.Block;
using Minecraft.Server.FourKit.Block;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Called when a sign is changed by a player.
/// </summary>
public class SignChangeEvent : BlockEvent, Cancellable
{
private readonly Player _player;
private readonly string[] _lines;
private bool _cancel;
internal SignChangeEvent(Block theBlock, Player thePlayer, string[] theLines)
: base(theBlock)
{
_player = thePlayer;
_lines = theLines;
_cancel = false;
}
/// <summary>
/// Gets the player changing the sign involved in this event.
/// </summary>
/// <returns>The Player involved in this event.</returns>
public Player getPlayer() => _player;
/// <summary>
/// Gets all of the lines of text from the sign involved in this event.
/// </summary>
/// <returns>The String array for the sign's lines new text.</returns>
public string[] getLines() => _lines;
/// <summary>
/// Gets a single line of text from the sign involved in this event.
/// </summary>
/// <param name="index">Index of the line to get.</param>
/// <returns>The String containing the line of text associated with the provided index.</returns>
/// <exception cref="IndexOutOfRangeException">Thrown when the provided index is &gt; 3 or &lt; 0.</exception>
public string getLine(int index)
{
if (index < 0 || index > 3)
throw new IndexOutOfRangeException($"Line index must be between 0 and 3, got {index}");
return _lines[index];
}
/// <summary>
/// Sets a single line for the sign involved in this event.
/// </summary>
/// <param name="index">Index of the line to set.</param>
/// <param name="line">Text to set.</param>
/// <exception cref="IndexOutOfRangeException">Thrown when the provided index is &gt; 3 or &lt; 0.</exception>
public void setLine(int index, string line)
{
if (index < 0 || index > 3)
throw new IndexOutOfRangeException($"Line index must be between 0 and 3, got {index}");
_lines[index] = line;
}
/// <inheritdoc />
public bool isCancelled() => _cancel;
/// <inheritdoc />
public void setCancelled(bool cancel) => _cancel = cancel;
}

View file

@ -0,0 +1,14 @@
namespace Minecraft.Server.FourKit.Event;
/// <summary>
/// Interface for events that can be cancelled by a plugin.
/// When cancelled, the server will skip the default action.
/// </summary>
public interface Cancellable
{
/// <summary>Gets whether this event is cancelled.</summary>
bool isCancelled();
/// <summary>Sets whether this event is cancelled.</summary>
void setCancelled(bool cancel);
}

View file

@ -0,0 +1,22 @@
namespace Minecraft.Server.FourKit.Event.Entity;
using FourKitEntity = Minecraft.Server.FourKit.Entity;
/// <summary>
/// Called when an entity is damaged by an entity.
/// </summary>
public class EntityDamageByEntityEvent : EntityDamageEvent
{
private readonly FourKitEntity.Entity _damager;
internal EntityDamageByEntityEvent(FourKitEntity.Entity damager, FourKitEntity.Entity damagee, EntityDamageEvent.DamageCause cause, double damage)
: base(damagee, cause, damage)
{
_damager = damager;
}
/// <summary>
/// Returns the entity that damaged the defender.
/// </summary>
/// <returns>The Entity that damaged the defender.</returns>
public FourKitEntity.Entity getDamager() => _damager;
}

View file

@ -0,0 +1,117 @@
namespace Minecraft.Server.FourKit.Event.Entity;
using FourKitEntity = Minecraft.Server.FourKit.Entity;
/// <summary>
/// Stores data for damage events.
/// </summary>
public class EntityDamageEvent : EntityEvent, Cancellable
{
/// <summary>
/// An enum to specify the cause of the damage.
/// </summary>
public enum DamageCause
{
/// <summary>Damage caused by being in the area when a block explodes.</summary>
BLOCK_EXPLOSION,
/// <summary>Damage caused when an entity contacts a block such as a Cactus.</summary>
CONTACT,
/// <summary>Custom damage.</summary>
CUSTOM,
/// <summary>Damage caused by running out of air while in water.</summary>
DROWNING,
/// <summary>Damage caused when an entity attacks another entity.</summary>
ENTITY_ATTACK,
/// <summary>Damage caused by being in the area when an entity, such as a Creeper, explodes.</summary>
ENTITY_EXPLOSION,
/// <summary>Damage caused when an entity falls a distance greater than 3 blocks.</summary>
FALL,
/// <summary>Damage caused by being hit by a falling block which deals damage.</summary>
FALLING_BLOCK,
/// <summary>Damage caused by direct exposure to fire.</summary>
FIRE,
/// <summary>Damage caused due to burns caused by fire.</summary>
FIRE_TICK,
/// <summary>Damage caused by direct exposure to lava.</summary>
LAVA,
/// <summary>Damage caused by being struck by lightning.</summary>
LIGHTNING,
/// <summary>Damage caused by being hit by a damage potion or spell.</summary>
MAGIC,
/// <summary>Damage caused due to a snowman melting.</summary>
MELTING,
/// <summary>Damage caused due to an ongoing poison effect.</summary>
POISON,
/// <summary>Damage caused when attacked by a projectile.</summary>
PROJECTILE,
/// <summary>Damage caused by starving due to having an empty hunger bar.</summary>
STARVATION,
/// <summary>Damage caused by being put in a block.</summary>
SUFFOCATION,
/// <summary>Damage caused by committing suicide using the command "/kill".</summary>
SUICIDE,
/// <summary>Damage caused in retaliation to another attack by the Thorns enchantment.</summary>
THORNS,
/// <summary>Damage caused by falling into the void.</summary>
VOID,
/// <summary>Damage caused by Wither potion effect.</summary>
WITHER,
}
private readonly DamageCause _cause;
private double _damage;
private readonly double _finalDamage;
private bool _cancel;
internal EntityDamageEvent(FourKitEntity.Entity damagee, DamageCause cause, double damage)
: base(damagee)
{
_cause = cause;
_damage = damage;
_finalDamage = damage;
_cancel = false;
}
/// <summary>
/// Gets the cause of the damage.
/// </summary>
/// <returns>A <see cref="DamageCause"/> value detailing the cause of the damage.</returns>
public DamageCause getCause() => _cause;
/// <summary>
/// Gets the raw amount of damage caused by the event.
/// </summary>
/// <returns>The raw amount of damage.</returns>
public double getDamage() => _damage;
/// <summary>
/// Gets the amount of damage caused by the event after all damage
/// reduction is applied.
/// </summary>
/// <returns>The amount of damage after reduction.</returns>
public double getFinalDamage() => _finalDamage;
/// <summary>
/// Gets the cancellation state of this event.
/// </summary>
/// <returns><c>true</c> if this event is cancelled.</returns>
public bool isCancelled() => _cancel;
/// <summary>
/// Sets the cancellation state of this event. A cancelled event will not
/// be executed in the server, but will still pass to other plugins.
/// </summary>
/// <param name="cancel"><c>true</c> if you wish to cancel this event.</param>
public void setCancelled(bool cancel)
{
_cancel = cancel;
}
/// <summary>
/// Sets the raw amount of damage caused by the event.
/// </summary>
/// <param name="damage">The raw amount of damage.</param>
public void setDamage(double damage)
{
_damage = damage;
}
}

View file

@ -0,0 +1,53 @@
namespace Minecraft.Server.FourKit.Event.Entity;
using FourKitEntity = Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Thrown whenever a LivingEntity dies.
/// </summary>
public class EntityDeathEvent : EntityEvent
{
private readonly List<ItemStack> _drops;
private int _droppedExp;
internal EntityDeathEvent(FourKitEntity.LivingEntity entity, List<ItemStack> drops)
: this(entity, drops, 0)
{
}
internal EntityDeathEvent(FourKitEntity.LivingEntity what, List<ItemStack> drops, int droppedExp)
: base(what)
{
_drops = drops;
_droppedExp = droppedExp;
}
/// <summary>
/// Returns the Entity involved in this event.
/// </summary>
/// <returns>Entity who is involved in this event.</returns>
public new FourKitEntity.LivingEntity getEntity() => (FourKitEntity.LivingEntity)entity;
/// <summary>
/// Gets how much EXP should be dropped from this death.
/// This does not indicate how much EXP should be taken from the entity
/// in question, merely how much should be created after its death.
/// </summary>
/// <returns>Amount of EXP to drop.</returns>
public int getDroppedExp() => _droppedExp;
/// <summary>
/// Sets how much EXP should be dropped from this death.
/// This does not indicate how much EXP should be taken from the entity
/// in question, merely how much should be created after its death.
/// </summary>
/// <param name="exp">Amount of EXP to drop.</param>
public void setDroppedExp(int exp) => _droppedExp = exp;
/// <summary>
/// Gets all the items which will drop when the entity dies.
/// </summary>
/// <returns>Items to drop when the entity dies.</returns>
public List<ItemStack> getDrops() => _drops;
}

View file

@ -0,0 +1,28 @@
namespace Minecraft.Server.FourKit.Event.Entity;
using FourKitEntity = Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents an Entity-related event.
/// </summary>
public abstract class EntityEvent : Event
{
protected FourKitEntity.Entity entity;
protected EntityEvent(FourKitEntity.Entity what)
{
entity = what;
}
/// <summary>
/// Returns the Entity involved in this event.
/// </summary>
/// <returns>Entity who is involved in this event.</returns>
public FourKitEntity.Entity getEntity() => entity;
/// <summary>
/// Gets the EntityType of the Entity involved in this event.
/// </summary>
/// <returns>EntityType of the Entity involved in this event.</returns>
public FourKitEntity.EntityType getEntityType() => entity.getType();
}

View file

@ -0,0 +1,110 @@
namespace Minecraft.Server.FourKit.Event.Entity;
using FourKitEntity = Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Thrown whenever a Player dies.
/// </summary>
public class PlayerDeathEvent : EntityDeathEvent
{
private string _deathMessage;
private int _newExp;
private int _newLevel;
private bool _keepLevel;
private bool _keepInventory;
internal PlayerDeathEvent(FourKitEntity.Player player, List<ItemStack> drops, int droppedExp, string deathMessage)
: this(player, drops, droppedExp, 0, deathMessage)
{
}
/// <summary>
/// Creates a new <see cref="PlayerDeathEvent"/>.
/// </summary>
/// <param name="player">The Player who died.</param>
/// <param name="drops">The items to drop when the player dies.</param>
/// <param name="droppedExp">The amount of experience to drop.</param>
/// <param name="newExp">The new EXP the Player should have at respawn.</param>
/// <param name="deathMessage">The death message to display.</param>
public PlayerDeathEvent(FourKitEntity.Player player, List<ItemStack> drops, int droppedExp, int newExp, string deathMessage)
: base(player, drops, droppedExp)
{
_deathMessage = deathMessage;
_newExp = newExp;
_newLevel = 0;
_keepLevel = false;
_keepInventory = false;
}
/// <summary>
/// Returns the Entity involved in this event.
/// </summary>
/// <returns>Entity who is involved in this event.</returns>
public new FourKitEntity.Player getEntity() => (FourKitEntity.Player)entity;
/// <summary>
/// Get the death message that will appear to everyone on the server.
/// </summary>
/// <returns>Message to appear to other players on the server.</returns>
public string getDeathMessage() => _deathMessage;
/// <summary>
/// Set the death message that will appear to everyone on the server.
/// </summary>
/// <param name="deathMessage">Message to appear to other players on the server.</param>
public void setDeathMessage(string deathMessage) => _deathMessage = deathMessage;
/// <summary>
/// Gets how much EXP the Player should have at respawn.
/// This does not indicate how much EXP should be dropped, please see
/// <see cref="EntityDeathEvent.getDroppedExp"/> for that.
/// </summary>
/// <returns>New EXP of the respawned player.</returns>
public int getNewExp() => _newExp;
/// <summary>
/// Sets how much EXP the Player should have at respawn.
/// This does not indicate how much EXP should be dropped, please see
/// <see cref="EntityDeathEvent.setDroppedExp"/> for that.
/// </summary>
/// <param name="exp">New EXP of the respawned player.</param>
public void setNewExp(int exp) => _newExp = exp;
/// <summary>
/// Gets the Level the Player should have at respawn.
/// </summary>
/// <returns>New Level of the respawned player.</returns>
public int getNewLevel() => _newLevel;
/// <summary>
/// Sets the Level the Player should have at respawn.
/// </summary>
/// <param name="level">New Level of the respawned player.</param>
public void setNewLevel(int level) => _newLevel = level;
/// <summary>
/// Gets if the Player should keep all EXP at respawn.
/// This flag overrides other EXP settings.
/// </summary>
/// <returns><c>true</c> if Player should keep all pre-death exp.</returns>
public bool getKeepLevel() => _keepLevel;
/// <summary>
/// Sets if the Player should keep all EXP at respawn.
/// This overrides all other EXP settings.
/// </summary>
/// <param name="keepLevel"><c>true</c> to keep all current value levels.</param>
public void setKeepLevel(bool keepLevel) => _keepLevel = keepLevel;
/// <summary>
/// Gets if the Player keeps inventory on death.
/// </summary>
/// <returns><c>true</c> if the player keeps inventory on death.</returns>
public bool getKeepInventory() => _keepInventory;
/// <summary>
/// Sets if the Player keeps inventory on death.
/// </summary>
/// <param name="keepInventory"><c>true</c> to keep the inventory.</param>
public void setKeepInventory(bool keepInventory) => _keepInventory = keepInventory;
}

View file

@ -0,0 +1,10 @@
namespace Minecraft.Server.FourKit.Event;
/// <summary>
/// Base class for all events dispatched by the server.
/// </summary>
public abstract class Event
{
/// <summary>Gets the name of this event (defaults to the class name).</summary>
public virtual string getEventName() => GetType().Name;
}

View file

@ -0,0 +1,40 @@
namespace Minecraft.Server.FourKit.Event;
/// <summary>
/// Marks a method inside a <see cref="Listener"/> as an event handler.
/// This class is not named "EventHandler" due to a naming conflict with the existing System.EventHandler
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class EventHandlerAttribute : Attribute
{
/// <summary>
/// Priority of this handler. Lower values run first.
/// Default is <see cref="EventPriority.Normal"/>.
/// </summary>
public EventPriority Priority { get; set; } = EventPriority.Normal;
/// <summary>
/// Whether this handler should be skipped when the event is already
/// cancelled by a lower-priority handler. Default is <c>false</c>.
/// </summary>
public bool IgnoreCancelled { get; set; } = false;
}
/// <summary>
/// Execution priority for event handlers.
/// </summary>
public enum EventPriority
{
/// <summary>Event call is of very low importance and should be ran first, to allow other plugins to further customise the outcome</summary>
Lowest = 0,
/// <summary>Event call is of low importance</summary>
Low = 1,
/// <summary>Event call is neither important nor unimportant, and may be ran normally</summary>
Normal = 2,
/// <summary>Event call is of high importance</summary>
High = 3,
/// <summary>Event call is critical and must have the final say in what happens to the event</summary>
Highest = 4,
/// <summary>Event is listened to purely for monitoring the outcome of an event. Should not modify the event.</summary>
Monitor = 5
}

View file

@ -0,0 +1,93 @@
namespace Minecraft.Server.FourKit.Event.Inventory;
/// <summary>
/// What the client did to trigger this action (not the result).
/// </summary>
public enum ClickType
{
/// <summary>The left (or primary) mouse button.</summary>
LEFT,
/// <summary>Holding shift while pressing the left mouse button.</summary>
SHIFT_LEFT,
/// <summary>The right mouse button.</summary>
RIGHT,
/// <summary>Holding shift while pressing the right mouse button.</summary>
SHIFT_RIGHT,
/// <summary>Clicking the left mouse button on the grey area around the inventory.</summary>
WINDOW_BORDER_LEFT,
/// <summary>Clicking the right mouse button on the grey area around the inventory.</summary>
WINDOW_BORDER_RIGHT,
/// <summary>The middle mouse button, or a "scrollwheel click".</summary>
MIDDLE,
/// <summary>One of the number keys 1-9, correspond to slots on the hotbar.</summary>
NUMBER_KEY,
/// <summary>Pressing the left mouse button twice in quick succession.</summary>
DOUBLE_CLICK,
/// <summary>The "Drop" key (defaults to Q).</summary>
DROP,
/// <summary>Holding Ctrl while pressing the "Drop" key (defaults to Q).</summary>
CONTROL_DROP,
/// <summary>Any action done with the Creative inventory open.</summary>
CREATIVE,
/// <summary>A type of inventory manipulation not yet recognized by Bukkit.</summary>
UNKNOWN,
}
/// <summary>
/// Extension methods for <see cref="ClickType"/>.
/// </summary>
public static class ClickTypeExtensions
{
/// <summary>
/// Gets whether this ClickType represents the pressing of a key on a keyboard.
/// </summary>
/// <param name="click">The click type.</param>
/// <returns>true if this ClickType represents the pressing of a key.</returns>
public static bool isKeyboardClick(this ClickType click)
{
return click == ClickType.NUMBER_KEY || click == ClickType.DROP || click == ClickType.CONTROL_DROP;
}
/// <summary>
/// Gets whether this ClickType represents an action that can only be performed
/// by a Player in creative mode.
/// </summary>
/// <param name="click">The click type.</param>
/// <returns>true if this action requires Creative mode.</returns>
public static bool isCreativeAction(this ClickType click)
{
return click == ClickType.CREATIVE || click == ClickType.MIDDLE;
}
/// <summary>
/// Gets whether this ClickType represents a right click.
/// </summary>
/// <param name="click">The click type.</param>
/// <returns>true if this ClickType represents a right click.</returns>
public static bool isRightClick(this ClickType click)
{
return click == ClickType.RIGHT || click == ClickType.SHIFT_RIGHT;
}
/// <summary>
/// Gets whether this ClickType represents a left click.
/// </summary>
/// <param name="click">The click type.</param>
/// <returns>true if this ClickType represents a left click.</returns>
public static bool isLeftClick(this ClickType click)
{
return click == ClickType.LEFT || click == ClickType.SHIFT_LEFT
|| click == ClickType.DOUBLE_CLICK || click == ClickType.CREATIVE;
}
/// <summary>
/// Gets whether this ClickType indicates that the shift key was pressed
/// down when the click was made.
/// </summary>
/// <param name="click">The click type.</param>
/// <returns>true if the action uses Shift.</returns>
public static bool isShiftClick(this ClickType click)
{
return click == ClickType.SHIFT_LEFT || click == ClickType.SHIFT_RIGHT;
}
}

View file

@ -0,0 +1,46 @@
namespace Minecraft.Server.FourKit.Event.Inventory;
/// <summary>
/// An estimation of what the result will be.
/// </summary>
public enum InventoryAction
{
/// <summary>Nothing will happen from the click.</summary>
NOTHING,
/// <summary>All of the items on the clicked slot are moved to the cursor.</summary>
PICKUP_ALL,
/// <summary>Some of the items on the clicked slot are moved to the cursor.</summary>
PICKUP_SOME,
/// <summary>Half of the items on the clicked slot are moved to the cursor.</summary>
PICKUP_HALF,
/// <summary>One of the items on the clicked slot are moved to the cursor.</summary>
PICKUP_ONE,
/// <summary>All of the items on the cursor are moved to the clicked slot.</summary>
PLACE_ALL,
/// <summary>Some of the items from the cursor are moved to the clicked slot (usually up to the max stack size).</summary>
PLACE_SOME,
/// <summary>A single item from the cursor is moved to the clicked slot.</summary>
PLACE_ONE,
/// <summary>The clicked item and the cursor are exchanged.</summary>
SWAP_WITH_CURSOR,
/// <summary>The entire cursor item is dropped.</summary>
DROP_ALL_CURSOR,
/// <summary>One item is dropped from the cursor.</summary>
DROP_ONE_CURSOR,
/// <summary>The entire clicked slot is dropped.</summary>
DROP_ALL_SLOT,
/// <summary>One item is dropped from the clicked slot.</summary>
DROP_ONE_SLOT,
/// <summary>The item is moved to the opposite inventory if a space is found.</summary>
MOVE_TO_OTHER_INVENTORY,
/// <summary>The clicked item is moved to the hotbar, and the item currently there is re-added to the player's inventory.</summary>
HOTBAR_MOVE_AND_READD,
/// <summary>The clicked slot and the picked hotbar slot are swapped.</summary>
HOTBAR_SWAP,
/// <summary>A max-size stack of the clicked item is put on the cursor.</summary>
CLONE_STACK,
/// <summary>The inventory is searched for the same material, and they are put on the cursor up to Material.getMaxStackSize().</summary>
COLLECT_TO_CURSOR,
/// <summary>An unrecognized ClickType.</summary>
UNKNOWN,
}

View file

@ -0,0 +1,157 @@
namespace Minecraft.Server.FourKit.Event.Inventory;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// This event is called when a player clicks a slot in an inventory.
/// <para>
/// Because InventoryClickEvent occurs within a modification of the Inventory,
/// not all Inventory related methods are safe to use.
/// </para>
/// <para>
/// The following should never be invoked by an EventHandler for
/// InventoryClickEvent using the HumanEntity or InventoryView associated
/// with this event:
/// <list type="bullet">
/// <item><description><see cref="HumanEntity.closeInventory()"/></description></item>
/// <item><description><see cref="HumanEntity.openInventory(Inventory)"/></description></item>
/// <item><description><see cref="InventoryView.close()"/></description></item>
/// </list>
/// </para>
/// </summary>
public class InventoryClickEvent : InventoryInteractEvent
{
private readonly SlotType _slotType;
private readonly int _rawSlot;
private readonly int _whichSlot;
private readonly ClickType _click;
private readonly InventoryAction _action;
private readonly int _hotbarKey;
private ItemStack? _currentItem;
internal InventoryClickEvent(InventoryView view, SlotType type, int slot,
ClickType click, InventoryAction action)
: this(view, type, slot, click, action, -1)
{
}
internal InventoryClickEvent(InventoryView view, SlotType type, int slot,
ClickType click, InventoryAction action, int key)
: base(view)
{
_slotType = type;
_rawSlot = slot;
_click = click;
_action = action;
_hotbarKey = key;
_currentItem = view.getItem(slot);
_whichSlot = view.convertSlot(slot);
}
/// <summary>
/// Gets the inventory that was clicked, or null if outside of window.
/// </summary>
/// <returns>The clicked inventory.</returns>
public Inventory? getClickedInventory()
{
if (_rawSlot == InventoryView.OUTSIDE)
return null;
int topSize = getView().getTopInventory().getSize();
if (_rawSlot < topSize)
return getView().getTopInventory();
return getView().getBottomInventory();
}
/// <summary>
/// Gets the type of slot that was clicked.
/// </summary>
/// <returns>The slot type.</returns>
public SlotType getSlotType() => _slotType;
/// <summary>
/// Gets the current ItemStack on the cursor.
/// </summary>
/// <returns>The cursor ItemStack.</returns>
public ItemStack? getCursor() => getView().getCursor();
/// <summary>
/// Gets the ItemStack currently in the clicked slot.
/// </summary>
/// <returns>The item in the clicked slot.</returns>
public ItemStack? getCurrentItem() => _currentItem;
/// <summary>
/// Gets whether or not the ClickType for this event represents a right click.
/// </summary>
/// <returns>true if the ClickType uses the right mouse button.</returns>
public bool isRightClick() => _click.isRightClick();
/// <summary>
/// Gets whether or not the ClickType for this event represents a left click.
/// </summary>
/// <returns>true if the ClickType uses the left mouse button.</returns>
public bool isLeftClick() => _click.isLeftClick();
/// <summary>
/// Gets whether the ClickType for this event indicates that the key was
/// pressed down when the click was made.
/// </summary>
/// <returns>true if the ClickType uses Shift or Ctrl.</returns>
public bool isShiftClick() => _click.isShiftClick();
/// <summary>
/// Sets the item on the cursor.
/// </summary>
/// <param name="stack">The new cursor item.</param>
[Obsolete("This changes the ItemStack in their hand before any calculations are applied to the Inventory.")]
public void setCursor(ItemStack? stack) => getView().setCursor(stack);
/// <summary>
/// Sets the ItemStack currently in the clicked slot.
/// </summary>
/// <param name="stack">The item to be placed in the current slot.</param>
public void setCurrentItem(ItemStack? stack)
{
_currentItem = stack;
if (_rawSlot >= 0)
getView().setItem(_rawSlot, stack);
}
/// <summary>
/// The slot number that was clicked, ready for passing to
/// <see cref="Inventory.getItem(int)"/>. Note that there may be two slots
/// with the same slot number, since a view links two different inventories.
/// </summary>
/// <returns>The slot number.</returns>
public int getSlot() => _whichSlot;
/// <summary>
/// The raw slot number clicked, ready for passing to
/// <see cref="InventoryView.getItem(int)"/>. This slot number is unique
/// for the view.
/// </summary>
/// <returns>The raw slot number.</returns>
public int getRawSlot() => _rawSlot;
/// <summary>
/// If the ClickType is NUMBER_KEY, this method will return the index of
/// the pressed key (0-8).
/// </summary>
/// <returns>The number on the key minus 1 (range 0-8); or -1 if not a NUMBER_KEY action.</returns>
public int getHotbarButton() => _hotbarKey;
/// <summary>
/// Gets the InventoryAction that triggered this event.
/// This action cannot be changed, and represents what the normal outcome
/// of the event will be. To change the behavior of this InventoryClickEvent,
/// changes must be manually applied.
/// </summary>
/// <returns>The InventoryAction that triggered this event.</returns>
public InventoryAction getAction() => _action;
/// <summary>
/// Gets the ClickType for this event.
/// This is insulated against changes to the inventory by other plugins.
/// </summary>
/// <returns>The type of inventory click.</returns>
public ClickType getClick() => _click;
}

View file

@ -0,0 +1,45 @@
namespace Minecraft.Server.FourKit.Event.Inventory;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents a player related inventory event.
/// </summary>
public class InventoryEvent : Event
{
/// <summary>The inventory view associated with this event.</summary>
protected readonly InventoryView transaction;
internal InventoryEvent(InventoryView transaction)
{
this.transaction = transaction;
}
/// <summary>
/// Gets the primary Inventory involved in this transaction.
/// </summary>
/// <returns>The upper inventory.</returns>
public global::Minecraft.Server.FourKit.Inventory.Inventory getInventory()
{
return transaction.getTopInventory();
}
/// <summary>
/// Gets the list of players viewing the primary (upper) inventory
/// involved in this event.
/// </summary>
/// <returns>A list of people viewing.</returns>
public List<HumanEntity> getViewers()
{
return transaction.getTopInventory().getViewers();
}
/// <summary>
/// Gets the view object itself.
/// </summary>
/// <returns>The InventoryView.</returns>
public InventoryView getView()
{
return transaction;
}
}

View file

@ -0,0 +1,39 @@
namespace Minecraft.Server.FourKit.Event.Inventory;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// An abstract base class for events that describe an interaction between a
/// <see cref="HumanEntity"/> and the contents of an <see cref="Inventory"/>.
/// </summary>
public abstract class InventoryInteractEvent : InventoryEvent, Cancellable
{
private bool _cancelled;
internal protected InventoryInteractEvent(InventoryView transaction) : base(transaction)
{
}
/// <summary>
/// Gets the player who performed the click.
/// </summary>
/// <returns>The clicking player.</returns>
public HumanEntity getWhoClicked()
{
return transaction.getPlayer();
}
/// <summary>
/// Gets the cancellation state of this event. A cancelled event will not
/// be executed in the server, but will still pass to other plugins.
/// </summary>
/// <returns>true if this event is cancelled.</returns>
public bool isCancelled() => _cancelled;
/// <summary>
/// Sets the cancellation state of this event. A cancelled event will not
/// be executed in the server, but will still pass to other plugins.
/// </summary>
/// <param name="cancel">true if you wish to cancel this event.</param>
public void setCancelled(bool cancel) => _cancelled = cancel;
}

View file

@ -0,0 +1,38 @@
namespace Minecraft.Server.FourKit.Event.Inventory;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Called when a player opens an inventory. Cancelling this event will prevent
/// the inventory screen from showing.
/// </summary>
public class InventoryOpenEvent : InventoryEvent, Cancellable
{
private bool _cancelled;
internal InventoryOpenEvent(InventoryView transaction) : base(transaction)
{
}
/// <summary>
/// Returns the player involved in this event.
/// </summary>
/// <returns>Player who is involved in this event.</returns>
public HumanEntity getPlayer() => transaction.getPlayer();
/// <summary>
/// Gets the cancellation state of this event. A cancelled event will not
/// be executed in the server, but will still pass to other plugins.
/// If an inventory open event is cancelled, the inventory screen will not show.
/// </summary>
/// <returns>true if this event is cancelled.</returns>
public bool isCancelled() => _cancelled;
/// <summary>
/// Sets the cancellation state of this event. A cancelled event will not
/// be executed in the server, but will still pass to other plugins.
/// If an inventory open event is cancelled, the inventory screen will not show.
/// </summary>
/// <param name="cancel">true if you wish to cancel this event.</param>
public void setCancelled(bool cancel) => _cancelled = cancel;
}

View file

@ -0,0 +1,9 @@
namespace Minecraft.Server.FourKit.Event;
/// <summary>
/// Simple interface for tagging all EventListeners
/// Register instances with <see cref="FourKit.addListener(Listener)"/>.
/// </summary>
public interface Listener
{
}

View file

@ -0,0 +1,74 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Fired when a player sends a chat message.
///
/// <para>When the event finishes execution the server formats the final
/// output using the same format specifiers from Java.
/// <c>%1$s</c> is the player's display name and <c>%2$s</c> is the
/// message, exactly like Bukkits <c>PlayerChatEvent</c>.</para>
/// </summary>
public class PlayerChatEvent : PlayerEvent, Cancellable
{
private string _message;
private string _format;
private bool _cancelled;
internal PlayerChatEvent(Player player, string message) : base(player)
{
_message = message;
_format = "<%1$s> %2$s";
}
/// <summary>
/// Gets the message that the player is attempting to send.
/// This message will be used with <see cref="getFormat"/>.
/// </summary>
/// <returns>Message the player is attempting to send.</returns>
public string getMessage() => _message;
/// <summary>
/// Sets the message that the player will send.
/// This message will be used with <see cref="getFormat"/>.
/// </summary>
/// <param name="message">New message that the player will send.</param>
public void setMessage(string message)
{
_message = message;
}
/// <summary>
/// Gets the format used to display this chat message.
///
/// <para>When this event finishes execution, the first format parameter
/// (<c>%1$s</c>) is <c>Player.getDisplayName()</c> and the second
/// parameter (<c>%2$s</c>) is <c>getMessage()</c>.</para>
/// </summary>
/// <returns>A Java-style positional format string compatible with Bukkit.</returns>
public string getFormat() => _format;
/// <summary>
/// Sets the format used to display this chat message.
///
/// <para>When this event finishes execution, the first format parameter
/// (<c>%1$s</c>) is <c>Player.getDisplayName()</c> and the second
/// parameter (<c>%2$s</c>) is <c>getMessage()</c>.</para>
/// </summary>
/// <param name="format">A Java-style positional format string (e.g. <c>"&lt;%1$s&gt; %2$s"</c>).</param>
/// <exception cref="ArgumentNullException">If format is <c>null</c>.</exception>
public void setFormat(string format)
{
ArgumentNullException.ThrowIfNull(format);
_format = format;
}
/// <inheritdoc />
public bool isCancelled() => _cancelled;
/// <inheritdoc />
public void setCancelled(bool cancel)
{
_cancelled = cancel;
}
}

View file

@ -0,0 +1,46 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
// Yo this event pissed me the fuck off
/// <summary>
/// Fired when a player drops an item from their inventory.
/// If cancelled, the item will not be dropped and the player keeps it.
/// The dropped item can be modified by plugins.
/// </summary>
public class PlayerDropItemEvent : PlayerEvent, Cancellable
{
private ItemStack _itemDrop;
private bool _cancelled;
internal PlayerDropItemEvent(Player player, ItemStack drop)
: base(player)
{
_itemDrop = drop;
}
/// <summary>
/// Gets the ItemDrop created by the player.
/// </summary>
/// <returns>The ItemStack being dropped.</returns>
public ItemStack getItemDrop() => _itemDrop;
/// <summary>
/// Sets the item to be dropped. Plugins can modify which item
/// is actually dropped.
/// </summary>
/// <param name="item">The new item to drop.</param>
public void setItemDrop(ItemStack item)
{
_itemDrop = item;
}
/// <inheritdoc/>
public bool isCancelled() => _cancelled;
/// <inheritdoc/>
public void setCancelled(bool cancel)
{
_cancelled = cancel;
}
}

View file

@ -0,0 +1,19 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Base class for events related to a <see cref="FourKit.Player"/>.
/// </summary>
public abstract class PlayerEvent : Event
{
private readonly Player _player;
internal protected PlayerEvent(Player player)
{
_player = player;
}
/// <summary>Returns the player involved in this event.</summary>
public Player getPlayer() => _player;
}

View file

@ -0,0 +1,32 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents an event that is called when a player right clicks an entity.
/// </summary>
public class PlayerInteractEntityEvent : PlayerEvent, Cancellable
{
/// <summary>The entity that was right-clicked.</summary>
protected Entity clickedEntity;
private bool _cancelled;
internal PlayerInteractEntityEvent(Player who, Entity clickedEntity)
: base(who)
{
this.clickedEntity = clickedEntity;
}
/// <inheritdoc/>
public bool isCancelled() => _cancelled;
/// <inheritdoc/>
public void setCancelled(bool cancel) => _cancelled = cancel;
/// <summary>
/// Gets the entity that was right-clicked by the player.
/// </summary>
/// <returns>entity right clicked by player</returns>
public Entity getRightClicked() => clickedEntity;
}

View file

@ -0,0 +1,111 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Block;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
using FourKitBlock = Minecraft.Server.FourKit.Block.Block;
/// <summary>
/// Called when a player interacts with an object or air.
/// </summary>
public class PlayerInteractEvent : PlayerEvent, Cancellable
{
private readonly Action _action;
private readonly ItemStack? _item;
private readonly FourKitBlock? _clickedBlock;
private readonly BlockFace _clickedFace;
private bool _cancelled;
private bool _useItemInHand = true;
internal PlayerInteractEvent(Player who, Action action, ItemStack? item, FourKitBlock? clickedBlock, BlockFace clickedFace)
: base(who)
{
_action = action;
_item = item;
_clickedBlock = clickedBlock;
_clickedFace = clickedFace;
}
/// <summary>
/// Returns the action type.
/// </summary>
/// <returns>Action returns the type of interaction.</returns>
public Action getAction() => _action;
/// <inheritdoc/>
public bool isCancelled() => _cancelled;
/// <summary>
/// Sets the cancellation state of this event. A canceled event will not be
/// executed in the server, but will still pass to other plugins.
///
/// Canceling this event will prevent use of food (player won't lose the
/// food item), prevent bows/snowballs/eggs from firing, etc. (player won't
/// lose the ammo).
/// </summary>
/// <param name="cancel">true if you wish to cancel this event.</param>
public void setCancelled(bool cancel) => _cancelled = cancel;
/// <summary>
/// Returns the item in hand represented by this event.
/// </summary>
/// <returns>ItemStack the item used.</returns>
public ItemStack? getItem() => _item;
/// <summary>
/// Convenience method. Returns the material of the item represented by this event.
/// </summary>
/// <returns>Material the material of the item used.</returns>
public Material getMaterial() => _item?.getType() ?? Material.AIR;
/// <summary>
/// Check if this event involved a block.
/// </summary>
/// <returns>true if it did.</returns>
public bool hasBlock() => _clickedBlock != null;
/// <summary>
/// Check if this event involved an item.
/// </summary>
/// <returns>true if it did.</returns>
public bool hasItem() => _item != null;
/// <summary>
/// Convenience method to inform the user whether this was a block placement event.
/// </summary>
/// <returns>true if the item in hand was a block.</returns>
public bool isBlockInHand()
{
if (_item == null) return false;
int id = (int)_item.getType();
return id >= 1 && id <= 255;
}
/// <summary>
/// Returns the clicked block.
/// </summary>
/// <returns>Block returns the block clicked with this item.</returns>
public FourKitBlock? getClickedBlock() => _clickedBlock;
/// <summary>
/// Returns the face of the block that was clicked.
/// </summary>
/// <returns>BlockFace returns the face of the block that was clicked.</returns>
public BlockFace getBlockFace() => _clickedFace;
/// <summary>
/// This controls the action to take with the item the player is holding.
/// This includes both blocks and items (such as flint and steel or records).
/// When this is set to default, it will be allowed if no action is taken on
/// the interacted block.
/// </summary>
/// <returns>the action to take with the item in hand.</returns>
public bool useItemInHand() => _useItemInHand;
/// <summary>
/// Sets whether to use the item in hand.
/// </summary>
/// <param name="useItemInHand">the action to take with the item in hand.</param>
public void setUseItemInHand(bool useItemInHand) => _useItemInHand = useItemInHand;
}

View file

@ -0,0 +1,30 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Called when a player joins a server
/// </summary>
public class PlayerJoinEvent : PlayerEvent
{
private string _joinMessage;
internal PlayerJoinEvent(Player player) : base(player)
{
_joinMessage = $"{player.getName()} joined the game";
}
/// <summary>
/// Gets the join message to send to all online players
/// </summary>
/// <returns>string join message</returns>
public string getJoinMessage() => _joinMessage;
/// <summary>
/// Sets the join message to send to all online players
/// </summary>
/// <param name="joinMessage">join message.</param>
public void setJoinMessage(string? joinMessage)
{
_joinMessage = joinMessage ?? string.Empty;
}
}

View file

@ -0,0 +1,62 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Fired when a player is kicked from the server.
/// If cancelled, the kick will not take place and the player remains connected.
/// Plugins may modify the kick reason and the leave message broadcast to
/// all online players.
/// </summary>
public class PlayerKickEvent : PlayerEvent, Cancellable
{
private DisconnectReason _reason;
private string _leaveMessage;
private bool _cancelled;
internal PlayerKickEvent(Player playerKicked, DisconnectReason kickReason, string leaveMessage)
: base(playerKicked)
{
_reason = kickReason;
_leaveMessage = leaveMessage;
}
/// <summary>
/// Gets the reason why the player is getting kicked.
/// </summary>
/// <returns>The disconnect reason.</returns>
public DisconnectReason getReason() => _reason;
/// <summary>
/// Sets the reason why the player is getting kicked.
/// </summary>
/// <param name="kickReason">The new disconnect reason.</param>
public void setReason(DisconnectReason kickReason)
{
_reason = kickReason;
}
/// <summary>
/// Gets the leave message sent to all online players.
/// </summary>
/// <returns>The leave message.</returns>
public string getLeaveMessage() => _leaveMessage;
/// <summary>
/// Sets the leave message sent to all online players.
/// </summary>
/// <param name="leaveMessage">The new leave message.</param>
public void setLeaveMessage(string leaveMessage)
{
_leaveMessage = leaveMessage;
}
/// <inheritdoc/>
public bool isCancelled() => _cancelled;
/// <inheritdoc/>
public void setCancelled(bool cancel)
{
_cancelled = cancel;
}
}

View file

@ -0,0 +1,59 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Fired when a player moves. Plugins may modify the destination
/// or cancel the movement entirely.
/// </summary>
public class PlayerMoveEvent : PlayerEvent, Cancellable
{
private Location _from;
private Location _to;
private bool _cancelled;
internal PlayerMoveEvent(Player player, Location from, Location to) : base(player)
{
_from = from;
_to = to;
}
/// <summary>
/// Gets the location this player moved from.
/// </summary>
/// <returns>The from location.</returns>
public Location getFrom() => _from;
/// <summary>
/// Gets the location this player moved to.
/// </summary>
/// <returns>The to location.</returns>
public Location getTo() => _to;
/// <summary>
/// Sets the location to mark as where the player moved from.
/// </summary>
/// <param name="from">The new from location.</param>
public void setFrom(Location from)
{
_from = from;
}
/// <summary>
/// Sets the location that this player will move to.
/// </summary>
/// <param name="to">The new to location.</param>
public void setTo(Location to)
{
_to = to;
}
/// <inheritdoc />
public bool isCancelled() => _cancelled;
/// <inheritdoc />
public void setCancelled(bool cancel)
{
_cancelled = cancel;
}
}

View file

@ -0,0 +1,41 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Thrown when a player picks an item up from the ground.
/// If cancelled the item will not be picked up.
/// </summary>
public class PlayerPickupItemEvent : PlayerEvent, Cancellable
{
private readonly Item _item;
private readonly int _remaining;
private bool _cancelled;
internal PlayerPickupItemEvent(Player player, Item item, int remaining)
: base(player)
{
_item = item;
_remaining = remaining;
}
/// <summary>
/// Gets the Item picked up by the player.
/// </summary>
/// <returns>The <see cref="Item"/> entity.</returns>
public Item getItem() => _item;
/// <summary>
/// Gets the amount remaining on the ground, if any.
/// </summary>
/// <returns>Amount remaining on the ground.</returns>
public int getRemaining() => _remaining;
/// <inheritdoc/>
public bool isCancelled() => _cancelled;
/// <inheritdoc/>
public void setCancelled(bool cancel)
{
_cancelled = cancel;
}
}

View file

@ -0,0 +1,22 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Called when a player is about to teleport because it is in contact with a portal.
/// </summary>
public class PlayerPortalEvent : PlayerTeleportEvent
{
internal PlayerPortalEvent(Player player, Location from, Location to)
: base(player, from, to, TeleportCause.UNKNOWN) { }
/// <summary>
/// Constructs a new PlayerPortalEvent with the given cause.
/// </summary>
/// <param name="player">The player entering the portal.</param>
/// <param name="from">The location the player is coming from.</param>
/// <param name="to">The location the player is teleporting to.</param>
/// <param name="cause">The cause of this teleportation (should be a portal-related cause).</param>
public PlayerPortalEvent(Player player, Location from, Location to, TeleportCause cause)
: base(player, from, to, cause) { }
}

View file

@ -0,0 +1,32 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Fired when a player disconnects from the server.
/// Plugins may read or modify the quit message that is broadcast to all
/// online players.
/// </summary>
public class PlayerQuitEvent : PlayerEvent
{
private string _quitMessage;
internal PlayerQuitEvent(Player player) : base(player)
{
_quitMessage = $"{player.getName()} left the game";
}
/// <summary>
/// Gets the quit message to send to all online players.
/// </summary>
/// <returns>The quit message.</returns>
public string getQuitMessage() => _quitMessage;
/// <summary>
/// Sets the quit message to send to all online players.
/// </summary>
/// <param name="quitMessage">The new quit message, or <c>null</c> to suppress it.</param>
public void setQuitMessage(string? quitMessage)
{
_quitMessage = quitMessage ?? string.Empty;
}
}

View file

@ -0,0 +1,44 @@
namespace Minecraft.Server.FourKit.Event.Player;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Holds information for player teleport events.
/// </summary>
public class PlayerTeleportEvent : PlayerMoveEvent
{
/// <summary>
/// Represents the cause of a player teleportation.
/// </summary>
public enum TeleportCause
{
/// <summary>Indicates the teleportation was caused by a player throwing an Ender Pearl.</summary>
ENDER_PEARL,
/// <summary>Indicates the teleportation was caused by a player executing a command.</summary>
COMMAND,
/// <summary>Indicates the teleportation was caused by a plugin.</summary>
PLUGIN,
/// <summary>Indicates the teleportation was caused by a player entering a Nether portal.</summary>
NETHER_PORTAL,
/// <summary>Indicates the teleportation was caused by a player entering an End portal.</summary>
END_PORTAL,
/// <summary>Indicates the teleportation was caused by an event not covered by this enum.</summary>
UNKNOWN,
}
private readonly TeleportCause _cause;
internal PlayerTeleportEvent(Player player, Location from, Location to)
: this(player, from, to, TeleportCause.UNKNOWN) { }
internal PlayerTeleportEvent(Player player, Location from, Location to, TeleportCause cause)
: base(player, from, to)
{
_cause = cause;
}
/// <summary>
/// Gets the cause of this teleportation event.
/// </summary>
/// <returns>The cause of the event.</returns>
public TeleportCause getCause() => _cause;
}

View file

@ -0,0 +1,92 @@
using System.Reflection;
using Minecraft.Server.FourKit.Event;
namespace Minecraft.Server.FourKit;
internal sealed class EventDispatcher
{
private readonly struct RegisteredHandler : IComparable<RegisteredHandler>
{
public readonly Listener Instance;
public readonly MethodInfo Method;
public readonly EventPriority Priority;
public readonly bool IgnoreCancelled;
public RegisteredHandler(Listener instance, MethodInfo method, EventPriority priority, bool ignoreCancelled)
{
Instance = instance;
Method = method;
Priority = priority;
IgnoreCancelled = ignoreCancelled;
}
public int CompareTo(RegisteredHandler other) => Priority.CompareTo(other.Priority);
}
private readonly Dictionary<Type, List<RegisteredHandler>> _handlers = new();
private readonly object _lock = new();
public void Register(Listener listener)
{
var methods = listener.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
lock (_lock)
{
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<Event.EventHandlerAttribute>();
if (attr == null)
continue;
var parameters = method.GetParameters();
if (parameters.Length != 1)
{
Console.WriteLine($"[FourKit] Warning: @EventHandler method {method.Name} must have exactly 1 parameter, skipping.");
continue;
}
var eventType = parameters[0].ParameterType;
if (!typeof(Event.Event).IsAssignableFrom(eventType))
{
Console.WriteLine($"[FourKit] Warning: @EventHandler method {method.Name} parameter must extend Event, skipping.");
continue;
}
if (!_handlers.TryGetValue(eventType, out var list))
{
list = new List<RegisteredHandler>();
_handlers[eventType] = list;
}
list.Add(new RegisteredHandler(listener, method, attr.Priority, attr.IgnoreCancelled));
_handlers[eventType] = list.OrderBy(h => h.Priority).ToList();
}
}
}
public void Fire(Event.Event evt)
{
List<RegisteredHandler>? handlers;
lock (_lock)
{
if (!_handlers.TryGetValue(evt.GetType(), out handlers))
return;
handlers = new List<RegisteredHandler>(handlers);
}
var cancellable = evt as Cancellable;
foreach (var handler in handlers)
{
if (handler.IgnoreCancelled && cancellable != null && cancellable.isCancelled())
continue;
try
{
handler.Method.Invoke(handler.Instance, [evt]);
}
catch (Exception ex)
{
Console.WriteLine($"[FourKit] Error in handler {handler.Instance.GetType().Name}.{handler.Method.Name}: {ex.InnerException?.Message ?? ex.Message}");
}
}
}
}

View file

@ -0,0 +1,319 @@
namespace Minecraft.Server.FourKit;
using Minecraft.Server.FourKit.Command;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Event;
using Minecraft.Server.FourKit.Inventory;
public static class FourKit
{
private static readonly EventDispatcher _dispatcher = new();
private static readonly Dictionary<string, Player> _players = new(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<int, Player> _playersByEntityId = new();
private static readonly object _playerLock = new();
internal const int MAX_CHAT_LENGTH = 123;
private static readonly Dictionary<int, World> _worldsByDimId = new();
private static readonly Dictionary<string, int> _worldNameToDimId = new(StringComparer.OrdinalIgnoreCase)
{
// grr
["world"] = 0,
["world_nether"] = -1,
["world_the_end"] = 1,
};
private static readonly object _worldLock = new();
/// <summary>
/// Gets a world by its name. Supported names: "world" (overworld),
/// "world_nether" (nether), "world_the_end" (the end).
/// </summary>
/// <param name="name">The name of the world to retrieve.</param>
/// <returns>The world with the given name, or null if none exists.</returns>
public static World? getWorld(string name)
{
if (_worldNameToDimId.TryGetValue(name, out int dimId))
return getWorld(dimId);
return null;
}
/// <summary>
/// Gets a world by its dimension ID (0 = overworld, -1 = nether, 1 = the end).
/// </summary>
/// <param name="dimId">The dimension ID.</param>
/// <returns>The world for that dimension, creating it if necessary.</returns>
public static World getWorld(int dimId)
{
lock (_worldLock)
{
if (!_worldsByDimId.TryGetValue(dimId, out var world))
{
string name = dimId switch
{
0 => "world",
-1 => "world_nether",
1 => "world_the_end",
_ => $"world_dim{dimId}",
};
world = new World(dimId, name);
_worldsByDimId[dimId] = world;
}
return world;
}
}
/// <summary>
/// Registers all the events in the given listener class
/// </summary>
public static void addListener(Listener listener)
{
_dispatcher.Register(listener);
//Console.WriteLine($"[FourKit] Registered listener: {listener.GetType().Name}");
}
public static Player? getPlayer(string name)
{
lock (_playerLock)
{
_players.TryGetValue(name, out var p);
return p;
}
}
public static IReadOnlyList<Player> getOnlinePlayers()
{
lock (_playerLock)
{
return _players.Values.Where(p => p.IsOnline).ToList().AsReadOnly();
}
}
internal static Player? GetPlayerByEntityId(int entityId)
{
lock (_playerLock)
{
_playersByEntityId.TryGetValue(entityId, out var p);
return p;
}
}
internal static Player TrackPlayer(int entityId, string name)
{
lock (_playerLock)
{
if (_playersByEntityId.TryGetValue(entityId, out var existing))
{
var oldName = existing.getName();
existing.SetNameInternal(name);
existing.IsOnline = true;
if (!string.Equals(oldName, name, StringComparison.OrdinalIgnoreCase))
{
_players.Remove(oldName);
_players[name] = existing;
}
return existing;
}
var player = new Player(entityId, name);
_players[name] = player;
_playersByEntityId[entityId] = player;
return player;
}
}
internal static Player? UntrackPlayer(int entityId)
{
lock (_playerLock)
{
if (_playersByEntityId.TryGetValue(entityId, out var player))
{
player.IsOnline = false;
_playersByEntityId.Remove(entityId);
_players.Remove(player.getName());
return player;
}
return null;
}
}
internal static void UpdatePlayerEntityId(int oldEntityId, int newEntityId)
{
lock (_playerLock)
{
if (_playersByEntityId.TryGetValue(oldEntityId, out var player))
{
_playersByEntityId.Remove(oldEntityId);
player.SetEntityIdInternal(newEntityId);
_playersByEntityId[newEntityId] = player;
}
}
}
internal static void FireEvent(Event.Event evt)
{
_dispatcher.Fire(evt);
}
private static readonly Dictionary<string, PluginCommand> _commands = new(StringComparer.OrdinalIgnoreCase);
private static readonly object _commandLock = new();
/// <summary>
/// Gets a <see cref="PluginCommand"/> with the given name, creating it
/// if it does not already exist. The returned command can be configured
/// with <see cref="PluginCommand.setExecutor"/>,
/// <see cref="Command.setDescription"/>, etc.
/// </summary>
/// <param name="name">Name of the command.</param>
/// <returns>The command for that name.</returns>
public static PluginCommand getCommand(string name)
{
lock (_commandLock)
{
if (!_commands.TryGetValue(name, out var cmd))
{
cmd = new PluginCommand(name);
_commands[name] = cmd;
}
return cmd;
}
}
internal static bool DispatchCommand(CommandSender sender, string commandLine)
{
string trimmed = commandLine.StartsWith('/') ? commandLine[1..] : commandLine;
if (string.IsNullOrEmpty(trimmed)) return false;
string[] parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries);
string label = parts[0];
string[] args = parts.Length > 1 ? parts[1..] : [];
PluginCommand? cmd;
lock (_commandLock)
{
if (!_commands.TryGetValue(label, out cmd))
{
foreach (var entry in _commands.Values)
{
if (entry.getAliases().Exists(a => string.Equals(a, label, StringComparison.OrdinalIgnoreCase)))
{
cmd = entry;
break;
}
}
}
}
if (cmd == null || cmd.getExecutor() == null) return false;
try
{
return cmd.execute(sender, label, args);
}
catch (Exception ex)
{
ServerLog.Error("fourkit", $"Error executing command '/{label}': {ex}");
sender.sendMessage($"An internal error occurred while executing /{label}.");
return false;
}
}
internal static ConsoleCommandSender getConsoleSender() => ConsoleCommandSender.Instance;
internal static bool HasCommand(string label)
{
lock (_commandLock)
{
if (_commands.TryGetValue(label, out var cmd) && cmd.getExecutor() != null)
return true;
foreach (var entry in _commands.Values)
{
if (entry.getExecutor() != null &&
entry.getAliases().Exists(a => string.Equals(a, label, StringComparison.OrdinalIgnoreCase)))
return true;
}
}
return false;
}
internal static List<(string usage, string description)> GetRegisteredCommandHelp()
{
var result = new List<(string, string)>();
lock (_commandLock)
{
foreach (var cmd in _commands.Values)
{
if (cmd.getExecutor() != null)
result.Add((cmd.getUsage(), cmd.getDescription()));
}
}
return result;
}
/// <summary>
/// Broadcasts a message to all online players.
/// </summary>
/// <param name="message">The message to broadcast.</param>
public static void broadcastMessage(string message)
{
if (string.IsNullOrEmpty(message) || NativeBridge.BroadcastMessage == null)
return;
if (message.Length > MAX_CHAT_LENGTH)
message = message[..MAX_CHAT_LENGTH];
IntPtr ptr = System.Runtime.InteropServices.Marshal.StringToCoTaskMemUTF8(message);
try
{
NativeBridge.BroadcastMessage(ptr, System.Text.Encoding.UTF8.GetByteCount(message));
}
finally
{
System.Runtime.InteropServices.Marshal.FreeCoTaskMem(ptr);
}
}
/// <summary>
/// Creates a new <see cref="Inventory.Inventory"/> with the specified size.
/// The inventory will be of type <see cref="InventoryType.CHEST"/> with
/// the default title.
/// </summary>
/// <param name="size">The size of the inventory (must be a multiple of 9).</param>
/// <returns>A new Inventory.</returns>
public static Inventory.Inventory createInventory(int size)
{
return new Inventory.Inventory("Chest", InventoryType.CHEST, size);
}
/// <summary>
/// Creates a new <see cref="Inventory.Inventory"/> with the specified size
/// and custom title.
/// </summary>
/// <param name="size">The size of the inventory (must be a multiple of 9).</param>
/// <param name="title">The title that will be shown to players.</param>
/// <returns>A new Inventory.</returns>
public static Inventory.Inventory createInventory(int size, string title)
{
return new Inventory.Inventory(title, InventoryType.CHEST, size);
}
/// <summary>
/// Creates a new <see cref="Inventory.Inventory"/> of the specified
/// <see cref="InventoryType"/> with the default title and size.
/// </summary>
/// <param name="type">The type of inventory to create.</param>
/// <returns>A new Inventory.</returns>
public static Inventory.Inventory createInventory(InventoryType type)
{
return new Inventory.Inventory(type.getDefaultTitle(), type, type.getDefaultSize());
}
/// <summary>
/// Creates a new <see cref="Inventory.Inventory"/> of the specified
/// <see cref="InventoryType"/> with a custom title.
/// </summary>
/// <param name="type">The type of inventory to create.</param>
/// <param name="title">The title that will be shown to players.</param>
/// <returns>A new Inventory.</returns>
public static Inventory.Inventory createInventory(InventoryType type, string title)
{
return new Inventory.Inventory(title, type, type.getDefaultSize());
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
namespace Minecraft.Server.FourKit;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents the various type of game modes that <see cref="HumanEntity"/>s may have
/// </summary>
public enum GameMode
{
/// <summary>
/// Survival mode is the "normal" gameplay type, with no special features.
/// </summary>
SURVIVAL = 0,
/// <summary>
/// Creative mode may fly, build instantly, become invulnerable and create free items.
/// </summary>
CREATIVE = 1,
/// <summary>
/// Adventure mode cannot break blocks without the correct tools.
/// </summary>
ADVENTURE = 2,
}

View file

@ -0,0 +1,25 @@
namespace Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents the inventory of a Beacon.
/// Single slot at index 0.
/// </summary>
public class BeaconInventory : Inventory
{
internal BeaconInventory(string title, int size, int entityId)
: base(title, InventoryType.BEACON, size, entityId)
{
}
/// <summary>
/// Get the item in the beacon's single slot.
/// </summary>
/// <returns>The item.</returns>
public ItemStack? getBeaconItem() => getItem(0);
/// <summary>
/// Set the item in the beacon's single slot.
/// </summary>
/// <param name="item">The item to set.</param>
public void setBeaconItem(ItemStack? item) => setItem(0, item);
}

View file

@ -0,0 +1,56 @@
namespace Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents the inventory of a Double Chest.
/// 54 slots total — left side is slots 026, right side is slots 2753.
/// </summary>
public class DoubleChestInventory : Inventory
{
private readonly Inventory _left;
private readonly Inventory _right;
internal DoubleChestInventory(string title, int size, int entityId)
: base(title, InventoryType.CHEST, size, entityId)
{
_left = new InventorySlice("Left chest", this, 0, 27);
_right = new InventorySlice("Right chest", this, 27, 27);
}
/// <summary>
/// Get the left half of this double chest.
/// </summary>
/// <returns>The left side inventory.</returns>
public Inventory getLeftSide() => _left;
/// <summary>
/// Get the right half of this double chest.
/// </summary>
/// <returns>The right side inventory.</returns>
public Inventory getRightSide() => _right;
// todo: get rid of this class
private sealed class InventorySlice : Inventory
{
private readonly Inventory _parent;
private readonly int _offset;
internal InventorySlice(string name, Inventory parent, int offset, int size)
: base(name, InventoryType.CHEST, size)
{
_parent = parent;
_offset = offset;
}
public override ItemStack? getItem(int index)
{
if (index < 0 || index >= getSize()) return null;
return _parent.getItem(_offset + index);
}
public override void setItem(int index, ItemStack? item)
{
if (index >= 0 && index < getSize())
_parent.setItem(_offset + index, item);
}
}
}

View file

@ -0,0 +1,25 @@
namespace Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents the inventory of an Enchanting Table.
/// Single slot at index 0.
/// </summary>
public class EnchantingInventory : Inventory
{
internal EnchantingInventory(string title, int size, int entityId)
: base(title, InventoryType.ENCHANTING, size, entityId)
{
}
/// <summary>
/// Get the item being enchanted.
/// </summary>
/// <returns>The item.</returns>
public ItemStack? getEnchantItem() => getItem(0);
/// <summary>
/// Set the item being enchanted.
/// </summary>
/// <param name="item">The item to set.</param>
public void setEnchantItem(ItemStack? item) => setItem(0, item);
}

View file

@ -0,0 +1,49 @@
namespace Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents the inventory of a Furnace.
/// Slot layout: 0 = smelting input, 1 = fuel, 2 = result.
/// </summary>
public class FurnaceInventory : Inventory
{
internal FurnaceInventory(string title, int size, int entityId)
: base(title, InventoryType.FURNACE, size, entityId)
{
}
/// <summary>
/// Get the current item in the result slot.
/// </summary>
/// <returns>The item.</returns>
public ItemStack? getResult() => getItem(2);
/// <summary>
/// Get the current fuel.
/// </summary>
/// <returns>The item.</returns>
public ItemStack? getFuel() => getItem(1);
/// <summary>
/// Get the item currently smelting.
/// </summary>
/// <returns>The item.</returns>
public ItemStack? getSmelting() => getItem(0);
/// <summary>
/// Set the current fuel.
/// </summary>
/// <param name="stack">The item.</param>
public void setFuel(ItemStack? stack) => setItem(1, stack);
/// <summary>
/// Set the current item in the result slot.
/// </summary>
/// <param name="stack">The item.</param>
public void setResult(ItemStack? stack) => setItem(2, stack);
/// <summary>
/// Set the item currently smelting.
/// </summary>
/// <param name="stack">The item.</param>
public void setSmelting(ItemStack? stack) => setItem(0, stack);
}

View file

@ -0,0 +1,37 @@
namespace Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents the inventory of a Horse.
/// Slot layout: 0 = saddle, 1 = armor, 2+ = chest slots.
/// </summary>
public class HorseInventory : Inventory
{
internal HorseInventory(string title, int size, int entityId)
: base(title, InventoryType.CHEST, size < 2 ? 2 : size, entityId)
{
}
/// <summary>
/// Gets the item in the horse's saddle slot.
/// </summary>
/// <returns>The saddle item.</returns>
public ItemStack? getSaddle() => getItem(0);
/// <summary>
/// Sets the item in the horse's saddle slot.
/// </summary>
/// <param name="stack">The saddle item.</param>
public void setSaddle(ItemStack? stack) => setItem(0, stack);
/// <summary>
/// Gets the item in the horse's armor slot.
/// </summary>
/// <returns>The armor item.</returns>
public ItemStack? getArmor() => getItem(1);
/// <summary>
/// Sets the item in the horse's armor slot.
/// </summary>
/// <param name="stack">The armor item.</param>
public void setArmor(ItemStack? stack) => setItem(1, stack);
}

View file

@ -0,0 +1,574 @@
namespace Minecraft.Server.FourKit.Inventory;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents an inventory containing items. Behavior relating to
/// <see cref="Material.AIR"/> is unspecified.
/// </summary>
public class Inventory : IEnumerable<ItemStack>
{
internal static bool _slotModifiedByPlugin;
private readonly string _name;
private readonly InventoryType _type;
internal readonly ItemStack?[] _items;
private readonly int _nativeEntityId = -1;
internal Inventory(string name, InventoryType type, int size)
{
_name = name;
_type = type;
_items = new ItemStack?[size];
}
internal Inventory(string name, InventoryType type, int size, int entityId)
: this(name, type, size)
{
_nativeEntityId = entityId;
}
protected internal virtual void EnsureSynced()
{
if (_nativeEntityId < 0 || NativeBridge.GetContainerContents == null)
return;
int[] buf = new int[_items.Length * 3];
var gh = GCHandle.Alloc(buf, GCHandleType.Pinned);
try
{
NativeBridge.GetContainerContents(_nativeEntityId, gh.AddrOfPinnedObject(), _items.Length);
}
finally
{
gh.Free();
}
for (int i = 0; i < _items.Length; i++)
{
int id = buf[i * 3];
int count = buf[i * 3 + 1];
int aux = buf[i * 3 + 2];
if (id > 0 && count > 0)
_items[i] = new ItemStack(id, count, (short)aux);
else
_items[i] = null;
}
}
/// <summary>
/// Returns the size of the inventory.
/// </summary>
/// <returns>The size of the inventory.</returns>
public int getSize() => _items.Length;
/// <summary>
/// Returns the name of the inventory.
/// </summary>
/// <returns>The String with the name of the inventory.</returns>
public string getName() => _name;
/// <summary>
/// Returns the ItemStack found in the slot at the given index.
/// </summary>
/// <param name="index">The index of the Slot's ItemStack to return.</param>
/// <returns>The ItemStack in the slot.</returns>
public virtual ItemStack? getItem(int index)
{
EnsureSynced();
if (index < 0 || index >= _items.Length) return null;
return _items[index];
}
/// <summary>
/// Stores the ItemStack at the given index of the inventory.
/// </summary>
/// <param name="index">The index where to put the ItemStack.</param>
/// <param name="item">The ItemStack to set.</param>
public virtual void setItem(int index, ItemStack? item)
{
if (index >= 0 && index < _items.Length)
{
_items[index] = item;
_slotModifiedByPlugin = true;
}
if (_nativeEntityId >= 0 && NativeBridge.SetContainerSlot != null && index >= 0 && index < _items.Length)
{
int id = item?.getTypeId() ?? 0;
int count = item?.getAmount() ?? 0;
int aux = item?.getDurability() ?? 0;
NativeBridge.SetContainerSlot(_nativeEntityId, index, id, count, aux);
}
}
/// <summary>
/// Stores the given ItemStacks in the inventory. This will try to fill
/// existing stacks and empty slots as well as it can.
/// The returned Dictionary contains what it couldn't store, where the key
/// is the index of the parameter, and the value is the ItemStack at that
/// index of the params parameter.
/// </summary>
/// <param name="items">The ItemStacks to add.</param>
/// <returns>A Dictionary containing items that didn't fit.</returns>
public Dictionary<int, ItemStack> addItem(params ItemStack[] items)
{
EnsureSynced();
var leftover = new Dictionary<int, ItemStack>();
for (int i = 0; i < items.Length; i++)
{
var toAdd = items[i];
if (toAdd == null) continue;
int remaining = toAdd.getAmount();
for (int slot = 0; slot < _items.Length && remaining > 0; slot++)
{
var existing = _items[slot];
if (existing != null && existing.getType() == toAdd.getType() &&
existing.getDurability() == toAdd.getDurability())
{
int canFit = 64 - existing.getAmount();
if (canFit > 0)
{
int added = Math.Min(canFit, remaining);
existing.setAmount(existing.getAmount() + added);
setItem(slot, existing);
remaining -= added;
}
}
}
for (int slot = 0; slot < _items.Length && remaining > 0; slot++)
{
if (_items[slot] == null)
{
int added = Math.Min(64, remaining);
setItem(slot, new ItemStack(toAdd.getType(), added, toAdd.getDurability()));
remaining -= added;
}
}
if (remaining > 0)
leftover[i] = new ItemStack(toAdd.getType(), remaining, toAdd.getDurability());
}
return leftover;
}
/// <summary>
/// Removes the given ItemStacks from the inventory.
/// It will try to remove 'as much as possible' from the types and amounts
/// you give as arguments.
/// The returned Dictionary contains what it couldn't remove.
/// </summary>
/// <param name="items">The ItemStacks to remove.</param>
/// <returns>A Dictionary containing items that couldn't be removed.</returns>
public Dictionary<int, ItemStack> removeItem(params ItemStack[] items)
{
EnsureSynced();
var leftover = new Dictionary<int, ItemStack>();
for (int i = 0; i < items.Length; i++)
{
var toRemove = items[i];
if (toRemove == null) continue;
int remaining = toRemove.getAmount();
for (int slot = 0; slot < _items.Length && remaining > 0; slot++)
{
var existing = _items[slot];
if (existing != null && existing.getType() == toRemove.getType() &&
existing.getDurability() == toRemove.getDurability())
{
int removed = Math.Min(existing.getAmount(), remaining);
existing.setAmount(existing.getAmount() - removed);
remaining -= removed;
if (existing.getAmount() <= 0)
setItem(slot, null);
else
setItem(slot, existing);
}
}
if (remaining > 0)
leftover[i] = new ItemStack(toRemove.getType(), remaining, toRemove.getDurability());
}
return leftover;
}
/// <summary>
/// Returns all ItemStacks from the inventory.
/// </summary>
/// <returns>An array of ItemStacks from the inventory.</returns>
public ItemStack?[] getContents()
{
EnsureSynced();
return (ItemStack?[])_items.Clone();
}
/// <summary>
/// Completely replaces the inventory's contents. Removes all existing
/// contents and replaces it with the ItemStacks given in the array.
/// </summary>
/// <param name="items">A complete replacement for the contents; the length must
/// be less than or equal to <see cref="getSize"/>.</param>
public void setContents(ItemStack?[] items)
{
int len = Math.Min(items.Length, _items.Length);
for (int i = 0; i < _items.Length; i++)
setItem(i, i < len ? items[i] : null);
}
/// <summary>
/// Checks if the inventory contains any ItemStacks with the given material id.
/// </summary>
/// <param name="materialId">The material id to check for.</param>
/// <returns><c>true</c> if an ItemStack in this inventory contains the material id.</returns>
public bool contains(int materialId)
{
EnsureSynced();
foreach (var item in _items)
if (item != null && item.getTypeId() == materialId) return true;
return false;
}
/// <summary>
/// Checks if the inventory contains any ItemStacks with the given material.
/// </summary>
/// <param name="material">The material to check for.</param>
/// <returns><c>true</c> if an ItemStack is found with the given Material.</returns>
public bool contains(Material material)
{
EnsureSynced();
foreach (var item in _items)
if (item != null && item.getType() == material) return true;
return false;
}
/// <summary>
/// Checks if the inventory contains any ItemStacks matching the given ItemStack.
/// This will only return true if both the type and the amount of the stack match.
/// </summary>
/// <param name="item">The ItemStack to match against.</param>
/// <returns><c>false</c> if item is null, <c>true</c> if any exactly matching ItemStacks were found.</returns>
public bool contains(ItemStack? item)
{
if (item == null) return false;
EnsureSynced();
foreach (var slot in _items)
if (slot != null && slot.getType() == item.getType() &&
slot.getAmount() == item.getAmount() &&
slot.getDurability() == item.getDurability()) return true;
return false;
}
/// <summary>
/// Checks if the inventory contains any ItemStacks with the given material id,
/// adding to at least the minimum amount specified.
/// </summary>
/// <param name="materialId">The material id to check for.</param>
/// <param name="amount">The minimum amount to look for.</param>
/// <returns><c>true</c> if this contains any matching ItemStack with the given material id and amount.</returns>
public bool contains(int materialId, int amount)
{
EnsureSynced();
int total = 0;
foreach (var item in _items)
if (item != null && item.getTypeId() == materialId) total += item.getAmount();
return total >= amount;
}
/// <summary>
/// Checks if the inventory contains any ItemStacks with the given material,
/// adding to at least the minimum amount specified.
/// </summary>
/// <param name="material">The material to check for.</param>
/// <param name="amount">The minimum amount.</param>
/// <returns><c>true</c> if enough ItemStacks were found to add to the given amount.</returns>
public bool contains(Material material, int amount)
{
if (amount <= 0) return true;
EnsureSynced();
int total = 0;
foreach (var item in _items)
if (item != null && item.getType() == material) total += item.getAmount();
return total >= amount;
}
/// <summary>
/// Checks if the inventory contains at least the minimum amount specified
/// of exactly matching ItemStacks. An ItemStack only counts if both the type
/// and the amount of the stack match.
/// </summary>
/// <param name="item">The ItemStack to match against.</param>
/// <param name="amount">How many identical stacks to check for.</param>
/// <returns><c>false</c> if item is null, <c>true</c> if amount of exactly matching ItemStacks were found.</returns>
public bool contains(ItemStack? item, int amount)
{
if (item == null) return false;
if (amount <= 0) return true;
EnsureSynced();
int count = 0;
foreach (var slot in _items)
if (slot != null && slot.getType() == item.getType() &&
slot.getAmount() == item.getAmount() &&
slot.getDurability() == item.getDurability()) count++;
return count >= amount;
}
/// <summary>
/// Checks if the inventory contains ItemStacks matching the given ItemStack
/// whose amounts sum to at least the minimum amount specified.
/// </summary>
/// <param name="item">The ItemStack to match against.</param>
/// <param name="amount">The minimum amount.</param>
/// <returns><c>false</c> if item is null, <c>true</c> if enough ItemStacks were found to add to the given amount.</returns>
public bool containsAtLeast(ItemStack? item, int amount)
{
if (item == null) return false;
if (amount <= 0) return true;
EnsureSynced();
int total = 0;
foreach (var slot in _items)
if (slot != null && slot.getType() == item.getType() &&
slot.getDurability() == item.getDurability())
total += slot.getAmount();
return total >= amount;
}
/// <summary>
/// Returns a Dictionary with all slots and ItemStacks in the inventory with given material id.
/// </summary>
/// <param name="materialId">The material id to look for.</param>
/// <returns>A Dictionary containing the slot index, ItemStack pairs.</returns>
public Dictionary<int, ItemStack> all(int materialId)
{
EnsureSynced();
var result = new Dictionary<int, ItemStack>();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getTypeId() == materialId)
result[i] = _items[i]!;
return result;
}
/// <summary>
/// Returns a Dictionary with all slots and ItemStacks in the inventory with the given Material.
/// </summary>
/// <param name="material">The material to look for.</param>
/// <returns>A Dictionary containing the slot index, ItemStack pairs.</returns>
public Dictionary<int, ItemStack> all(Material material)
{
EnsureSynced();
var result = new Dictionary<int, ItemStack>();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getType() == material)
result[i] = _items[i]!;
return result;
}
/// <summary>
/// Finds all slots in the inventory containing any ItemStacks with the given ItemStack.
/// This will only match slots if both the type and the amount of the stack match.
/// </summary>
/// <param name="item">The ItemStack to match against.</param>
/// <returns>A dictionary from slot indexes to item at index.</returns>
public Dictionary<int, ItemStack> all(ItemStack? item)
{
var result = new Dictionary<int, ItemStack>();
if (item == null) return result;
EnsureSynced();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getType() == item.getType() &&
_items[i]!.getAmount() == item.getAmount() &&
_items[i]!.getDurability() == item.getDurability())
result[i] = _items[i]!;
return result;
}
/// <summary>
/// Finds the first slot in the inventory containing an ItemStack with the given material id.
/// </summary>
/// <param name="materialId">The material id to look for.</param>
/// <returns>The slot index of the given material id or -1 if not found.</returns>
public int first(int materialId)
{
EnsureSynced();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getTypeId() == materialId) return i;
return -1;
}
/// <summary>
/// Finds the first slot in the inventory containing an ItemStack with the given material.
/// </summary>
/// <param name="material">The material to look for.</param>
/// <returns>The slot index of the given Material or -1 if not found.</returns>
public int first(Material material)
{
EnsureSynced();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getType() == material) return i;
return -1;
}
/// <summary>
/// Returns the first slot in the inventory containing an ItemStack with the given stack.
/// This will only match a slot if both the type and the amount of the stack match.
/// </summary>
/// <param name="item">The ItemStack to match against.</param>
/// <returns>The slot index of the given ItemStack or -1 if not found.</returns>
public int first(ItemStack? item)
{
if (item == null) return -1;
EnsureSynced();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getType() == item.getType() &&
_items[i]!.getAmount() == item.getAmount() &&
_items[i]!.getDurability() == item.getDurability()) return i;
return -1;
}
/// <summary>
/// Returns the first empty Slot.
/// </summary>
/// <returns>The first empty Slot found, or -1 if no empty slots.</returns>
public int firstEmpty()
{
EnsureSynced();
for (int i = 0; i < _items.Length; i++)
if (_items[i] == null) return i;
return -1;
}
/// <summary>
/// Removes all stacks in the inventory matching the given material id.
/// </summary>
/// <param name="materialId">The material to remove.</param>
public void remove(int materialId)
{
EnsureSynced();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getTypeId() == materialId)
setItem(i, null);
}
/// <summary>
/// Removes all stacks in the inventory matching the given material.
/// </summary>
/// <param name="material">The material to remove.</param>
public void remove(Material material)
{
EnsureSynced();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getType() == material)
setItem(i, null);
}
/// <summary>
/// Removes all stacks in the inventory matching the given stack.
/// This will only match a slot if both the type and the amount of the stack match.
/// </summary>
/// <param name="item">The ItemStack to match against.</param>
public void remove(ItemStack? item)
{
if (item == null) return;
EnsureSynced();
for (int i = 0; i < _items.Length; i++)
if (_items[i] != null && _items[i]!.getType() == item.getType() &&
_items[i]!.getAmount() == item.getAmount() &&
_items[i]!.getDurability() == item.getDurability())
setItem(i, null);
}
/// <summary>
/// Clears out a particular slot in the index.
/// </summary>
/// <param name="index">The index to empty.</param>
public void clear(int index)
{
setItem(index, null);
}
/// <summary>
/// Clears out the whole Inventory.
/// </summary>
public void clear()
{
for (int i = 0; i < _items.Length; i++)
setItem(i, null);
}
/// <summary>
/// Returns the title of this inventory.
/// </summary>
/// <returns>A String with the title.</returns>
public string getTitle() => _name;
/// <summary>
/// Returns what type of inventory this is.
/// </summary>
/// <returns>The InventoryType representing the type of inventory.</returns>
public InventoryType getType() => _type;
/// <summary>
/// Gets a list of players viewing the inventory.
/// </summary>
/// <returns>A list of HumanEntities who are viewing this Inventory.</returns>
public virtual List<HumanEntity> getViewers()
{
if (_nativeEntityId < 0 || NativeBridge.GetContainerViewerEntityIds == null)
return new List<HumanEntity>();
int[] ids = new int[64];
var ghIds = GCHandle.Alloc(ids, GCHandleType.Pinned);
int[] countBuf = new int[1];
var ghCount = GCHandle.Alloc(countBuf, GCHandleType.Pinned);
try
{
NativeBridge.GetContainerViewerEntityIds(_nativeEntityId, ghIds.AddrOfPinnedObject(), 64, ghCount.AddrOfPinnedObject());
}
finally
{
ghIds.Free();
ghCount.Free();
}
int count = countBuf[0];
var viewers = new List<HumanEntity>();
for (int i = 0; i < count; i++)
{
var player = FourKit.GetPlayerByEntityId(ids[i]);
if (player != null)
viewers.Add(player);
}
return viewers;
}
/// <summary>
/// Returns an iterator starting at the given index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>An enumerator.</returns>
public IEnumerator<ItemStack> iterator(int index)
{
EnsureSynced();
int start = index >= 0 ? index : _items.Length + index;
return GetEnumeratorFrom(start);
}
/// <inheritdoc/>
public IEnumerator<ItemStack> GetEnumerator()
{
EnsureSynced();
return GetEnumeratorFrom(0);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private IEnumerator<ItemStack> GetEnumeratorFrom(int start)
{
for (int i = start; i < _items.Length; i++)
if (_items[i] != null)
yield return _items[i]!;
}
}

View file

@ -0,0 +1,10 @@
namespace Minecraft.Server.FourKit.Inventory;
public interface InventoryHolder
{
/// <summary>
/// Get the object's inventory.
/// </summary>
/// <returns>The inventory.</returns>
Inventory getInventory();
}

View file

@ -0,0 +1,115 @@
namespace Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents the different types of inventories available.
/// </summary>
public enum InventoryType
{
/// <summary>A chest inventory, with 0, 9, 18, 27, 36, 45, or 54 slots of type CONTAINER.</summary>
CHEST,
/// <summary>A dispenser inventory, with 9 slots of type CONTAINER.</summary>
DISPENSER,
/// <summary>A dropper inventory, with 9 slots of type CONTAINER.</summary>
DROPPER,
/// <summary>A furnace inventory, with a RESULT slot, a CRAFTING slot, and a FUEL slot.</summary>
FURNACE,
/// <summary>A workbench inventory, with 9 CRAFTING slots and a RESULT slot.</summary>
WORKBENCH,
/// <summary>A player's crafting inventory, with 4 CRAFTING slots and a RESULT slot.</summary>
CRAFTING,
/// <summary>An enchantment table inventory, with one CRAFTING slot and three enchanting buttons.</summary>
ENCHANTING,
/// <summary>A brewing stand inventory, with one FUEL slot and three CRAFTING slots.</summary>
BREWING,
/// <summary>A player's inventory, with 9 QUICKBAR slots, 27 CONTAINER slots, and 4 ARMOR slots.</summary>
PLAYER,
/// <summary>The creative mode inventory, with only 9 QUICKBAR slots and nothing else.</summary>
CREATIVE,
/// <summary>The merchant inventory, with 2 TRADE-IN slots, and 1 RESULT slot.</summary>
MERCHANT,
/// <summary>The ender chest inventory, with 27 slots.</summary>
ENDER_CHEST,
/// <summary>An anvil inventory, with 2 CRAFTING slots and 1 RESULT slot.</summary>
ANVIL,
/// <summary>A beacon inventory, with 1 CRAFTING slot.</summary>
BEACON,
/// <summary>A hopper inventory, with 5 slots of type CONTAINER.</summary>
HOPPER
}
/// <summary>
/// Represents a slot type within an inventory.
/// </summary>
public enum SlotType
{
/// <summary>A result slot in a furnace or crafting inventory.</summary>
RESULT,
/// <summary>A slot in the crafting matrix, or the input slot in a furnace inventory, the potion slot in the brewing stand, or the enchanting slot.</summary>
CRAFTING,
/// <summary>An armour slot in the player's inventory.</summary>
ARMOR,
/// <summary>A regular slot in the container or the player's inventory.</summary>
CONTAINER,
/// <summary>A slot in the bottom row or quickbar.</summary>
QUICKBAR,
/// <summary>A pseudo-slot representing the area outside the inventory window.</summary>
OUTSIDE,
/// <summary>The fuel slot in a furnace inventory, or the ingredient slot in a brewing stand inventory.</summary>
FUEL,
}
/// <summary>
/// Provides default size and title information for each <see cref="InventoryType"/>.
/// </summary>
public static class InventoryTypeExtensions
{
/// <summary>
/// Gets the default size for this inventory type.
/// </summary>
/// <param name="type">The inventory type.</param>
/// <returns>The default number of slots.</returns>
public static int getDefaultSize(this InventoryType type) => type switch
{
InventoryType.CHEST => 27,
InventoryType.DISPENSER => 9,
InventoryType.DROPPER => 9,
InventoryType.FURNACE => 3,
InventoryType.WORKBENCH => 10,
InventoryType.CRAFTING => 5,
InventoryType.ENCHANTING => 1,
InventoryType.BREWING => 4,
InventoryType.PLAYER => 40,
InventoryType.CREATIVE => 9,
InventoryType.MERCHANT => 3,
InventoryType.ENDER_CHEST => 27,
InventoryType.ANVIL => 3,
InventoryType.BEACON => 1,
InventoryType.HOPPER => 5,
_ => 0,
};
/// <summary>
/// Gets the default title for this inventory type.
/// </summary>
/// <param name="type">The inventory type.</param>
/// <returns>The default title string.</returns>
public static string getDefaultTitle(this InventoryType type) => type switch
{
InventoryType.CHEST => "Chest",
InventoryType.DISPENSER => "Dispenser",
InventoryType.DROPPER => "Dropper",
InventoryType.FURNACE => "Furnace",
InventoryType.WORKBENCH => "Crafting",
InventoryType.CRAFTING => "Crafting",
InventoryType.ENCHANTING => "Enchant",
InventoryType.BREWING => "Brewing",
InventoryType.PLAYER => "Player",
InventoryType.CREATIVE => "Creative",
InventoryType.MERCHANT => "Trading",
InventoryType.ENDER_CHEST => "Ender Chest",
InventoryType.ANVIL => "Repairing",
InventoryType.BEACON => "Beacon",
InventoryType.HOPPER => "Item Hopper",
_ => "Inventory",
};
}

View file

@ -0,0 +1,225 @@
namespace Minecraft.Server.FourKit.Inventory;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents a view linking two inventories and a single player
/// (whose inventory may or may not be one of the two).
/// </summary>
public class InventoryView
{
/// <summary>
/// Represents the raw slot ID for clicks outside the inventory window.
/// </summary>
public static readonly int OUTSIDE = -999;
private readonly Inventory _topInventory;
private readonly Inventory _bottomInventory;
private readonly HumanEntity _player;
private readonly InventoryType _type;
/// <summary>
/// Creates a new InventoryView linking two inventories and a player.
/// </summary>
/// <param name="topInventory">The upper inventory.</param>
/// <param name="bottomInventory">The lower inventory.</param>
/// <param name="player">The player viewing.</param>
/// <param name="type">The inventory type.</param>
public InventoryView(Inventory topInventory, Inventory bottomInventory, HumanEntity player, InventoryType type)
{
_topInventory = topInventory;
_bottomInventory = bottomInventory;
_player = player;
_type = type;
}
/// <summary>
/// Get the upper inventory involved in this transaction.
/// </summary>
/// <returns>The inventory.</returns>
public virtual Inventory getTopInventory() => _topInventory;
/// <summary>
/// Get the lower inventory involved in this transaction.
/// </summary>
/// <returns>The inventory.</returns>
public virtual Inventory getBottomInventory() => _bottomInventory;
/// <summary>
/// Get the player viewing.
/// </summary>
/// <returns>The player.</returns>
public virtual HumanEntity getPlayer() => _player;
/// <summary>
/// Determine the type of inventory involved in the transaction. This indicates
/// the window style being shown. It will never return PLAYER, since that is
/// common to all windows.
/// </summary>
/// <returns>The inventory type.</returns>
public virtual InventoryType getType() => _type;
/// <summary>
/// Sets one item in this inventory view by its raw slot ID.
/// </summary>
/// <remarks>
/// If slot ID -999 is chosen, it may be expected that the item is dropped
/// on the ground. This is not required behaviour, however.
/// </remarks>
/// <param name="slot">The raw slot ID.</param>
/// <param name="item">The new item to put in the slot, or null to clear it.</param>
public void setItem(int slot, ItemStack? item)
{
if (slot == OUTSIDE) return;
int topSize = _topInventory.getSize();
if (slot < topSize)
_topInventory.setItem(slot, item);
else
_bottomInventory.setItem(slot - topSize, item);
}
/// <summary>
/// Gets one item in this inventory view by its raw slot ID.
/// </summary>
/// <param name="slot">The raw slot ID.</param>
/// <returns>The item currently in the slot.</returns>
public ItemStack? getItem(int slot)
{
if (slot == OUTSIDE) return null;
int topSize = _topInventory.getSize();
if (slot < topSize)
return _topInventory.getItem(slot);
return _bottomInventory.getItem(slot - topSize);
}
/// <summary>
/// Sets the item on the cursor of one of the viewing players.
/// </summary>
/// <param name="item">The item to put on the cursor, or null to remove it.</param>
public void setCursor(ItemStack? item)
{
_player.setItemOnCursor(item);
}
/// <summary>
/// Get the item on the cursor of one of the viewing players.
/// </summary>
/// <returns>The item on the player's cursor, or null if they aren't holding one.</returns>
public ItemStack? getCursor()
{
return _player.getItemOnCursor();
}
/// <summary>
/// Converts a raw slot ID into its local slot ID into whichever of the two
/// inventories the slot points to.
/// </summary>
/// <param name="rawSlot">The raw slot ID.</param>
/// <returns>The converted slot ID.</returns>
public int convertSlot(int rawSlot)
{
int topSize = _topInventory.getSize();
if (rawSlot < topSize)
return rawSlot;
return rawSlot - topSize;
}
/// <summary>
/// Closes the inventory view.
/// </summary>
public void close()
{
}
/// <summary>
/// Check the total number of slots in this view, combining the upper
/// and lower inventories.
/// </summary>
/// <returns>The total size.</returns>
public int countSlots()
{
return _topInventory.getSize() + _bottomInventory.getSize();
}
/// <summary>
/// Sets an extra property of this inventory if supported by that inventory,
/// for example the state of a progress bar.
/// </summary>
/// <param name="prop">The window property to update.</param>
/// <param name="value">The new value for the window property.</param>
/// <returns>true if the property was updated successfully.</returns>
public bool setProperty(Property prop, int value)
{
return false;
}
/// <summary>
/// Get the title of this inventory window.
/// </summary>
/// <returns>The title.</returns>
public string getTitle()
{
return _topInventory.getTitle();
}
/// <summary>
/// Represents various extra properties of certain inventory windows.
/// </summary>
public enum Property
{
/// <summary>The progress of the down-pointing arrow in a brewing inventory.</summary>
BREW_TIME,
/// <summary>The progress of the flame in a furnace inventory.</summary>
BURN_TIME,
/// <summary>The progress of the right-pointing arrow in a furnace inventory.</summary>
COOK_TIME,
/// <summary>In an enchanting inventory, the top button's experience level value.</summary>
ENCHANT_BUTTON1,
/// <summary>In an enchanting inventory, the middle button's experience level value.</summary>
ENCHANT_BUTTON2,
/// <summary>In an enchanting inventory, the bottom button's experience level value.</summary>
ENCHANT_BUTTON3,
/// <summary>How many total ticks the current fuel should last.</summary>
TICKS_FOR_CURRENT_FUEL
}
}
/// <summary>
/// Extension methods for <see cref="InventoryView.Property"/>.
/// </summary>
public static class InventoryViewPropertyExtensions
{
/// <summary>
/// Gets the <see cref="InventoryType"/> that this property belongs to.
/// </summary>
/// <param name="prop">The property.</param>
/// <returns>The inventory type.</returns>
public static InventoryType getType(this InventoryView.Property prop) => prop switch
{
InventoryView.Property.BREW_TIME => InventoryType.BREWING,
InventoryView.Property.BURN_TIME => InventoryType.FURNACE,
InventoryView.Property.COOK_TIME => InventoryType.FURNACE,
InventoryView.Property.ENCHANT_BUTTON1 => InventoryType.ENCHANTING,
InventoryView.Property.ENCHANT_BUTTON2 => InventoryType.ENCHANTING,
InventoryView.Property.ENCHANT_BUTTON3 => InventoryType.ENCHANTING,
InventoryView.Property.TICKS_FOR_CURRENT_FUEL => InventoryType.FURNACE,
_ => InventoryType.CHEST,
};
/// <summary>
/// Gets the window-property id for this <see cref="InventoryView.Property"/>.
/// </summary>
/// <param name="prop">The property.</param>
/// <returns>The id.</returns>
public static int getId(this InventoryView.Property prop) => prop switch
{
InventoryView.Property.BREW_TIME => 0,
InventoryView.Property.BURN_TIME => 0,
InventoryView.Property.COOK_TIME => 2,
InventoryView.Property.ENCHANT_BUTTON1 => 0,
InventoryView.Property.ENCHANT_BUTTON2 => 1,
InventoryView.Property.ENCHANT_BUTTON3 => 2,
InventoryView.Property.TICKS_FOR_CURRENT_FUEL => 1,
_ => -1,
};
}

View file

@ -0,0 +1,102 @@
namespace Minecraft.Server.FourKit.Inventory;
/// <summary>
/// Represents a stack of items.
/// </summary>
public class ItemStack
{
private Material _type;
private int _amount;
private short _durability;
/// <summary>
/// Creates a new ItemStack of the specified material with amount 1.
/// </summary>
public ItemStack(Material type) : this(type, 1) { }
/// <summary>
/// Creates a new ItemStack of the specified material and amount.
/// </summary>
public ItemStack(Material type, int amount) : this(type, amount, 0) { }
/// <summary>
/// Creates a new ItemStack of the specified material, amount and durability.
/// </summary>
public ItemStack(Material type, int amount, short durability)
{
_type = type;
_amount = amount;
_durability = durability;
}
/// <summary>
/// Creates a new ItemStack from a raw type ID with amount 1.
/// </summary>
public ItemStack(int typeId) : this(typeId, 1) { }
/// <summary>
/// Creates a new ItemStack from a raw type ID and amount.
/// </summary>
public ItemStack(int typeId, int amount) : this(typeId, amount, 0) { }
/// <summary>
/// Creates a new ItemStack from a raw type ID, amount and durability.
/// </summary>
public ItemStack(int typeId, int amount, short durability)
{
_type = Enum.IsDefined(typeof(Material), typeId) ? (Material)typeId : Material.AIR;
_amount = amount;
_durability = durability;
}
/// <summary>
/// Gets the type of this item.
/// </summary>
/// <returns>Type of the items in this stack.</returns>
public Material getType() => _type;
/// <summary>
/// Sets the type of this item.
/// </summary>
/// <param name="type">New type to set the items in this stack to.</param>
public void setType(Material type) => _type = type;
/// <summary>
/// Gets the type id for this item.
/// </summary>
/// <returns>Type Id of the items in this stack.</returns>
public int getTypeId() => (int)_type;
/// <summary>
/// Sets the type id for this item.
/// </summary>
/// <param name="type">New type id to set the items in this stack to.</param>
public void setTypeId(int type)
{
_type = Enum.IsDefined(typeof(Material), type) ? (Material)type : Material.AIR;
}
/// <summary>
/// Gets the amount of items in this stack.
/// </summary>
/// <returns>Amount of items in this stack.</returns>
public int getAmount() => _amount;
/// <summary>
/// Sets the amount of items in this stack.
/// </summary>
/// <param name="amount">New amount of items in this stack.</param>
public void setAmount(int amount) => _amount = amount;
/// <summary>
/// Gets the durability of this item.
/// </summary>
/// <returns>Durability of this item.</returns>
public short getDurability() => _durability;
/// <summary>
/// Sets the durability of this item.
/// </summary>
/// <param name="durability">Durability of this item.</param>
public void setDurability(short durability) => _durability = durability;
}

View file

@ -0,0 +1,202 @@
namespace Minecraft.Server.FourKit.Inventory;
using System.Runtime.InteropServices;
using Minecraft.Server.FourKit.Entity;
/// <summary>
/// Represents a player's inventory, including armor slots and the held item.
/// </summary>
public class PlayerInventory : Inventory
{
private const int INVENTORY_SIZE = 40;
private const int ARMOR_START = 36;
private const int QUICKBAR_SIZE = 9;
private int _heldItemSlot;
internal HumanEntity? _holder;
internal PlayerInventory()
: base("Player", InventoryType.PLAYER, INVENTORY_SIZE)
{
}
protected internal override void EnsureSynced()
{
if (_holder == null || NativeBridge.GetPlayerInventory == null)
return;
int entityId = _holder.getEntityId();
int[] buf = new int[121];
var gh = GCHandle.Alloc(buf, GCHandleType.Pinned);
try
{
NativeBridge.GetPlayerInventory(entityId, gh.AddrOfPinnedObject());
}
finally
{
gh.Free();
}
for (int i = 0; i < INVENTORY_SIZE; i++)
{
int id = buf[i * 3];
int count = buf[i * 3 + 1];
int aux = buf[i * 3 + 2];
if (id > 0 && count > 0)
_items[i] = new ItemStack(id, count, (short)aux);
else
_items[i] = null;
}
_heldItemSlot = buf[120];
}
/// <inheritdoc/>
public override void setItem(int index, ItemStack? item)
{
base.setItem(index, item);
_slotModifiedByPlugin = true;
if (_holder != null && NativeBridge.SetPlayerInventorySlot != null &&
index >= 0 && index < INVENTORY_SIZE)
{
int id = item?.getTypeId() ?? 0;
int count = item?.getAmount() ?? 0;
int aux = item?.getDurability() ?? 0;
NativeBridge.SetPlayerInventorySlot(_holder.getEntityId(), index, id, count, aux);
}
}
/// <summary>
/// Returns all ItemStacks from the armor slots.
/// </summary>
/// <returns>An array of ItemStacks for the armor slots.</returns>
public ItemStack?[] getArmorContents()
{
EnsureSynced();
var armor = new ItemStack?[4];
for (int i = 0; i < 4; i++)
armor[i] = _items[ARMOR_START + i];
return armor;
}
/// <summary>
/// Gets the ItemStack in the helmet slot.
/// </summary>
/// <returns>The helmet ItemStack.</returns>
public ItemStack? getHelmet() => getItem(ARMOR_START + 3);
/// <summary>
/// Gets the ItemStack in the chestplate slot.
/// </summary>
/// <returns>The chestplate ItemStack.</returns>
public ItemStack? getChestplate() => getItem(ARMOR_START + 2);
/// <summary>
/// Gets the ItemStack in the leggings slot.
/// </summary>
/// <returns>The leggings ItemStack.</returns>
public ItemStack? getLeggings() => getItem(ARMOR_START + 1);
/// <summary>
/// Gets the ItemStack in the boots slot.
/// </summary>
/// <returns>The boots ItemStack.</returns>
public ItemStack? getBoots() => getItem(ARMOR_START);
/// <summary>
/// Sets all four armor slots at once.
/// </summary>
/// <param name="items">An array of ItemStacks for the armor slots.</param>
public void setArmorContents(ItemStack?[] items)
{
int len = Math.Min(items.Length, 4);
for (int i = 0; i < len; i++)
setItem(ARMOR_START + i, items[i]);
}
/// <summary>
/// Sets the helmet slot.
/// </summary>
/// <param name="helmet">The ItemStack to set.</param>
public void setHelmet(ItemStack? helmet) => setItem(ARMOR_START + 3, helmet);
/// <summary>
/// Sets the chestplate slot.
/// </summary>
/// <param name="chestplate">The ItemStack to set.</param>
public void setChestplate(ItemStack? chestplate) => setItem(ARMOR_START + 2, chestplate);
/// <summary>
/// Sets the leggings slot.
/// </summary>
/// <param name="leggings">The ItemStack to set.</param>
public void setLeggings(ItemStack? leggings) => setItem(ARMOR_START + 1, leggings);
/// <summary>
/// Sets the boots slot.
/// </summary>
/// <param name="boots">The ItemStack to set.</param>
public void setBoots(ItemStack? boots) => setItem(ARMOR_START, boots);
/// <summary>
/// Gets the item the player is currently holding.
/// </summary>
/// <returns>The held ItemStack.</returns>
public ItemStack? getItemInHand() => getItem(_heldItemSlot);
/// <summary>
/// Sets the item in the player's hand.
/// </summary>
/// <param name="stack">The ItemStack to set.</param>
public void setItemInHand(ItemStack? stack) => setItem(_heldItemSlot, stack);
/// <summary>
/// Gets the slot number of the currently held item.
/// </summary>
/// <returns>The held item slot index (0-8).</returns>
public int getHeldItemSlot()
{
EnsureSynced();
return _heldItemSlot;
}
/// <summary>
/// Sets the slot number of the currently held item.
/// </summary>
/// <param name="slot">The slot index (0-8).</param>
public void setHeldItemSlot(int slot)
{
if (slot < 0 || slot >= QUICKBAR_SIZE)
throw new ArgumentException($"Slot must be between 0 and {QUICKBAR_SIZE - 1} inclusive.");
_heldItemSlot = slot;
}
/// <summary>
/// Clears all matching items from the inventory. Setting either value
/// to -1 will skip its check, while setting both to -1 will clear all
/// items in your inventory unconditionally.
/// </summary>
/// <param name="id">The material id to match, or -1 for any.</param>
/// <param name="data">The data value to match, or -1 for any.</param>
/// <returns>The number of stacks cleared.</returns>
public int clear(int id, int data)
{
EnsureSynced();
int count = 0;
for (int i = 0; i < getSize(); i++)
{
var item = _items[i];
if (item == null) continue;
if (id != -1 && item.getTypeId() != id) continue;
if (data != -1 && item.getDurability() != data) continue;
setItem(i, null);
count++;
}
return count;
}
/// <summary>
/// Gets the holder of this inventory.
/// </summary>
/// <returns>The HumanEntity that owns this inventory.</returns>
public HumanEntity? getHolder() => _holder;
}

View file

@ -0,0 +1,189 @@
namespace Minecraft.Server.FourKit;
/// <summary>
/// Represents a 3-dimensional position in a world.
/// </summary>
public class Location
{
internal double X { get; set; }
internal double Y { get; set; }
internal double Z { get; set; }
internal float Yaw { get; set; }
internal float Pitch { get; set; }
internal World? LocationWorld { get; set; }
/// <summary>
/// Constructs a new Location with the given coordinates and direction.
/// </summary>
/// <param name="world">The world in which this location resides.</param>
/// <param name="x">The x-coordinate.</param>
/// <param name="y">The y-coordinate.</param>
/// <param name="z">The z-coordinate.</param>
/// <param name="yaw">The absolute rotation on the x-plane, in degrees.</param>
/// <param name="pitch">The absolute rotation on the y-plane, in degrees.</param>
public Location(World? world, double x, double y, double z, float yaw, float pitch)
{
LocationWorld = world;
X = x;
Y = y;
Z = z;
Yaw = yaw;
Pitch = pitch;
}
/// <summary>
/// Constructs a new Location with the given coordinates.
/// </summary>
/// <param name="world">The world in which this location resides.</param>
/// <param name="x">The x-coordinate.</param>
/// <param name="y">The y-coordinate.</param>
/// <param name="z">The z-coordinate.</param>
public Location(World? world, double x, double y, double z) : this(world, x, y, z, 0f, 0f) { }
/// <summary>
/// Creates a new <see cref="Location"/> with the given coordinates and no world.
/// </summary>
/// <param name="x">The x-coordinate.</param>
/// <param name="y">The y-coordinate.</param>
/// <param name="z">The z-coordinate.</param>
public Location(double x, double y, double z) : this(null, x, y, z, 0f, 0f) { }
// use for internal
internal Location() : this(null, 0, 0, 0, 0f, 0f) { }
/// <summary>
/// Gets the x-coordinate of this location.
/// </summary>
/// <returns>The x-coordinate.</returns>
public double getX() => X;
/// <summary>
/// Sets the x-coordinate of this location.
/// </summary>
/// <param name="x">The new x-coordinate.</param>
public void setX(double x) => X = x;
/// <summary>
/// Gets the y-coordinate of this location.
/// </summary>
/// <returns>The y-coordinate.</returns>
public double getY() => Y;
/// <summary>
/// Sets the y-coordinate of this location.
/// </summary>
/// <param name="y">The new y-coordinate.</param>
public void setY(double y) => Y = y;
/// <summary>
/// Gets the z-coordinate of this location.
/// </summary>
/// <returns>The z-coordinate.</returns>
public double getZ() => Z;
/// <summary>
/// Sets the z-coordinate of this location.
/// </summary>
/// <param name="z">The new z-coordinate.</param>
public void setZ(double z) => Z = z;
/// <summary>
/// Gets the yaw of this location, measured in degrees.
/// </summary>
/// <returns>The yaw.</returns>
public float getYaw() => Yaw;
/// <summary>
/// Sets the yaw of this location, measured in degrees.
/// </summary>
/// <param name="yaw">The new yaw.</param>
public void setYaw(float yaw) => Yaw = yaw;
/// <summary>
/// Gets the pitch of this location, measured in degrees.
/// </summary>
/// <returns>The pitch.</returns>
public float getPitch() => Pitch;
/// <summary>
/// Sets the pitch of this location, measured in degrees.
/// </summary>
/// <param name="pitch">The new pitch.</param>
public void setPitch(float pitch) => Pitch = pitch;
/// <summary>
/// Gets the world that this location resides in.
/// </summary>
/// <returns>The world, or <c>null</c> if not set.</returns>
public World? getWorld() => LocationWorld;
/// <summary>
/// Sets the world that this location resides in.
/// </summary>
/// <param name="world">The new world.</param>
public void setWorld(World? world) => LocationWorld = world;
/// <summary>
/// Gets the floored value of the X component, indicating the block that
/// this location is contained with.
/// </summary>
/// <returns>The block X.</returns>
public int getBlockX() => (int)Math.Floor(X);
/// <summary>
/// Gets the floored value of the Y component, indicating the block that
/// this location is contained with.
/// </summary>
/// <returns>The block Y.</returns>
public int getBlockY() => (int)Math.Floor(Y);
/// <summary>
/// Gets the floored value of the Z component, indicating the block that
/// this location is contained with.
/// </summary>
/// <returns>The block Z.</returns>
public int getBlockZ() => (int)Math.Floor(Z);
/// <summary>
/// Gets the magnitude of the location, defined as sqrt(x^2+y^2+z^2).
/// </summary>
/// <returns>The magnitude.</returns>
public double length() => Math.Sqrt(X * X + Y * Y + Z * Z);
/// <summary>
/// Gets the magnitude of the location squared.
/// </summary>
/// <returns>The magnitude squared.</returns>
public double lengthSquared() => X * X + Y * Y + Z * Z;
/// <summary>
/// Adds the location by another.
/// </summary>
/// <param name="x">The x-coordinate to add.</param>
/// <param name="y">The y-coordinate to add.</param>
/// <param name="z">The z-coordinate to add.</param>
/// <returns>This location, for chaining.</returns>
public Location add(double x, double y, double z)
{
X += x;
Y += y;
Z += z;
return this;
}
/// <summary>
/// Adds the location by another.
/// </summary>
/// <param name="vec">The location to add.</param>
/// <returns>This location, for chaining.</returns>
public Location add(Location vec)
{
X += vec.X;
Y += vec.Y;
Z += vec.Z;
return this;
}
/// <inheritdoc/>
public override string ToString() => $"Location(world={LocationWorld}, x={X}, y={Y}, z={Z}, yaw={Yaw}, pitch={Pitch})";
}

View file

@ -0,0 +1,343 @@
namespace Minecraft.Server.FourKit;
/// <summary>
/// An enum of all material IDs accepted by the official server and client.
/// </summary>
public enum Material
{
AIR = 0,
STONE = 1,
GRASS = 2,
DIRT = 3,
COBBLESTONE = 4,
WOOD = 5,
SAPLING = 6,
BEDROCK = 7,
WATER = 8,
STATIONARY_WATER = 9,
LAVA = 10,
STATIONARY_LAVA = 11,
SAND = 12,
GRAVEL = 13,
GOLD_ORE = 14,
IRON_ORE = 15,
COAL_ORE = 16,
LOG = 17,
LEAVES = 18,
SPONGE = 19,
GLASS = 20,
LAPIS_ORE = 21,
LAPIS_BLOCK = 22,
DISPENSER = 23,
SANDSTONE = 24,
NOTE_BLOCK = 25,
BED_BLOCK = 26,
POWERED_RAIL = 27,
DETECTOR_RAIL = 28,
PISTON_STICKY_BASE = 29,
WEB = 30,
LONG_GRASS = 31,
DEAD_BUSH = 32,
PISTON_BASE = 33,
PISTON_EXTENSION = 34,
WOOL = 35,
PISTON_MOVING_PIECE = 36,
YELLOW_FLOWER = 37,
RED_ROSE = 38,
BROWN_MUSHROOM = 39,
RED_MUSHROOM = 40,
GOLD_BLOCK = 41,
IRON_BLOCK = 42,
DOUBLE_STEP = 43,
STEP = 44,
BRICK = 45,
TNT = 46,
BOOKSHELF = 47,
MOSSY_COBBLESTONE = 48,
OBSIDIAN = 49,
TORCH = 50,
FIRE = 51,
MOB_SPAWNER = 52,
WOOD_STAIRS = 53,
CHEST = 54,
REDSTONE_WIRE = 55,
DIAMOND_ORE = 56,
DIAMOND_BLOCK = 57,
WORKBENCH = 58,
CROPS = 59,
SOIL = 60,
FURNACE = 61,
BURNING_FURNACE = 62,
SIGN_POST = 63,
WOODEN_DOOR = 64,
LADDER = 65,
RAILS = 66,
COBBLESTONE_STAIRS = 67,
WALL_SIGN = 68,
LEVER = 69,
STONE_PLATE = 70,
IRON_DOOR_BLOCK = 71,
WOOD_PLATE = 72,
REDSTONE_ORE = 73,
GLOWING_REDSTONE_ORE = 74,
REDSTONE_TORCH_OFF = 75,
REDSTONE_TORCH_ON = 76,
STONE_BUTTON = 77,
SNOW = 78,
ICE = 79,
SNOW_BLOCK = 80,
CACTUS = 81,
CLAY = 82,
SUGAR_CANE_BLOCK = 83,
JUKEBOX = 84,
FENCE = 85,
PUMPKIN = 86,
NETHERRACK = 87,
SOUL_SAND = 88,
GLOWSTONE = 89,
PORTAL = 90,
JACK_O_LANTERN = 91,
CAKE_BLOCK = 92,
DIODE_BLOCK_OFF = 93,
DIODE_BLOCK_ON = 94,
STAINED_GLASS = 95,
TRAP_DOOR = 96,
MONSTER_EGGS = 97,
SMOOTH_BRICK = 98,
HUGE_MUSHROOM_1 = 99,
HUGE_MUSHROOM_2 = 100,
IRON_FENCE = 101,
THIN_GLASS = 102,
MELON_BLOCK = 103,
PUMPKIN_STEM = 104,
MELON_STEM = 105,
VINE = 106,
FENCE_GATE = 107,
BRICK_STAIRS = 108,
SMOOTH_STAIRS = 109,
MYCEL = 110,
WATER_LILY = 111,
NETHER_BRICK = 112,
NETHER_FENCE = 113,
NETHER_BRICK_STAIRS = 114,
NETHER_WARTS = 115,
ENCHANTMENT_TABLE = 116,
BREWING_STAND = 117,
CAULDRON = 118,
ENDER_PORTAL = 119,
ENDER_PORTAL_FRAME = 120,
ENDER_STONE = 121,
DRAGON_EGG = 122,
REDSTONE_LAMP_OFF = 123,
REDSTONE_LAMP_ON = 124,
WOOD_DOUBLE_STEP = 125,
WOOD_STEP = 126,
COCOA = 127,
SANDSTONE_STAIRS = 128,
EMERALD_ORE = 129,
ENDER_CHEST = 130,
TRIPWIRE_HOOK = 131,
TRIPWIRE = 132,
EMERALD_BLOCK = 133,
SPRUCE_WOOD_STAIRS = 134,
BIRCH_WOOD_STAIRS = 135,
JUNGLE_WOOD_STAIRS = 136,
COMMAND = 137,
BEACON = 138,
COBBLE_WALL = 139,
FLOWER_POT = 140,
CARROT = 141,
POTATO = 142,
WOOD_BUTTON = 143,
SKULL = 144,
ANVIL = 145,
TRAPPED_CHEST = 146,
GOLD_PLATE = 147,
IRON_PLATE = 148,
REDSTONE_COMPARATOR_OFF = 149,
REDSTONE_COMPARATOR_ON = 150,
DAYLIGHT_DETECTOR = 151,
REDSTONE_BLOCK = 152,
QUARTZ_ORE = 153,
HOPPER = 154,
QUARTZ_BLOCK = 155,
QUARTZ_STAIRS = 156,
ACTIVATOR_RAIL = 157,
DROPPER = 158,
STAINED_CLAY = 159,
STAINED_GLASS_PANE = 160,
HAY_BLOCK = 170,
CARPET = 171,
HARD_CLAY = 172,
COAL_BLOCK = 173,
// items
IRON_SPADE = 256,
IRON_PICKAXE = 257,
IRON_AXE = 258,
FLINT_AND_STEEL = 259,
APPLE = 260,
BOW = 261,
ARROW = 262,
COAL_ITEM = 263,
DIAMOND = 264,
IRON_INGOT = 265,
GOLD_INGOT = 266,
IRON_SWORD = 267,
WOOD_SWORD = 268,
WOOD_SPADE = 269,
WOOD_PICKAXE = 270,
WOOD_AXE = 271,
STONE_SWORD = 272,
STONE_SPADE = 273,
STONE_PICKAXE = 274,
STONE_AXE = 275,
DIAMOND_SWORD = 276,
DIAMOND_SPADE = 277,
DIAMOND_PICKAXE = 278,
DIAMOND_AXE = 279,
STICK = 280,
BOWL = 281,
MUSHROOM_SOUP = 282,
GOLD_SWORD = 283,
GOLD_SPADE = 284,
GOLD_PICKAXE = 285,
GOLD_AXE = 286,
STRING = 287,
FEATHER = 288,
SULPHUR = 289,
WOOD_HOE = 290,
STONE_HOE = 291,
IRON_HOE = 292,
DIAMOND_HOE = 293,
GOLD_HOE = 294,
SEEDS = 295,
WHEAT = 296,
BREAD = 297,
LEATHER_HELMET = 298,
LEATHER_CHESTPLATE = 299,
LEATHER_LEGGINGS = 300,
LEATHER_BOOTS = 301,
CHAINMAIL_HELMET = 302,
CHAINMAIL_CHESTPLATE = 303,
CHAINMAIL_LEGGINGS = 304,
CHAINMAIL_BOOTS = 305,
IRON_HELMET = 306,
IRON_CHESTPLATE = 307,
IRON_LEGGINGS = 308,
IRON_BOOTS = 309,
DIAMOND_HELMET = 310,
DIAMOND_CHESTPLATE = 311,
DIAMOND_LEGGINGS = 312,
DIAMOND_BOOTS = 313,
GOLD_HELMET = 314,
GOLD_CHESTPLATE = 315,
GOLD_LEGGINGS = 316,
GOLD_BOOTS = 317,
FLINT = 318,
PORK = 319,
GRILLED_PORK = 320,
PAINTING = 321,
GOLDEN_APPLE = 322,
SIGN = 323,
WOOD_DOOR = 324,
BUCKET = 325,
WATER_BUCKET = 326,
LAVA_BUCKET = 327,
MINECART = 328,
SADDLE = 329,
IRON_DOOR = 330,
REDSTONE = 331,
SNOW_BALL = 332,
BOAT = 333,
LEATHER = 334,
MILK_BUCKET = 335,
CLAY_BRICK = 336,
CLAY_BALL = 337,
SUGAR_CANE = 338,
PAPER = 339,
BOOK = 340,
SLIME_BALL = 341,
STORAGE_MINECART = 342,
POWERED_MINECART = 343,
EGG = 344,
COMPASS = 345,
FISHING_ROD = 346,
WATCH = 347,
GLOWSTONE_DUST = 348,
RAW_FISH = 349,
COOKED_FISH = 350,
INK_SACK = 351,
BONE = 352,
SUGAR = 353,
CAKE = 354,
BED = 355,
DIODE = 356,
COOKIE = 357,
MAP = 358,
SHEARS = 359,
MELON = 360,
PUMPKIN_SEEDS = 361,
MELON_SEEDS = 362,
RAW_BEEF = 363,
COOKED_BEEF = 364,
RAW_CHICKEN = 365,
COOKED_CHICKEN = 366,
ROTTEN_FLESH = 367,
ENDER_PEARL = 368,
BLAZE_ROD = 369,
GHAST_TEAR = 370,
GOLD_NUGGET = 371,
NETHER_STALK = 372,
POTION = 373,
GLASS_BOTTLE = 374,
SPIDER_EYE = 375,
FERMENTED_SPIDER_EYE = 376,
BLAZE_POWDER = 377,
MAGMA_CREAM = 378,
BREWING_STAND_ITEM = 379,
CAULDRON_ITEM = 380,
EYE_OF_ENDER = 381,
SPECKLED_MELON = 382,
MONSTER_EGG = 383,
EXP_BOTTLE = 384,
FIREBALL = 385,
EMERALD = 388,
ITEM_FRAME = 389,
FLOWER_POT_ITEM = 390,
CARROT_ITEM = 391,
POTATO_ITEM = 392,
BAKED_POTATO = 393,
POISONOUS_POTATO = 394,
EMPTY_MAP = 395,
GOLDEN_CARROT = 396,
SKULL_ITEM = 397,
CARROT_STICK = 398,
NETHER_STAR = 399,
PUMPKIN_PIE = 400,
FIREWORK = 401,
FIREWORK_CHARGE = 402,
ENCHANTED_BOOK = 403,
REDSTONE_COMPARATOR = 404,
NETHER_BRICK_ITEM = 405,
QUARTZ = 406,
EXPLOSIVE_MINECART = 407,
HOPPER_MINECART = 408,
IRON_BARDING = 417,
GOLD_BARDING = 418,
DIAMOND_BARDING = 419,
LEASH = 420,
NAME_TAG = 421,
GOLD_RECORD = 2256,
GREEN_RECORD = 2257,
RECORD_3 = 2258,
RECORD_4 = 2259,
RECORD_5 = 2260,
RECORD_6 = 2261,
RECORD_7 = 2262,
RECORD_9 = 2263,
RECORD_10 = 2264,
RECORD_11 = 2265,
RECORD_12 = 2266,
RECORD_8 = 2267,
}

View file

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Minecraft.Server.FourKit</RootNamespace>
<AssemblyName>Minecraft.Server.FourKit</AssemblyName>
<EnableDynamicLoading>true</EnableDynamicLoading>
<BaseOutputPath>bin</BaseOutputPath>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,192 @@
using System.Runtime.InteropServices;
namespace Minecraft.Server.FourKit;
// EAT SHIT AND DIE
internal static class NativeBridge
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeDamageDelegate(int entityId, float amount);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetHealthDelegate(int entityId, float health);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeTeleportDelegate(int entityId, double x, double y, double z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetGameModeDelegate(int entityId, int gameMode);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeBroadcastMessageDelegate(IntPtr utf8, int byteLen);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetFallDistanceDelegate(int entityId, float distance);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeGetPlayerSnapshotDelegate(int entityId, IntPtr outBuf);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSendMessageDelegate(int entityId, IntPtr utf8, int byteLen);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetWalkSpeedDelegate(int entityId, float speed);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeTeleportEntityDelegate(int entityId, int dimId, double x, double y, double z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeGetTileIdDelegate(int dimId, int x, int y, int z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetTileDelegate(int dimId, int x, int y, int z, int tileId, int data);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetTileDataDelegate(int dimId, int x, int y, int z, int data);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeBreakBlockDelegate(int dimId, int x, int y, int z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeGetHighestBlockYDelegate(int dimId, int x, int z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeGetWorldInfoDelegate(int dimId, IntPtr outBuf);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetWorldTimeDelegate(int dimId, long time);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetWeatherDelegate(int dimId, int storm, int thundering, int thunderDuration);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeCreateExplosionDelegate(int dimId, double x, double y, double z, float power, int setFire, int breakBlocks);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeStrikeLightningDelegate(int dimId, double x, double y, double z, int effectOnly);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeSetSpawnLocationDelegate(int dimId, int x, int y, int z);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeDropItemDelegate(int dimId, double x, double y, double z, int itemId, int count, int auxValue, int naturally);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeKickPlayerDelegate(int entityId, int reason);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeBanPlayerDelegate(int entityId, IntPtr reasonUtf8, int reasonByteLen);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeBanPlayerIpDelegate(int entityId, IntPtr reasonUtf8, int reasonByteLen);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int NativeGetPlayerAddressDelegate(int entityId, IntPtr outIpBuf, int outIpBufSize, IntPtr outPort);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeGetPlayerInventoryDelegate(int entityId, IntPtr outBuffer);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetPlayerInventorySlotDelegate(int entityId, int slot, int itemId, int count, int aux);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeGetContainerContentsDelegate(int entityId, IntPtr outBuffer, int maxSlots);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetContainerSlotDelegate(int entityId, int slot, int itemId, int count, int aux);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeGetContainerViewerEntityIdsDelegate(int entityId, IntPtr outIds, int maxCount, IntPtr outCount);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeCloseContainerDelegate(int entityId);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeOpenVirtualContainerDelegate(int entityId, int nativeType, IntPtr titleUtf8, int titleByteLen, int slotCount, IntPtr itemsBuf);
internal static NativeDamageDelegate? DamagePlayer;
internal static NativeSetHealthDelegate? SetPlayerHealth;
internal static NativeTeleportDelegate? TeleportPlayer;
internal static NativeSetGameModeDelegate? SetPlayerGameMode;
internal static NativeBroadcastMessageDelegate? BroadcastMessage;
internal static NativeSetFallDistanceDelegate? SetFallDistance;
internal static NativeGetPlayerSnapshotDelegate? GetPlayerSnapshot;
internal static NativeSendMessageDelegate? SendMessage;
internal static NativeSetWalkSpeedDelegate? SetWalkSpeed;
internal static NativeTeleportEntityDelegate? TeleportEntity;
internal static NativeGetTileIdDelegate? GetTileId;
internal static NativeSetTileDelegate? SetTile;
internal static NativeSetTileDataDelegate? SetTileData;
internal static NativeBreakBlockDelegate? BreakBlock;
internal static NativeGetHighestBlockYDelegate? GetHighestBlockY;
internal static NativeGetWorldInfoDelegate? GetWorldInfo;
internal static NativeSetWorldTimeDelegate? SetWorldTime;
internal static NativeSetWeatherDelegate? SetWeather;
internal static NativeCreateExplosionDelegate? CreateExplosion;
internal static NativeStrikeLightningDelegate? StrikeLightning;
internal static NativeSetSpawnLocationDelegate? SetSpawnLocation;
internal static NativeDropItemDelegate? DropItem;
internal static NativeKickPlayerDelegate? KickPlayer;
internal static NativeBanPlayerDelegate? BanPlayer;
internal static NativeBanPlayerIpDelegate? BanPlayerIp;
internal static NativeGetPlayerAddressDelegate? GetPlayerAddress;
internal static NativeGetPlayerInventoryDelegate? GetPlayerInventory;
internal static NativeSetPlayerInventorySlotDelegate? SetPlayerInventorySlot;
internal static NativeGetContainerContentsDelegate? GetContainerContents;
internal static NativeSetContainerSlotDelegate? SetContainerSlot;
internal static NativeGetContainerViewerEntityIdsDelegate? GetContainerViewerEntityIds;
internal static NativeCloseContainerDelegate? CloseContainer;
internal static NativeOpenVirtualContainerDelegate? OpenVirtualContainer;
internal static void SetCallbacks(IntPtr damage, IntPtr setHealth, IntPtr teleport, IntPtr setGameMode, IntPtr broadcastMessage, IntPtr setFallDistance, IntPtr getPlayerSnapshot, IntPtr sendMessage, IntPtr setWalkSpeed, IntPtr teleportEntity)
{
DamagePlayer = Marshal.GetDelegateForFunctionPointer<NativeDamageDelegate>(damage);
SetPlayerHealth = Marshal.GetDelegateForFunctionPointer<NativeSetHealthDelegate>(setHealth);
TeleportPlayer = Marshal.GetDelegateForFunctionPointer<NativeTeleportDelegate>(teleport);
SetPlayerGameMode = Marshal.GetDelegateForFunctionPointer<NativeSetGameModeDelegate>(setGameMode);
BroadcastMessage = Marshal.GetDelegateForFunctionPointer<NativeBroadcastMessageDelegate>(broadcastMessage);
SetFallDistance = Marshal.GetDelegateForFunctionPointer<NativeSetFallDistanceDelegate>(setFallDistance);
GetPlayerSnapshot = Marshal.GetDelegateForFunctionPointer<NativeGetPlayerSnapshotDelegate>(getPlayerSnapshot);
SendMessage = Marshal.GetDelegateForFunctionPointer<NativeSendMessageDelegate>(sendMessage);
SetWalkSpeed = Marshal.GetDelegateForFunctionPointer<NativeSetWalkSpeedDelegate>(setWalkSpeed);
TeleportEntity = Marshal.GetDelegateForFunctionPointer<NativeTeleportEntityDelegate>(teleportEntity);
}
internal static void SetWorldCallbacks(IntPtr getTileId, IntPtr setTile, IntPtr setTileData, IntPtr breakBlock, IntPtr getHighestBlockY, IntPtr getWorldInfo, IntPtr setWorldTime, IntPtr setWeather, IntPtr createExplosion, IntPtr strikeLightning, IntPtr setSpawnLocation, IntPtr dropItem)
{
GetTileId = Marshal.GetDelegateForFunctionPointer<NativeGetTileIdDelegate>(getTileId);
SetTile = Marshal.GetDelegateForFunctionPointer<NativeSetTileDelegate>(setTile);
SetTileData = Marshal.GetDelegateForFunctionPointer<NativeSetTileDataDelegate>(setTileData);
BreakBlock = Marshal.GetDelegateForFunctionPointer<NativeBreakBlockDelegate>(breakBlock);
GetHighestBlockY = Marshal.GetDelegateForFunctionPointer<NativeGetHighestBlockYDelegate>(getHighestBlockY);
GetWorldInfo = Marshal.GetDelegateForFunctionPointer<NativeGetWorldInfoDelegate>(getWorldInfo);
SetWorldTime = Marshal.GetDelegateForFunctionPointer<NativeSetWorldTimeDelegate>(setWorldTime);
SetWeather = Marshal.GetDelegateForFunctionPointer<NativeSetWeatherDelegate>(setWeather);
CreateExplosion = Marshal.GetDelegateForFunctionPointer<NativeCreateExplosionDelegate>(createExplosion);
StrikeLightning = Marshal.GetDelegateForFunctionPointer<NativeStrikeLightningDelegate>(strikeLightning);
SetSpawnLocation = Marshal.GetDelegateForFunctionPointer<NativeSetSpawnLocationDelegate>(setSpawnLocation);
DropItem = Marshal.GetDelegateForFunctionPointer<NativeDropItemDelegate>(dropItem);
}
internal static void SetPlayerCallbacks(IntPtr kickPlayer, IntPtr banPlayer, IntPtr banPlayerIp, IntPtr getPlayerAddress)
{
KickPlayer = Marshal.GetDelegateForFunctionPointer<NativeKickPlayerDelegate>(kickPlayer);
BanPlayer = Marshal.GetDelegateForFunctionPointer<NativeBanPlayerDelegate>(banPlayer);
BanPlayerIp = Marshal.GetDelegateForFunctionPointer<NativeBanPlayerIpDelegate>(banPlayerIp);
GetPlayerAddress = Marshal.GetDelegateForFunctionPointer<NativeGetPlayerAddressDelegate>(getPlayerAddress);
}
internal static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer)
{
GetPlayerInventory = Marshal.GetDelegateForFunctionPointer<NativeGetPlayerInventoryDelegate>(getPlayerInventory);
SetPlayerInventorySlot = Marshal.GetDelegateForFunctionPointer<NativeSetPlayerInventorySlotDelegate>(setPlayerInventorySlot);
GetContainerContents = Marshal.GetDelegateForFunctionPointer<NativeGetContainerContentsDelegate>(getContainerContents);
SetContainerSlot = Marshal.GetDelegateForFunctionPointer<NativeSetContainerSlotDelegate>(setContainerSlot);
GetContainerViewerEntityIds = Marshal.GetDelegateForFunctionPointer<NativeGetContainerViewerEntityIdsDelegate>(getContainerViewerEntityIds);
CloseContainer = Marshal.GetDelegateForFunctionPointer<NativeCloseContainerDelegate>(closeContainer);
OpenVirtualContainer = Marshal.GetDelegateForFunctionPointer<NativeOpenVirtualContainerDelegate>(openVirtualContainer);
}
}

View file

@ -0,0 +1,106 @@
namespace Minecraft.Server.FourKit.Net;
/// <summary>
/// Represents an Internet Protocol (IP) address.
/// </summary>
public class InetAddress
{
private readonly string _hostAddress;
internal InetAddress(string hostAddress)
{
_hostAddress = hostAddress ?? string.Empty;
}
/// <summary>
/// Returns the IP address string in textual presentation.
/// </summary>
/// <returns>The IP address as a string.</returns>
public string getHostAddress() => _hostAddress;
/// <summary>
/// Gets the host name for this IP address.
/// For this implementation, returns the IP address string.
/// </summary>
/// <returns>The host name.</returns>
public string getHostName() => _hostAddress;
/// <summary>
/// Returns the raw IP address of this InetAddress object as bytes.
/// </summary>
/// <returns>The raw IP address bytes, or an empty array if parsing fails.</returns>
public byte[] getAddress()
{
if (System.Net.IPAddress.TryParse(_hostAddress, out var ip))
return ip.GetAddressBytes();
return [];
}
/// <summary>
/// Checks whether this is a loopback address (127.x.x.x or ::1).
/// </summary>
/// <returns><c>true</c> if this is a loopback address.</returns>
public bool isLoopbackAddress()
{
if (System.Net.IPAddress.TryParse(_hostAddress, out var ip))
return System.Net.IPAddress.IsLoopback(ip);
return false;
}
/// <summary>
/// Checks whether this is a site-local (private) address.
/// </summary>
/// <returns><c>true</c> if this is a site-local address.</returns>
public bool isSiteLocalAddress()
{
if (!System.Net.IPAddress.TryParse(_hostAddress, out var ip))
return false;
byte[] bytes = ip.GetAddressBytes();
if (bytes.Length != 4) return false;
// 10.x.x.x, 172.16-31.x.x, 192.168.x.x
if (bytes[0] == 10) return true;
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true;
if (bytes[0] == 192 && bytes[1] == 168) return true;
return false;
}
/// <summary>
/// Checks whether this is a link-local address (169.254.x.x).
/// </summary>
/// <returns><c>true</c> if this is a link-local address.</returns>
public bool isLinkLocalAddress()
{
if (!System.Net.IPAddress.TryParse(_hostAddress, out var ip))
return false;
byte[] bytes = ip.GetAddressBytes();
if (bytes.Length != 4) return false;
return bytes[0] == 169 && bytes[1] == 254;
}
/// <summary>
/// Checks whether this is a multicast address (224-239.x.x.x).
/// </summary>
/// <returns><c>true</c> if this is a multicast address.</returns>
public bool isMulticastAddress()
{
if (!System.Net.IPAddress.TryParse(_hostAddress, out var ip))
return false;
byte[] bytes = ip.GetAddressBytes();
if (bytes.Length != 4) return false;
return bytes[0] >= 224 && bytes[0] <= 239;
}
/// <summary>
/// Checks whether this is the wildcard (any) address (0.0.0.0).
/// </summary>
/// <returns><c>true</c> if this is the wildcard address.</returns>
public bool isAnyLocalAddress()
{
if (System.Net.IPAddress.TryParse(_hostAddress, out var ip))
return ip.Equals(System.Net.IPAddress.Any) || ip.Equals(System.Net.IPAddress.IPv6Any);
return false;
}
/// <inheritdoc/>
public override string ToString() => _hostAddress;
}

View file

@ -0,0 +1,79 @@
namespace Minecraft.Server.FourKit.Net;
/// <summary>
/// Represents an IP Socket Address (IP address + port number).
/// </summary>
public class InetSocketAddress
{
private readonly InetAddress _address;
private readonly string _hostname;
private readonly int _port;
/// <summary>
/// Creates a socket address from an IP address and a port number.
/// </summary>
/// <param name="addr">The IP address.</param>
/// <param name="port">The port number.</param>
public InetSocketAddress(InetAddress addr, int port)
{
_address = addr;
_hostname = addr.getHostAddress();
_port = port;
}
/// <summary>
/// Creates a socket address from a hostname and a port number.
/// </summary>
/// <param name="hostname">The hostname or IP address string.</param>
/// <param name="port">The port number.</param>
public InetSocketAddress(string hostname, int port)
{
_hostname = hostname ?? string.Empty;
_address = new InetAddress(_hostname);
_port = port;
}
/// <summary>
/// Creates a socket address where the IP address is the wildcard address
/// and the port number a specified value.
/// </summary>
/// <param name="port">The port number.</param>
public InetSocketAddress(int port)
{
_hostname = "0.0.0.0";
_address = new InetAddress(_hostname);
_port = port;
}
/// <summary>
/// Gets the InetAddress.
/// </summary>
/// <returns>The InetAddress.</returns>
public InetAddress getAddress() => _address;
/// <summary>
/// Gets the hostname.
/// </summary>
/// <returns>The hostname, or the IP address string if created from an address.</returns>
public string getHostName() => _hostname;
/// <summary>
/// Gets the port number.
/// </summary>
/// <returns>The port number.</returns>
public int getPort() => _port;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(_hostname, _port);
/// <inheritdoc/>
public override bool Equals(object? obj)
{
if (obj is InetSocketAddress other)
return _hostname == other._hostname && _port == other._port;
return false;
}
/// <inheritdoc/>
public override string ToString() => _hostname + ":" + _port;
}

View file

@ -0,0 +1,40 @@
namespace Minecraft.Server.FourKit.Plugin;
/// <summary>
/// Base class that every plugin must extend.
/// <code>
/// public string name => "MyPlugin";
/// public string version => "1.0.0";
/// public string author => "Me";
///
/// public void onEnable() { /* startup logic */ }
/// public void onDisable() { /* shutdown logic */ }
/// </code>
/// </summary>
public abstract class ServerPlugin
{
/// <summary>
/// The name of this plugin. <b>Must be declared in your plugin class.</b>
/// </summary>
public virtual string name { get; } = string.Empty;
/// <summary>
/// The version of this plugin.
/// </summary>
public virtual string version { get; } = "1.0.0";
/// <summary>
/// The author of this plugin.
/// </summary>
public virtual string author { get; } = "Unknown";
/// <summary>
/// Called when this plugin is enabled
/// </summary>
public virtual void onEnable() { }
/// <summary>
/// Called when this plugin is disabled
/// </summary>
public virtual void onDisable() { }
}

View file

@ -0,0 +1,46 @@
using Minecraft.Server.FourKit.Plugin;
using System.Reflection;
using System.Runtime.Loader;
namespace Minecraft.Server.FourKit;
internal sealed class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
private readonly string _pluginDirectory;
public PluginLoadContext(string pluginPath)
: base(isCollectible: false)
{
_pluginDirectory = Path.GetDirectoryName(Path.GetFullPath(pluginPath))!;
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
if (assemblyName.Name == typeof(ServerPlugin).Assembly.GetName().Name)
return typeof(ServerPlugin).Assembly;
string? path = _resolver.ResolveAssemblyToPath(assemblyName);
if (path != null)
return LoadFromAssemblyPath(path);
if (assemblyName.Name != null)
{
string fallback = Path.Combine(_pluginDirectory, assemblyName.Name + ".dll");
if (File.Exists(fallback))
return LoadFromAssemblyPath(fallback);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string? path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (path != null)
return LoadUnmanagedDllFromPath(path);
return IntPtr.Zero;
}
}

View file

@ -0,0 +1,183 @@
using Minecraft.Server.FourKit.Plugin;
using System.Reflection;
namespace Minecraft.Server.FourKit;
internal sealed class PluginLoader
{
private static readonly BindingFlags DeclaredPublic =
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
private readonly List<ServerPlugin> _plugins = new();
public IReadOnlyList<ServerPlugin> Plugins => _plugins.AsReadOnly();
public void LoadPlugins(string pluginsDirectory)
{
if (!Directory.Exists(pluginsDirectory))
{
ServerLog.Info("fourkit", $"Creating plugins directory: {pluginsDirectory}");
Directory.CreateDirectory(pluginsDirectory);
return;
}
var rootDlls = Directory.GetFiles(pluginsDirectory, "*.dll");
if (rootDlls.Length > 0)
{
ServerLog.Info("fourkit", $"Found {rootDlls.Length} DLL(s) in plugins root.");
foreach (var dll in rootDlls)
{
try { LoadPluginAssembly(dll); }
catch (Exception ex) { ServerLog.Error("fourkit", $"Failed to load {Path.GetFileName(dll)}: {ex.Message}"); }
}
}
foreach (var subDir in Directory.GetDirectories(pluginsDirectory))
{
string folderName = Path.GetFileName(subDir);
var allDlls = Directory.GetFiles(subDir, "*.dll");
if (allDlls.Length == 0)
continue;
string? mainDll = allDlls.FirstOrDefault(f =>
string.Equals(Path.GetFileNameWithoutExtension(f), folderName, StringComparison.OrdinalIgnoreCase));
if (mainDll != null)
{
try { LoadPluginAssembly(mainDll); }
catch (Exception ex) { ServerLog.Error("fourkit", $"Failed to load {Path.GetFileName(mainDll)}: {ex.Message}"); }
}
else
{
foreach (var dll in allDlls)
{
try { LoadPluginAssembly(dll); }
catch (Exception ex) { ServerLog.Error("fourkit", $"Failed to load {Path.GetFileName(dll)}: {ex.Message}"); }
}
}
}
}
private void LoadPluginAssembly(string dllPath)
{
var context = new PluginLoadContext(dllPath);
var assembly = context.LoadFromAssemblyPath(Path.GetFullPath(dllPath));
int found = 0;
foreach (var type in assembly.GetTypes())
{
if (type.IsAbstract || type.IsInterface)
continue;
if (!typeof(ServerPlugin).IsAssignableFrom(type))
continue;
var plugin = (ServerPlugin?)Activator.CreateInstance(type);
if (plugin == null)
continue;
_plugins.Add(plugin);
found++;
string pName = GetPluginString(plugin, "name", "getName", "GetName", plugin.GetType().Name);
string pVersion = GetPluginString(plugin, "version", "getVersion", "GetVersion", "1.0");
string pAuthor = GetPluginString(plugin, "author", "getAuthor", "GetAuthor", "Unknown");
if (!HasDeclaredMember(type, "name"))
ServerLog.Warn("fourkit", $"Plugin {type.Name} does not declare a 'name' property.");
if (!HasDeclaredMember(type, "version"))
ServerLog.Warn("fourkit", $"Plugin {type.Name} does not declare a 'version' property.");
if (!HasDeclaredMember(type, "author"))
ServerLog.Warn("fourkit", $"Plugin {type.Name} does not declare an 'author' property.");
ServerLog.Info("fourkit", $"Loaded plugin: {pName} v{pVersion} by {pAuthor}");
}
if (found == 0)
{
ServerLog.Warn("fourkit", $"No ServerPlugin classes found in {Path.GetFileName(dllPath)}");
}
}
public void EnableAll()
{
foreach (var plugin in _plugins)
{
try
{
InvokePluginMethod(plugin, "onEnable", "OnEnable");
string pName = GetPluginString(plugin, "name", "getName", "GetName", plugin.GetType().Name);
ServerLog.Info("fourkit", $"Enabled: {pName}");
}
catch (Exception ex)
{
string pName = GetPluginString(plugin, "name", "getName", "GetName", plugin.GetType().Name);
ServerLog.Error("fourkit", $"Error enabling {pName}: {ex.Message}");
}
}
}
public void DisableAll()
{
for (int i = _plugins.Count - 1; i >= 0; i--)
{
try
{
InvokePluginMethod(_plugins[i], "onDisable", "OnDisable");
string pName = GetPluginString(_plugins[i], "name", "getName", "GetName", _plugins[i].GetType().Name);
ServerLog.Info("fourkit", $"Disabled: {pName}");
}
catch (Exception ex)
{
string pName = GetPluginString(_plugins[i], "name", "getName", "GetName", _plugins[i].GetType().Name);
ServerLog.Error("fourkit", $"Error disabling {pName}: {ex.Message}");
}
}
}
private static void InvokePluginMethod(ServerPlugin plugin, string camelName, string pascalName)
{
Type type = plugin.GetType();
MethodInfo? method = type.GetMethod(camelName, DeclaredPublic, Type.EmptyTypes)
?? type.GetMethod(pascalName, DeclaredPublic, Type.EmptyTypes);
if (method != null)
{
method.Invoke(plugin, null);
}
}
private static bool HasDeclaredMember(Type type, string name)
{
return type.GetProperty(name, DeclaredPublic) != null
|| type.GetMethod("get" + char.ToUpper(name[0]) + name[1..], DeclaredPublic, Type.EmptyTypes) != null;
}
private static string GetPluginString(
ServerPlugin plugin, string propertyName,
string getterCamel, string getterPascal,
string fallback)
{
Type type = plugin.GetType();
PropertyInfo? prop = type.GetProperty(propertyName, DeclaredPublic);
if (prop != null && prop.PropertyType == typeof(string))
{
return (string?)prop.GetValue(plugin) ?? fallback;
}
MethodInfo? getter = type.GetMethod(getterCamel, DeclaredPublic, Type.EmptyTypes);
if (getter != null && getter.ReturnType == typeof(string))
{
return (string?)getter.Invoke(plugin, null) ?? fallback;
}
getter = type.GetMethod(getterPascal, DeclaredPublic, Type.EmptyTypes);
if (getter != null && getter.ReturnType == typeof(string))
{
return (string?)getter.Invoke(plugin, null) ?? fallback;
}
return fallback;
}
}

View file

@ -0,0 +1,19 @@
namespace Minecraft.Server.FourKit;
internal static class ServerLog
{
private static string Timestamp() =>
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
public static void Debug(string category, string message) =>
Console.WriteLine($"[{Timestamp()}][DEBUG][{category}] {message}");
public static void Info(string category, string message) =>
Console.WriteLine($"[{Timestamp()}][INFO][{category}] {message}");
public static void Warn(string category, string message) =>
Console.WriteLine($"[{Timestamp()}][WARN][{category}] {message}");
public static void Error(string category, string message) =>
Console.WriteLine($"[{Timestamp()}][ERROR][{category}] {message}");
}

View file

@ -0,0 +1,373 @@
using System.Runtime.InteropServices;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Inventory;
namespace Minecraft.Server.FourKit;
/// <summary>
/// Represents a world, which may contain entities, chunks and blocks.
/// </summary>
public class World
{
private readonly int _dimensionId;
private readonly string _name;
internal World(int dimensionId, string name)
{
_dimensionId = dimensionId;
_name = name;
}
internal int getDimensionId() => _dimensionId;
/// <summary>
/// Gets the unique name of this world.
/// </summary>
/// <returns>Name of this world.</returns>
public string getName() => _name;
/// <summary>
/// Gets the Block at the given coordinates.
/// </summary>
/// <param name="x">X-coordinate of the block.</param>
/// <param name="y">Y-coordinate of the block.</param>
/// <param name="z">Z-coordinate of the block.</param>
/// <returns>Block at the given coordinates.</returns>
public Block.Block getBlockAt(int x, int y, int z)
{
return new Block.Block(this, x, y, z);
}
/// <summary>
/// Gets the Block at the given Location.
/// </summary>
/// <param name="location">Location of the block.</param>
/// <returns>Block at the given location.</returns>
public Block.Block getBlockAt(Location location)
{
return getBlockAt(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
/// <summary>
/// Gets the block type ID at the given coordinates.
/// </summary>
/// <param name="x">X-coordinate of the block.</param>
/// <param name="y">Y-coordinate of the block.</param>
/// <param name="z">Z-coordinate of the block.</param>
/// <returns>Type ID of the block.</returns>
public int getBlockTypeIdAt(int x, int y, int z)
{
if (NativeBridge.GetTileId != null)
return NativeBridge.GetTileId(_dimensionId, x, y, z);
return 0;
}
/// <summary>
/// Gets the block type ID at the given Location.
/// </summary>
/// <param name="location">Location of the block.</param>
/// <returns>Type ID of the block.</returns>
public int getBlockTypeIdAt(Location location)
{
return getBlockTypeIdAt(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
/// <summary>
/// Gets the highest non-air coordinate at the given coordinates.
/// </summary>
/// <param name="x">X-coordinate.</param>
/// <param name="z">Z-coordinate.</param>
/// <returns>The Y-coordinate of the highest non-air block.</returns>
public int getHighestBlockYAt(int x, int z)
{
if (NativeBridge.GetHighestBlockY != null)
return NativeBridge.GetHighestBlockY(_dimensionId, x, z);
return 0;
}
/// <summary>
/// Gets the highest non-air coordinate at the given Location.
/// </summary>
/// <param name="location">Location to check.</param>
/// <returns>The Y-coordinate of the highest non-air block.</returns>
public int getHighestBlockYAt(Location location)
{
return getHighestBlockYAt(location.getBlockX(), location.getBlockZ());
}
/// <summary>
/// Gets the highest non-empty block at the given coordinates.
/// </summary>
/// <param name="x">X-coordinate.</param>
/// <param name="z">Z-coordinate.</param>
/// <returns>Highest non-empty block.</returns>
public Block.Block getHighestBlockAt(int x, int z)
{
int y = getHighestBlockYAt(x, z);
return getBlockAt(x, y, z);
}
/// <summary>
/// Gets the highest non-empty block at the given Location.
/// </summary>
/// <param name="location">Coordinates to get the highest block at.</param>
/// <returns>Highest non-empty block.</returns>
public Block.Block getHighestBlockAt(Location location)
{
return getHighestBlockAt(location.getBlockX(), location.getBlockZ());
}
private double[] GetWorldInfoSnapshot()
{
double[] buf = new double[7];
if (NativeBridge.GetWorldInfo != null)
{
var gh = GCHandle.Alloc(buf, GCHandleType.Pinned);
try
{
NativeBridge.GetWorldInfo(_dimensionId, gh.AddrOfPinnedObject());
}
finally
{
gh.Free();
}
}
return buf;
}
/// <summary>
/// Gets the default spawn Location of this world.
/// </summary>
/// <returns>The spawn location of this world.</returns>
public Location getSpawnLocation()
{
double[] info = GetWorldInfoSnapshot();
return new Location(this, info[0], info[1], info[2], 0f, 0f);
}
/// <summary>
/// Sets the spawn location of the world.
/// </summary>
/// <param name="x">X-coordinate.</param>
/// <param name="y">Y-coordinate.</param>
/// <param name="z">Z-coordinate.</param>
/// <returns>True if the spawn was set successfully.</returns>
public bool setSpawnLocation(int x, int y, int z)
{
if (NativeBridge.SetSpawnLocation != null)
return NativeBridge.SetSpawnLocation(_dimensionId, x, y, z) != 0;
return false;
}
/// <summary>
/// Gets the Seed for this world.
/// </summary>
/// <returns>This world's Seed.</returns>
public long getSeed()
{
double[] info = GetWorldInfoSnapshot();
return (long)info[3];
}
/// <summary>
/// Gets the relative in-game time of this world.
/// </summary>
/// <returns>The current relative time.</returns>
public long getTime()
{
double[] info = GetWorldInfoSnapshot();
return (long)info[4];
}
/// <summary>
/// Sets the relative in-game time on the server.
/// </summary>
/// <param name="time">The new relative time to set the in-game time to.</param>
public void setTime(long time)
{
NativeBridge.SetWorldTime?.Invoke(_dimensionId, time);
}
/// <summary>
/// Sets the in-game time on the server.
/// </summary>
/// <param name="time">The new absolute time to set this world to.</param>
public void setFullTime(long time)
{
NativeBridge.SetWorldTime?.Invoke(_dimensionId, time);
}
/// <summary>
/// Set whether there is a storm.
/// </summary>
/// <param name="hasStorm">Whether there is rain and snow.</param>
public void setStorm(bool hasStorm)
{
NativeBridge.SetWeather?.Invoke(_dimensionId, hasStorm ? 1 : 0, -1, -1);
}
/// <summary>
/// Set whether it is thundering.
/// </summary>
/// <param name="thundering">Whether it is thundering.</param>
public void setThundering(bool thundering)
{
NativeBridge.SetWeather?.Invoke(_dimensionId, -1, thundering ? 1 : 0, -1);
}
/// <summary>
/// Set the thundering duration.
/// </summary>
/// <param name="duration">Duration in ticks.</param>
public void setThunderDuration(int duration)
{
NativeBridge.SetWeather?.Invoke(_dimensionId, -1, -1, duration);
}
/// <summary>
/// Get a list of all players in this World.
/// </summary>
/// <returns>A list of all Players currently residing in this world.</returns>
public List<Player> getPlayers()
{
var all = FourKit.getOnlinePlayers();
var result = new List<Player>();
foreach (var p in all)
{
var loc = p.getLocation();
if (loc?.LocationWorld == this)
result.Add(p);
}
return result;
}
/// <summary>
/// Creates explosion at given coordinates with given power.
/// </summary>
/// <param name="x">X-coordinate.</param>
/// <param name="y">Y-coordinate.</param>
/// <param name="z">Z-coordinate.</param>
/// <param name="power">The power of explosion, where 4F is TNT.</param>
/// <returns>false if explosion was canceled, otherwise true.</returns>
public bool createExplosion(double x, double y, double z, float power)
{
return createExplosion(x, y, z, power, false, true);
}
/// <summary>
/// Creates explosion at given coordinates with given power and optionally
/// setting blocks on fire.
/// </summary>
/// <param name="x">X-coordinate.</param>
/// <param name="y">Y-coordinate.</param>
/// <param name="z">Z-coordinate.</param>
/// <param name="power">The power of explosion, where 4F is TNT.</param>
/// <param name="setFire">Whether or not to set blocks on fire.</param>
/// <returns>false if explosion was canceled, otherwise true.</returns>
public bool createExplosion(double x, double y, double z, float power, bool setFire)
{
return createExplosion(x, y, z, power, setFire, true);
}
/// <summary>
/// Creates explosion at given coordinates with given power and optionally
/// setting blocks on fire or breaking blocks.
/// </summary>
/// <param name="x">X-coordinate.</param>
/// <param name="y">Y-coordinate.</param>
/// <param name="z">Z-coordinate.</param>
/// <param name="power">The power of explosion, where 4F is TNT.</param>
/// <param name="setFire">Whether or not to set blocks on fire.</param>
/// <param name="breakBlocks">Whether or not to have blocks be destroyed.</param>
/// <returns>false if explosion was canceled, otherwise true.</returns>
public bool createExplosion(double x, double y, double z, float power, bool setFire, bool breakBlocks)
{
if (NativeBridge.CreateExplosion != null)
return NativeBridge.CreateExplosion(_dimensionId, x, y, z, power, setFire ? 1 : 0, breakBlocks ? 1 : 0) != 0;
return false;
}
/// <summary>
/// Creates explosion at given coordinates with given power and optionally
/// setting blocks on fire or breaking blocks.
/// </summary>
/// <param name="loc">Location to blow up.</param>
/// <param name="power">The power of explosion, where 4F is TNT.</param>
/// <param name="setFire">Whether or not to set blocks on fire.</param>
/// <param name="breakBlocks">Whether or not to have blocks be destroyed.</param>
/// <returns>false if explosion was canceled, otherwise true.</returns>
public bool createExplosion(Location loc, float power, bool setFire, bool breakBlocks)
{
if (NativeBridge.CreateExplosion != null)
return NativeBridge.CreateExplosion(_dimensionId, loc.X, loc.Y, loc.Z, power, setFire ? 1 : 0, breakBlocks ? 1 : 0) != 0;
return false;
}
/// <summary>
/// Creates explosion at given coordinates with given power.
/// </summary>
/// <param name="loc">Location to blow up.</param>
/// <param name="power">The power of explosion, where 4F is TNT.</param>
/// <returns>false if explosion was canceled, otherwise true.</returns>
public bool createExplosion(Location loc, float power)
{
return createExplosion(loc.X, loc.Y, loc.Z, power);
}
/// <summary>
/// Creates explosion at given coordinates with given power and optionally
/// setting blocks on fire.
/// </summary>
/// <param name="loc">Location to blow up.</param>
/// <param name="power">The power of explosion, where 4F is TNT.</param>
/// <param name="setFire">Whether or not to set blocks on fire.</param>
/// <returns>false if explosion was canceled, otherwise true.</returns>
public bool createExplosion(Location loc, float power, bool setFire)
{
return createExplosion(loc.X, loc.Y, loc.Z, power, setFire);
}
/// <summary>
/// Strikes lightning at the given Location.
/// </summary>
/// <param name="loc">The location to strike lightning.</param>
/// <returns>true if lightning was successfully summoned.</returns>
public bool strikeLightning(Location loc)
{
if (NativeBridge.StrikeLightning != null)
return NativeBridge.StrikeLightning(_dimensionId, loc.X, loc.Y, loc.Z, 0) != 0;
return false;
}
/// <summary>
/// Strikes lightning at the given Location without doing damage.
/// </summary>
/// <param name="loc">The location to strike lightning.</param>
/// <returns>true if lightning was successfully summoned.</returns>
public bool strikeLightningEffect(Location loc)
{
if (NativeBridge.StrikeLightning != null)
return NativeBridge.StrikeLightning(_dimensionId, loc.X, loc.Y, loc.Z, 1) != 0;
return false;
}
/// <summary>
/// Drops an item at the specified Location.
/// </summary>
/// <param name="location">Location to drop the item.</param>
/// <param name="item">ItemStack to drop.</param>
public void dropItem(Location location, ItemStack item)
{
NativeBridge.DropItem?.Invoke(_dimensionId, location.X, location.Y, location.Z, item.getTypeId(), item.getAmount(), item.getDurability(), 0);
}
/// <summary>
/// Drops an item at the specified Location with a random offset.
/// </summary>
/// <param name="location">Location to drop the item.</param>
/// <param name="item">ItemStack to drop.</param>
public void dropItemNaturally(Location location, ItemStack item)
{
NativeBridge.DropItem?.Invoke(_dimensionId, location.X, location.Y, location.Z, item.getTypeId(), item.getAmount(), item.getDurability(), 1);
}
}

View file

@ -0,0 +1,15 @@
@page install Installing Fourkit
Before being able to run FourKit, you need [.NET 10 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
After you install .NET 10, grab the [latest release from github](https://github.com/sylvessa/MinecraftConsoles/releases/tag/nightly-dedicated-server)
If you are just updating FourKit, you only need to install Minecraft.Server.exe and Minecraft.Server.FourKit.dll and replace them.
After installing, run Minecraft.Server.exe, and place any plugins in the plugins folder that gets automatically made.
Plugins end as a .DLL file. Some plugins require it to be in its own folder (due to having dependencies)
Below, we will go over how to setup your development environment if you want to make plugins:
@ref setup

View file

@ -0,0 +1,7 @@
@mainpage FourKit
FourKit is a C# Server Plugin API designed to be similar to existing Java Plugin API's.
FourKit is made for use with [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-10.0.5-windows-x64-installer).
@ref install

View file

@ -0,0 +1,303 @@
@page plugin-creation Creating your first Plugin
@section main-plugin Initialization
This will go over how to create your first plugin.
If you havent already, be sure to set up your development environment first:
@ref setup
Plugins must have a class that extends \ref Minecraft.Server.FourKit.Plugin.ServerPlugin "ServerPlugin".
```csharp
using Minecraft.Server.FourKit;
using Minecraft.Server.FourKit.Plugin;
public class CoolPlugin : ServerPlugin
{
public override string name => "My Cool Plugin";
public override string version => "1.0.0";
public override string author => "Me";
public override void onEnable() { }
public override void onDisable() { }
}
```
`onEnable()` is ran when the server starts. This is where you add listeners and anything else you need to do on startup.
`onDisable()` runs when the server stops. You can do stuff like cleaning up here.
@section listeners Listeners
Listeners are vital for events to be intercepted by your plugin. This will go over the usage and how to get started.
Listeners must implement the \ref Minecraft.Server.FourKit.Event.Listener "Listener" interface. Your listener class should look like this:
```csharp
using Minecraft.Server.FourKit.Event;
public class MyListener : Listener
{
}
```
### Registering your listener
To register a listener, you need to add it to FourKit, a common place to do this is in `onEnable()` in your plugin.
```csharp
public override void onEnable() {
FourKit.addListener(new MyListener());
}
```
### Listening to Events
Now that we've registered the listener, we need to make it actually listen to events!
To listen to any given event in your listener class, you MUST create a method with the \ref Minecraft.Server.FourKit.Event.EventHandlerAttribute "EventHandler" attribute attached and the event specified by the type in the methods argument. The method can be named whatever you wish. Example:
```csharp
using Minecraft.Server.FourKit.Event;
using Minecraft.Server.FourKit.Event.Player;
public class MyListener : Listener
{
[EventHandler]
public void onPlayerJoin(PlayerJoinEvent e) {
}
}
```
This method will fire whenever a player joins the server. We can make it broadcast a greeting to the whole server:
```csharp
using Minecraft.Server.FourKit.Event;
using Minecraft.Server.FourKit.Event.Player;
public class MyListener : Listener
{
[EventHandler]
public void onPlayerJoin(PlayerJoinEvent e) {
FourKit.broadcastMessage("Welcome!");
}
}
```
### Manipulating Events
You may modify what happens with most events and also obtain information about the given event. These functions are stored in the Event object in your method. Let's modify the message that is broadcasted when a player joins the server:
```csharp
using Minecraft.Server.FourKit.Event;
using Minecraft.Server.FourKit.Event.Player;
public class MyListener : Listener
{
[EventHandler]
public void onPlayerJoin(PlayerJoinEvent e) {
event.setJoinMessage("Welcome, " + event.getPlayer().getName() + "!");
}
}
```
### What can I listen to?
You can browse through the \ref Minecraft.Server.FourKit.Event "Event" namespace to see all events that you can use.
@ref usage-of-all-events
@section advanced-functions Advanced Functions
### EventHandler parameters
The EventHandler attribute accepts a couple parameters.
**Priority** - indicates the priority. There are six different priorities, in order of execution:
- `Lowest`
- `Low`
- `Normal` (Default)
- `High`
- `Highest`
- `Monitor`
These constants refer to the \ref Minecraft.Server.FourKit.Event.EventPriority "EventPriority" enum.
<b><span style="color: red;">Note:</span> The Monitor priority should only be used for reading only. This priority is useful for logging plugins to see the results of an event and modifying values may interfere with those types of plugins.</b>
**IgnoreCancelled** - A boolean which indicates whether or not your listener should fire if the event has been cancelled before it is the listener's turn to handle the event. False by default.
Example:
```csharp
using Minecraft.Server.FourKit.Event;
using Minecraft.Server.FourKit.Event.Player;
public class MyListener : Listener
{
// executes before the second method because it has a much lower priority.
[EventHandler(Priority = EventPriority.Lowest)]
public void onPlayerChat1(PlayerChatEvent e) {
event.setCancelled(true);
}
// Will not execute unless another listener with a lower priority has uncancelled the event.
[EventHandler(Priority = EventPriority.Highest, IgnoreCancelled = true)]
public void onPlayerChat2(PlayerChatEvent e) {
Console.WriteLine("This should not execute.");
}
}
```
@section commands Creating Commands
A big thing you will probably want to do is learn how to create commands.
They are not like bukkit, you dont fill out a yml file.
### Creating our Command class
Lets start by creating our actual command handler. You must have a class that extends the \ref Minecraft.Server.FourKit.Command.CommandExecutor "CommandExecutor" class.
```csharp
public class CoolCommand : CommandExecutor
{
public bool onCommand(CommandSender sender, Command command, string label, string[] args)
{
return true;
}
}
```
`sender` is the actual command sender. This can be either a \ref Minecraft.Server.FourKit.Entity.Player "Player" or a \ref Minecraft.Server.FourKit.Command.ConsoleCommandSender "ConsoleCommandSender"
`command` is the actual command.
`label` is the command name they used to execute.
`args` is the command arguments passed.
You might notice that the `onCommand` func returns a boolean. This indicates if the command executed successfully.
### Registering the command
Now, lets actually register this command. To register the command, you have to use `FourKit.getCommand("command").setExecutor()`
```csharp
public void onEnable()
{
FourKit.getCommand("cool").setExecutor(new CoolCommand());
}
```
Now we can run the command by running `/cool` in chat or typing `cool` in console!
getCommand() returns a \ref Minecraft.Server.FourKit.Command.PluginCommand "PluginCommand" class. You can see all the functions you can use from here!
Now, when we run `help` in console, we should see this:
```
[2026-03-20 23:31:19.462][INFO][console] Plugin commands:
[2026-03-20 23:31:19.463][INFO][console] /cool
```
### Defining usage and description for the command
We can even add a description and define usage to the command!
```csharp
FourKit.getCommand("cool").setDescription("my awesome command");
FourKit.getCommand("cool").setUsage("/cool <arg1>");
```
Now it shows this:
```
[2026-03-20 23:38:06.470][INFO][console] Plugin commands:
[2026-03-20 23:38:06.470][INFO][console] /cool <arg1> - my awesome command
```
### Checking who is running the command
Now that we can do all this, we can check who is running the command. Best way to do this is check if the sender is an instance of \ref Minecraft.Server.FourKit.Entity.Player "Player" or \ref Minecraft.Server.FourKit.Command.ConsoleCommandSender "ConsoleCommandSender".
```csharp
public bool onCommand(CommandSender sender, Command command, string label, string[] args)
{
if (sender is ConsoleCommandSender)
{
// sender is console.
sender.sendMessage("Whats good console");
return true;
}
// sender is player
Player p = (Player)sender;
p.sendMessage("Do it work?");
return true;
}
```
When console runs this command, they will see "Whats good console" in console. When a Player runs this command, they will see "Do it work?" in chat.
From there on, you can do whatever you want in the command. You can modify the player, such as teleport them somewhere. You can do whatever you want!
@section dependencies Dependencies
Say you want to make a plugin that links a Discord bot to your plugin. This is possible! You can use something like [Discord.NET](https://docs.discordnet.dev/) for that.
When a plugin needs dependencies, you also need to bring over the DLL's for the dependencies.
You can put them into a folder under the plugins folder next to the main plugin dll.
Example folder structure:
- plugins/
- plugins/MyPlugin/MyPlugin.dll
- plugins/MyPlugin/CoolDependency.dll
When a plugin folder is made, make sure the main dll matches the name of the folder, or else it will skip it.
You can also avoid this by using [Fody Costura](https://github.com/Fody/Costura) and bundle the dependencies into your DLL.
### Fody Costura
Fody Costura isnt very well documented, but heres the general usage guide that has worked for users:
<!-- why doesnt doxygen support 1. 2. 3. etc? -->
<ol>
<li>
Install Fody Costura
<p>This can be through NuGet, or through anything you wish to use for getting dependencies.</p>
</li>
<li>
Create a FodyWeavers.xml in your project root:
```xml
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura>
<ExcludeAssemblies>
Minecraft.Server.FourKit
</ExcludeAssemblies>
</Costura>
</Weavers>
```
<p>This will exclude bundling fourkit in the DLL too.</p>
</li>
<li>
Make csproj copy dependency DLL files over to build dir
<p>You can do this by adding <code>&lt;CopyLocalLockFileAssemblies&gt;true&lt;/CopyLocalLockFileAssemblies&gt;</code> to the property group.</p>
</li>
<li>
Build
<p>After you've done all this, it should build and put all dependencies into one DLL in your output folder.</p>
</li>
</ol>

View file

@ -0,0 +1,83 @@
@page setup Setting up your Development Environment
You can use any IDE of choice. I recommend using Visual Studio for new developers.
Also make sure you have [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) installed.
@section visual-studio Visual Studio
This tutorial will go over creating a plugin in Visual Studio.
When installing Visual Studio, make sure you have .NET SDK installed under Individual Components in the Visual Studio Installer.
After installing all the prerequisities, go to create a new Project and select Class Library for C#
![class library](https://raw.githubusercontent.com/sylvessa/sylvessa/refs/heads/main/classlibrary.png)
After selecting class library and pressing OK, it should prompt what .NET version to use. Be sure to select .NET 10
You can name the project whatever you want.
After all thats done, you should enter the code environment.
First thing you should do is right click your project and add a reference to the compiled FourKit DLL.
![right click](https://raw.githubusercontent.com/sylvessa/sylvessa/refs/heads/main/rightclickvs.png)
Afterwards a popup should appear. Click Browse and then browse to the DLL and then click OK.
After that you can continue to making a plugin!
@section dotnet Dotnet CLI
If you dont want to install Visual Studio, you can use the dotnet CLI program.
You can create a new dll project by running `dotnet new classlib -n MyAwesomePlugin`
Be sure to add a reference to the FourKit DLL in your csproj file (example for it being in parent dir):
```xml
<ItemGroup>
<Reference Include="Minecraft.Server.FourKit">
<HintPath>..\Minecraft.Server.FourKit.dll</HintPath>
</Reference>
</ItemGroup>
```
After this, you can build using `dotnet build`! Your compiled DLL should be in the `bin` folder.
To build as release: `dotnet build -c Release`
Make sure the target framework is set to `net10.0`!
Example full csproj file:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Minecraft.Server.FourKit">
<HintPath>..\Minecraft.Server.FourKit.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
```
@section compiling Building your plugin
When you build your plugin, it will compile as a DLL.
You place this DLL into the `plugins` folder in your server directory.
You can also even have plugins with dependencies!
Below page will discuss actually making your plugin function, and using dependencies:
@ref plugin-creation

View file

@ -0,0 +1,38 @@
@page usage-of-all-events Usage of all Events
This will go over how to utilize each event that FourKit provides.
@section entitydamageevent EntityDamageEvent and EntityDamageByEntityEvent
\ref Minecraft.Server.FourKit.Event.Entity.EntityDamageEvent "EntityDamageEvent" and \ref Minecraft.Server.FourKit.Event.Entity.EntityDamageByEntityEvent "EntityDamageByEntityEvent" are fired when any entity is damaged.
When one of these events are fired, it passes an entity. It can be a Player, or any LivingEntity
You can check what type of entity it is with `getEntityType()`. This will return a \ref Minecraft.Server.FourKit.Entity.EntityType "EntityType" enum.
From there, you can cast the entity to the actual type (in this case \ref Minecraft.Server.FourKit.Entity.LivingEntity "LivingEntity"):
```csharp
[EventHandler]
public void entityDamage(EntityDamageEvent e)
{
LivingEntity entity = (LivingEntity)e.getEntity();
}
```
Or if its a player, you can even cast it to \ref Minecraft.Server.FourKit.Entity.Player "Player":
```csharp
[EventHandler]
public void entityDamage(EntityDamageEvent e)
{
if (e.getEntityType() == EntityType.PLAYER)
{
Player player = (Player)e.getEntity();
}
}
```
As of right now, this event is only fired for \ref Minecraft.Server.FourKit.Entity.LivingEntity "LivingEntity" and \ref Minecraft.Server.FourKit.Entity.Player "Player"s.
<h1>Page currently under construction</h1>

View file

@ -13,6 +13,8 @@ set(MINECRAFT_SERVER_SOURCES
add_executable(Minecraft.Server ${MINECRAFT_SERVER_SOURCES})
add_dependencies(Minecraft.Server Minecraft.Server.FourKit)
target_include_directories(Minecraft.Server PRIVATE
"${CMAKE_BINARY_DIR}/generated/" # This is for the generated BuildVer.h
"${CMAKE_SOURCE_DIR}/Minecraft.Client/"
@ -83,3 +85,23 @@ add_copyredist_target(Minecraft.Server)
if(PLATFORM_NAME STREQUAL "Windows64")
add_gamehdd_target(Minecraft.Server)
endif()
# cop over fourkit files
set(FOURKIT_OUTPUT_DIR "${CMAKE_BINARY_DIR}/Minecraft.Server.FourKit/bin/$<CONFIG>")
set(FOURKIT_DLL "${FOURKIT_OUTPUT_DIR}/Minecraft.Server.FourKit.dll")
set(FOURKIT_DEP_JSON "${FOURKIT_OUTPUT_DIR}/Minecraft.Server.FourKit.deps.json")
set(FOURKIT_RUNTIME_JSON "${FOURKIT_OUTPUT_DIR}/Minecraft.Server.FourKit.runtimeconfig.json")
add_custom_command(TARGET Minecraft.Server POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${FOURKIT_DLL}"
"$<TARGET_FILE_DIR:Minecraft.Server>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${FOURKIT_DEP_JSON}"
"$<TARGET_FILE_DIR:Minecraft.Server>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${FOURKIT_RUNTIME_JSON}"
"$<TARGET_FILE_DIR:Minecraft.Server>"
COMMENT "Copying Minecraft.Server.FourKit to Minecraft.Server"
)

View file

@ -26,6 +26,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"
@ -161,6 +162,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;
}
@ -207,6 +212,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
{

View file

@ -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

View file

@ -0,0 +1,80 @@
#pragma once
#include <string>
namespace FourKitBridge
{
void Initialize();
void Shutdown();
void FirePlayerJoin(int entityId, const std::wstring &name, const std::wstring &uuid);
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 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);
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);
}

View file

@ -12,6 +12,7 @@
#include "..\ServerProperties.h"
#include "..\ServerShutdown.h"
#include "..\WorldManager.h"
#include "..\FourKitBridge.h"
#include "..\Console\ServerCli.h"
#include "Tesselator.h"
#include "Windows64/4JLibs/inc/4J_Render.h"
@ -617,6 +618,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
@ -705,6 +708,7 @@ int main(int argc, char **argv)
}
LogInfof("shutdown", "Cleaning up and exiting.");
FourKitBridge::Shutdown();
WinsockNetLayer::Shutdown();
LogDebugf("shutdown", "Network layer shutdown complete.");
g_NetworkManager.Terminate();

View file

@ -378,6 +378,7 @@ set(_MINECRAFT_SERVER_COMMON_ROOT
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PlayerChunkMap.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PlayerCloudParticle.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PlayerConnection.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PlayerConnection.h"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PlayerList.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PlayerRenderer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Polygon.cpp"
@ -494,6 +495,15 @@ set(_MINECRAFT_SERVER_COMMON_ROOT
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/iob_shim.asm"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/stdafx.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/stubs.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/AbstractContainerMenu.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/CompoundContainer.h"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/ItemEntity.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/LivingEntity.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/LivingEntity.h"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/Player.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/Player.h"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/ThrownEnderPearl.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/Tile.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../include/lce_filesystem/lce_filesystem.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliInput.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliInput.h"
@ -510,6 +520,8 @@ set(_MINECRAFT_SERVER_COMMON_SERVER
"${CMAKE_CURRENT_SOURCE_DIR}/Windows64/ServerMain.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/WorldManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/WorldManager.h"
"${CMAKE_CURRENT_SOURCE_DIR}/FourKitBridge.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/FourKitBridge.h"
)
source_group("Server" FILES ${_MINECRAFT_SERVER_COMMON_SERVER})

View file

@ -4,6 +4,10 @@
#include "net.minecraft.world.level.redstone.h"
#include "Slot.h"
#include "AbstractContainerMenu.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "Mth.h"
#include "../Minecraft.Server/FourKitBridge.h"
#endif
// 4J Stu - The java does not have ctor here (being an abstract) but we need one to initialise the member variables
// TODO Make sure all derived classes also call this
@ -248,13 +252,64 @@ shared_ptr<ItemInstance> AbstractContainerMenu::clicked(int slotIndex, int butto
{
if (buttonNum == 0)
{
player->drop(inventory->getCarried());
inventory->setCarried(nullptr);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
auto carried = inventory->getCarried();
bool dropAllowed = true;
if (carried != nullptr && carried->count > 0)
{
int outId = carried->id, outCount = carried->count, outAux = carried->getAuxValue();
if (FourKitBridge::FirePlayerDropItem(
player->entityId, carried->id, carried->count, carried->getAuxValue(),
&outId, &outCount, &outAux))
dropAllowed = false;
else
{
player->drop(std::make_shared<ItemInstance>(outId, outCount, outAux));
inventory->setCarried(nullptr);
dropAllowed = false;
}
}
if (dropAllowed)
{
#endif
player->drop(inventory->getCarried());
inventory->setCarried(nullptr);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
}
}
#endif
}
if (buttonNum == 1)
{
player->drop(inventory->getCarried()->remove(1));
if (inventory->getCarried()->count == 0) inventory->setCarried(nullptr);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
auto carried = inventory->getCarried();
bool dropAllowed = true;
if (carried != nullptr && carried->count > 0)
{
int outId = carried->id, outCount = 1, outAux = carried->getAuxValue();
if (FourKitBridge::FirePlayerDropItem(
player->entityId, carried->id, 1, carried->getAuxValue(),
&outId, &outCount, &outAux))
dropAllowed = false;
else
{
carried->remove(1);
if (carried->count == 0) inventory->setCarried(nullptr);
player->drop(std::make_shared<ItemInstance>(outId, outCount, outAux));
dropAllowed = false;
}
}
if (dropAllowed)
{
#endif
player->drop(inventory->getCarried()->remove(1));
if (inventory->getCarried()->count == 0) inventory->setCarried(nullptr);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
}
}
#endif
}
}
@ -476,7 +531,32 @@ shared_ptr<ItemInstance> AbstractContainerMenu::clicked(int slotIndex, int butto
Slot *slot = slots.at(slotIndex);
if (slot != nullptr && slot->hasItem() && slot->mayPickup(player))
{
shared_ptr<ItemInstance> item = slot->remove(buttonNum == 0 ? 1 : slot->getItem()->count);
int dropCount = buttonNum == 0 ? 1 : slot->getItem()->count;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// fix for issue reported by aiden
{
auto slotItem = slot->getItem();
bool dropAllowed = true;
if (slotItem != nullptr && slotItem->count > 0)
{
int outId = slotItem->id, outCount = dropCount, outAux = slotItem->getAuxValue();
if (FourKitBridge::FirePlayerDropItem(
player->entityId, slotItem->id, dropCount, slotItem->getAuxValue(),
&outId, &outCount, &outAux))
dropAllowed = false;
else
{
shared_ptr<ItemInstance> item = slot->remove(dropCount);
slot->onTake(player, item);
player->drop(std::make_shared<ItemInstance>(outId, outCount, outAux));
dropAllowed = false;
}
}
if (!dropAllowed)
return nullptr;
}
#endif
shared_ptr<ItemInstance> item = slot->remove(dropCount);
slot->onTake(player, item);
player->drop(item);
}

View file

@ -31,4 +31,7 @@ public:
virtual void startOpen();
virtual void stopOpen();
virtual bool canPlaceItem(int slot, shared_ptr<ItemInstance> item);
shared_ptr<Container> getFirstContainer() const { return c1; }
shared_ptr<Container> getSecondContainer() const { return c2; }
};

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