diff --git a/.github/LCRE-banner.png b/.github/LCE-Revelations-banner.png similarity index 100% rename from .github/LCRE-banner.png rename to .github/LCE-Revelations-banner.png diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 381434e1..97c194a1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -40,8 +40,8 @@ jobs: shell: pwsh run: | $source = "./build/windows64/Minecraft.Client/Release" - $zip = "LCREWindows64.zip" - $topLevel = "LCREWindows64" + $zip = "LCE-Revelations-Client-Win64.zip" + $topLevel = "LCE-Revelations-Client-Win64" # Collect files, excluding unwanted extensions $files = Get-ChildItem -Path $source -Recurse -File | @@ -78,7 +78,7 @@ jobs: shell: pwsh run: | New-Item -ItemType Directory -Force -Path staging - Copy-Item LCREWindows64.zip staging/ + Copy-Item LCE-Revelations-Client-Win64.zip staging/ Copy-Item ./build/windows64/Minecraft.Client/Release/Minecraft.Client.exe staging/ - name: Upload artifacts @@ -101,6 +101,11 @@ jobs: - name: Setup CMake uses: lukka/get-cmake@latest + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - name: Run CMake uses: lukka/run-cmake@v10 env: @@ -108,14 +113,49 @@ jobs: with: configurePreset: windows64 buildPreset: windows64-release - buildPresetAdditionalArgs: "['--target', 'Minecraft.Server']" + buildPresetAdditionalArgs: "['--target', 'Minecraft.Server', '--target', 'Minecraft.Server.FourKit']" - - name: Zip Build + - name: Zip Build (vanilla) shell: pwsh run: | $source = "./build/windows64/Minecraft.Server/Release" - $zip = "LCREServerWindows64.zip" - $topLevel = "LCREServerWindows64" + $zip = "LCE-Revelations-Server-Win64.zip" + $topLevel = "LCE-Revelations-Server-Win64" + + $files = Get-ChildItem -Path $source -Recurse -File | + Where-Object { $_.Extension -notin '.pch', '.zip', '.ipdb', '.iobj' } + + Add-Type -AssemblyName System.IO.Compression + Add-Type -AssemblyName System.IO.Compression.FileSystem + + $basePath = (Resolve-Path $source).Path + $fs = [System.IO.File]::Open($zip, [System.IO.FileMode]::Create) + try { + $archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create) + try { + Get-ChildItem -Path $basePath -Recurse -Directory | ForEach-Object { + $rel = $_.FullName.Substring($basePath.Length).TrimStart('\', '/') + $archive.CreateEntry("$topLevel/$($rel -replace '\\','/')/") | Out-Null + } + foreach ($file in $files) { + $rel = $file.FullName.Substring($basePath.Length).TrimStart('\', '/') + $entryName = "$topLevel/$($rel -replace '\\','/')" + [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile( + $archive, $file.FullName, $entryName, + [System.IO.Compression.CompressionLevel]::Optimal + ) | Out-Null + } + } finally { $archive.Dispose() } + } finally { $fs.Dispose() } + + Write-Host "Created $zip" + + - name: Zip Build (FourKit) + shell: pwsh + run: | + $source = "./build/windows64/Minecraft.Server.FourKit/Release" + $zip = "LCE-Revelations-Server-Win64-FourKit.zip" + $topLevel = "LCE-Revelations-Server-Win64-FourKit" $files = Get-ChildItem -Path $source -Recurse -File | Where-Object { $_.Extension -notin '.pch', '.zip', '.ipdb', '.iobj' } @@ -149,7 +189,8 @@ jobs: shell: pwsh run: | New-Item -ItemType Directory -Force -Path staging - Copy-Item LCREServerWindows64.zip staging/ + Copy-Item LCE-Revelations-Server-Win64.zip staging/ + Copy-Item LCE-Revelations-Server-Win64-FourKit.zip staging/ - name: Upload artifacts uses: actions/upload-artifact@v6 @@ -176,7 +217,8 @@ jobs: uses: actions/attest-build-provenance@v2 with: subject-path: | - artifacts/LCREServerWindows64.zip + artifacts/LCE-Revelations-Server-Win64.zip + artifacts/LCE-Revelations-Server-Win64-FourKit.zip - name: Get short SHA id: sha @@ -213,7 +255,11 @@ jobs: --title "Server: ${{ steps.sha.outputs.short }}" \ --notes "Dedicated Server runtime for Windows64. - Download \`LCREServerWindows64.zip\` and extract it to a folder where you'd like to keep the server runtime." \ + Two flavours are attached: + - \`LCE-Revelations-Server-Win64.zip\`: vanilla server, no plugin support, smallest download. + - \`LCE-Revelations-Server-Win64-FourKit.zip\`: server with the FourKit plugin host, bundled .NET 10 runtime, and an empty \`plugins/\` folder ready for plugin authors to drop DLLs into. + + Pick the flavour you want and extract it to a folder where you'd like to keep the server runtime." \ --latest=false release-client: @@ -235,7 +281,7 @@ jobs: uses: actions/attest-build-provenance@v2 with: subject-path: | - artifacts/LCREWindows64.zip + artifacts/LCE-Revelations-Client-Win64.zip artifacts/Minecraft.Client.exe - name: Get short SHA @@ -270,15 +316,15 @@ jobs: cat > notes.md <<'NOTES' # Instructions: **Newcomers:** - - If this is your first time, download `LCREWindows64.zip` and extract it wherever you would like to keep it. + - If this is your first time, download `LCE-Revelations-Client-Win64.zip` and extract it wherever you would like to keep it. - I would recommend to set your username prior to launch (create a file called `username.txt`, put your desired username into the file, and save). - To play, simply run `Minecraft.Client.exe`. **For those that wish to update their existing installation with the latest build:** - - Download `Minecraft.Client.exe` and `Minecraft.Client.pdb` and copy them over to your existing LCREWindows64 build (overwrite your old version of Minecraft.Client.exe and Minecraft.Client.pdb). + - Download `Minecraft.Client.exe` and `Minecraft.Client.pdb` and copy them over to your existing LCE-Revelations-Client-Win64 build (overwrite your old version of Minecraft.Client.exe and Minecraft.Client.pdb). **Steam Deck & Linux:** - - Y'all know the drill. Download the `LCREWindows64.zip`, extract it, add the `Minecraft.Client.exe` as a "Non-Steam Game" within the Steam library, turn on compatibility mode with Proton Experimental, and then run it! + - Y'all know the drill. Download the `LCE-Revelations-Client-Win64.zip`, extract it, add the `Minecraft.Client.exe` as a "Non-Steam Game" within the Steam library, turn on compatibility mode with Proton Experimental, and then run it! # Multiplayer instructions: LAN games are natively supported, and any LAN games will appear automatically on the right. However, if you'd like to play with your friends online (and if you don't want to require them to setup a vpn, and/or if you don't want to port forward), I would recommend the following setup. Please keep in mind, you do NOT need to do this to enjoy the game. This is just how I have it setup for me so my friends can join without any hassle: @@ -289,18 +335,12 @@ jobs: How-to: - Ensure your playit.gg agent is connected to your playit.gg account - - On the playit.gg website, setup a new tunnel (choose TCP). Ensure the configurable settings are set to the below values, assuming your agent is installed on the same computer as your online LCREMinecraft game is hosted from. + - On the playit.gg website, setup a new tunnel (choose TCP). Ensure the configurable settings are set to the below values, assuming your agent is installed on the same computer as your online LCE Revelations game is hosted from. - Configurable settings: - Local IP: `127.0.0.1` - Local Port: `25565` - Proxy Protocol: `None` - After creating your tunnel, navigate to the "Tunnels" main page. You'll see the IP address and port for your tunnel. This is what your friends will input when adding your server in order to join your online game! - - - # Why this fork exists: - Changes/additions that stray from the upstream repo (`smartcmd/MinecraftConsoles`: - - See: https://github.com/itsRevela/MinecraftConsoles?tab=readme-ov-file#latest - - I can tweak this fork while staying compatible with the upstream repo without needing to wait on my pull requests to get accepted upstream (while keeping this fork updated with the latest and greatest from upstream) NOTES - name: Create release diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4ff0dbc2..619c0f94 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,7 +1,7 @@ name: Pull Request Build on: - workflow_dispatch: + workflow_dispatch: pull_request: types: [opened, reopened, synchronize] paths-ignore: @@ -14,19 +14,24 @@ jobs: runs-on: windows-latest steps: - - name: Checkout - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 - - name: Setup MSVC - uses: ilammy/msvc-dev-cmd@v1 + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 - - name: Setup CMake - uses: lukka/get-cmake@latest + - name: Setup CMake + uses: lukka/get-cmake@latest - - name: Run CMake - uses: lukka/run-cmake@v10 - env: - VCPKG_ROOT: "" # Disable vcpkg for CI builds - with: - configurePreset: windows64 - buildPreset: windows64-debug \ No newline at end of file + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Run CMake + uses: lukka/run-cmake@v10 + env: + VCPKG_ROOT: "" # Disable vcpkg for CI builds + with: + configurePreset: windows64 + buildPreset: windows64-debug diff --git a/.gitignore b/.gitignore index b6060795..080326f3 100644 --- a/.gitignore +++ b/.gitignore @@ -428,3 +428,11 @@ tools/*.class tools/*.swf tools/staging/ tools/server-monitor/ + + +# Nix +result +result-* +.direnv/ +.xwin-cache/ +.xwin/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ff1e956..37638828 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,7 @@ list(APPEND MINECRAFT_SHARED_DEFINES ${PLATFORM_DEFINES}) 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.FourKit) add_subdirectory(Minecraft.Server) endif() diff --git a/COMPILE.md b/COMPILE.md index 4a9d56b9..f58fabc8 100644 --- a/COMPILE.md +++ b/COMPILE.md @@ -1,24 +1,55 @@ # Compile Instructions -## Visual Studio +## Prerequisites -1. Clone or download the repository -1. Open the repo folder in Visual Studio 2022+. -2. Wait for cmake to configure the project and load all assets (this may take a few minutes on the first run). -3. Right click a folder in the solution explorer and switch to the 'CMake Targets View' -4. Select platform and configuration from the dropdown. EG: `Windows64 - Debug` or `Windows64 - Release` -5. Pick the startup project `Minecraft.Client.exe` or `Minecraft.Server.exe` using the debug targets dropdown -6. Build and run the project: - - `Build > Build Solution` (or `Ctrl+Shift+B`) - - Start debugging with `F5`. +- **Visual Studio 2022** with the **Desktop development with C++** workload (this includes the CMake tools, MSVC toolchain, and Windows 10 SDK). +- **.NET 10 SDK**, required to build the FourKit plugin host (`Minecraft.Server.FourKit`). + - Download: https://dotnet.microsoft.com/download/dotnet/10.0 (pick the **x64 SDK** installer) + - The exact SDK version is pinned in `global.json` at the repo root. + - CMake will fail configure with a clear error message if .NET 10 is not installed, so you find out immediately rather than partway through a build. + - The build invokes `dotnet publish ... --runtime win-x64 --self-contained true`, so the published output bundles a complete .NET 10 runtime alongside the FourKit assembly. End users running the produced server do **not** need to install .NET themselves. + - All FourKit runtime files (DLL + .NET runtime + `hostfxr.dll`) land in a `runtime/` subfolder next to `Minecraft.Server.exe`. An empty `plugins/` folder is also created. Both are produced automatically by the build. + +## Visual Studio 2022 quick start (recommended) + +VS 2022 has built-in CMake support, so there is no need to generate a `.sln` file by hand. + +1. Install the prerequisites above. +2. Clone the repo. +3. In Visual Studio: `File > Open > Folder...` and select the **repo root** (the folder that contains `CMakeLists.txt`). +4. Wait for CMake to configure (~5 seconds on a warm cache, a few minutes on the first run while assets copy). +5. Pick a build configuration in the dropdown, for example `windows64-release`. +6. `Build > Build All` (or `F7`). Targets of interest: + - `Minecraft.Client`: the game client. + - `Minecraft.Server`: the **vanilla** dedicated server. Standalone C++ binary, no plugin host, no .NET dependency at runtime, smallest distribution. + - `Minecraft.Server.FourKit`: the **FourKit-enabled** dedicated server. Bundles the .NET 10 plugin host alongside the exe (in `runtime/`) and creates an empty `plugins/` folder for end users to drop plugin DLLs into. Building this target also triggers the `Minecraft.Server.FourKit.Managed` target which publishes the C# project. +7. Use the debug target dropdown to pick `Minecraft.Client.exe` or whichever server flavour you want, then `F5` to launch. + +### Server flavours + +Both server targets compile from the same source tree and produce a binary literally named `Minecraft.Server.exe`. The variant identity lives in the build directory: + +``` +build//Minecraft.Server/Release/ + Minecraft.Server.exe (vanilla, no plugin support) + Common/, Windows64/, ... + +build//Minecraft.Server.FourKit/Release/ + Minecraft.Server.exe (FourKit-enabled, same exe name on purpose) + runtime/ (self-contained .NET 10 + Minecraft.Server.FourKit.dll) + plugins/ (empty drop point) + Common/, Windows64/, ... +``` + +The FourKit target gets the `MINECRAFT_SERVER_FOURKIT_BUILD` preprocessor define. Inside `FourKitBridge.h`, the real plugin entry points are conditional on that define; the vanilla target sees inline no-op stubs instead, so gameplay code can call `FourKitBridge::Fire*` unconditionally and produce the right behaviour for each flavour without per-call-site `#ifdef`s. ### Dedicated server debug arguments -- Default debugger arguments for `Minecraft.Server`: +- Default debugger arguments for both `Minecraft.Server` and `Minecraft.Server.FourKit`: - `-port 25565 -bind 0.0.0.0 -name DedicatedServer` - You can override arguments in: - `Project Properties > Debugging > Command Arguments` -- `Minecraft.Server` post-build copies only the dedicated-server asset set: +- Both server targets post-build copy the dedicated-server asset set: - `Common/Media/MediaWindows64.arc` - `Common/res` - `Windows64/GameHDD` @@ -45,18 +76,36 @@ Build Release: cmake --build --preset windows64-release --target Minecraft.Client ``` -Build Dedicated Server (Debug): +Build vanilla Dedicated Server (Debug): ```powershell cmake --build --preset windows64-debug --target Minecraft.Server ``` -Build Dedicated Server (Release): +Build vanilla Dedicated Server (Release): ```powershell cmake --build --preset windows64-release --target Minecraft.Server ``` +Build FourKit Dedicated Server (Debug): + +```powershell +cmake --build --preset windows64-debug --target Minecraft.Server.FourKit +``` + +Build FourKit Dedicated Server (Release): + +```powershell +cmake --build --preset windows64-release --target Minecraft.Server.FourKit +``` + +Build everything (client + both server flavours): + +```powershell +cmake --build --preset windows64-release +``` + Run executable: ```powershell @@ -64,15 +113,76 @@ cd .\build\windows64\Minecraft.Client\Debug .\Minecraft.Client.exe ``` -Run dedicated server: +Run vanilla dedicated server: ```powershell cd .\build\windows64\Minecraft.Server\Debug .\Minecraft.Server.exe -port 25565 -bind 0.0.0.0 -name DedicatedServer ``` +Run FourKit dedicated server: + +```powershell +cd .\build\windows64\Minecraft.Server.FourKit\Debug +.\Minecraft.Server.exe -port 25565 -bind 0.0.0.0 -name DedicatedServer +``` + Notes: -- The CMake build is Windows-only and x64-only. -- Contributors on macOS or Linux need a Windows machine or VM to build the project. Running the game via Wine is separate from having a supported build environment. - Post-build asset copy is automatic for `Minecraft.Client` in CMake (Debug and Release variants). - The game relies on relative paths (for example `Common\Media\...`), so launching from the output directory is required. + +## CMake (Linux x64 Cross-Compile with Clang) + +Cross-compile Windows x64 binaries on Linux using LLVM/Clang and the Windows SDK obtained via xwin. + +### Prerequisites + +Install the following packages (example for Ubuntu): + +```bash +sudo apt install clang lld llvm cmake ninja-build rsync cargo +``` + +Install xwin for downloading the Windows SDK: + +```bash +cargo install xwin +``` + +### Compile + +Run this (Release): +```bash +./build-linux.sh +``` + +Or, for debug: +```bash +./build-linux.sh . Debug +``` + + +### NixOS / Nix + +For NixOS or systems with Nix installed, use the provided flake: + +```bash +nix build .#client +nix build .#server +``` + +Or enter the development shell with all dependencies: + +```bash +nix develop +``` + +Notes: +- Requires LLVM 15+ with clang-cl, lld-link, llvm-rc, and llvm-mt. +- Wine is required to run the compiled Windows executables on Linux. + +### Troubleshooting + +- **`'vswhere.exe' is not recognized`**: harmless warning. This appears if you ran `vcvars64.bat` from a plain command prompt instead of `Developer PowerShell for VS`. The Visual Studio Installer's `vswhere.exe` lives at `C:\Program Files (x86)\Microsoft Visual Studio\Installer\` and is not on the default `PATH`. Use the Developer PowerShell shortcut, or open the repo folder directly in VS (which handles the dev env for you). +- **`.NET 10 SDK not found` at configure time**: install the x64 SDK from https://dotnet.microsoft.com/download/dotnet/10.0 and re-run CMake configure (`Project > Configure Cache` in VS, or `cmake --preset windows64` from a shell). +- **Server starts but logs `hostfxr_initialize_for_dotnet_command_line failed`**: the `runtime/` folder next to `Minecraft.Server.exe` is missing or stale. Rebuild the `Minecraft.Server.FourKit` target (which re-stages `runtime/`), or do a clean rebuild of `Minecraft.Server`. diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index 424f957f..b89567c0 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -58,7 +58,7 @@ #ifdef _WINDOWS64 #include "Xbox/Network/NetworkPlayerXbox.h" #include "Common/Network/PlatformNetworkManagerStub.h" -#include "Windows64\Network\WinsockNetLayer.h" +#include "Windows64/Network/WinsockNetLayer.h" #endif @@ -4251,7 +4251,8 @@ void ClientConnection::handleSetPlayerTeamPacket(shared_ptr void ClientConnection::handleParticleEvent(shared_ptr packet) { - ePARTICLE_TYPE particleId = (ePARTICLE_TYPE)Integer::parseInt(packet->getName()); + wstring particleName = packet->getName(); + ePARTICLE_TYPE particleId = (ePARTICLE_TYPE)Integer::parseInt(particleName); for (int i = 0; i < packet->getCount(); i++) { diff --git a/Minecraft.Client/Common/Audio/SoundEngine.cpp b/Minecraft.Client/Common/Audio/SoundEngine.cpp index 31804e6b..eb2e0d81 100644 --- a/Minecraft.Client/Common/Audio/SoundEngine.cpp +++ b/Minecraft.Client/Common/Audio/SoundEngine.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include #ifdef __ORBIS__ #include @@ -1523,4 +1523,4 @@ char *SoundEngine::ConvertSoundPathToName(const wstring& name, bool bConvertSpac return buf; } -#endif \ No newline at end of file +#endif diff --git a/Minecraft.Client/Common/Network/GameNetworkManager.cpp b/Minecraft.Client/Common/Network/GameNetworkManager.cpp index 2105be9e..ca13e019 100644 --- a/Minecraft.Client/Common/Network/GameNetworkManager.cpp +++ b/Minecraft.Client/Common/Network/GameNetworkManager.cpp @@ -574,11 +574,10 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame } else if ( connection->isClosed() || !IsInSession()) { -// assert(false); - // Set to NULL because we're returning to home - // The level is not reset when you leave the progress UI which causes a crash - Minecraft::GetInstance()->setLevel(NULL); - +// assert(false); + // Set to NULL because we're returning to home + // The level is not reset when you leave the progress UI which causes a crash + Minecraft::GetInstance()->setLevel(NULL); MinecraftServer::HaltServer(); return false; } diff --git a/Minecraft.Client/Common/UI/UIControl_BeaconEffectButton.cpp b/Minecraft.Client/Common/UI/UIControl_BeaconEffectButton.cpp index 7ee79307..98aa4d96 100644 --- a/Minecraft.Client/Common/UI/UIControl_BeaconEffectButton.cpp +++ b/Minecraft.Client/Common/UI/UIControl_BeaconEffectButton.cpp @@ -9,6 +9,7 @@ UIControl_BeaconEffectButton::UIControl_BeaconEffectButton() m_selected = false; m_active = false; m_focus = false; + m_lastState = eState_Disabled; } bool UIControl_BeaconEffectButton::setupControl(UIScene *scene, IggyValuePath *parent, const string &controlName) diff --git a/Minecraft.Client/Common/UI/UIControl_Label.cpp b/Minecraft.Client/Common/UI/UIControl_Label.cpp index 14ea098c..3e9323a6 100644 --- a/Minecraft.Client/Common/UI/UIControl_Label.cpp +++ b/Minecraft.Client/Common/UI/UIControl_Label.cpp @@ -1,8 +1,8 @@ #include "stdafx.h" #include "UI.h" #include "UIControl_Label.h" -#include "..\..\..\Minecraft.World\StringHelpers.h" -#include "..\..\..\Minecraft.World\ArabicShaping.h" +#include "../../../Minecraft.World/StringHelpers.h" +#include "../../../Minecraft.World/ArabicShaping.h" UIControl_Label::UIControl_Label() { diff --git a/Minecraft.Client/Common/UI/UIControl_SaveList.cpp b/Minecraft.Client/Common/UI/UIControl_SaveList.cpp index 00aaa93a..0ce28b02 100644 --- a/Minecraft.Client/Common/UI/UIControl_SaveList.cpp +++ b/Minecraft.Client/Common/UI/UIControl_SaveList.cpp @@ -1,7 +1,7 @@ #include "stdafx.h" #include "UI.h" #include "UIControl_SaveList.h" -#include "..\..\..\Minecraft.World\ArabicShaping.h" +#include "../../../Minecraft.World/ArabicShaping.h" bool UIControl_SaveList::setupControl(UIScene *scene, IggyValuePath *parent, const string &controlName) { diff --git a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp index e0e388db..69ab82a4 100644 --- a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp +++ b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp @@ -1,9 +1,9 @@ #include "stdafx.h" #include "UI.h" #include "UIScene_ConnectingProgress.h" -#include "..\..\Minecraft.h" +#include "../../Minecraft.h" #ifdef _WINDOWS64 -#include "..\..\Windows64\Network\WinsockNetLayer.h" +#include "../../Windows64/Network/WinsockNetLayer.h" static int ConnectingProgress_OnRejectedDialogOK(LPVOID, int iPad, const C4JStorage::EMessageResult) { diff --git a/Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp b/Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp index 00991ca7..6384f7d5 100644 --- a/Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp @@ -226,4 +226,4 @@ void UIScene_DeathMenu::handlePress(F64 controlId, F64 childId) } break; } -} \ No newline at end of file +} diff --git a/Minecraft.Client/Common/UI/UIScene_HUD.cpp b/Minecraft.Client/Common/UI/UIScene_HUD.cpp index 676542d3..c9b99b52 100644 --- a/Minecraft.Client/Common/UI/UIScene_HUD.cpp +++ b/Minecraft.Client/Common/UI/UIScene_HUD.cpp @@ -909,4 +909,4 @@ void UIScene_HUD::handleGameTick() updateFrameTick(); } -} \ No newline at end of file +} diff --git a/Minecraft.Client/Common/XUI/XUI_Ctrl_4JList.cpp b/Minecraft.Client/Common/XUI/XUI_Ctrl_4JList.cpp index 4b7eb5f6..6e35f77c 100644 --- a/Minecraft.Client/Common/XUI/XUI_Ctrl_4JList.cpp +++ b/Minecraft.Client/Common/XUI/XUI_Ctrl_4JList.cpp @@ -1,6 +1,6 @@ #include "stdafx.h" #include "XUI_Ctrl_4JList.h" -#include "..\..\..\Minecraft.World\ArabicShaping.h" +#include "../../../Minecraft.World/ArabicShaping.h" static bool TimeSortFn(const void *a, const void *b); diff --git a/Minecraft.Client/Font.cpp b/Minecraft.Client/Font.cpp index 85b1391c..6e0bc7e4 100644 --- a/Minecraft.Client/Font.cpp +++ b/Minecraft.Client/Font.cpp @@ -8,7 +8,7 @@ #include "../Minecraft.World/net.minecraft.h" #include "../Minecraft.World/StringHelpers.h" #include "../Minecraft.World/Random.h" -#include "..\Minecraft.World\ArabicShaping.h" +#include "../Minecraft.World/ArabicShaping.h" Font::Font(Options *options, const wstring& name, Textures* textures, bool enforceUnicode, ResourceLocation *textureLocation, int cols, int rows, int charWidth, int charHeight, unsigned short charMap[]/* = nullptr */) : textures(textures) { diff --git a/Minecraft.Client/HumanoidModel.cpp b/Minecraft.Client/HumanoidModel.cpp index b82d745d..8649f6f9 100644 --- a/Minecraft.Client/HumanoidModel.cpp +++ b/Minecraft.Client/HumanoidModel.cpp @@ -1,7 +1,7 @@ #include "stdafx.h" #include "HumanoidModel.h" -#include "..\Minecraft.World\Mth.h" -#include "..\Minecraft.World\Entity.h" +#include "../Minecraft.World/Mth.h" +#include "../Minecraft.World/Entity.h" #include "ModelPart.h" // 4J added diff --git a/Minecraft.Client/LivingEntityRenderer.cpp b/Minecraft.Client/LivingEntityRenderer.cpp index 315e20dd..c6e7687e 100644 --- a/Minecraft.Client/LivingEntityRenderer.cpp +++ b/Minecraft.Client/LivingEntityRenderer.cpp @@ -10,6 +10,7 @@ #include "../Minecraft.World/Player.h" #include "Skins.h" + ResourceLocation LivingEntityRenderer::ENCHANT_GLINT_LOCATION = ResourceLocation(TN__BLUR__MISC_GLINT); int LivingEntityRenderer::MAX_ARMOR_LAYERS = 4; diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 2becee1e..4406cdab 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -35,7 +35,7 @@ #include "Windows64/Network/WinsockNetLayer.h" #endif #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) -#include "..\Minecraft.Server\ServerLogger.h" +#include "../Minecraft.Server/ServerLogger.h" #endif #include #ifdef SPLIT_SAVES @@ -2401,36 +2401,7 @@ void MinecraftServer::chunkPacketManagement_PreTick() s_tickStartTime = System::currentTimeMillis(); s_sentTo.clear(); - vector< shared_ptr > *players = connection->getPlayers(); - - if( players->size() ) - { - vector< shared_ptr > playersOrig = *players; - players->clear(); - - do - { - int longestTime = 0; - auto playerConnectionBest = playersOrig.begin(); - for( auto it = playersOrig.begin(); it != playersOrig.end(); it++) - { - int thisTime = 0; - INetworkPlayer *np = (*it)->getNetworkPlayer(); - if( np ) - { - thisTime = np->GetTimeSinceLastChunkPacket_ms(); - } - - if( thisTime > longestTime ) - { - playerConnectionBest = it; - longestTime = thisTime; - } - } - players->push_back(*playerConnectionBest); - playersOrig.erase(playerConnectionBest); - } while ( playersOrig.size() > 0 ); - } + connection->sortPlayersByChunkPriority(); } void MinecraftServer::chunkPacketManagement_PostTick() diff --git a/Minecraft.Client/Orbis/Network/SonyRemoteStorage_Orbis.cpp b/Minecraft.Client/Orbis/Network/SonyRemoteStorage_Orbis.cpp index 008879cb..bd2f85e3 100644 --- a/Minecraft.Client/Orbis/Network/SonyRemoteStorage_Orbis.cpp +++ b/Minecraft.Client/Orbis/Network/SonyRemoteStorage_Orbis.cpp @@ -14,7 +14,7 @@ // #include // #include // #include -// #include +// #include diff --git a/Minecraft.Client/PS3/Network/SonyRemoteStorage_PS3.cpp b/Minecraft.Client/PS3/Network/SonyRemoteStorage_PS3.cpp index ab7b4770..09c2c681 100644 --- a/Minecraft.Client/PS3/Network/SonyRemoteStorage_PS3.cpp +++ b/Minecraft.Client/PS3/Network/SonyRemoteStorage_PS3.cpp @@ -13,7 +13,7 @@ // #include // #include // #include -// #include +// #include diff --git a/Minecraft.Client/PS3/PS3Extras/boost_1_53_0/boost/spirit/home/support/char_encoding/unicode/create_tables.cpp b/Minecraft.Client/PS3/PS3Extras/boost_1_53_0/boost/spirit/home/support/char_encoding/unicode/create_tables.cpp index aa36fb4d..0f565dc4 100644 --- a/Minecraft.Client/PS3/PS3Extras/boost_1_53_0/boost/spirit/home/support/char_encoding/unicode/create_tables.cpp +++ b/Minecraft.Client/PS3/PS3Extras/boost_1_53_0/boost/spirit/home/support/char_encoding/unicode/create_tables.cpp @@ -453,7 +453,7 @@ void print_head(Out& out) << "\n" << " AUTOGENERATED. DO NOT EDIT!!!\n" << "==============================================================================*/\n" - << "#include \n" + << "#include /n" << "\n" << "namespace boost { namespace spirit { namespace ucd { namespace detail\n" << "{" diff --git a/Minecraft.Client/PSVita/Network/SonyRemoteStorage_Vita.cpp b/Minecraft.Client/PSVita/Network/SonyRemoteStorage_Vita.cpp index dd6c8372..2d79f9b6 100644 --- a/Minecraft.Client/PSVita/Network/SonyRemoteStorage_Vita.cpp +++ b/Minecraft.Client/PSVita/Network/SonyRemoteStorage_Vita.cpp @@ -14,7 +14,7 @@ // #include // #include // #include -// #include +// #include diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 3b1f0a45..940a3d64 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -18,7 +18,9 @@ #include "../Minecraft.Server/ServerLogManager.h" #include "../Minecraft.Server/Access/Access.h" #include "..\Minecraft.Server/Security/SecurityConfig.h" -#include "..\Minecraft.World\Socket.h" +#include "../Minecraft.World/Socket.h" +#include +#include #endif // #ifdef __PS3__ // #include "PS3/Network/NetworkPlayerSony.h" @@ -112,6 +114,54 @@ void PendingConnection::handlePreLogin(shared_ptr packet) } return; } + +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + std::string connectionIp = ""; + int connectionPort = 0; + + if (connection && connection->getSocket()) { + unsigned char smallId = connection->getSocket()->getSmallId(); + if (smallId != 0) { + if (!ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, &connectionIp)) + { + SOCKET sock = WinsockNetLayer::GetSocketForSmallId(smallId); + if (sock != INVALID_SOCKET) + { + sockaddr_in addr; + int addrLen = sizeof(addr); + if (getpeername(sock, (sockaddr*)&addr, &addrLen) == 0) + { + char ipBuf[64] = {}; + if (inet_ntop(AF_INET, &addr.sin_addr, ipBuf, sizeof(ipBuf))) + { + connectionIp = ipBuf; + connectionPort = (int)ntohs(addr.sin_port); + } + } + } + } else { + SOCKET sock = WinsockNetLayer::GetSocketForSmallId(smallId); + if (sock != INVALID_SOCKET) + { + sockaddr_in addr; + int addrLen = sizeof(addr); + if (getpeername(sock, (sockaddr*)&addr, &addrLen) == 0) + connectionPort = (int)ntohs(addr.sin_port); + } + } + + if (!connectionIp.empty()) { + if (FourKitBridge::FirePlayerPreLogin(packet->loginKey, connectionIp, connectionPort)) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + } + } + } + } +#endif + // printf("Server: handlePreLogin\n"); name = packet->loginKey; // 4J Stu - Change from the login packet as we know better on client end during the pre-login packet sendPreLoginResponse(); @@ -201,6 +251,62 @@ void PendingConnection::handleLogin(shared_ptr packet) //if (true)// 4J removed !server->onlineMode) bool sentDisconnect = false; +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + std::string connectionIp = ""; + int connectionPort = 0; + + if (!connection || !connection->getSocket()) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + + unsigned char smallId = connection->getSocket()->getSmallId(); + if (smallId == 0) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + + if (!ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, &connectionIp)) + { + SOCKET sock = WinsockNetLayer::GetSocketForSmallId(smallId); + if (sock != INVALID_SOCKET) + { + sockaddr_in addr; + int addrLen = sizeof(addr); + if (getpeername(sock, (sockaddr*)&addr, &addrLen) == 0) + { + char ipBuf[64] = {}; + if (inet_ntop(AF_INET, &addr.sin_addr, ipBuf, sizeof(ipBuf))) + { + connectionIp = ipBuf; + connectionPort = (int)ntohs(addr.sin_port); + } + } + } + if (connectionIp.empty()) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + } + else { + SOCKET sock = WinsockNetLayer::GetSocketForSmallId(smallId); + if (sock != INVALID_SOCKET) + { + sockaddr_in addr; + int addrLen = sizeof(addr); + if (getpeername(sock, (sockaddr*)&addr, &addrLen) == 0) + connectionPort = (int)ntohs(addr.sin_port); + } + } + + if (FourKitBridge::FirePlayerLogin(packet->userName, connectionIp, connectionPort, 1, &packet->m_onlineXuid, &packet->m_offlineXuid)) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + } +#endif + // Use the same Xuid choice as handleAcceptedLogin (offline first, online fallback). // PlayerUID loginXuid = packet->m_offlineXuid; @@ -395,6 +501,62 @@ void PendingConnection::handleAcceptedLogin(shared_ptr packet) return; } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + std::string connectionIp = ""; + int connectionPort = 0; + + if (!connection || !connection->getSocket()) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + + unsigned char smallId = connection->getSocket()->getSmallId(); + if (smallId == 0) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + + if (!ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, &connectionIp)) + { + SOCKET sock = WinsockNetLayer::GetSocketForSmallId(smallId); + if (sock != INVALID_SOCKET) + { + sockaddr_in addr; + int addrLen = sizeof(addr); + if (getpeername(sock, (sockaddr*)&addr, &addrLen) == 0) + { + char ipBuf[64] = {}; + if (inet_ntop(AF_INET, &addr.sin_addr, ipBuf, sizeof(ipBuf))) + { + connectionIp = ipBuf; + connectionPort = (int)ntohs(addr.sin_port); + } + } + } + if (connectionIp.empty()) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + } + else { + SOCKET sock = WinsockNetLayer::GetSocketForSmallId(smallId); + if (sock != INVALID_SOCKET) + { + sockaddr_in addr; + int addrLen = sizeof(addr); + if (getpeername(sock, (sockaddr*)&addr, &addrLen) == 0) + connectionPort = (int)ntohs(addr.sin_port); + } + } + + if (FourKitBridge::FirePlayerLogin(packet->userName, connectionIp, connectionPort, 2, &packet->m_onlineXuid, &packet->m_offlineXuid)) { + disconnect(DisconnectPacket::eDisconnect_EndOfStream); + return; + } + } +#endif + // Guests use the online xuid, everyone else uses the offline one PlayerUID playerXuid = packet->m_offlineXuid; if(playerXuid == INVALID_XUID) playerXuid = packet->m_onlineXuid; diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index 690cdc11..9e5ed0bb 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -24,6 +24,9 @@ #include "../Minecraft.World/Socket.h" #include "../Minecraft.World/net.minecraft.h" #include "../Minecraft.World/LevelData.h" +#include "../Minecraft.World/Pos.h" +#include "../Minecraft.World/Achievements.h" +#include "EntityTracker.h" #include "ServerConnection.h" #include "../Minecraft.World/GenericStats.h" #include "../Minecraft.World/JavaMath.h" @@ -37,6 +40,7 @@ #include "../Minecraft.Server/Security/IdentityTokenManager.h" #include "../Minecraft.Server/Security/SecurityConfig.h" #include "../Minecraft.Server/Security/ConnectionCipher.h" +#include "../Minecraft.Server/FourKitBridge.h" extern bool g_Win64DedicatedServer; #endif @@ -64,6 +68,7 @@ PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connecti aboveGroundTickCount = 0; xLastOk = yLastOk = zLastOk = 0; synched = true; + hasDoneFirstTickFourKit = false; didTick = false; lastKeepAliveId = 0; lastKeepAliveTime = 0; @@ -118,6 +123,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(), (unsigned long long)player->getOnlineXuid(), (unsigned long long)player->getXuid()); + hasDoneFirstTickFourKit = true; + } +#endif + if( done ) return; if( m_bCloseOnTick ) @@ -166,34 +179,39 @@ 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.store(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(); - // 4J Stu - Need to remove the player from the receiving list before their socket is NULLed so that we can find another player on their system - server->getPlayers()->removePlayerFromReceiving( player ); - send(std::make_shared(reason)); - connection->sendAndQuit(); - // 4J-PB - removed, since it needs to be localised in the language the client is in - //server->players->broadcastAll( shared_ptr( new ChatPacket(L"�e" + player->name + L" left the game.") ) ); - if(getWasKicked()) - { - server->getPlayers()->broadcastAll(std::make_shared(player->name, ChatPacket::e_ChatPlayerKickedFromGame)); - } - else - { - server->getPlayers()->broadcastAll(std::make_shared(player->name, ChatPacket::e_ChatPlayerLeftGame)); - } - - server->getPlayers()->remove(player); + // Mark done and release the lock BEFORE the heavy PlayerList work. + // The actual removal, broadcast, and socket teardown are queued for + // the next tick, which processes them without holding done_cs. done = true; LeaveCriticalSection(&done_cs); + + server->getPlayers()->queueDisconnect(player, static_cast(reason), + kickLeaveMessage, getWasKicked(), fourKitHandledQuit); } void PlayerConnection::handlePlayerInput(shared_ptr packet) @@ -277,6 +295,8 @@ void PlayerConnection::handleMovePlayer(shared_ptr 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) { @@ -340,7 +360,33 @@ void PlayerConnection::handleMovePlayer(shared_ptr 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; + if (player->bb == nullptr) return; bool oldOk = level->getCubes(player, player->bb->copy()->shrink(r, r, r))->empty(); if (player->onGround && !packet->onGround && yDist > 0) @@ -388,13 +434,19 @@ void PlayerConnection::handleMovePlayer(shared_ptr packet) } player->absMoveTo(xt, yt, zt, yRotT, xRotT); - bool newOk = level->getCubes(player, player->bb->copy()->shrink(r, r, r))->empty(); + AABB *playerBB = player->bb; + if (playerBB == nullptr) + { + teleport(xLastOk, yLastOk, zLastOk, yRotT, xRotT); + return; + } + bool newOk = level->getCubes(player, playerBB->copy()->shrink(r, r, r))->empty(); if (oldOk && (fail || !newOk) && !player->isSleeping()) { teleport(xLastOk, yLastOk, zLastOk, yRotT, xRotT); return; } - AABB *testBox = player->bb->copy()->grow(r, r, r)->expand(0, -0.55, 0); + AABB *testBox = playerBB->copy()->grow(r, r, r)->expand(0, -0.55, 0); // && server.level.getCubes(player, testBox).size() == 0 if (!server->isFlightAllowed() && !player->gameMode->isCreative() && !level->containsAnyBlocks(testBox) && !player->isAllowedToFly() ) { @@ -446,11 +498,55 @@ void PlayerConnection::handlePlayerAction(shared_ptr packet) if (packet->action == PlayerActionPacket::DROP_ITEM) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + shared_ptr 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); + shared_ptr dropItem = (outId == selected->id) + ? selected->copy() + : std::make_shared(outId, outCount, outAux); + dropItem->count = outCount; + if (outAux != selected->getAuxValue()) dropItem->setAuxValue(outAux); + player->drop(dropItem); + return; + } + } +#endif player->drop(false); return; } else if (packet->action == PlayerActionPacket::DROP_ALL_ITEMS) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + shared_ptr 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); + shared_ptr dropItem = (outId == selected->id) + ? selected->copy() + : std::make_shared(outId, outCount, outAux); + dropItem->count = outCount; + if (outAux != selected->getAuxValue()) dropItem->setAuxValue(outAux); + player->drop(dropItem); + return; + } + } +#endif player->drop(true); return; } @@ -488,6 +584,23 @@ void PlayerConnection::handlePlayerAction(shared_ptr packet) if (packet->action == PlayerActionPacket::START_DESTROY_BLOCK) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + lastLeftClickTick = tickCount; + { + shared_ptr 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(x, y, z, level)); @@ -495,9 +608,9 @@ void PlayerConnection::handlePlayerAction(shared_ptr packet) } else if (packet->action == PlayerActionPacket::STOP_DESTROY_BLOCK) { - bool destroyed = player->gameMode->stopDestroyBlock(x, y, z); + player->gameMode->stopDestroyBlock(x, y, z); server->getPlayers()->prioritiseTileChanges(x, y, z, level->dimension->id); // 4J added - make sure that the update packets for this get prioritised over other general world updates - if (!destroyed && level->getTile(x, y, z) != 0) player->connection->send(std::make_shared(x, y, z, level)); + if (level->getTile(x, y, z) != 0) player->connection->send(std::make_shared(x, y, z, level)); } else if (packet->action == PlayerActionPacket::ABORT_DESTROY_BLOCK) { @@ -520,19 +633,128 @@ void PlayerConnection::handleUseItem(shared_ptr packet) if (packet->getFace() == 255) { 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(Tile::tiles[savedItemId], savedItemCount); + } + informClient = true; + } + } + } +#endif } } +skipUseItemOn: informClient = true; } else @@ -601,27 +823,21 @@ 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( new ChatPacket(L"�e" + player->name + L" left the game.") ) ); - if(getWasKicked()) - { - server->getPlayers()->broadcastAll(std::make_shared(player->name, ChatPacket::e_ChatPlayerKickedFromGame)); - } - else - { - server->getPlayers()->broadcastAll(std::make_shared(player->name, ChatPacket::e_ChatPlayerLeftGame)); - } - server->getPlayers()->remove(player); + done = true; LeaveCriticalSection(&done_cs); + + server->getPlayers()->queueDisconnect(player, static_cast(reason), + L"", getWasKicked(), fourKitHandledQuit); } void PlayerConnection::openSecurityGate() @@ -740,8 +956,20 @@ void PlayerConnection::handleChat(shared_ptr 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(formatted)); + } + } +#else wstring formatted = L"<" + player->name + L"> " + message; server->getPlayers()->broadcastAll(shared_ptr(new ChatPacket(formatted))); +#endif chatSpamTickCount += SharedConstants::TICKS_PER_SECOND; if (chatSpamTickCount > SharedConstants::TICKS_PER_SECOND * 10) { @@ -751,6 +979,13 @@ void PlayerConnection::handleChat(shared_ptr packet) void PlayerConnection::handleCommand(const wstring& message) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + std::wstring commandLine = message; + if (FourKitBridge::FireCommandPreprocess(player->entityId, commandLine, commandLine)) + return; + if (FourKitBridge::HandlePlayerCommand(player->entityId, commandLine)) + return; +#endif // 4J - TODO #if 0 server.getCommandDispatcher().performCommand(player, message); @@ -762,6 +997,23 @@ void PlayerConnection::handleAnimate(shared_ptr 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(); } } @@ -872,10 +1124,31 @@ void PlayerConnection::handleInteract(shared_ptr 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(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) { return; @@ -1243,7 +1516,13 @@ void PlayerConnection::handleClientCommand(shared_ptr 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->level->getLevelData()->isHardcore()) { @@ -1254,7 +1533,13 @@ void PlayerConnection::handleClientCommand(shared_ptr 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 } } } @@ -1314,6 +1599,20 @@ void PlayerConnection::handleContainerClick(shared_ptr 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(packet->containerId, packet->uid, false)); + player->containerMenu->setSynched(player, false); + vector> 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 clicked = player->containerMenu->clicked(packet->slotNum, packet->buttonNum, packet->clickType, player); if (ItemInstance::matches(packet->item, clicked)) @@ -1341,6 +1640,15 @@ void PlayerConnection::handleContainerClick(shared_ptr pac // player.containerMenu.broadcastChanges(); } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (fourKitClickResult == 2) + { + vector> 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 } } @@ -1478,10 +1786,31 @@ void PlayerConnection::handleSignUpdate(shared_ptr packet) int y = packet->y; int z = packet->z; shared_ptr ste = dynamic_pointer_cast(te); + + wstring lines[4]; for (int i = 0; i < 4; i++) { - wstring lineText = packet->lines[i].substr(0,15); - ste->SetMessage( i, lineText ); + 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++) + { + ste->SetMessage( i, lines[i] ); } ste->SetVerified(false); ste->setChanged(); @@ -1774,45 +2103,58 @@ void PlayerConnection::handleCustomPayload(shared_ptr custo } else if (CustomPayloadPacket::SET_BEACON_PACKET.compare(customPayloadPacket->identifier) == 0) { - if ( dynamic_cast( player->containerMenu) != nullptr) - { - ByteArrayInputStream bais(customPayloadPacket->data); - DataInputStream input(&bais); - int primary = input.readInt(); - int secondary = input.readInt(); + if (dynamic_cast(player->containerMenu) != nullptr) + { + ByteArrayInputStream bais(customPayloadPacket->data); + DataInputStream input(&bais); + int primary = input.readInt(); + int secondary = input.readInt(); - BeaconMenu *beaconMenu = static_cast(player->containerMenu); - Slot *slot = beaconMenu->getSlot(0); - if (slot->hasItem()) - { - slot->remove(1); - shared_ptr beacon = beaconMenu->getBeacon(); - beacon->setPrimaryPower(primary); - beacon->setSecondaryPower(secondary); - beacon->setChanged(); - } - } + BeaconMenu *beaconMenu = static_cast(player->containerMenu); + Slot *slot = beaconMenu->getSlot(0); + + if (slot != nullptr && slot->hasItem()) + { + shared_ptr beacon = beaconMenu->getBeacon(); + + int prevPrimary = beacon->getPrimaryPower(); + int prevSecondary = beacon->getSecondaryPower(); + + beacon->setPrimaryPower(primary); + beacon->setSecondaryPower(secondary); + + // Only consume the payment item if the powers actually changed + if (beacon->getPrimaryPower() != prevPrimary || + beacon->getSecondaryPower() != prevSecondary) + { + slot->remove(1); + } + + beacon->setChanged(); + } + } } else if (CustomPayloadPacket::SET_ITEM_NAME_PACKET.compare(customPayloadPacket->identifier) == 0) { - AnvilMenu *menu = dynamic_cast( player->containerMenu); - if (menu) - { - if (customPayloadPacket->data.data == nullptr || customPayloadPacket->data.length < 1) - { - menu->setItemName(L""); - } - else - { - ByteArrayInputStream bais(customPayloadPacket->data); - DataInputStream dis(&bais); - wstring name = dis.readUTF(); - if (name.length() <= 30) - { - menu->setItemName(name); - } - } - } + AnvilMenu *menu = dynamic_cast(player->containerMenu); + if (menu) + { + if (customPayloadPacket->data.data == nullptr || customPayloadPacket->data.length < 1) + { + menu->setItemName(L""); + } + else + { + ByteArrayInputStream bais(customPayloadPacket->data); + DataInputStream dis(&bais); + wstring name = dis.readUTF(); + + if (name.length() <= 30) + { + menu->setItemName(name); + } + } + } } } diff --git a/Minecraft.Client/PlayerConnection.h b/Minecraft.Client/PlayerConnection.h index a7218070..0a2d80b9 100644 --- a/Minecraft.Client/PlayerConnection.h +++ b/Minecraft.Client/PlayerConnection.h @@ -30,6 +30,8 @@ private: int aboveGroundTickCount; bool didTick; + bool hasDoneFirstTickFourKit; + int lastLeftClickTick = 0; int lastKeepAliveId; int64_t lastKeepAliveTime; static Random random; diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index 05672b94..c79c0cfe 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -39,7 +39,7 @@ #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "../Minecraft.Server/Access/Access.h" -#include "../Minecraft.Server/Common\StringUtils.h" +#include "../Minecraft.Server/Common/StringUtils.h" #include "../Minecraft.Server/ServerLogger.h" #include "../Minecraft.Server/ServerLogManager.h" #include "../Minecraft.Server/ServerProperties.h" @@ -77,6 +77,8 @@ PlayerList::PlayerList(MinecraftServer *server) int rawMax = server->settings->getInt(L"max-players", 8); maxPlayers = static_cast(Mth::clamp(rawMax, 1, MINECRAFT_NET_MAX_PLAYERS)); doWhiteList = false; + InitializeCriticalSection(&m_playersCS); + InitializeCriticalSection(&m_disconnectCS); InitializeCriticalSection(&m_banCS); InitializeCriticalSection(&m_kickPlayersCS); InitializeCriticalSection(&m_closePlayersCS); @@ -91,11 +93,34 @@ PlayerList::~PlayerList() player->gameMode = nullptr; } + DeleteCriticalSection(&m_playersCS); + DeleteCriticalSection(&m_disconnectCS); DeleteCriticalSection(&m_banCS); DeleteCriticalSection(&m_kickPlayersCS); DeleteCriticalSection(&m_closePlayersCS); } +vector > PlayerList::getPlayersSnapshot() +{ + EnterCriticalSection(&m_playersCS); + vector > snapshot = players; + LeaveCriticalSection(&m_playersCS); + return snapshot; +} + +void PlayerList::queueDisconnect(shared_ptr player, int reason, const wstring& kickMessage, bool wasKicked, bool fourKitHandledQuit) +{ + PendingDisconnect pd; + pd.player = player; + pd.reason = reason; + pd.kickMessage = kickMessage; + pd.wasKicked = wasKicked; + pd.fourKitHandledQuit = fourKitHandledQuit; + EnterCriticalSection(&m_disconnectCS); + m_pendingDisconnects.push_back(pd); + LeaveCriticalSection(&m_disconnectCS); +} + bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr player, shared_ptr packet) { CompoundTag *playerTag = load(player); @@ -531,7 +556,9 @@ void PlayerList::add(shared_ptr player) broadcastAll(std::make_shared(player)); } + EnterCriticalSection(&m_playersCS); players.push_back(player); + LeaveCriticalSection(&m_playersCS); // 4J Added addPlayerToReceiving(player); @@ -548,13 +575,16 @@ void PlayerList::add(shared_ptr player) changeDimension(player, nullptr); level->addEntity(player); - for (size_t i = 0; i < players.size(); i++) { - shared_ptr op = players.at(i); - //player->connection->send(shared_ptr( new PlayerInfoPacket(op->name, true, op->latency) ) ); - if( op->connection->getNetworkPlayer() ) + vector > snapshot = getPlayersSnapshot(); + for (size_t i = 0; i < snapshot.size(); i++) { - player->connection->send(std::make_shared(op)); + shared_ptr op = snapshot.at(i); + //player->connection->send(shared_ptr( new PlayerInfoPacket(op->name, true, op->latency) ) ); + if( op->connection->getNetworkPlayer() ) + { + player->connection->send(std::make_shared(op)); + } } } @@ -580,9 +610,10 @@ void PlayerList::add(shared_ptr player) broadcastAll(std::make_shared(player, xuid, onlineXuid, xp, yp, zp, yRotp, xRotp, yHeadRotp)); // Send all existing players to the new player - for (size_t i = 0; i < players.size(); i++) + vector > snapshot = getPlayersSnapshot(); + for (size_t i = 0; i < snapshot.size(); i++) { - shared_ptr op = players.at(i); + shared_ptr op = snapshot.at(i); if (op != player && op->connection->getNetworkPlayer()) { PlayerUID opXuid = INVALID_XUID; @@ -605,9 +636,10 @@ void PlayerList::add(shared_ptr player) if(level->isAtLeastOnePlayerSleeping()) { shared_ptr firstSleepingPlayer = nullptr; - for (unsigned int i = 0; i < players.size(); i++) + vector > snapshot = getPlayersSnapshot(); + for (unsigned int i = 0; i < snapshot.size(); i++) { - shared_ptr thisPlayer = players[i]; + shared_ptr thisPlayer = snapshot[i]; if(thisPlayer->isSleeping()) { if(firstSleepingPlayer == nullptr) firstSleepingPlayer = thisPlayer; @@ -655,11 +687,13 @@ if (player->riding != nullptr) level->getTracker()->removeEntity(player); level->removeEntity(player); level->getChunkMap()->remove(player); + EnterCriticalSection(&m_playersCS); auto it = find(players.begin(), players.end(), player); if( it != players.end() ) { players.erase(it); } + LeaveCriticalSection(&m_playersCS); // Notify fork clients that this player has left the server so they can // clean up IQNet/Tab list entries. Uses a custom payload channel so the // wire format of existing packets is unchanged (upstream clients simply @@ -781,11 +815,13 @@ shared_ptr PlayerList::respawn(shared_ptr serverPlay } serverPlayer->getLevel()->getChunkMap()->remove(serverPlayer); + EnterCriticalSection(&m_playersCS); auto it = find(players.begin(), players.end(), serverPlayer); if( it != players.end() ) { players.erase(it); } + LeaveCriticalSection(&m_playersCS); server->getLevel(serverPlayer->dimension)->removeEntityImmediately(serverPlayer); Pos *bedPosition = serverPlayer->getRespawnPosition(); @@ -899,7 +935,9 @@ shared_ptr PlayerList::respawn(shared_ptr serverPlay level->getChunkMap()->add(player); level->addEntity(player); + EnterCriticalSection(&m_playersCS); players.push_back(player); + LeaveCriticalSection(&m_playersCS); player->initMenu(); player->setHealth(player->getHealth()); @@ -1135,10 +1173,11 @@ void PlayerList::tick() } // Report cipher completion and open security gate for all completed handshakes + vector > tickSnapshot = getPlayersSnapshot(); for (unsigned char smallId : completed) { // Open the security gate -- flush buffered game packets now that cipher is active - for (auto &p : players) + for (auto &p : tickSnapshot) { if (p == nullptr || p->connection == nullptr) continue; INetworkPlayer *np = p->connection->getNetworkPlayer(); @@ -1168,7 +1207,7 @@ void PlayerList::tick() for (unsigned char smallId : completed) { // Find the player by smallId - for (auto &p : players) + for (auto &p : tickSnapshot) { if (p == nullptr || p->connection == nullptr) continue; INetworkPlayer *np = p->connection->getNetworkPlayer(); @@ -1212,7 +1251,7 @@ void PlayerList::tick() } // Enforce identity token response timeout - for (auto &p : players) + for (auto &p : tickSnapshot) { if (p == nullptr || p->connection == nullptr) continue; int challengeTick = p->connection->getIdentityChallengeTick(); @@ -1244,27 +1283,78 @@ void PlayerList::tick() sendAllPlayerInfoIn = 0; } - if (sendAllPlayerInfoIn < players.size()) { - shared_ptr op = players[sendAllPlayerInfoIn]; + vector > infoSnapshot = getPlayersSnapshot(); + if (sendAllPlayerInfoIn < infoSnapshot.size()) + { + shared_ptr op = infoSnapshot[sendAllPlayerInfoIn]; //broadcastAll(shared_ptr( new PlayerInfoPacket(op->name, true, op->latency) ) ); if( op->connection->getNetworkPlayer() ) { broadcastAll(std::make_shared(op)); } } + } - EnterCriticalSection(&m_closePlayersCS); - while(!m_smallIdsToClose.empty()) + // Drain the pending disconnect queue. disconnect() enqueues here so it + // can release done_cs before the heavy cleanup runs on the tick thread. { - BYTE smallId = m_smallIdsToClose.front(); - m_smallIdsToClose.pop_front(); + std::deque dcCopy; + EnterCriticalSection(&m_disconnectCS); + dcCopy.swap(m_pendingDisconnects); + LeaveCriticalSection(&m_disconnectCS); + + while (!dcCopy.empty()) + { + PendingDisconnect pd = dcCopy.front(); + dcCopy.pop_front(); + + server->getPlayers()->removePlayerFromReceiving(pd.player); + if (pd.player->connection != nullptr) + { + pd.player->connection->send(std::make_shared(static_cast(pd.reason))); + pd.player->connection->connection->sendAndQuit(); + } + + if (!pd.kickMessage.empty()) + { + broadcastAll(std::make_shared(pd.kickMessage)); + } + else if (!pd.fourKitHandledQuit) + { + if (pd.wasKicked) + { + broadcastAll(std::make_shared(pd.player->name, ChatPacket::e_ChatPlayerKickedFromGame)); + } + else + { + broadcastAll(std::make_shared(pd.player->name, ChatPacket::e_ChatPlayerLeftGame)); + } + } + + remove(pd.player); + } + } + + // Drain the close queue: snapshot the deque, then release the CS before + // calling disconnect() which may itself try to acquire other locks. + std::deque closeCopy; + EnterCriticalSection(&m_closePlayersCS); + closeCopy.swap(m_smallIdsToClose); + LeaveCriticalSection(&m_closePlayersCS); + + { + vector > closeSnapshot = getPlayersSnapshot(); + while(!closeCopy.empty()) + { + BYTE smallId = closeCopy.front(); + closeCopy.pop_front(); shared_ptr player = nullptr; - for(unsigned int i = 0; i < players.size(); i++) + for(unsigned int i = 0; i < closeSnapshot.size(); i++) { - shared_ptr p = players.at(i); + shared_ptr p = closeSnapshot.at(i); // 4J Stu - May be being a bit overprotective with all the nullptr checks, but adding late in TU7 so want to be safe if (p != nullptr && p->connection != nullptr && p->connection->connection != nullptr && p->connection->connection->getSocket() != nullptr && p->connection->connection->getSocket()->getSmallId() == smallId ) { @@ -1286,13 +1376,19 @@ void PlayerList::tick() WinsockNetLayer::ClearSocketForSmallId(smallId); #endif } - LeaveCriticalSection(&m_closePlayersCS); + } + std::deque kickCopy; EnterCriticalSection(&m_kickPlayersCS); - while(!m_smallIdsToKick.empty()) + kickCopy.swap(m_smallIdsToKick); + LeaveCriticalSection(&m_kickPlayersCS); + { - BYTE smallId = m_smallIdsToKick.front(); - m_smallIdsToKick.pop_front(); + vector > kickSnapshot = getPlayersSnapshot(); + while(!kickCopy.empty()) + { + BYTE smallId = kickCopy.front(); + kickCopy.pop_front(); INetworkPlayer *selectedPlayer = g_NetworkManager.GetPlayerBySmallId(smallId); if( selectedPlayer != nullptr ) { @@ -1303,9 +1399,9 @@ void PlayerList::tick() // Kick this player from the game shared_ptr player = nullptr; - for(unsigned int i = 0; i < players.size(); i++) + for(unsigned int i = 0; i < kickSnapshot.size(); i++) { - shared_ptr p = players.at(i); + shared_ptr p = kickSnapshot.at(i); PlayerUID playersXuid = p->getOnlineXuid(); if (p != nullptr && ProfileManager.AreXUIDSEqual(playersXuid, xuid ) ) { @@ -1326,7 +1422,7 @@ void PlayerList::tick() } } } - LeaveCriticalSection(&m_kickPlayersCS); + } // Check our receiving players, and if they are dead see if we can replace them for(unsigned int dim = 0; dim < 2; ++dim) @@ -1360,29 +1456,32 @@ void PlayerList::prioritiseTileChanges(int x, int y, int z, int dimension) void PlayerList::broadcastAll(shared_ptr packet) { - for (unsigned int i = 0; i < players.size(); i++) + vector > snapshot = getPlayersSnapshot(); + for (unsigned int i = 0; i < snapshot.size(); i++) { - shared_ptr player = players[i]; + shared_ptr player = snapshot[i]; player->connection->send(packet); } } void PlayerList::broadcastAll(shared_ptr packet, int dimension) { - for (unsigned int i = 0; i < players.size(); i++) + vector > snapshot = getPlayersSnapshot(); + for (unsigned int i = 0; i < snapshot.size(); i++) { - shared_ptr player = players[i]; + shared_ptr player = snapshot[i]; if (player->dimension == dimension) player->connection->send(packet); } } wstring PlayerList::getPlayerNames() { + vector > snapshot = getPlayersSnapshot(); wstring msg; - for (unsigned int i = 0; i < players.size(); i++) + for (unsigned int i = 0; i < snapshot.size(); i++) { if (i > 0) msg += L", "; - msg += players[i]->name; + msg += snapshot[i]->name; } return msg; } @@ -1410,9 +1509,10 @@ bool PlayerList::isOp(shared_ptr player) shared_ptr PlayerList::getPlayer(const wstring& name) { - for (unsigned int i = 0; i < players.size(); i++) + vector > snapshot = getPlayersSnapshot(); + for (unsigned int i = 0; i < snapshot.size(); i++) { - shared_ptr p = players[i]; + shared_ptr p = snapshot[i]; if (p->name == name) // 4J - used to be case insensitive (using equalsIgnoreCase) - imagine we'll be shifting to XUIDs anyway { return p; @@ -1424,9 +1524,10 @@ shared_ptr PlayerList::getPlayer(const wstring& name) // 4J Added shared_ptr PlayerList::getPlayer(PlayerUID uid) { - for (unsigned int i = 0; i < players.size(); i++) + vector > snapshot = getPlayersSnapshot(); + for (unsigned int i = 0; i < snapshot.size(); i++) { - shared_ptr p = players[i]; + shared_ptr p = snapshot[i]; if (p->getXuid() == uid || p->getOnlineXuid() == uid) // 4J - used to be case insensitive (using equalsIgnoreCase) - imagine we'll be shifting to XUIDs anyway { return p; @@ -1437,15 +1538,16 @@ shared_ptr PlayerList::getPlayer(PlayerUID uid) shared_ptr PlayerList::getNearestPlayer(Pos *position, int range) { - if (players.empty()) return nullptr; - if (position == nullptr) return players.at(0); + vector > snapshot = getPlayersSnapshot(); + if (snapshot.empty()) return nullptr; + if (position == nullptr) return snapshot.at(0); shared_ptr current = nullptr; double dist = -1; int rangeSqr = range * range; - for (size_t i = 0; i < players.size(); i++) + for (size_t i = 0; i < snapshot.size(); i++) { - shared_ptr next = players.at(i); + shared_ptr next = snapshot.at(i); double newDist = position->distSqr(next->getCommandSenderWorldPosition()); if ((dist == -1 || newDist < dist) && (range <= 0 || newDist <= rangeSqr)) @@ -1566,9 +1668,10 @@ void PlayerList::broadcast(shared_ptr except, double x, double y, double sentTo.push_back(dynamic_pointer_cast(except)); } - for (unsigned int i = 0; i < players.size(); i++) + vector > snapshot = getPlayersSnapshot(); + for (unsigned int i = 0; i < snapshot.size(); i++) { - shared_ptr p = players[i]; + shared_ptr p = snapshot[i]; if (p == except) continue; if (p->dimension != dimension) continue; @@ -1630,14 +1733,15 @@ void PlayerList::saveAll(ProgressListener *progressListener, bool bDeleteGuestMa if(playerIo) { playerIo->saveAllCachedData(); - for (unsigned int i = 0; i < players.size(); i++) + vector > snapshot = getPlayersSnapshot(); + for (unsigned int i = 0; i < snapshot.size(); i++) { - playerIo->save(players[i]); + playerIo->save(snapshot[i]); //4J Stu - We don't want to save the map data for guests, so when we are sure that the player is gone delete the map - if(bDeleteGuestMaps && players[i]->isGuest()) playerIo->deleteMapFilesForPlayer(players[i]); + if(bDeleteGuestMaps && snapshot[i]->isGuest()) playerIo->deleteMapFilesForPlayer(snapshot[i]); - if(progressListener != nullptr) progressListener->progressStagePercentage((i * 100)/ static_cast(players.size())); + if(progressListener != nullptr) progressListener->progressStagePercentage((i * 100)/ static_cast(snapshot.size())); } playerIo->clearOldPlayerFiles(); playerIo->saveMapIdLookup(); @@ -1794,9 +1898,15 @@ void PlayerList::removePlayerFromReceiving(shared_ptr player, bool } INetworkPlayer *thisPlayer = player->connection->getNetworkPlayer(); + + // Snapshot the players vector to avoid iterator invalidation during concurrent modifications + EnterCriticalSection(&m_playersCS); + vector > playersSnapshot = players; + LeaveCriticalSection(&m_playersCS); + if( thisPlayer && playerRemoved ) { - for(auto& newPlayer : players) + for(auto& newPlayer : playersSnapshot) { INetworkPlayer *otherPlayer = newPlayer->connection->getNetworkPlayer(); @@ -1821,7 +1931,7 @@ void PlayerList::removePlayerFromReceiving(shared_ptr player, bool #endif // 4J Stu - Something went wrong, or possibly the QNet player left before we got here. // Re-check all active players and make sure they have someone on their system to receive all packets - for(auto& newPlayer : players) + for(auto& newPlayer : playersSnapshot) { INetworkPlayer *checkingPlayer = newPlayer->connection->getNetworkPlayer(); diff --git a/Minecraft.Client/PlayerList.h b/Minecraft.Client/PlayerList.h index 2cafe77e..1265ad24 100644 --- a/Minecraft.Client/PlayerList.h +++ b/Minecraft.Client/PlayerList.h @@ -24,11 +24,26 @@ private: // public static Logger logger = Logger.getLogger("Minecraft"); public: vector > players; + CRITICAL_SECTION m_playersCS; // Protects players vector for concurrent access + vector > getPlayersSnapshot(); private: MinecraftServer *server; unsigned int maxPlayers; + // Pending disconnect queue: disconnect() enqueues here, tick() drains it. + // This avoids holding done_cs across PlayerList operations (deadlock fix). + struct PendingDisconnect + { + shared_ptr player; + int reason; + wstring kickMessage; + bool wasKicked; + bool fourKitHandledQuit; + }; + deque m_pendingDisconnects; + CRITICAL_SECTION m_disconnectCS; + // 4J Added vector m_bannedXuids; CRITICAL_SECTION m_banCS; // 4J Added - protects m_bannedXuids for concurrent access @@ -82,6 +97,7 @@ public: void add(shared_ptr player); void move(shared_ptr player); void remove(shared_ptr player); + void queueDisconnect(shared_ptr player, int reason, const wstring& kickMessage, bool wasKicked, bool fourKitHandledQuit); shared_ptr getPlayerForLogin(PendingConnection *pendingConnection, const wstring& userName, PlayerUID xuid, PlayerUID OnlineXuid); shared_ptr respawn(shared_ptr serverPlayer, int targetDimension, bool keepAllPlayerData); void toggleDimension(shared_ptr player, int targetDimension); diff --git a/Minecraft.Client/ServerConnection.cpp b/Minecraft.Client/ServerConnection.cpp index d2738bad..7bf05a9b 100644 --- a/Minecraft.Client/ServerConnection.cpp +++ b/Minecraft.Client/ServerConnection.cpp @@ -11,8 +11,8 @@ #include "../Minecraft.World/net.minecraft.world.level.h" #include "MultiPlayerLevel.h" #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) -#include "..\Minecraft.Server\Security\SecurityConfig.h" -#include "..\Minecraft.Server\ServerLogManager.h" +#include "../Minecraft.Server/Security/SecurityConfig.h" +#include "../Minecraft.Server/ServerLogManager.h" #endif ServerConnection::ServerConnection(MinecraftServer *server) @@ -20,6 +20,7 @@ ServerConnection::ServerConnection(MinecraftServer *server) // 4J - added initialiser connectionCounter = 0; InitializeCriticalSection(&pending_cs); + InitializeCriticalSection(&players_cs); this->server = server; } @@ -27,6 +28,7 @@ ServerConnection::ServerConnection(MinecraftServer *server) ServerConnection::~ServerConnection() { DeleteCriticalSection(&pending_cs); + DeleteCriticalSection(&players_cs); } // 4J - added to handle incoming connections, to replace thread that original used to have @@ -38,7 +40,9 @@ void ServerConnection::NewIncomingSocket(Socket *socket) void ServerConnection::addPlayerConnection(shared_ptr uc) { + EnterCriticalSection(&players_cs); players.push_back(uc); + LeaveCriticalSection(&players_cs); } void ServerConnection::handleConnection(shared_ptr uc) @@ -76,7 +80,9 @@ void ServerConnection::stop() } // Snapshot to avoid iterator invalidation if disconnect modifies the vector. + EnterCriticalSection(&players_cs); std::vector > playerSnapshot = players; + LeaveCriticalSection(&players_cs); for (unsigned int i = 0; i < playerSnapshot.size(); i++) { shared_ptr player = playerSnapshot[i]; @@ -118,26 +124,34 @@ void ServerConnection::tick() } LeaveCriticalSection(&pending_cs); - for (unsigned int i = 0; i < players.size(); i++) { - shared_ptr player = players[i]; - shared_ptr serverPlayer = player->getPlayer(); - if( serverPlayer ) + EnterCriticalSection(&players_cs); + vector< shared_ptr > tempPlayers = players; + LeaveCriticalSection(&players_cs); + + for (unsigned int i = 0; i < tempPlayers.size(); i++) { - serverPlayer->updateFrameTick(); - serverPlayer->doChunkSendingTick(false); + shared_ptr player = tempPlayers[i]; + shared_ptr serverPlayer = player->getPlayer(); + if( serverPlayer ) + { + serverPlayer->updateFrameTick(); + serverPlayer->doChunkSendingTick(false); + } + player->tick(); + if (player->done) + { + EnterCriticalSection(&players_cs); + auto it = find(players.begin(), players.end(), player); + if (it != players.end()) players.erase(it); + LeaveCriticalSection(&players_cs); + } + else + { + player->connection->flush(); + } } - player->tick(); - if (player->done) - { - players.erase(players.begin()+i); - i--; - } - else - { - player->connection->flush(); - } - } + } } @@ -163,7 +177,10 @@ void ServerConnection::handleTextureReceived(const wstring &textureName) { m_pendingTextureRequests.erase(it); } - for (auto& player : players) + EnterCriticalSection(&players_cs); + vector< shared_ptr > tempPlayers = players; + LeaveCriticalSection(&players_cs); + for (auto& player : tempPlayers) { if (!player->done) { @@ -179,7 +196,10 @@ void ServerConnection::handleTextureAndGeometryReceived(const wstring &textureNa { m_pendingTextureRequests.erase(it); } - for (auto& player : players) + EnterCriticalSection(&players_cs); + vector< shared_ptr > tempPlayers = players; + LeaveCriticalSection(&players_cs); + for (auto& player : tempPlayers) { if (!player->done) { @@ -231,4 +251,46 @@ void ServerConnection::handleServerSettingsChanged(shared_ptr > * ServerConnection::getPlayers() { return &players; +} + +vector< shared_ptr > ServerConnection::getPlayersSnapshot() +{ + EnterCriticalSection(&players_cs); + vector< shared_ptr > snapshot = players; + LeaveCriticalSection(&players_cs); + return snapshot; +} + +void ServerConnection::sortPlayersByChunkPriority() +{ + EnterCriticalSection(&players_cs); + if( players.size() ) + { + vector< shared_ptr > playersOrig = players; + players.clear(); + + do + { + int longestTime = 0; + auto playerConnectionBest = playersOrig.begin(); + for( auto it = playersOrig.begin(); it != playersOrig.end(); it++) + { + int thisTime = 0; + INetworkPlayer *np = (*it)->getNetworkPlayer(); + if( np ) + { + thisTime = np->GetTimeSinceLastChunkPacket_ms(); + } + + if( thisTime > longestTime ) + { + playerConnectionBest = it; + longestTime = thisTime; + } + } + players.push_back(*playerConnectionBest); + playersOrig.erase(playerConnectionBest); + } while ( playersOrig.size() > 0 ); + } + LeaveCriticalSection(&players_cs); } \ No newline at end of file diff --git a/Minecraft.Client/ServerConnection.h b/Minecraft.Client/ServerConnection.h index 9ef80973..aa4f20d2 100644 --- a/Minecraft.Client/ServerConnection.h +++ b/Minecraft.Client/ServerConnection.h @@ -20,6 +20,7 @@ private: int connectionCounter; private: CRITICAL_SECTION pending_cs; // 4J added + CRITICAL_SECTION players_cs; // Protects players vector for concurrent access vector< shared_ptr > pending; vector< shared_ptr > players; @@ -47,4 +48,6 @@ public: void handleTextureAndGeometryReceived(const wstring &textureName); void handleServerSettingsChanged(shared_ptr packet); vector< shared_ptr > *getPlayers(); + vector< shared_ptr > getPlayersSnapshot(); + void sortPlayersByChunkPriority(); }; diff --git a/Minecraft.Client/ServerLevel.cpp b/Minecraft.Client/ServerLevel.cpp index e9092ee9..f2a5c75f 100644 --- a/Minecraft.Client/ServerLevel.cpp +++ b/Minecraft.Client/ServerLevel.cpp @@ -39,6 +39,9 @@ #include "../Minecraft.World/ProgressListener.h" #include "PS3/PS3Extras/ShutdownManager.h" #include "PlayerChunkMap.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#endif WeighedTreasureArray ServerLevel::RANDOM_BONUS_ITEMS; @@ -324,6 +327,8 @@ void ServerLevel::updateSleepingPlayerList() for (auto& player : players) { + if (player->fk_sleepingIgnored) + continue; if (!player->isSleeping()) { allPlayersSleeping = false; @@ -368,6 +373,8 @@ bool ServerLevel::allPlayersAreSleeping() // all players are sleeping, but have they slept long enough? for (auto& player : players) { + if (player->fk_sleepingIgnored) + continue; // System.out.println(player->entityId + ": " + player->getSleepTimer()); if (!player->isSleepingLongEnough()) { @@ -507,15 +514,21 @@ void ServerLevel::tickTiles() int val = (randValue >> 2); int x = (val & 15); int z = ((val >> 8) & 15); - int yy = getTopRainBlock(x + xo, z + zo); - if (shouldFreeze(x + xo, yy - 1, z + zo)) - { - setTileAndUpdate(x + xo, yy - 1, z + zo, Tile::ice_Id); - } - if (isRaining() && shouldSnow(x + xo, yy, z + zo)) - { - setTileAndUpdate(x + xo, yy, z + zo, Tile::topSnow_Id); - } + int yy = getTopRainBlock(x + xo, z + zo); + if (shouldFreeze(x + xo, yy - 1, z + zo)) + { + #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (!FourKitBridge::FireBlockForm(dimension->id, x + xo, yy - 1, z + zo, Tile::ice_Id, 0)) + #endif + setTileAndUpdate(x + xo, yy - 1, z + zo, Tile::ice_Id); + } + if (isRaining() && shouldSnow(x + xo, yy, z + zo)) + { + #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (!FourKitBridge::FireBlockForm(dimension->id, x + xo, yy, z + zo, Tile::topSnow_Id, 0)) + #endif + setTileAndUpdate(x + xo, yy, z + zo, Tile::topSnow_Id); + } if (isRaining()) { Biome *b = getBiome(x + xo, z + zo); diff --git a/Minecraft.Client/ServerPlayer.cpp b/Minecraft.Client/ServerPlayer.cpp index 5a3f3797..ead24838 100644 --- a/Minecraft.Client/ServerPlayer.cpp +++ b/Minecraft.Client/ServerPlayer.cpp @@ -33,6 +33,154 @@ #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" + +static std::wstring FormatDeathMessage(const shared_ptr& packet) +{ + if (!packet) return L""; + + std::wstring message; + bool replacePlayer = false; + bool replaceEntitySource = false; + bool replaceItem = false; + // coug chough + // de hättn echt an gscheidern string konverter für de todesmeldungen macha soin + // a globaler helfer waar wahrscheinlich ganz bärig gwen + // waaaah + + switch (packet->m_messageType) + { + case ChatPacket::e_ChatCustom: + return packet->m_stringArgs.size() > 0 ? packet->m_stringArgs[0] : L""; + case ChatPacket::e_ChatDeathInFire: + message = app.GetString(IDS_DEATH_INFIRE); replacePlayer = true; break; + case ChatPacket::e_ChatDeathOnFire: + message = app.GetString(IDS_DEATH_ONFIRE); replacePlayer = true; break; + case ChatPacket::e_ChatDeathLava: + message = app.GetString(IDS_DEATH_LAVA); replacePlayer = true; break; + case ChatPacket::e_ChatDeathInWall: + message = app.GetString(IDS_DEATH_INWALL); replacePlayer = true; break; + case ChatPacket::e_ChatDeathDrown: + message = app.GetString(IDS_DEATH_DROWN); replacePlayer = true; break; + case ChatPacket::e_ChatDeathStarve: + message = app.GetString(IDS_DEATH_STARVE); replacePlayer = true; break; + case ChatPacket::e_ChatDeathCactus: + message = app.GetString(IDS_DEATH_CACTUS); replacePlayer = true; break; + case ChatPacket::e_ChatDeathFall: + message = app.GetString(IDS_DEATH_FALL); replacePlayer = true; break; + case ChatPacket::e_ChatDeathOutOfWorld: + message = app.GetString(IDS_DEATH_OUTOFWORLD); replacePlayer = true; break; + case ChatPacket::e_ChatDeathGeneric: + message = app.GetString(IDS_DEATH_GENERIC); replacePlayer = true; break; + case ChatPacket::e_ChatDeathExplosion: + message = app.GetString(IDS_DEATH_EXPLOSION); replacePlayer = true; break; + case ChatPacket::e_ChatDeathMagic: + message = app.GetString(IDS_DEATH_MAGIC); replacePlayer = true; break; + case ChatPacket::e_ChatDeathWither: + message = app.GetString(IDS_DEATH_WITHER); replacePlayer = true; break; + case ChatPacket::e_ChatDeathDragonBreath: + message = app.GetString(IDS_DEATH_DRAGON_BREATH); replacePlayer = true; break; + case ChatPacket::e_ChatDeathAnvil: + message = app.GetString(IDS_DEATH_FALLING_ANVIL); replacePlayer = true; break; + case ChatPacket::e_ChatDeathFallingBlock: + message = app.GetString(IDS_DEATH_FALLING_TILE); replacePlayer = true; break; + case ChatPacket::e_ChatDeathFellAccidentLadder: + message = app.GetString(IDS_DEATH_FELL_ACCIDENT_LADDER); replacePlayer = true; break; + case ChatPacket::e_ChatDeathFellAccidentVines: + message = app.GetString(IDS_DEATH_FELL_ACCIDENT_VINES); replacePlayer = true; break; + case ChatPacket::e_ChatDeathFellAccidentWater: + message = app.GetString(IDS_DEATH_FELL_ACCIDENT_WATER); replacePlayer = true; break; + case ChatPacket::e_ChatDeathFellAccidentGeneric: + message = app.GetString(IDS_DEATH_FELL_ACCIDENT_GENERIC); replacePlayer = true; break; + case ChatPacket::e_ChatDeathFellKiller: + message = app.GetString(IDS_DEATH_FALL); replacePlayer = true; break; + case ChatPacket::e_ChatDeathMob: + message = app.GetString(IDS_DEATH_MOB); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathPlayer: + message = app.GetString(IDS_DEATH_PLAYER); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathArrow: + message = app.GetString(IDS_DEATH_ARROW); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathFireball: + message = app.GetString(IDS_DEATH_FIREBALL); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathThrown: + message = app.GetString(IDS_DEATH_THROWN); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathIndirectMagic: + message = app.GetString(IDS_DEATH_INDIRECT_MAGIC); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathThorns: + message = app.GetString(IDS_DEATH_THORNS); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathExplosionPlayer: + message = app.GetString(IDS_DEATH_EXPLOSION_PLAYER); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathInFirePlayer: + message = app.GetString(IDS_DEATH_INFIRE_PLAYER); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathOnFirePlayer: + message = app.GetString(IDS_DEATH_ONFIRE_PLAYER); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathLavaPlayer: + message = app.GetString(IDS_DEATH_LAVA_PLAYER); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathDrownPlayer: + message = app.GetString(IDS_DEATH_DROWN_PLAYER); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathCactusPlayer: + message = app.GetString(IDS_DEATH_CACTUS_PLAYER); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathFellAssist: + message = app.GetString(IDS_DEATH_FELL_ASSIST); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathFellFinish: + message = app.GetString(IDS_DEATH_FELL_FINISH); replacePlayer = true; replaceEntitySource = true; break; + case ChatPacket::e_ChatDeathPlayerItem: + message = app.GetString(IDS_DEATH_PLAYER_ITEM); replacePlayer = true; replaceEntitySource = true; replaceItem = true; break; + case ChatPacket::e_ChatDeathArrowItem: + message = app.GetString(IDS_DEATH_ARROW_ITEM); replacePlayer = true; replaceEntitySource = true; replaceItem = true; break; + case ChatPacket::e_ChatDeathFireballItem: + message = app.GetString(IDS_DEATH_FIREBALL_ITEM); replacePlayer = true; replaceEntitySource = true; replaceItem = true; break; + case ChatPacket::e_ChatDeathThrownItem: + message = app.GetString(IDS_DEATH_THROWN_ITEM); replacePlayer = true; replaceEntitySource = true; replaceItem = true; break; + case ChatPacket::e_ChatDeathIndirectMagicItem: + message = app.GetString(IDS_DEATH_INDIRECT_MAGIC_ITEM); replacePlayer = true; replaceEntitySource = true; replaceItem = true; break; + case ChatPacket::e_ChatDeathFellAssistItem: + message = app.GetString(IDS_DEATH_FELL_ASSIST_ITEM); replacePlayer = true; replaceEntitySource = true; replaceItem = true; break; + case ChatPacket::e_ChatDeathFellFinishItem: + message = app.GetString(IDS_DEATH_FELL_FINISH_ITEM); replacePlayer = true; replaceEntitySource = true; replaceItem = true; break; + default: + message = app.GetString(IDS_DEATH_GENERIC); replacePlayer = true; break; + } + + if (replacePlayer) + { + std::wstring playerName = packet->m_stringArgs.size() > 0 ? packet->m_stringArgs[0] : L""; + message = replaceAll(message, L"{*PLAYER*}", playerName); + } + + if (replaceEntitySource) + { + std::wstring sourceName; + if (!packet->m_intArgs.empty() && packet->m_intArgs[0] == eTYPE_SERVERPLAYER) + { + sourceName = packet->m_stringArgs.size() > 1 ? packet->m_stringArgs[1] : L""; + } + else + { + if (packet->m_stringArgs.size() > 1 && !packet->m_stringArgs[1].empty()) + { + sourceName = packet->m_stringArgs[1]; + } + else if (!packet->m_intArgs.empty()) + { + sourceName = app.getEntityName((eINSTANCEOF)packet->m_intArgs[0]); + } + } + message = replaceAll(message, L"{*SOURCE*}", sourceName); + } + + if (replaceItem) + { + std::wstring itemName = packet->m_stringArgs.size() > 2 ? packet->m_stringArgs[2] : L""; + message = replaceAll(message, L"{*ITEM*}", itemName); + } + + return message; +} +#endif + ServerPlayer::ServerPlayer(MinecraftServer *server, Level *level, const wstring& name, ServerPlayerGameMode *gameMode) : Player(level, name) { @@ -567,6 +715,45 @@ shared_ptr ServerPlayer::getCarried(int slot) void ServerPlayer::die(DamageSource *source) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + shared_ptr 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(outDeathMsg)); + + // LCE-Revelations: Hardcore mode enforcement. Donor's FourKit-aware die() + // rewrite dropped this branch; we restore it AFTER FirePlayerDeath so + // plugins still see the death event before the player is banned and + // switched to Adventure mode. + if (level->getLevelData()->isHardcore()) + { + setGameMode(GameType::ADVENTURE); + // Ban this player's XUID and queue disconnect. + // The force-save is triggered inside banPlayerForHardcoreDeath after + // the disconnect is queued, so the client doesn't get stuck on a save + // screen. + server->getPlayers()->banPlayerForHardcoreDeath(this); + } + + if (keepInventory == 0 && !level->getGameRules()->getBoolean(GameRules::RULE_KEEPINVENTORY)) + { + inventory->dropAll(); + } +#else server->getPlayers()->broadcastAll(getCombatTracker()->getDeathMessagePacket()); // 4J Added: Hardcore mode — switch to Adventure mode on death (can look but not break/place blocks) @@ -584,6 +771,7 @@ void ServerPlayer::die(DamageSource *source) { inventory->dropAll(); } +#endif vector *objectives = level->getScoreboard()->findObjectiveFor(ObjectiveCriteria::DEATH_COUNT); if(objectives) @@ -777,6 +965,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()); @@ -784,7 +976,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; } @@ -792,10 +1001,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(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; @@ -825,6 +1072,10 @@ void ServerPlayer::take(shared_ptr e, int orgCount) Player::BedSleepingResult ServerPlayer::startSleepInBed(int x, int y, int z, bool bTestUse) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (!bTestUse && FourKitBridge::FireBedEnter(entityId, dimension, x, y, z)) + return OTHER_PROBLEM; +#endif BedSleepingResult result = Player::startSleepInBed(x, y, z, bTestUse); if (result == OK) { @@ -838,12 +1089,22 @@ Player::BedSleepingResult ServerPlayer::startSleepInBed(int x, int y, int z, boo void ServerPlayer::stopSleepInBed(bool forcefulWakeUp, bool updateLevelList, bool saveRespawnPoint) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int bedX = bedPosition ? bedPosition->x : 0; + int bedY = bedPosition ? bedPosition->y : 0; + int bedZ = bedPosition ? bedPosition->z : 0; + bool wasSleeping = isSleeping(); +#endif if (isSleeping()) { getLevel()->getTracker()->broadcastAndSend(shared_from_this(), std::make_shared(shared_from_this(), AnimatePacket::WAKE_UP)); } Player::stopSleepInBed(forcefulWakeUp, updateLevelList, saveRespawnPoint); if (connection != nullptr) connection->teleport(x, y, z, yRot, xRot); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (wasSleeping) + FourKitBridge::FireBedLeave(entityId, dimension, bedX, bedY, bedZ); +#endif } void ServerPlayer::ride(shared_ptr e) @@ -886,10 +1147,18 @@ bool ServerPlayer::startCrafting(int x, int y, int z) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::WORKBENCH, L"", 9, false)); + refreshContainer(containerMenu); } else { @@ -904,20 +1173,36 @@ bool ServerPlayer::openFireworks(int x, int y, int z) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::FIREWORKS, L"", 9, false)); + refreshContainer(containerMenu); } else if(dynamic_cast(containerMenu) != nullptr) { closeContainer(); nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::FIREWORKS, L"", 9, false)); + refreshContainer(containerMenu); } else { @@ -932,10 +1217,18 @@ bool ServerPlayer::startEnchanting(int x, int y, int z, const wstring &name) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::ENCHANTMENT, name.empty() ? L"" : name, 9, !name.empty())); + refreshContainer(containerMenu); } else { @@ -950,10 +1243,18 @@ bool ServerPlayer::startRepairing(int x, int y, int z) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(containerCounter, ContainerOpenPacket::REPAIR_TABLE, L"", 9, false)); containerMenu = new AnvilMenu(inventory, level, x, y, z, dynamic_pointer_cast(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(containerCounter, ContainerOpenPacket::REPAIR_TABLE, L"", 9, false)); + refreshContainer(containerMenu); } else { @@ -973,11 +1274,18 @@ bool ServerPlayer::openContainer(shared_ptr container) int containerType = container->getContainerType(); assert(containerType >= 0); - connection->send(std::make_shared(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(containerCounter, containerType, container->getCustomName(), container->getContainerSize(), container->hasCustomName())); + refreshContainer(containerMenu); } else { @@ -992,10 +1300,18 @@ bool ServerPlayer::openHopper(shared_ptr container) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::HOPPER, container->getCustomName(), container->getContainerSize(), container->hasCustomName())); + refreshContainer(containerMenu); } else { @@ -1010,10 +1326,18 @@ bool ServerPlayer::openHopper(shared_ptr container) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::HOPPER, container->getCustomName(), container->getContainerSize(), container->hasCustomName())); + refreshContainer(containerMenu); } else { @@ -1028,10 +1352,18 @@ bool ServerPlayer::openFurnace(shared_ptr furnace) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::FURNACE, furnace->getCustomName(), furnace->getContainerSize(), furnace->hasCustomName())); + refreshContainer(containerMenu); } else { @@ -1046,10 +1378,18 @@ bool ServerPlayer::openTrap(shared_ptr trap) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, trap->GetType() == eTYPE_DROPPERTILEENTITY ? ContainerOpenPacket::DROPPER : ContainerOpenPacket::TRAP, trap->getCustomName(), trap->getContainerSize(), trap->hasCustomName())); + refreshContainer(containerMenu); } else { @@ -1064,10 +1404,18 @@ bool ServerPlayer::openBrewingStand(shared_ptr brewingSt if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::BREWING_STAND, brewingStand->getCustomName(), brewingStand->getContainerSize(), brewingStand->hasCustomName())); + refreshContainer(containerMenu); } else { @@ -1082,10 +1430,20 @@ bool ServerPlayer::openBeacon(shared_ptr beacon) if(containerMenu == inventoryMenu) { nextContainerCounter(); - connection->send(std::make_shared(containerCounter, ContainerOpenPacket::BEACON, beacon->getCustomName(), beacon->getContainerSize(), beacon->hasCustomName())); containerMenu = new BeaconMenu(inventory, beacon); containerMenu->containerId = containerCounter; +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireInventoryOpen(entityId, ContainerOpenPacket::BEACON, beacon->getCustomName(), beacon->getContainerSize())) + { + doCloseContainer(); + return true; + } +#endif + // Send the open packet BEFORE addSlotListener so the client has the + // menu ready when the beacon data (levels, powers) arrives. + connection->send(std::make_shared(containerCounter, ContainerOpenPacket::BEACON, beacon->getCustomName(), beacon->getContainerSize(), beacon->hasCustomName())); containerMenu->addSlotListener(this); + refreshContainer(containerMenu); } else { @@ -1105,7 +1463,15 @@ bool ServerPlayer::openTrading(shared_ptr traderTarget, const wstring containerMenu->addSlotListener(this); shared_ptr container = static_cast(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(containerCounter, ContainerOpenPacket::TRADER_NPC, name.empty() ? L"" : name, container->getContainerSize(), !name.empty())); + refreshContainer(containerMenu); MerchantRecipeList *offers = traderTarget->getOffers(dynamic_pointer_cast(shared_from_this())); if (offers != nullptr) @@ -1135,10 +1501,18 @@ bool ServerPlayer::openHorseInventory(shared_ptr horse, shared_ptr< closeContainer(); } nextContainerCounter(); - connection->send(std::make_shared(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(containerCounter, ContainerOpenPacket::HORSE, horse->getCustomName(), container->getContainerSize(), container->hasCustomName(), horse->entityId)); + refreshContainer(containerMenu); return true; } @@ -1621,9 +1995,9 @@ bool ServerPlayer::hasPermission(EGameCommand command) // // // 4J - Don't need // //if (server.isSingleplayer() && server.getSingleplayerName().equals(name)) -/// //{ +// //{ // // server.setDifficulty(packet.getDifficulty()); -/// //} +// //} //} int ServerPlayer::getViewDistance() diff --git a/Minecraft.Client/ServerPlayerGameMode.cpp b/Minecraft.Client/ServerPlayerGameMode.cpp index 2416dd82..4e01d56a 100644 --- a/Minecraft.Client/ServerPlayerGameMode.cpp +++ b/Minecraft.Client/ServerPlayerGameMode.cpp @@ -10,9 +10,18 @@ #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 @@ -171,7 +180,7 @@ void ServerPlayerGameMode::startDestroyBlock(int x, int y, int z, int face) } } -bool ServerPlayerGameMode::stopDestroyBlock(int x, int y, int z) +void ServerPlayerGameMode::stopDestroyBlock(int x, int y, int z) { if (x == xDestroyBlock && y == yDestroyBlock && z == zDestroyBlock) { @@ -187,7 +196,6 @@ bool ServerPlayerGameMode::stopDestroyBlock(int x, int y, int z) isDestroyingBlock = false; level->destroyTileProgress(player->entityId, x, y, z, -1); destroyBlock(x, y, z); - return true; } else if (!hasDelayedDestroy) { @@ -198,11 +206,9 @@ bool ServerPlayerGameMode::stopDestroyBlock(int x, int y, int z) delayedDestroyY = y; delayedDestroyZ = z; delayedTickStart = destroyProgressStart; - return true; } } } - return false; } void ServerPlayerGameMode::abortDestroyBlock(int x, int y, int z) @@ -249,7 +255,43 @@ 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)) + { + // (SYLV)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) + { + player->connection->send(std::make_shared(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. @@ -307,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(level, x + .5, y + .5, z + .5, xpDrop)); + } + } +#endif } } return changed; diff --git a/Minecraft.Client/ServerPlayerGameMode.h b/Minecraft.Client/ServerPlayerGameMode.h index 6b18debc..509e1676 100644 --- a/Minecraft.Client/ServerPlayerGameMode.h +++ b/Minecraft.Client/ServerPlayerGameMode.h @@ -45,7 +45,7 @@ public: void tick(); void startDestroyBlock(int x, int y, int z, int face); - bool stopDestroyBlock(int x, int y, int z); + void stopDestroyBlock(int x, int y, int z); void abortDestroyBlock(int x, int y, int z); private: diff --git a/Minecraft.Client/TeleportCommand.cpp b/Minecraft.Client/TeleportCommand.cpp index d9575da1..ebe42efc 100644 --- a/Minecraft.Client/TeleportCommand.cpp +++ b/Minecraft.Client/TeleportCommand.cpp @@ -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 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()); diff --git a/Minecraft.Client/VillagerZombieModel.cpp b/Minecraft.Client/VillagerZombieModel.cpp index 2eae8afb..b8a607f3 100644 --- a/Minecraft.Client/VillagerZombieModel.cpp +++ b/Minecraft.Client/VillagerZombieModel.cpp @@ -1,7 +1,7 @@ #include "stdafx.h" #include "ModelPart.h" #include "VillagerZombieModel.h" -#include "..\Minecraft.World\Mth.h" +#include "../Minecraft.World/Mth.h" void VillagerZombieModel::_init(float g, float yOffset, bool isArmor) { diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index e2971436..e763e3d5 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -22,7 +22,7 @@ #include "../../Minecraft.h" #include -#pragma comment(lib, "Dnsapi.lib") +#pragma comment(lib, "dnsapi.lib") #include "../4JLibs/inc/4J_Profile.h" #include diff --git a/Minecraft.Server.FourKit/.editorconfig b/Minecraft.Server.FourKit/.editorconfig new file mode 100644 index 00000000..d7a83b70 --- /dev/null +++ b/Minecraft.Server.FourKit/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# IDE1006: Naming Styles +dotnet_diagnostic.IDE1006.severity = suggestion diff --git a/Minecraft.Server.FourKit/Block/Action.cs b/Minecraft.Server.FourKit/Block/Action.cs new file mode 100644 index 00000000..00e113d0 --- /dev/null +++ b/Minecraft.Server.FourKit/Block/Action.cs @@ -0,0 +1,26 @@ +namespace Minecraft.Server.FourKit.Block; + +/// +/// Represents the action type for a player interaction. +/// +public enum Action +{ + /// Left-clicking the air. + LEFT_CLICK_AIR = 0, + + /// Left-clicking a block. + LEFT_CLICK_BLOCK = 1, + + /// Right-clicking the air. + RIGHT_CLICK_AIR = 2, + + /// Right-clicking a block. + RIGHT_CLICK_BLOCK = 3, + + /// + /// Stepping onto or into a block (Ass-pressure). + /// Examples: Jumping on soil, Standing on pressure plate, + /// Triggering redstone ore, Triggering tripwire. + /// + PHYSICAL = 4 +} diff --git a/Minecraft.Server.FourKit/Block/Block.cs b/Minecraft.Server.FourKit/Block/Block.cs new file mode 100644 index 00000000..1a31081a --- /dev/null +++ b/Minecraft.Server.FourKit/Block/Block.cs @@ -0,0 +1,166 @@ +namespace Minecraft.Server.FourKit.Block; + +/// +/// Represents a block. This is a live object, and only one Block may exist for +/// any given location in a world. +/// +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; + } + + /// + /// Gets the Location of the block. + /// + /// Location of the block. + public Location getLocation() + { + return new Location(_world, _x, _y, _z, 0f, 0f); + } + + /// + /// Gets the type of this block. + /// + /// Block type. + public Material getType() + { + int id = getTypeId(); + return Enum.IsDefined(typeof(Material), id) ? (Material)id : Material.AIR; + } + + /// + /// Gets the type ID of this block. + /// + /// Block type ID. + public int getTypeId() + { + if (NativeBridge.GetTileId != null) + return NativeBridge.GetTileId(_world.getDimensionId(), _x, _y, _z); + return 0; + } + + /// + /// Gets the world which contains this Block. + /// + /// World containing this block. + public World getWorld() => _world; + + /// + /// Gets the x-coordinate of this block. + /// + /// X-coordinate. + public int getX() => _x; + + /// + /// Gets the y-coordinate of this block. + /// + /// Y-coordinate. + public int getY() => _y; + + /// + /// Gets the z-coordinate of this block. + /// + /// Z-coordinate. + public int getZ() => _z; + + /// + /// Sets the type of this block. + /// + /// Material to change this block to. + public void setType(Material type) + { + setTypeId((int)type); + } + + /// + /// Sets the type ID of this block. + /// + /// Type ID to change this block to. + /// Whether the change was successful. + public bool setTypeId(int type) + { + NativeBridge.SetTile?.Invoke(_world.getDimensionId(), _x, _y, _z, type, 0); + return true; + } + + /// + /// Gets the metadata value for this block. + /// + /// Block specific metadata. + public byte getData() + { + return (byte)NativeBridge.GetTileData(_world.getDimensionId(), _x, _y, _z); + } + + /// + /// Sets the metadata value for this block. + /// + /// New block specific metadata. + public void setData(byte data) + { + NativeBridge.SetTileData?.Invoke(_world.getDimensionId(), _x, _y, _z, data); + } + + /// + /// Breaks the block and spawns items as if a player had digged it. + /// + /// true if the block was destroyed. + public bool breakNaturally() + { + if (NativeBridge.BreakBlock != null) + return NativeBridge.BreakBlock(_world.getDimensionId(), _x, _y, _z) != 0; + return false; + } + + /// + /// Gets the block at the given offsets + /// + /// X offset + /// Y offset + /// Z offset + /// Block at the given offsets + public Block getRelative(int modX, int modY, int modZ) + { + return getWorld().getBlockAt(getX() + modX, getY() + modY, getZ() + modZ); + } + + /// + /// Gets the block at the given face + /// This method is equal to getRelative(face, 1) + /// + /// BlockFace to get relative to + /// Block at the given face + public Block getRelative(BlockFace face) + { + return getRelative(face, 1); + } + + /// + /// Gets the block at the given distance of the given face + /// For example, the following method places water at 100,102,100; two + /// blocks above 100,100,100. + /// + /// Block block = world.getBlockAt(100, 100, 100); + /// Block shower = block.getRelative(BlockFace.UP, 2); + /// shower.setType(Material.WATER); + /// + /// + /// BlockFace to get relative to + /// Distance to get relative to + /// Block at the given distance of the given face + public Block getRelative(BlockFace face, int distance) + { + return getRelative(face.getModX() * distance, face.getModY() * distance, face.getModZ() * distance); + } + +} diff --git a/Minecraft.Server.FourKit/Block/BlockFace.cs b/Minecraft.Server.FourKit/Block/BlockFace.cs new file mode 100644 index 00000000..0e67b8be --- /dev/null +++ b/Minecraft.Server.FourKit/Block/BlockFace.cs @@ -0,0 +1,118 @@ +namespace Minecraft.Server.FourKit.Block; + +/// +/// Represents the face of a block. +/// +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 +{ + /// + /// Get the amount of X-coordinates to modify to get the represented block. + /// + /// The block face. + /// Amount of X-coordinates to modify. + 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 + }; + + /// + /// Get the amount of Y-coordinates to modify to get the represented block. + /// + /// The block face. + /// Amount of Y-coordinates to modify. + public static int getModY(this BlockFace face) => face switch + { + BlockFace.UP => 1, + BlockFace.DOWN => -1, + _ => 0 + }; + + /// + /// Get the amount of Z-coordinates to modify to get the represented block. + /// + /// The block face. + /// Amount of Z-coordinates to modify. + 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 + }; + + /// + /// Gets the opposite face of this block face. + /// + /// The block face. + /// The opposite block face. + 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 + }; +} diff --git a/Minecraft.Server.FourKit/Block/BlockState.cs b/Minecraft.Server.FourKit/Block/BlockState.cs new file mode 100644 index 00000000..343abbb1 --- /dev/null +++ b/Minecraft.Server.FourKit/Block/BlockState.cs @@ -0,0 +1,197 @@ +namespace Minecraft.Server.FourKit.Block; + +/// +/// Represents a captured state of a block, which will not change +/// automatically. +/// +/// Unlike , which only one object can exist per +/// coordinate, BlockState can exist multiple times for any given Block. +/// Note that another plugin may change the state of the block and you will +/// not know, or they may change the block to another type entirely, causing +/// your BlockState to become invalid. +/// +public class BlockState +{ + private readonly World _world; + private readonly int _x; + private readonly int _y; + private readonly int _z; + private int _typeId; + private int _data; + + internal BlockState(World world, int x, int y, int z, int typeId, int data) + { + _world = world; + _x = x; + _y = y; + _z = z; + _typeId = typeId; + _data = data; + } + + /// + /// Gets the block represented by this BlockState. + /// + /// Block that this BlockState represents. + public Block getBlock() + { + return new Block(_world, _x, _y, _z); + } + + /// + /// Gets the metadata for this block. + /// + /// Block specific metadata. + public int getData() => _data; + + /// + /// Sets the metadata for this block. + /// + /// New block specific metadata. + public void setData(int data) + { + _data = data; + } + + /// + /// Gets the type of this block. + /// + /// Block type. + public Material getType() + { + return Enum.IsDefined(typeof(Material), _typeId) ? (Material)_typeId : Material.AIR; + } + + /// + /// Gets the type ID of this block. + /// + /// Block type ID. + public int getTypeId() => _typeId; + + /// + /// Gets the world which contains this Block. + /// + /// World containing this block. + public World getWorld() => _world; + + /// + /// Gets the x-coordinate of this block. + /// + /// X-coordinate. + public int getX() => _x; + + /// + /// Gets the y-coordinate of this block. + /// + /// Y-coordinate. + public int getY() => _y; + + /// + /// Gets the z-coordinate of this block. + /// + /// Z-coordinate. + public int getZ() => _z; + + /// + /// Gets the location of this block. + /// + /// Location. + public Location getLocation() + { + return new Location(_world, _x, _y, _z, 0f, 0f); + } + + /// + /// Stores the location of this block in the provided Location object. + /// If the provided Location is null this method does nothing and returns + /// null. + /// + /// The location object to store in. + /// The Location object provided or null. + public Location? getLocation(Location? loc) + { + if (loc == null) return null; + loc.X = _x; + loc.Y = _y; + loc.Z = _z; + loc.LocationWorld = _world; + return loc; + } + + /// + /// Sets the type of this block. + /// + /// Material to change this block to. + public void setType(Material type) + { + _typeId = (int)type; + } + + /// + /// Sets the type ID of this block. + /// + /// Type ID to change this block to. + /// Whether the change was accepted. + public bool setTypeId(int type) + { + _typeId = type; + return true; + } + + /// + /// Attempts to update the block represented by this state, setting it to + /// the new values as defined by this state. + /// This has the same effect as calling update(false). + /// + /// true if the update was successful, otherwise + /// false. + public bool update() + { + return update(false); + } + + /// + /// Attempts to update the block represented by this state, setting it to + /// the new values as defined by this state. + /// This has the same effect as calling + /// update(force, true). + /// + /// true to forcefully set the state. + /// true if the update was successful, otherwise + /// false. + public bool update(bool force) + { + return update(force, true); + } + + /// + /// Attempts to update the block represented by this state, setting it to + /// the new values as defined by this state. + /// Unless is true, this will not modify the + /// state of a block if it is no longer the same type as it was when this + /// state was taken. It will return false in this eventuality. + /// If is true, it will set the type of the + /// block to match the new state, set the state data and then return + /// true. + /// If is true, it will trigger a + /// physics update on surrounding blocks which could cause them to update + /// or disappear. + /// + /// true to forcefully set the state. + /// false to cancel updating physics on + /// surrounding blocks. + /// true if the update was successful, otherwise + /// false. + public bool update(bool force, bool applyPhysics) + { + if (NativeBridge.GetTileId == null || NativeBridge.SetTile == null) + return false; + + int currentType = NativeBridge.GetTileId(_world.getDimensionId(), _x, _y, _z); + if (!force && currentType != _typeId) + return false; + + NativeBridge.SetTile(_world.getDimensionId(), _x, _y, _z, _typeId, _data); + return true; + } +} diff --git a/Minecraft.Server.FourKit/CMakeLists.txt b/Minecraft.Server.FourKit/CMakeLists.txt new file mode 100644 index 00000000..8ef0698b --- /dev/null +++ b/Minecraft.Server.FourKit/CMakeLists.txt @@ -0,0 +1,117 @@ +set(FOURKIT_PROJECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(FOURKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/bin/$") +set(FOURKIT_CSPROJ "${FOURKIT_PROJECT_DIR}/Minecraft.Server.FourKit.csproj") + +# --- +# .NET 10 SDK check +# --- +# Fail configure with a clear message if .NET 10 SDK isn't installed. Otherwise +# the failure happens deep inside the dotnet publish step with a much less +# obvious error. The version pin in /global.json governs which 10.x SDK is +# selected at build time; this check just confirms one is installed at all. +find_program(DOTNET_EXECUTABLE NAMES dotnet + HINTS "$ENV{ProgramFiles}/dotnet" "$ENV{ProgramW6432}/dotnet" + DOC ".NET CLI executable") +if(NOT DOTNET_EXECUTABLE) + message(FATAL_ERROR + "FourKit requires the .NET 10 SDK but the 'dotnet' command was not found on PATH.\n" + "Install it from https://dotnet.microsoft.com/download/dotnet/10.0 (x64 SDK)\n" + "and reconfigure CMake.") +endif() + +execute_process( + COMMAND "${DOTNET_EXECUTABLE}" --list-sdks + OUTPUT_VARIABLE FOURKIT_DOTNET_SDKS + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE +) +if(NOT FOURKIT_DOTNET_SDKS MATCHES "(^|\n)10\\.[0-9]+\\.[0-9]+") + if(FOURKIT_DOTNET_SDKS STREQUAL "") + set(_sdk_list " (none installed)") + else() + set(_sdk_list "${FOURKIT_DOTNET_SDKS}") + endif() + message(FATAL_ERROR + "FourKit requires a .NET 10 SDK but none was detected.\n" + "Install it from https://dotnet.microsoft.com/download/dotnet/10.0 (x64 SDK)\n" + "and reconfigure CMake.\n" + "\n" + "Currently installed .NET SDKs:\n" + "${_sdk_list}") +endif() +message(STATUS "FourKit: .NET CLI at ${DOTNET_EXECUTABLE}") + +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 "$,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}/") + +# Self-contained publish: bundles .NET 10 runtime so end users need nothing pre-installed. +# The publish AND the staging copy both live on this target so they re-run together +# whenever any FourKit C# source changes, even on incremental builds where the +# Minecraft.Server.FourKit C++ exe does not re-link. +# +# Target name note: the CMake target is "Minecraft.Server.FourKit.Managed" so it +# does not collide with the C++ executable target named "Minecraft.Server.FourKit". +# The .csproj file name, the assembly name, and the C# namespace all stay +# Minecraft.Server.FourKit because those are the donor's identifiers. +add_custom_target(Minecraft.Server.FourKit.Managed ALL + COMMAND dotnet publish "${FOURKIT_CSPROJ}" + --configuration "${DOTNET_CONFIG}" + --runtime win-x64 + --self-contained true + --output "${FOURKIT_OUTPUT_DIR}" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${FOURKIT_OUTPUT_DIR}" + "$/runtime" + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/plugins" + WORKING_DIRECTORY "${FOURKIT_PROJECT_DIR}" + SOURCES ${FOURKIT_SOURCES} + COMMENT "dotnet publish Minecraft.Server.FourKit + stage runtime/plugins next to server" + VERBATIM +) + +set_target_properties(Minecraft.Server.FourKit.Managed PROPERTIES + OUTPUT_NAME "Minecraft.Server.FourKit.Managed" +) + +# --------------------------------------------------------------------------- +# FourKit-enabled C++ server executable. +# +# Built from the same shared source list as the vanilla Minecraft.Server, +# plus the seven FourKit native bridge files. The MINECRAFT_SERVER_FOURKIT_BUILD +# preprocessor define switches FourKitBridge.h from inline no-op stubs to its +# real declarations, and the FourKitRuntime.cpp / FourKitBridge.cpp / +# FourKitNatives.cpp / FourKitMappers.cpp impls link in. +# +# This target is defined in the Minecraft.Server.FourKit/ source dir (not +# Minecraft.Server/) so the Visual Studio generator emits its .vcxproj files +# into build//Minecraft.Server.FourKit/, keeping the variant identity +# consistent across source dir, project file, runtime output dir, and Docker +# image base. +# --------------------------------------------------------------------------- +include("${CMAKE_SOURCE_DIR}/Minecraft.Server/cmake/sources/Common.cmake") +include("${CMAKE_SOURCE_DIR}/cmake/CommonSources.cmake") +include("${CMAKE_SOURCE_DIR}/cmake/ServerTarget.cmake") + +set(MINECRAFT_SERVER_FOURKIT_SOURCES + ${MINECRAFT_SERVER_COMMON} + ${SOURCES_COMMON} + ${_MINECRAFT_SERVER_COMMON_SERVER_FOURKIT} +) + +add_executable(Minecraft.Server.FourKit ${MINECRAFT_SERVER_FOURKIT_SOURCES}) +configure_lce_server_target(Minecraft.Server.FourKit) +target_compile_definitions(Minecraft.Server.FourKit PRIVATE MINECRAFT_SERVER_FOURKIT_BUILD) +add_dependencies(Minecraft.Server.FourKit Minecraft.Server.FourKit.Managed) diff --git a/Minecraft.Server.FourKit/Command/Command.cs b/Minecraft.Server.FourKit/Command/Command.cs new file mode 100644 index 00000000..68da4546 --- /dev/null +++ b/Minecraft.Server.FourKit/Command/Command.cs @@ -0,0 +1,110 @@ +namespace Minecraft.Server.FourKit.Command; + +/// +/// Represents a Command, which executes various tasks upon user input. +/// +public abstract class Command +{ + private string _name; + private string _description; + private string _usage; + private List _aliases; + + /// + /// Creates a new command with the given name and no aliases. + /// + /// Name of this command. + protected Command(string name) + { + _name = name; + _description = string.Empty; + _usage = "/" + name; + _aliases = new List(); + } + + /// + /// Creates a new command with the given name, description, and aliases. + /// + /// Name of this command. + /// A brief description of this command. + /// A list of aliases for this command. + protected Command(string name, string description, List aliases) + { + _name = name; + _description = description ?? string.Empty; + _usage = "/" + name; + _aliases = aliases ?? new List(); + } + + /// + /// Executes the command, returning its success. + /// + /// Source of the command. + /// Alias of the command which was used. + /// Passed command arguments. + /// true if the command was successful, otherwise false. + public abstract bool execute(CommandSender sender, string commandLabel, string[] args); + + /// + /// Returns a list of active aliases of this command. + /// + /// List of aliases. + public List getAliases() => new(_aliases); + + /// + /// Gets a brief description of this command. + /// + /// Description of this command. + public string getDescription() => _description; + + /// + /// Returns the current label for this command. + /// + /// Current label. + public string getLabel() => _name; + + /// + /// Returns the name of this command. + /// + /// Name of this command. + public string getName() => _name; + + /// + /// Gets an example usage of this command. + /// + /// Usage string. + public string getUsage() => _usage; + + /// + /// Sets the list of aliases to request on registration for this command. + /// + /// Aliases to register. + /// This command. + public Command setAliases(List aliases) + { + _aliases = aliases ?? new List(); + return this; + } + + /// + /// Sets a brief description of this command. + /// + /// New command description. + /// This command. + public Command setDescription(string description) + { + _description = description ?? string.Empty; + return this; + } + + /// + /// Sets the example usage of this command. + /// + /// New example usage. + /// This command. + public Command setUsage(string usage) + { + _usage = usage ?? string.Empty; + return this; + } +} diff --git a/Minecraft.Server.FourKit/Command/CommandExecutor.cs b/Minecraft.Server.FourKit/Command/CommandExecutor.cs new file mode 100644 index 00000000..18631364 --- /dev/null +++ b/Minecraft.Server.FourKit/Command/CommandExecutor.cs @@ -0,0 +1,17 @@ +namespace Minecraft.Server.FourKit.Command; + +/// +/// Represents a class which contains a single method for executing commands. +/// +public interface CommandExecutor +{ + /// + /// Executes the given command, returning its success. + /// + /// Source of the command. + /// Command which was executed. + /// Alias of the command which was used. + /// Passed command arguments. + /// true if a valid command, otherwise false. + bool onCommand(CommandSender sender, Command command, string label, string[] args); +} diff --git a/Minecraft.Server.FourKit/Command/CommandSender.cs b/Minecraft.Server.FourKit/Command/CommandSender.cs new file mode 100644 index 00000000..32aa56a7 --- /dev/null +++ b/Minecraft.Server.FourKit/Command/CommandSender.cs @@ -0,0 +1,25 @@ +namespace Minecraft.Server.FourKit.Command; + +/// +/// Represents something that can send commands and receive messages. +/// +public interface CommandSender +{ + /// + /// Sends this sender a message. + /// + /// Message to be displayed. + void sendMessage(string message); + + /// + /// Sends this sender multiple messages. + /// + /// An array of messages to be displayed. + void sendMessage(string[] messages); + + /// + /// Gets the name of this command sender. + /// + /// Name of the sender. + string getName(); +} diff --git a/Minecraft.Server.FourKit/Command/ConsoleCommandSender.cs b/Minecraft.Server.FourKit/Command/ConsoleCommandSender.cs new file mode 100644 index 00000000..76f1a2ad --- /dev/null +++ b/Minecraft.Server.FourKit/Command/ConsoleCommandSender.cs @@ -0,0 +1,27 @@ +namespace Minecraft.Server.FourKit.Command; + +/// +/// Represents the server console as a command sender. +/// +public class ConsoleCommandSender : CommandSender +{ + internal static readonly ConsoleCommandSender Instance = new(); + + private ConsoleCommandSender() { } + + /// + public void sendMessage(string message) + { + ServerLog.Info("console", message); + } + + /// + public void sendMessage(string[] messages) + { + foreach (var msg in messages) + sendMessage(msg); + } + + /// + public string getName() => "CONSOLE"; +} diff --git a/Minecraft.Server.FourKit/Command/PluginCommand.cs b/Minecraft.Server.FourKit/Command/PluginCommand.cs new file mode 100644 index 00000000..94621863 --- /dev/null +++ b/Minecraft.Server.FourKit/Command/PluginCommand.cs @@ -0,0 +1,44 @@ +namespace Minecraft.Server.FourKit.Command; + +/// +/// Represents a belonging to a plugin. +/// +public class PluginCommand : Command +{ + private CommandExecutor? _executor; + + // should this remain internal? + /// + /// Creates a new plugin command with the given name. + /// Use to obtain instances. + /// + /// Name of this command. + internal PluginCommand(string name) : base(name) + { + } + + /// + public override bool execute(CommandSender sender, string commandLabel, string[] args) + { + if (_executor != null) + return _executor.onCommand(sender, this, commandLabel, args); + return false; + } + + /// + /// Gets the associated with this command. + /// + /// The command executor, or null. + public CommandExecutor? getExecutor() => _executor; + + /// + /// Sets the to run when the command is dispatched. + /// + /// New executor to set. + /// true if the executor was set. + public bool setExecutor(CommandExecutor executor) + { + _executor = executor; + return true; + } +} diff --git a/Minecraft.Server.FourKit/Enchantments/AquaAffinityEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/AquaAffinityEnchantment.cs new file mode 100644 index 00000000..970ea0df --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/AquaAffinityEnchantment.cs @@ -0,0 +1,26 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class AquaAffinityEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.LEATHER_HELMET, Material.CHAINMAIL_HELMET, Material.GOLD_HELMET, Material.IRON_HELMET, Material.DIAMOND_HELMET, + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ARMOR_HEAD; + + public override EnchantmentType getEnchantType() => EnchantmentType.WATER_WORKER; + + public override int getMaxLevel() => 1; + + public override string getName() => "aquaaffinity"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/BaneOfArthopodsEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/BaneOfArthopodsEnchantment.cs new file mode 100644 index 00000000..1d5654e3 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/BaneOfArthopodsEnchantment.cs @@ -0,0 +1,31 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +//name could be changed +public class BaneOfArthropodsEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.GOLD_SWORD, Material.DIAMOND_SWORD, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.DAMAGE_ALL, + EnchantmentType.DAMAGE_UNDEAD, + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.WEAPON; + + public override EnchantmentType getEnchantType() => EnchantmentType.DAMAGE_ARTHOPODS; + + public override int getMaxLevel() => 5; + + public override string getName() => "arthopods"; //could be changed + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/BlastProtectionEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/BlastProtectionEnchantment.cs new file mode 100644 index 00000000..3034302b --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/BlastProtectionEnchantment.cs @@ -0,0 +1,34 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class BlastProtectionEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, + Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, + Material.GOLD_HELMET, Material.GOLD_CHESTPLATE, Material.GOLD_LEGGINGS, Material.GOLD_BOOTS, + Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, + Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.PROTECTION_ENVIRONMENTAL, + EnchantmentType.PROTECTION_FIRE, + EnchantmentType.PROTECTION_PROJECTILE, + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ARMOR; + + public override EnchantmentType getEnchantType() => EnchantmentType.PROTECTION_EXPLOSIVE; + + public override int getMaxLevel() => 4; + + public override string getName() => "blastprotection"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/EfficiencyEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/EfficiencyEnchantment.cs new file mode 100644 index 00000000..5428ae87 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/EfficiencyEnchantment.cs @@ -0,0 +1,32 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class EfficiencyEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.GOLD_SWORD, Material.DIAMOND_SWORD, + Material.WOOD_PICKAXE, Material.STONE_PICKAXE, Material.IRON_PICKAXE, Material.GOLD_PICKAXE, Material.DIAMOND_PICKAXE, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + Material.WOOD_SPADE, Material.STONE_SPADE, Material.IRON_SPADE, Material.GOLD_SPADE, Material.DIAMOND_SPADE, + Material.WOOD_HOE, Material.STONE_HOE, Material.IRON_HOE, Material.GOLD_HOE, Material.DIAMOND_HOE, + + Material.SHEARS + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.TOOL; + + public override EnchantmentType getEnchantType() => EnchantmentType.DIG_SPEAD; + + public override int getMaxLevel() => 5; + + public override string getName() => "efficiency"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/Enchantment.cs b/Minecraft.Server.FourKit/Enchantments/Enchantment.cs new file mode 100644 index 00000000..e3daa549 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/Enchantment.cs @@ -0,0 +1,274 @@ +namespace Minecraft.Server.FourKit.Enchantments; + +using Minecraft.Server.FourKit.Inventory; + +/// +/// Represents the applicable target for a Enchantment +/// +public enum EnchantmentTarget +{ + /// + /// Allows the Enchantment to be placed on all items + /// + ALL, + /// + /// Allows the Enchantment to be placed on armor + /// + ARMOR, + /// + /// Allows the Enchantment to be placed on feet slot armor + /// + ARMOR_FEET, + /// + /// Allows the Enchantment to be placed on head slot armor + /// + ARMOR_HEAD, + /// + /// Allows the Enchantment to be placed on leg slot armor + /// + ARMOR_LEGS, + /// + /// Allows the Enchantment to be placed on torso slot armor + /// + ARMOR_TORSO, + /// + /// Allows the Enchantment to be placed on bows. + /// + BOW, + /// + /// Allows the Enchantment to be placed on tools (spades, pickaxe, hoes, axes) + /// + TOOL, + /// + /// Allows the Enchantment to be placed on weapons (swords) + /// + WEAPON, +} + +//these numbers match the same ones that the enchants register as, Enchantment.cpp - 41 + +/// +/// The various type of enchantments that may be added to armour or weapons +/// +public enum EnchantmentType +{ + /// + /// Provides extra damage when shooting arrows from bows + /// + ARROW_DAMAGE = 48, + /// + /// Sets entities on fire when hit by arrows shot from a bow + /// + ARROW_FIRE = 50, + /// + /// Provides infinite arrows when shooting a bow + /// + ARROW_INFINITE = 51, + /// + /// Provides a knockback when an entity is hit by an arrow from a bow + /// + ARROW_KNOCKBACK = 49, + /// + /// Increases damage against all targets + /// + DAMAGE_ALL = 16, + /// + /// Increases damage against arthropod targets + /// + DAMAGE_ARTHOPODS = 18, + /// + /// Increases damage against undead targets + /// + DAMAGE_UNDEAD = 17, + /// + /// Increases the rate at which you mine/dig + /// + DIG_SPEAD = 32, + /// + /// Decreases the rate at which a tool looses durability + /// + DURABILITY = 34, + /// + /// When attacking a target, has a chance to set them on fire + /// + FIRE_ASPECT = 20, + /// + /// All damage to other targets will knock them back when hit + /// + KNOCKBACK = 19, + /// + /// Provides a chance of gaining extra loot when destroying blocks + /// + LOOT_BONUS_BLOCKS = 35, + /// + /// Provides a chance of gaining extra loot when killing monsters + /// + LOOT_BONUS_MOBS = 21, + /// + /// Decreases the rate of air loss whilst underwater + /// + OXYGEN = 5, + /// + /// Provides protection against environmental damage + /// + PROTECTION_ENVIRONMENTAL = 0, + /// + /// Provides protection against explosive damage + /// + PROTECTION_EXPLOSIVE = 3, + /// + /// Provides protection against fall damage + /// + PROTECTION_FALL = 2, + /// + /// Provides protection against fire damage + /// + PROTECTION_FIRE = 1, + /// + /// Provides protection against projectile damage + /// + PROTECTION_PROJECTILE = 4, + /// + /// Allows blocks to drop themselves instead of fragments (for example, stone instead of cobblestone) + /// + SILK_TOUCH = 33, + /// + /// Damages the attacker + /// + THORNS = 7, + /// + /// Increases the speed at which a player may mine underwater + /// + WATER_WORKER = 6 +} + +public abstract class Enchantment +{ + public static Enchantment PowerEnchantment => _registry[EnchantmentType.ARROW_DAMAGE]; + public static Enchantment FlameEnchantment => _registry[EnchantmentType.ARROW_FIRE]; + public static Enchantment InfinityEnchantment => _registry[EnchantmentType.ARROW_INFINITE]; + public static Enchantment PunchEnchantment => _registry[EnchantmentType.ARROW_KNOCKBACK]; + public static Enchantment SharpnessEnchantment => _registry[EnchantmentType.DAMAGE_ALL]; + public static Enchantment BaneOfArthropodsEnchantment => _registry[EnchantmentType.DAMAGE_ARTHOPODS]; + public static Enchantment SmiteEnchantment => _registry[EnchantmentType.DAMAGE_UNDEAD]; + public static Enchantment EfficiencyEnchantment => _registry[EnchantmentType.DIG_SPEAD]; + public static Enchantment UnbreakingEnchantment => _registry[EnchantmentType.DURABILITY]; + public static Enchantment FireAspectEnchantment => _registry[EnchantmentType.FIRE_ASPECT]; + public static Enchantment KnockbackEnchantment => _registry[EnchantmentType.KNOCKBACK]; + public static Enchantment FortuneEnchantment => _registry[EnchantmentType.LOOT_BONUS_BLOCKS]; + public static Enchantment LootingEnchantment => _registry[EnchantmentType.LOOT_BONUS_MOBS]; + public static Enchantment RespirationEnchantment => _registry[EnchantmentType.OXYGEN]; + public static Enchantment ProtectionEnchantment => _registry[EnchantmentType.PROTECTION_ENVIRONMENTAL]; + public static Enchantment BlastProtectionEnchantment => _registry[EnchantmentType.PROTECTION_EXPLOSIVE]; + public static Enchantment FeatherFallingEnchantment => _registry[EnchantmentType.PROTECTION_FALL]; + public static Enchantment FireProtectionEnchantment => _registry[EnchantmentType.PROTECTION_FIRE]; + public static Enchantment ProjectileProtectionEnchantment => _registry[EnchantmentType.PROTECTION_PROJECTILE]; + public static Enchantment SilkTouchEnchantment => _registry[EnchantmentType.SILK_TOUCH]; + public static Enchantment ThornsEnchantment => _registry[EnchantmentType.THORNS]; + public static Enchantment AquaAffinityEnchantment => _registry[EnchantmentType.WATER_WORKER]; + + + private static Dictionary _registry = new Dictionary() + { + { EnchantmentType.ARROW_DAMAGE, new PowerEnchantment() }, + { EnchantmentType.ARROW_FIRE, new FlameEnchantment() }, + { EnchantmentType.ARROW_INFINITE, new InfinityEnchantment() }, + { EnchantmentType.ARROW_KNOCKBACK, new PunchEnchantment() }, + { EnchantmentType.DAMAGE_ALL, new SharpnessEnchantment() }, + { EnchantmentType.DAMAGE_ARTHOPODS, new BaneOfArthropodsEnchantment() }, + { EnchantmentType.DAMAGE_UNDEAD, new SmiteEnchantment() }, + { EnchantmentType.DIG_SPEAD, new EfficiencyEnchantment() }, + { EnchantmentType.DURABILITY, new UnbreakingEnchantment() }, + { EnchantmentType.FIRE_ASPECT, new FireAspectEnchantment() }, + { EnchantmentType.KNOCKBACK, new KnockbackEnchantment() }, + { EnchantmentType.LOOT_BONUS_BLOCKS, new FortuneEnchantment() }, + { EnchantmentType.LOOT_BONUS_MOBS, new LootingEnchantment() }, + { EnchantmentType.OXYGEN, new RespirationEnchantment() }, + { EnchantmentType.PROTECTION_ENVIRONMENTAL, new ProtectionEnchantment() }, + { EnchantmentType.PROTECTION_EXPLOSIVE, new BlastProtectionEnchantment() }, + { EnchantmentType.PROTECTION_FALL, new FeatherFallingEnchantment() }, + { EnchantmentType.PROTECTION_FIRE, new FireProtectionEnchantment() }, + { EnchantmentType.PROTECTION_PROJECTILE, new ProjectileProtectionEnchantment() }, + { EnchantmentType.SILK_TOUCH, new SilkTouchEnchantment() }, + { EnchantmentType.THORNS, new ThornsEnchantment() }, + { EnchantmentType.WATER_WORKER, new AquaAffinityEnchantment() }, + }; + + /// + /// Checks if this Enchantment may be applied to the given . This does not check if it conflicts with any enchantments already applied to the item. + /// + /// Item to test + /// True if the enchantment may be applied, otherwise False + public abstract bool canEnchantItem(ItemStack item); + + /// + /// Check if this enchantment conflicts with another enchantment. + /// + /// The enchantment to check against + /// True if there is a conflict. + public abstract bool conflictsWith(Enchantment other); + //public abstract Enchantment getById(int id); //deprecated by bukkit + + /// + /// Gets the Enchantment at the specified name + /// + /// Name to fetch. + /// Resulting Enchantment, or null if not found + public static Enchantment? getByName(string name) + { + foreach (KeyValuePair enchantmentPair in _registry) + { + if (enchantmentPair.Value.getName().Equals(name)) return enchantmentPair.Value; + } + + return null; + } + + /// + /// Gets the Enchantment at the specified type + /// + /// Type to fetch. + /// Resulting Enchantment, or null if not found + public static Enchantment getByType(EnchantmentType type) + { + return _registry[type]; //we should always have the enchant based on the type + } + + //public abstract int getId(); //deprecated by bukkit + + /// + /// Gets the type of that may fit this Enchantment. + /// + /// Gets the type of that may fit this Enchantment. + public abstract EnchantmentTarget getItemTarget(); + + /// + /// Returns the . + /// + /// Gets the enchantment type. + public abstract EnchantmentType getEnchantType(); + + /// + /// Gets the maximum level that this Enchantment may become. + /// + /// Maximum level of the Enchantment + public abstract int getMaxLevel(); + + + /// + /// Gets the unique name of this enchantment + /// + /// Unique name + public abstract string getName(); + + /// + /// Gets the level that this Enchantment should start at + /// + /// Starting level of the Enchantment + public abstract int getStartLevel(); + + //public static bool isAcceptingRegistrations(); //we dont have enchant registrations + //public static void registerEnchantment(Enchantment enchantment); //we dont have enchant registrations + //public static void stopAcceptingRegistrations(); //we dont have enchant registrations + +} \ No newline at end of file diff --git a/Minecraft.Server.FourKit/Enchantments/FeatherFallingEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/FeatherFallingEnchantment.cs new file mode 100644 index 00000000..f27e56d2 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/FeatherFallingEnchantment.cs @@ -0,0 +1,26 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class FeatherFallingEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.LEATHER_BOOTS, Material.LEATHER_BOOTS, Material.CHAINMAIL_BOOTS, Material.GOLD_BOOTS, Material.IRON_BOOTS, Material.DIAMOND_BOOTS, + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ARMOR_FEET; + + public override EnchantmentType getEnchantType() => EnchantmentType.PROTECTION_FALL; + + public override int getMaxLevel() => 4; + + public override string getName() => "featherfalling"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/FireAspectEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/FireAspectEnchantment.cs new file mode 100644 index 00000000..c7a8b5c5 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/FireAspectEnchantment.cs @@ -0,0 +1,27 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class FireAspectEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.GOLD_SWORD, Material.DIAMOND_SWORD, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.WEAPON; + + public override EnchantmentType getEnchantType() => EnchantmentType.FIRE_ASPECT; + + public override int getMaxLevel() => 2; + + public override string getName() => "fireaspect"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/FireProtectionEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/FireProtectionEnchantment.cs new file mode 100644 index 00000000..09f2c8ea --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/FireProtectionEnchantment.cs @@ -0,0 +1,34 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class FireProtectionEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, + Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, + Material.GOLD_HELMET, Material.GOLD_CHESTPLATE, Material.GOLD_LEGGINGS, Material.GOLD_BOOTS, + Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, + Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.PROTECTION_ENVIRONMENTAL, + EnchantmentType.PROTECTION_EXPLOSIVE, + EnchantmentType.PROTECTION_PROJECTILE, + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ARMOR; + + public override EnchantmentType getEnchantType() => EnchantmentType.PROTECTION_FIRE; + + public override int getMaxLevel() => 4; + + public override string getName() => "fireprotection"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/FlameEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/FlameEnchantment.cs new file mode 100644 index 00000000..4166170b --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/FlameEnchantment.cs @@ -0,0 +1,26 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class FlameEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.BOW + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.BOW; + + public override EnchantmentType getEnchantType() => EnchantmentType.ARROW_FIRE; + + public override int getMaxLevel() => 1; + + public override string getName() => "flame"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/FortuneEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/FortuneEnchantment.cs new file mode 100644 index 00000000..d3477b27 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/FortuneEnchantment.cs @@ -0,0 +1,31 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class FortuneEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_PICKAXE, Material.STONE_PICKAXE, Material.IRON_PICKAXE, Material.GOLD_PICKAXE, Material.DIAMOND_PICKAXE, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + Material.WOOD_SPADE, Material.STONE_SPADE, Material.IRON_SPADE, Material.GOLD_SPADE, Material.DIAMOND_SPADE, + Material.WOOD_HOE, Material.STONE_HOE, Material.IRON_HOE, Material.GOLD_HOE, Material.DIAMOND_HOE, + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.SILK_TOUCH + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.TOOL; + + public override EnchantmentType getEnchantType() => EnchantmentType.LOOT_BONUS_BLOCKS; + + public override int getMaxLevel() => 3; + + public override string getName() => "fortune"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/InfinityEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/InfinityEnchantment.cs new file mode 100644 index 00000000..f1c697b7 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/InfinityEnchantment.cs @@ -0,0 +1,26 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class InfinityEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.BOW + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.BOW; + + public override EnchantmentType getEnchantType() => EnchantmentType.ARROW_INFINITE; + + public override int getMaxLevel() => 1; + + public override string getName() => "infinity"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/KnockbackEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/KnockbackEnchantment.cs new file mode 100644 index 00000000..5a508a42 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/KnockbackEnchantment.cs @@ -0,0 +1,27 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class KnockbackEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.GOLD_SWORD, Material.DIAMOND_SWORD, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.WEAPON; + + public override EnchantmentType getEnchantType() => EnchantmentType.KNOCKBACK; + + public override int getMaxLevel() => 2; + + public override string getName() => "knockback"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/LootingEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/LootingEnchantment.cs new file mode 100644 index 00000000..710b6518 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/LootingEnchantment.cs @@ -0,0 +1,27 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class LootingEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_PICKAXE, Material.STONE_PICKAXE, Material.IRON_PICKAXE, Material.GOLD_PICKAXE, Material.DIAMOND_PICKAXE, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.WEAPON; + + public override EnchantmentType getEnchantType() => EnchantmentType.LOOT_BONUS_MOBS; + + public override int getMaxLevel() => 3; + + public override string getName() => "looting"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/PowerEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/PowerEnchantment.cs new file mode 100644 index 00000000..f5e93e5a --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/PowerEnchantment.cs @@ -0,0 +1,26 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class PowerEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.BOW + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.BOW; + + public override EnchantmentType getEnchantType() => EnchantmentType.ARROW_DAMAGE; + + public override int getMaxLevel() => 5; + + public override string getName() => "power"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/ProjectileProtectionEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/ProjectileProtectionEnchantment.cs new file mode 100644 index 00000000..31706216 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/ProjectileProtectionEnchantment.cs @@ -0,0 +1,34 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class ProjectileProtectionEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, + Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, + Material.GOLD_HELMET, Material.GOLD_CHESTPLATE, Material.GOLD_LEGGINGS, Material.GOLD_BOOTS, + Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, + Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.PROTECTION_ENVIRONMENTAL, + EnchantmentType.PROTECTION_EXPLOSIVE, + EnchantmentType.PROTECTION_FIRE, + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ARMOR; + + public override EnchantmentType getEnchantType() => EnchantmentType.PROTECTION_PROJECTILE; + + public override int getMaxLevel() => 4; + + public override string getName() => "projectileprotection"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/ProtectionEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/ProtectionEnchantment.cs new file mode 100644 index 00000000..70fe5d85 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/ProtectionEnchantment.cs @@ -0,0 +1,34 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class ProtectionEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, + Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, + Material.GOLD_HELMET, Material.GOLD_CHESTPLATE, Material.GOLD_LEGGINGS, Material.GOLD_BOOTS, + Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, + Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.PROTECTION_EXPLOSIVE, + EnchantmentType.PROTECTION_FIRE, + EnchantmentType.PROTECTION_PROJECTILE, + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ARMOR; + + public override EnchantmentType getEnchantType() => EnchantmentType.PROTECTION_ENVIRONMENTAL; + + public override int getMaxLevel() => 4; + + public override string getName() => "protection"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/PunchEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/PunchEnchantment.cs new file mode 100644 index 00000000..3fb5b3e0 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/PunchEnchantment.cs @@ -0,0 +1,26 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class PunchEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.BOW + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.BOW; + + public override EnchantmentType getEnchantType() => EnchantmentType.ARROW_KNOCKBACK; + + public override int getMaxLevel() => 2; + + public override string getName() => "punch"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/RespirationEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/RespirationEnchantment.cs new file mode 100644 index 00000000..8d599077 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/RespirationEnchantment.cs @@ -0,0 +1,26 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class RespirationEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.LEATHER_HELMET, Material.CHAINMAIL_HELMET, Material.GOLD_HELMET, Material.IRON_HELMET, Material.DIAMOND_HELMET, + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ARMOR_HEAD; + + public override EnchantmentType getEnchantType() => EnchantmentType.OXYGEN; + + public override int getMaxLevel() => 3; + + public override string getName() => "respiration"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/SharpnessEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/SharpnessEnchantment.cs new file mode 100644 index 00000000..00421a7b --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/SharpnessEnchantment.cs @@ -0,0 +1,30 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class SharpnessEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.GOLD_SWORD, Material.DIAMOND_SWORD, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.DAMAGE_ARTHOPODS, + EnchantmentType.DAMAGE_UNDEAD + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.WEAPON; + + public override EnchantmentType getEnchantType() => EnchantmentType.DAMAGE_ALL; + + public override int getMaxLevel() => 5; + + public override string getName() => "sharpness"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/SilkTouchEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/SilkTouchEnchantment.cs new file mode 100644 index 00000000..01c2bbce --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/SilkTouchEnchantment.cs @@ -0,0 +1,33 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class SilkTouchEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_PICKAXE, Material.STONE_PICKAXE, Material.IRON_PICKAXE, Material.GOLD_PICKAXE, Material.DIAMOND_PICKAXE, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + Material.WOOD_SPADE, Material.STONE_SPADE, Material.IRON_SPADE, Material.GOLD_SPADE, Material.DIAMOND_SPADE, + Material.WOOD_HOE, Material.STONE_HOE, Material.IRON_HOE, Material.GOLD_HOE, Material.DIAMOND_HOE, + + Material.SHEARS + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.LOOT_BONUS_BLOCKS + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.TOOL; + + public override EnchantmentType getEnchantType() => EnchantmentType.SILK_TOUCH; + + public override int getMaxLevel() => 1; + + public override string getName() => "silktouch"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/SmiteEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/SmiteEnchantment.cs new file mode 100644 index 00000000..6bc95082 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/SmiteEnchantment.cs @@ -0,0 +1,30 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class SmiteEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.GOLD_SWORD, Material.DIAMOND_SWORD, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + }; + + static readonly EnchantmentType[] conflictedEnchants = { + EnchantmentType.DAMAGE_ALL, + EnchantmentType.DAMAGE_ARTHOPODS + }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.WEAPON; + + public override EnchantmentType getEnchantType() => EnchantmentType.DAMAGE_UNDEAD; + + public override int getMaxLevel() => 5; + + public override string getName() => "smite"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/ThornsEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/ThornsEnchantment.cs new file mode 100644 index 00000000..fc6ea8e2 --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/ThornsEnchantment.cs @@ -0,0 +1,30 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class ThornsEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, + Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, + Material.GOLD_HELMET, Material.GOLD_CHESTPLATE, Material.GOLD_LEGGINGS, Material.GOLD_BOOTS, + Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, + Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ARMOR; + + public override EnchantmentType getEnchantType() => EnchantmentType.THORNS; + + public override int getMaxLevel() => 3; + + public override string getName() => "thorns"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Enchantments/UnbreakingEnchantment.cs b/Minecraft.Server.FourKit/Enchantments/UnbreakingEnchantment.cs new file mode 100644 index 00000000..a83039ab --- /dev/null +++ b/Minecraft.Server.FourKit/Enchantments/UnbreakingEnchantment.cs @@ -0,0 +1,39 @@ +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit.Enchantments; + +public class UnbreakingEnchantment : Enchantment +{ + static readonly Material[] supportedItems = { + Material.WOOD_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.GOLD_SWORD, Material.DIAMOND_SWORD, + Material.WOOD_PICKAXE, Material.STONE_PICKAXE, Material.IRON_PICKAXE, Material.GOLD_PICKAXE, Material.DIAMOND_PICKAXE, + Material.WOOD_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLD_AXE, Material.DIAMOND_AXE, + Material.WOOD_SPADE, Material.STONE_SPADE, Material.IRON_SPADE, Material.GOLD_SPADE, Material.DIAMOND_SPADE, + Material.WOOD_HOE, Material.STONE_HOE, Material.IRON_HOE, Material.GOLD_HOE, Material.DIAMOND_HOE, + + Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, + Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, + Material.GOLD_HELMET, Material.GOLD_CHESTPLATE, Material.GOLD_LEGGINGS, Material.GOLD_BOOTS, + Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, + Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, + + Material.FISHING_ROD, Material.BOW, + Material.SHEARS, Material.FLINT_AND_STEEL, Material.CARROT_STICK + }; + + static readonly EnchantmentType[] conflictedEnchants = { }; + + public override bool canEnchantItem(ItemStack item) => supportedItems.Contains(item.getType()); + + public override bool conflictsWith(Enchantment other) => conflictedEnchants.Contains(other.getEnchantType()); + + public override EnchantmentTarget getItemTarget() => EnchantmentTarget.ALL; + + public override EnchantmentType getEnchantType() => EnchantmentType.DURABILITY; + + public override int getMaxLevel() => 3; + + public override string getName() => "unbreaking"; + + public override int getStartLevel() => 1; +} diff --git a/Minecraft.Server.FourKit/Entity/Damageable.cs b/Minecraft.Server.FourKit/Entity/Damageable.cs new file mode 100644 index 00000000..2fa92114 --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/Damageable.cs @@ -0,0 +1,79 @@ +namespace Minecraft.Server.FourKit.Entity; + +/// +/// Represents an that can take damage and has health. +/// +public class Damageable : Entity +{ + private double _health = 20.0; + private double _maxHealth = 20.0; + private readonly double _originalMaxHealth = 20.0; + + /// + /// Deals the given amount of damage to this entity. + /// This calls into the native server to apply real damage. + /// + /// Amount of damage to deal. + public void damage(double amount) + { + NativeBridge.DamagePlayer?.Invoke(getEntityId(), (float)amount); + } + + /// + /// Gets the entity's health from 0 to , where 0 is dead. + /// + /// The current health. + public double getHealth() => _health; + + /// + /// Gets the maximum health this entity has. + /// + /// The maximum health. + public double getMaxHealth() => _maxHealth; + + /// + /// Resets the max health to the original amount. + /// + public void resetMaxHealth() + { + _maxHealth = _originalMaxHealth; + if (_health > _maxHealth) + _health = _maxHealth; + } + + /// + /// Sets the entity's health from 0 to , where 0 is dead. + /// This calls into the native server to apply the health change. + /// + /// New health value. + public void setHealth(double health) + { + NativeBridge.SetPlayerHealth?.Invoke(getEntityId(), (float)Math.Clamp(health, 0.0, _maxHealth)); + } + + /// + /// Sets the maximum health this entity can have. + /// If the entity's current health exceeds the new maximum, it is clamped. + /// + /// New maximum health value. + public void setMaxHealth(double health) + { + _maxHealth = health; + if (_health > _maxHealth) + _health = _maxHealth; + } + + // --- Internal setter used by the bridge --- + + /// + /// Updates health directly. Called internally by the bridge. + /// + /// The new health value. + internal void SetHealthInternal(double health) => _health = health; + + /// + /// Updates max health directly. Called internally by the bridge. + /// + /// The new max health value. + internal void SetMaxHealthInternal(double maxHealth) => _maxHealth = maxHealth; +} diff --git a/Minecraft.Server.FourKit/Entity/DisconnectReason.cs b/Minecraft.Server.FourKit/Entity/DisconnectReason.cs new file mode 100644 index 00000000..82919c87 --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/DisconnectReason.cs @@ -0,0 +1,66 @@ +namespace Minecraft.Server.FourKit.Entity; +// eh + +/// +/// Enum representing the reason a player was disconnected from the server. +/// mirrored from DisconnectPacket::eDisconnectReason. +/// +public enum DisconnectReason +{ + /// No specific reason. + NONE = 0, + /// The player quit voluntarily. + QUITTING = 1, + /// The connection was closed. + CLOSED = 2, + /// The login took too long. + LOGIN_TOO_LONG = 3, + /// The player had an illegal stance. + ILLEGAL_STANCE = 4, + /// The player had an illegal position. + ILLEGAL_POSITION = 5, + /// The player moved too quickly. + MOVED_TOO_QUICKLY = 6, + /// The player was flying when not allowed. + NO_FLYING = 7, + /// The player was kicked by an operator or plugin. + KICKED = 8, + /// The connection timed out. + TIME_OUT = 9, + /// Packet overflow. + OVERFLOW = 10, + /// End of stream reached unexpectedly. + END_OF_STREAM = 11, + /// The server is full. + SERVER_FULL = 12, + /// The server is outdated. + OUTDATED_SERVER = 13, + /// The client is outdated. + OUTDATED_CLIENT = 14, + /// An unexpected packet was received. + UNEXPECTED_PACKET = 15, + /// Connection creation failed. + CONNECTION_CREATION_FAILED = 16, + /// The host does not have multiplayer privileges. + NO_MULTIPLAYER_PRIVILEGES_HOST = 17, + /// The joining player does not have multiplayer privileges. + NO_MULTIPLAYER_PRIVILEGES_JOIN = 18, + /// All local players lack UGC permissions. + NO_UGC_ALL_LOCAL = 19, + /// A single local player lacks UGC permissions. + NO_UGC_SINGLE_LOCAL = 20, + /// All local players have content restrictions. + CONTENT_RESTRICTED_ALL_LOCAL = 21, + /// A single local player has content restrictions. + CONTENT_RESTRICTED_SINGLE_LOCAL = 22, + /// A remote player lacks UGC permissions. + NO_UGC_REMOTE = 23, + /// No friends in the game. + NO_FRIENDS_IN_GAME = 24, + /// The player was banned. + BANNED = 25, + /// The player is not friends with the host. + NOT_FRIENDS_WITH_HOST = 26, + /// NAT type mismatch. + NAT_MISMATCH = 27, +} diff --git a/Minecraft.Server.FourKit/Entity/Entity.cs b/Minecraft.Server.FourKit/Entity/Entity.cs new file mode 100644 index 00000000..d6b60a8f --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/Entity.cs @@ -0,0 +1,192 @@ +namespace Minecraft.Server.FourKit.Entity; + +using Minecraft.Server.FourKit.Util; + +/// +/// Represents a base entity in the world +/// +public 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; + private bool _onGround; + private double _velocityX, _velocityY, _velocityZ; + + /// + /// Gets the entity's current position. + /// + /// a new copy of containing the position of this entity + public Location getLocation() => _location; + + /// + /// Returns a unique id for this entity + /// + /// Entity id + public virtual int getEntityId() => _entityId; + + /// + /// Get the type of the entity. + /// + /// The of this entity. + public new virtual EntityType getType() => _entityType; + public new virtual EntityType GetType() => _entityType; + + /// + /// Returns a unique and persistent id for this entity. Note that this is not the standard UUID for players. + /// + /// A unique to this entity. + public Guid getUniqueId() => _uniqueId; + + /// + /// Teleports this entity to the given location. + /// This calls into the native server to perform the actual teleport. + /// + /// The destination location. + /// true if the teleport was successful. + public virtual bool teleport(Location location) + { + int targetDimId = location.LocationWorld?.getDimensionId() ?? _dimensionId; + NativeBridge.TeleportEntity?.Invoke(getEntityId(), targetDimId, location.getX(), location.getY(), location.getZ()); + SetLocation(location); + return true; + } + + /// + /// Sets the fall distance for this entity. + /// + /// The fall distance value. + public void setFallDistance(float distance) + { + _fallDistance = distance; + NativeBridge.SetFallDistance?.Invoke(getEntityId(), distance); + } + + /// + /// Returns the distance this entity has fallen. + /// + /// The current fall distance. + public float getFallDistance() => _fallDistance; + + /// + /// Gets the current world this entity resides in. + /// + /// World containing this entity. + public World getWorld() => FourKit.getWorld(_dimensionId); + + /// + /// Returns true if the entity is supported by a block. This value is a + /// state updated by the server and is not recalculated unless the entity moves. + /// + /// True if entity is on ground. + public bool isOnGround() => _onGround; + + /// + /// Gets this entity's current velocity. + /// + /// Current travelling velocity of this entity. + public Vector getVelocity() => new Vector(_velocityX, _velocityY, _velocityZ); + + /// + /// Sets this entity's velocity. + /// + /// New velocity to travel with. + public void setVelocity(Vector velocity) + { + _velocityX = velocity.getX(); + _velocityY = velocity.getY(); + _velocityZ = velocity.getZ(); + NativeBridge.SetVelocity?.Invoke(getEntityId(), velocity.getX(), velocity.getY(), velocity.getZ()); + } + + /// + /// Returns whether this entity is inside a vehicle. + /// + /// true if the entity is in a vehicle. + public bool isInsideVehicle() + { + return (NativeBridge.GetVehicleId?.Invoke(getEntityId()) ?? -1) >= 0; + } + + /// + /// Leave the current vehicle. If the entity is currently in a vehicle + /// (and is removed from it), true will be returned, otherwise + /// false will be returned. + /// + /// true if the entity was in a vehicle. + public bool leaveVehicle() + { + return NativeBridge.LeaveVehicle?.Invoke(getEntityId()) != 0; + } + + /// + /// Get the vehicle that this entity is inside. If there is no vehicle, + /// null will be returned. + /// + /// The current vehicle, or null. + public Entity? getVehicle() + { + int vehicleId = NativeBridge.GetVehicleId?.Invoke(getEntityId()) ?? -1; + if (vehicleId < 0) return null; + return FourKit.GetEntityByEntityId(vehicleId); + } + + /// + /// Eject any passenger. + /// + /// true if there was a passenger. + public bool eject() + { + return NativeBridge.Eject?.Invoke(getEntityId()) != 0; + } + + /// + /// Gets the primary passenger of a vehicle. For vehicles that could + /// have multiple passengers, this will only return the primary passenger. + /// + /// The passenger entity, or null. + public Entity? getPassenger() + { + int passengerId = NativeBridge.GetPassengerId?.Invoke(getEntityId()) ?? -1; + if (passengerId < 0) return null; + return FourKit.GetEntityByEntityId(passengerId); + } + + /// + /// Set the passenger of a vehicle. + /// + /// The new passenger. + /// false if it could not be done for whatever reason. + public bool setPassenger(Entity passenger) + { + if (passenger == null || NativeBridge.SetPassenger == null) return false; + return NativeBridge.SetPassenger(getEntityId(), passenger.getEntityId()) != 0; + } + + // 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; + internal void SetOnGroundInternal(bool onGround) => _onGround = onGround; + internal void SetVelocityInternal(double x, double y, double z) + { + _velocityX = x; + _velocityY = y; + _velocityZ = z; + } +} diff --git a/Minecraft.Server.FourKit/Entity/EntityType.cs b/Minecraft.Server.FourKit/Entity/EntityType.cs new file mode 100644 index 00000000..5da46b78 --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/EntityType.cs @@ -0,0 +1,132 @@ +namespace Minecraft.Server.FourKit.Entity; + +/// +/// Represents the type of an . +/// +public enum EntityType +{ + /// An arrow projectile; may get stuck in the ground. + ARROW, + /// A bat. + BAT, + /// A blaze. + BLAZE, + /// A placed boat. + BOAT, + /// A cave spider. + CAVE_SPIDER, + /// A chicken. + CHICKEN, + /// A complex entity part. + COMPLEX_PART, + /// A cow. + COW, + /// A creeper. + CREEPER, + /// An item resting on the ground. + DROPPED_ITEM, + /// A flying chicken egg. + EGG, + /// An ender crystal. + ENDER_CRYSTAL, + /// An ender dragon. + ENDER_DRAGON, + /// A flying ender pearl. + ENDER_PEARL, + /// An ender eye signal. + ENDER_SIGNAL, + /// An enderman. + ENDERMAN, + /// An experience orb. + EXPERIENCE_ORB, + /// A block that is going to or is about to fall. + FALLING_BLOCK, + /// A flying large fireball, as thrown by a Ghast for example. + FIREBALL, + /// A firework rocket. + FIREWORK, + /// A fishing line and bobber. + FISHING_HOOK, + /// A ghast. + GHAST, + /// A giant. + GIANT, + /// A horse. + HORSE, + /// An iron golem. + IRON_GOLEM, + /// An item frame on a wall. + ITEM_FRAME, + /// A leash attached to a fencepost. + LEASH_HITCH, + /// A bolt of lightning. + LIGHTNING, + /// A magma cube. + MAGMA_CUBE, + /// A minecart. + MINECART, + /// A minecart with a chest. + MINECART_CHEST, + /// A minecart with a command block. + MINECART_COMMAND, + /// A minecart with a furnace. + MINECART_FURNACE, + /// A minecart with a hopper. + MINECART_HOPPER, + /// A minecart with a mob spawner. + MINECART_MOB_SPAWNER, + /// A minecart with TNT. + MINECART_TNT, + /// A mooshroom. + MUSHROOM_COW, + /// An ocelot. + OCELOT, + /// A painting on a wall. + PAINTING, + /// A pig. + PIG, + /// A zombie pigman. + PIG_ZOMBIE, + /// A player. + PLAYER, + /// Primed TNT that is about to explode. + PRIMED_TNT, + /// A sheep. + SHEEP, + /// A silverfish. + SILVERFISH, + /// A skeleton. + SKELETON, + /// A slime. + SLIME, + /// A flying small fireball, such as thrown by a Blaze or player. + SMALL_FIREBALL, + /// A flying snowball. + SNOWBALL, + /// A snowman. + SNOWMAN, + /// A spider. + SPIDER, + /// A flying splash potion. + SPLASH_POTION, + /// A squid. + SQUID, + /// A flying experience bottle. + THROWN_EXP_BOTTLE, + /// An unknown entity without an Entity Class. + UNKNOWN, + /// A villager. + VILLAGER, + /// A weather entity. + WEATHER, + /// A witch. + WITCH, + /// A wither. + WITHER, + /// A flying wither skull projectile. + WITHER_SKULL, + /// A wolf. + WOLF, + /// A zombie. + ZOMBIE, +} diff --git a/Minecraft.Server.FourKit/Entity/HumanEntity.cs b/Minecraft.Server.FourKit/Entity/HumanEntity.cs new file mode 100644 index 00000000..497ce7bc --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/HumanEntity.cs @@ -0,0 +1,175 @@ +namespace Minecraft.Server.FourKit.Entity; + +using System.Runtime.InteropServices; +using Minecraft.Server.FourKit.Inventory; + +/// +/// Represents a human entity in the world (e.g. a player). +/// +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; + private bool _sleeping; + private int _sleepTicks; + + /// + /// Gets this human's current . + /// + /// The current game mode. + public GameMode getGameMode() => _gameMode; + + /// + /// Returns the name of this player. + /// + /// The display name. + public string getName() => _name; + + /// + /// Sets this human's current . + /// + /// The new game mode. + public void setGameMode(GameMode mode) + { + NativeBridge.SetPlayerGameMode?.Invoke(getEntityId(), (int)mode); + } + + /// + /// Get the player's inventory. + /// + /// The inventory of the player, this also contains the armor slots. + Inventory InventoryHolder.getInventory() => getInventory(); + + /// + /// Get the player's inventory. + /// This also contains the armor slots. + /// + /// The player's inventory. + public PlayerInventory getInventory() + { + return _playerInventory; + } + + /// + /// Get the player's EnderChest inventory. + /// + /// The EnderChest of the player. + public Inventory getEnderChest() + { + return _enderChestInventory; + } + + /// + /// Returns the ItemStack currently in your hand, can be empty. + /// + /// The ItemStack of the item you are currently holding. + public ItemStack? getItemInHand() + { + return _playerInventory.getItemInHand(); + } + + /// + /// Sets the item to the given ItemStack, this will replace whatever the + /// user was holding. + /// + /// The ItemStack which will end up in the hand. + public void setItemInHand(ItemStack? item) + { + _playerInventory.setItemInHand(item); + } + + /// + /// Returns the ItemStack currently on your cursor, can be empty. + /// Will always be empty if the player currently has no open window. + /// + /// The ItemStack of the item you are currently moving around. + public ItemStack? getItemOnCursor() => _cursorItem; + + /// + /// 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. + /// + /// The ItemStack which will end up in the hand. + public void setItemOnCursor(ItemStack? item) => _cursorItem = item; + + /// + /// If the player currently has an inventory window open, this method will + /// close it on both the server and client side. + /// + public void closeInventory() + { + NativeBridge.CloseContainer?.Invoke(getEntityId()); + } + + /// + /// Opens an inventory window with the specified inventory on the top. + /// + /// The inventory to open. + /// The newly opened InventoryView, or null if it could not be opened. + 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; + + internal void SetSleepingInternal(bool sleeping) => _sleeping = sleeping; + + internal void SetSleepTicksInternal(int ticks) => _sleepTicks = ticks; + + /// + /// Returns whether this player is slumbering. + /// + /// slumber state + public bool isSleeping() => _sleeping; + + /// + /// Get the sleep ticks of the player. This value may be capped. + /// + /// slumber ticks + public int getSleepTicks() => _sleepTicks; +} diff --git a/Minecraft.Server.FourKit/Entity/Item.cs b/Minecraft.Server.FourKit/Entity/Item.cs new file mode 100644 index 00000000..60fc3c39 --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/Item.cs @@ -0,0 +1,32 @@ +namespace Minecraft.Server.FourKit.Entity; + +using Minecraft.Server.FourKit.Inventory; + +/// +/// Represents a dropped item on the ground. +/// +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; + } + + /// + /// Gets the item stack associated with this item. + /// + /// An item stack. + public ItemStack getItemStack() => _itemStack; + + /// + /// Sets the item stack of this item. + /// + /// The new item stack. + public void setItemStack(ItemStack stack) => _itemStack = stack; +} diff --git a/Minecraft.Server.FourKit/Entity/LivingEntity.cs b/Minecraft.Server.FourKit/Entity/LivingEntity.cs new file mode 100644 index 00000000..456047bb --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/LivingEntity.cs @@ -0,0 +1,51 @@ +namespace Minecraft.Server.FourKit.Entity; + +/// +/// Represents a living entity in the world that has health and can take damage. +/// +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); + } + + /// + /// Gets the height of the living entity's eyes above its . + /// + /// The eye height. + public double getEyeHeight() => _eyeHeight; + + /// + /// Gets the height of the living entity's eyes above its . + /// + /// If true, returns the standing eye height regardless of sneak state. + /// The eye height. + 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 --- + + /// + /// Updates the eye height. Called internally by the bridge. + /// + /// The new eye height. + internal void SetEyeHeightInternal(double eyeHeight) => _eyeHeight = eyeHeight; +} diff --git a/Minecraft.Server.FourKit/Entity/OfflinePlayer.cs b/Minecraft.Server.FourKit/Entity/OfflinePlayer.cs new file mode 100644 index 00000000..dd43caf7 --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/OfflinePlayer.cs @@ -0,0 +1,26 @@ +namespace Minecraft.Server.FourKit.Entity; + +/// +/// Represents a player identity that may or may not currently be online. +/// +public interface OfflinePlayer +{ + /// Returns the name of this player. + /// The player's name. + string getName(); + + /// Gets a Player object that this represents, if there is one. + /// A instance if the player is online; otherwise null. + Player? getPlayer(); + + /// + /// Returns the UUID that uniquely identifies this player across sessions. + /// This is the player-specific UUID, not the entity UUID. + /// + /// The player's unique identifier. + Guid getUniqueId(); + + /// Checks if this player is currently online. + /// true if the player is online; otherwise false. + bool isOnline(); +} diff --git a/Minecraft.Server.FourKit/Entity/Player.cs b/Minecraft.Server.FourKit/Entity/Player.cs new file mode 100644 index 00000000..4499502b --- /dev/null +++ b/Minecraft.Server.FourKit/Entity/Player.cs @@ -0,0 +1,665 @@ +namespace Minecraft.Server.FourKit.Entity; + +using System.Runtime.InteropServices; +using Minecraft.Server.FourKit.Command; +using Minecraft.Server.FourKit.Experimental; +using Minecraft.Server.FourKit.Inventory; +using Minecraft.Server.FourKit.Net; + +/// +/// Represents a player connected to the server. +/// +public class Player : HumanEntity, OfflinePlayer, CommandSender +{ + private float _saturation = 5.0f; + private float _walkSpeed = 0.2f; + private float _exhaustion; + private int _foodLevel = 20; + private int _level; + private float _exp; + private int _totalExperience; + private Guid _playerUniqueId; + private ulong _playerRawOnlineXUID; + private ulong _playerRawOfflineXUID; + private string? _displayName; + private bool _sneaking; + private bool _sprinting; + private bool _allowFlight; + private bool _sleepingIgnored; + + private PlayerConnection _connection; + + internal bool IsOnline { get; set; } + + internal Player(int entityId, string name) + { + SetEntityIdInternal(entityId); + SetEntityTypeInternal(EntityType.PLAYER); + SetNameInternal(name); + IsOnline = true; + _playerInventory._holder = this; + _connection = new PlayerConnection(this); + } + + /// + public override EntityType getType() => EntityType.PLAYER; + + /// + public override EntityType GetType() => EntityType.PLAYER; + + /// + public override bool teleport(Location location) + { + int targetDimId = location.LocationWorld?.getDimensionId() ?? getLocation().LocationWorld?.getDimensionId() ?? 0; + NativeBridge.TeleportEntity?.Invoke(getEntityId(), targetDimId, location.X, location.Y, location.Z); + SetLocation(location); + return true; + } + + /// + /// Experimental. Gets the player's , which can be used + /// to send raw packet data directly to the client. + /// + /// The player's connection. + public PlayerConnection getConnection() => _connection; + + /// + public Player? getPlayer() => IsOnline ? this : null; + + /// + /// 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 . + /// + /// The display name. + public string getDisplayName() => _displayName ?? getName(); + + /// + /// Sets the "friendly" name to display of this player. + /// + /// The display name, or null to reset to . + public void setDisplayName(string? name) + { + _displayName = name; + } + + /// + public bool isOnline() => IsOnline; + + /// + /// Returns the UUID that uniquely identifies this player across sessions. + /// This is the player-specific UUID, not the entity UUID. + /// + /// The player's unique identifier. + public new Guid getUniqueId() => _playerUniqueId; + + + /// + /// Experimental. Gets the raw online XUID (Xbox User ID) for this player. + /// The online XUID is used for guests. + /// + /// The raw online XUID value. + public ulong getRawOnlineXUID() => _playerRawOnlineXUID; + + /// + /// Experimental. Gets the raw offline XUID (Xbox User ID) for this player. + /// The offline XUID is the main XUID used by the client. + /// + /// The raw offline XUID value. + public ulong getRawOfflineXUID() => _playerRawOfflineXUID; + + /// + /// Gets the player's estimated ping in milliseconds. + /// This value represents a weighted average of the response time to application layer ping packets sent. This value does not represent the network round trip time and as such may have less granularity and be impacted by other sources. For these reasons it should not be used for anti-cheat purposes. Its recommended use is only as a qualitative indicator of connection quality. + /// + /// The player's estimated ping in milliseconds. + public int getPing() + { + if (NativeBridge.GetPlayerLatency == null) + return -1; + + return NativeBridge.GetPlayerLatency(getEntityId()); + } + + /// + /// Gets the player's current saturation level. + /// Saturation acts as a buffer before hunger begins to deplete. + /// + /// The current saturation level. + public float getSaturation() => _saturation; + + /// + /// Gets the current allowed speed that a client can walk. + /// The default value is 0.2. + /// + /// The current walk speed. + public float getWalkSpeed() => _walkSpeed; + + /// + /// Sets the speed at which a client will walk. + /// This calls into the native server to apply the change. + /// + /// The new walk speed. + public void setWalkSpeed(float value) + { + _walkSpeed = value; + NativeBridge.SetWalkSpeed?.Invoke(getEntityId(), value); + } + + /// + /// Returns if the player is in sneak mode. + /// + /// True if player is in sneak mode. + public bool isSneaking() => _sneaking; + + /// + /// Gets whether the player is sprinting or not. + /// + /// True if player is sprinting. + public bool isSprinting() => _sprinting; + + /// + /// Sets whether the player is ignored as not sleeping. If everyone is + /// either sleeping or has this flag set, then time will advance to the + /// next day. If everyone has this flag set but no one is actually in + /// bed, then nothing will happen. + /// + /// Whether to ignore. + public void setSleepingIgnored(bool isSleeping) + { + _sleepingIgnored = isSleeping; + NativeBridge.SetSleepingIgnored?.Invoke(getEntityId(), isSleeping ? 1 : 0); + } + + /// + /// Returns whether the player is sleeping ignored. + /// + /// Whether player is ignoring sleep. + public bool isSleepingIgnored() => _sleepingIgnored; + + /// + /// Play a sound for a player at the location. + /// This function will fail silently if Location or Sound are null. + /// + /// The location to play the sound. + /// The sound to play. + /// The volume of the sound. + /// The pitch of the sound. + public void playSound(Location location, Sound sound, float volume, float pitch) + { + if (location == null) + return; + NativeBridge.PlaySound?.Invoke(getEntityId(), (int)sound, location.X, location.Y, location.Z, volume, pitch); + } + + /// + /// Determines if the Player is allowed to fly via jump key double-tap + /// like in creative mode. + /// + /// True if the player is allowed to fly. + public bool getAllowFlight() => _allowFlight; + + /// + /// Sets if the Player is allowed to fly via jump key double-tap like + /// in creative mode. + /// + /// If flight should be allowed. + public void setAllowFlight(bool flight) + { + _allowFlight = flight; + NativeBridge.SetAllowFlight?.Invoke(getEntityId(), flight ? 1 : 0); + } + + /// + 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); + } + } + + /// + public void sendMessage(string[] messages) + { + foreach (var msg in messages) + sendMessage(msg); + } + + /// + /// Kicks player with the default reason. + /// + public void kickPlayer() + { + NativeBridge.KickPlayer?.Invoke(getEntityId(), (int)DisconnectReason.KICKED); + } + + /// + /// Bans the player by UID with the specified reason and disconnects them. + /// + /// The ban reason. + /// true if the ban was applied successfully. + 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); + } + } + + /// + /// Bans the player's IP address with the specified reason. + /// + /// The ban reason. + /// true if the IP ban was applied successfully. + 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); + } + } + + /// + /// Gets the socket address of this player. + /// + /// The player's socket address, or null if the address could not be determined. + 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); + } + } + + /// + /// Gets the players current experience level. + /// + /// Current experience level. + public int getLevel() => _level; + + /// + /// Sets the players current experience level. + /// + /// New experience level. + public void setLevel(int level) + { + _level = level; + NativeBridge.SetLevel?.Invoke(getEntityId(), level); + } + + /// + /// Gets the players current experience points towards the next level. + /// This is a percentage value. 0 is "no progress" and 1 is "next level". + /// + /// Current experience points. + public float getExp() => _exp; + + /// + /// Sets the players current experience points towards the next level. + /// This is a percentage value. 0 is "no progress" and 1 is "next level". + /// + /// New experience points. + public void setExp(float exp) + { + _exp = exp; + NativeBridge.SetExp?.Invoke(getEntityId(), exp); + } + + /// + /// Gives the player the amount of experience specified. + /// + /// Exp amount to give. + public void giveExp(int amount) + { + NativeBridge.GiveExp?.Invoke(getEntityId(), amount); + } + + /// + /// Gives the player the amount of experience levels specified. + /// Levels can be taken by specifying a negative amount. + /// + /// Amount of experience levels to give or take. + public void giveExpLevels(int amount) + { + NativeBridge.GiveExpLevels?.Invoke(getEntityId(), amount); + } + + /// + /// Gets the players current exhaustion level. + /// Exhaustion controls how fast the food level drops. While you have a + /// certain amount of exhaustion, your saturation will drop to zero, and + /// then your food will drop to zero. + /// + /// Exhaustion level. + public float getExhaustion() => _exhaustion; + + /// + /// Sets the players current exhaustion level. + /// + /// Exhaustion level. + public void setExhaustion(float value) + { + _exhaustion = value; + NativeBridge.SetExhaustion?.Invoke(getEntityId(), value); + } + + /// + /// Sets the players current saturation level. + /// + /// Saturation level. + public void setSaturation(float value) + { + _saturation = value; + NativeBridge.SetSaturation?.Invoke(getEntityId(), value); + } + + /// + /// Gets the players current food level. + /// + /// Food level. + public int getFoodLevel() => _foodLevel; + + /// + /// Sets the players current food level. + /// + /// New food level. + public void setFoodLevel(int value) + { + _foodLevel = value; + NativeBridge.SetFoodLevel?.Invoke(getEntityId(), value); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. Only this player will see the particle. + /// + /// The particle to spawn. + /// The location to spawn at. + /// The number of particles. + public void spawnParticle(Particle particle, Location location, int count) + { + spawnParticleInternal(particle, location.X, location.Y, location.Z, count, 0, 0, 0, 0, null); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. Only this player will see the particle. + /// + /// The particle to spawn. + /// The position on the x axis to spawn at. + /// The position on the y axis to spawn at. + /// The position on the z axis to spawn at. + /// The number of particles. + public void spawnParticle(Particle particle, double x, double y, double z, int count) + { + spawnParticleInternal(particle, x, y, z, count, 0, 0, 0, 0, null); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. Only this player will see the particle. + /// + /// The particle to spawn. + /// The location to spawn at. + /// The number of particles. + /// The data to use for the particle or null. + /// The type of the particle data. + public void spawnParticle(Particle particle, Location location, int count, T? data) + { + spawnParticleInternal(particle, location.X, location.Y, location.Z, count, 0, 0, 0, 0, data); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. Only this player will see the particle. + /// + /// The particle to spawn. + /// The position on the x axis to spawn at. + /// The position on the y axis to spawn at. + /// The position on the z axis to spawn at. + /// The number of particles. + /// The data to use for the particle or null. + /// The type of the particle data. + public void spawnParticle(Particle particle, double x, double y, double z, int count, T? data) + { + spawnParticleInternal(particle, x, y, z, count, 0, 0, 0, 0, data); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. The position of each particle will be + /// randomized positively and negatively by the offset parameters + /// on each axis. Only this player will see the particle. + /// + /// The particle to spawn. + /// The location to spawn at. + /// The number of particles. + /// The maximum random offset on the X axis. + /// The maximum random offset on the Y axis. + /// The maximum random offset on the Z axis. + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ) + { + spawnParticleInternal(particle, location.X, location.Y, location.Z, count, offsetX, offsetY, offsetZ, 0, null); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. The position of each particle will be + /// randomized positively and negatively by the offset parameters + /// on each axis. Only this player will see the particle. + /// + /// The particle to spawn. + /// The position on the x axis to spawn at. + /// The position on the y axis to spawn at. + /// The position on the z axis to spawn at. + /// The number of particles. + /// The maximum random offset on the X axis. + /// The maximum random offset on the Y axis. + /// The maximum random offset on the Z axis. + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ) + { + spawnParticleInternal(particle, x, y, z, count, offsetX, offsetY, offsetZ, 0, null); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. The position of each particle will be + /// randomized positively and negatively by the offset parameters + /// on each axis. Only this player will see the particle. + /// + /// The particle to spawn. + /// The location to spawn at. + /// The number of particles. + /// The maximum random offset on the X axis. + /// The maximum random offset on the Y axis. + /// The maximum random offset on the Z axis. + /// The data to use for the particle or null. + /// The type of the particle data. + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, T? data) + { + spawnParticleInternal(particle, location.X, location.Y, location.Z, count, offsetX, offsetY, offsetZ, 0, data); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. The position of each particle will be + /// randomized positively and negatively by the offset parameters + /// on each axis. Only this player will see the particle. + /// + /// The particle to spawn. + /// The position on the x axis to spawn at. + /// The position on the y axis to spawn at. + /// The position on the z axis to spawn at. + /// The number of particles. + /// The maximum random offset on the X axis. + /// The maximum random offset on the Y axis. + /// The maximum random offset on the Z axis. + /// The data to use for the particle or null. + /// The type of the particle data. + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, T? data) + { + spawnParticleInternal(particle, x, y, z, count, offsetX, offsetY, offsetZ, 0, data); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. The position of each particle will be + /// randomized positively and negatively by the offset parameters + /// on each axis. Only this player will see the particle. + /// + /// The particle to spawn. + /// The location to spawn at. + /// The number of particles. + /// The maximum random offset on the X axis. + /// The maximum random offset on the Y axis. + /// The maximum random offset on the Z axis. + /// The extra data for this particle, depends on the particle used (normally speed). + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, double extra) + { + spawnParticleInternal(particle, location.X, location.Y, location.Z, count, offsetX, offsetY, offsetZ, extra, null); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. The position of each particle will be + /// randomized positively and negatively by the offset parameters + /// on each axis. Only this player will see the particle. + /// + /// The particle to spawn. + /// The position on the x axis to spawn at. + /// The position on the y axis to spawn at. + /// The position on the z axis to spawn at. + /// The number of particles. + /// The maximum random offset on the X axis. + /// The maximum random offset on the Y axis. + /// The maximum random offset on the Z axis. + /// The extra data for this particle, depends on the particle used (normally speed). + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra) + { + spawnParticleInternal(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, null); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. The position of each particle will be + /// randomized positively and negatively by the offset parameters + /// on each axis. Only this player will see the particle. + /// + /// The particle to spawn. + /// The location to spawn at. + /// The number of particles. + /// The maximum random offset on the X axis. + /// The maximum random offset on the Y axis. + /// The maximum random offset on the Z axis. + /// The extra data for this particle, depends on the particle used (normally speed). + /// The data to use for the particle or null. + /// The type of the particle data. + public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, double extra, T? data) + { + spawnParticleInternal(particle, location.X, location.Y, location.Z, count, offsetX, offsetY, offsetZ, extra, data); + } + + /// + /// Spawns the particle (the number of times specified by count) + /// at the target location. The position of each particle will be + /// randomized positively and negatively by the offset parameters + /// on each axis. Only this player will see the particle. + /// + /// The particle to spawn. + /// The position on the x axis to spawn at. + /// The position on the y axis to spawn at. + /// The position on the z axis to spawn at. + /// The number of particles. + /// The maximum random offset on the X axis. + /// The maximum random offset on the Y axis. + /// The maximum random offset on the Z axis. + /// The extra data for this particle, depends on the particle used (normally speed). + /// The data to use for the particle or null. + /// The type of the particle data. + public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, T? data) + { + spawnParticleInternal(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data); + } + + private void spawnParticleInternal(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, object? data) + { + if (NativeBridge.SpawnParticle == null) + return; + + int particleId = (int)particle; + if (data is ItemStack itemStack && + (particle == Particle.ITEM_CRACK || particle == Particle.BLOCK_CRACK)) + { + int id = itemStack.getTypeId(); + int aux = itemStack.getDurability(); + particleId = (int)particle | ((id & 0x0FFF) << 8) | (aux & 0xFF); + } + + NativeBridge.SpawnParticle(getEntityId(), particleId, + (float)x, (float)y, (float)z, + (float)offsetX, (float)offsetY, (float)offsetZ, + (float)extra, count); + } + + // INTERNAL + internal void SetSaturationInternal(float saturation) => _saturation = saturation; + internal void SetWalkSpeedInternal(float walkSpeed) => _walkSpeed = walkSpeed; + internal void SetPlayerUniqueIdInternal(Guid id) => _playerUniqueId = id; + internal void SetPlayerRawOnlineXUIDInternal(ulong xuid) => _playerRawOnlineXUID = xuid; + internal void SetPlayerRawOfflineXUIDInternal(ulong xuid) => _playerRawOfflineXUID = xuid; + internal void SetSneakingInternal(bool sneaking) => _sneaking = sneaking; + internal void SetSprintingInternal(bool sprinting) => _sprinting = sprinting; + internal void SetAllowFlightInternal(bool allowFlight) => _allowFlight = allowFlight; + internal void SetSleepingIgnoredInternal(bool ignored) => _sleepingIgnored = ignored; + internal void SetLevelInternal(int level) => _level = level; + internal void SetExpInternal(float exp) => _exp = exp; + internal void SetTotalExperienceInternal(int totalExp) => _totalExperience = totalExp; + internal void SetFoodLevelInternal(int foodLevel) => _foodLevel = foodLevel; + internal void SetExhaustionInternal(float exhaustion) => _exhaustion = exhaustion; +} diff --git a/Minecraft.Server.FourKit/Enums/LoginType.cs b/Minecraft.Server.FourKit/Enums/LoginType.cs new file mode 100644 index 00000000..644cd748 --- /dev/null +++ b/Minecraft.Server.FourKit/Enums/LoginType.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Minecraft.Server.FourKit.Enums; + +public enum LoginType +{ + INITIAL = 1, + ACCEPTED = 2, +} diff --git a/Minecraft.Server.FourKit/Enums/TreeType.cs b/Minecraft.Server.FourKit/Enums/TreeType.cs new file mode 100644 index 00000000..9affb61a --- /dev/null +++ b/Minecraft.Server.FourKit/Enums/TreeType.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Minecraft.Server.FourKit.Enums; + +/// +/// Tree and organic structure types. +/// +public enum TreeType { + /// + /// No tree type. + /// + None = 0, + + /// + /// Redwood tree, shaped like a pine tree. + /// + SPRUCE = 1, + + /// + /// Birch tree. + /// + BIRCH = 2, + + /// + /// Standard jungle tree; 4 blocks wide and tall. + /// + JUNGLE = 3, + + /// + /// Regular tree, extra tall with branches. + /// + BIG_OAK = 4, + + /// + /// Regular tree, no branches. + /// + OAK = 5, + + /// + /// Big brown mushroom; tall and umbrella-like. + /// + BROWN_MUSHROOM = 6, + + /// + /// Big red mushroom; short and fat. + /// + RED_MUSHROOM = 7, +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockBreakEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockBreakEvent.cs new file mode 100644 index 00000000..410fc5bf --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockBreakEvent.cs @@ -0,0 +1,50 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; +using Minecraft.Server.FourKit.Entity; + +/// +/// 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: +/// +/// The player is not in creative or adventure mode +/// The player can loot the block (ie: does not destroy it completely, by using the correct tool) +/// The player does not have silk touch +/// The block drops experience in vanilla Minecraft +/// +/// +/// 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. +/// +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; + } + + /// + /// Gets the Player that is breaking the block involved in this event. + /// + /// The Player that is breaking the block involved in this event. + public Player getPlayer() => _player; + + /// + public bool isCancelled() => _cancel; + + /// + public void setCancelled(bool cancel) + { + _cancel = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockBurnEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockBurnEvent.cs new file mode 100644 index 00000000..3745bae3 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockBurnEvent.cs @@ -0,0 +1,30 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Called when a block is destroyed as a result of being burnt by fire. +/// +/// If a Block Burn event is cancelled, the block will not be destroyed +/// as a result of being burnt by fire. +/// +public class BlockBurnEvent : BlockEvent, Cancellable +{ + private bool _cancel; + + internal BlockBurnEvent(Block block) : base(block) + { + _cancel = false; + } + + + /// + public bool isCancelled() => _cancel; + + + /// + public void setCancelled(bool cancel) + { + _cancel = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockEvent.cs new file mode 100644 index 00000000..a29c1f81 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockEvent.cs @@ -0,0 +1,22 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Represents a Block-related event. +/// +public abstract class BlockEvent : Event +{ + private readonly Block _block; + + internal protected BlockEvent(Block block) + { + _block = block; + } + + /// + /// Gets the block involved in this event. + /// + /// The Block which is involved in this event. + public Block getBlock() => _block; +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockExpEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockExpEvent.cs new file mode 100644 index 00000000..4e9e9bb9 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockExpEvent.cs @@ -0,0 +1,31 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// An event that is called when a block yields experience. +/// +public class BlockExpEvent : BlockEvent +{ + private int _exp; + internal BlockExpEvent(Block block, int exp) + : base(block) + { + _exp = exp; + } + + /// + /// Get the experience dropped by the block after the event has processed. + /// + /// The experience to drop. + public int getExpToDrop() => _exp; + + /// + /// Set the amount of experience dropped by the block after the event has processed. + /// + /// 1 or higher to drop experience, else nothing will drop. + public void setExpToDrop(int exp) + { + _exp = exp; + } +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockFormEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockFormEvent.cs new file mode 100644 index 00000000..d94a9df4 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockFormEvent.cs @@ -0,0 +1,23 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Called when a block is formed or spreads based on world conditions. +/// Use to catch blocks that actually spread +/// and don't just "randomly" form. +/// +/// Examples: +/// +/// Snow forming due to a snow storm. +/// Ice forming in a snowy Biome like Taiga or Tundra. +/// +/// +/// If a Block Form event is cancelled, the block will not be formed. +/// +public class BlockFormEvent : BlockGrowEvent, Cancellable +{ + internal BlockFormEvent(Block block, BlockState newState) : base(block, newState) + { + } +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockFromToEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockFromToEvent.cs new file mode 100644 index 00000000..b75a24b4 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockFromToEvent.cs @@ -0,0 +1,59 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Represents events with a source block and a destination block, currently +/// only applies to liquid (lava and water) and teleporting dragon eggs. +/// +/// If a Block From To event is cancelled, the block will not move +/// (the liquid will not flow). +/// +public class BlockFromToEvent : BlockEvent, Cancellable +{ + private readonly Block _to; + private readonly BlockFace _face; + private bool _cancel; + + internal BlockFromToEvent(Block block, BlockFace face) : base(block) + { + _face = face; + _to = block.getRelative(face); + _cancel = false; + } + + internal BlockFromToEvent(Block block, Block toBlock) : base(block) + { + _to = toBlock; + _face = BlockFace.SELF; + _cancel = false; + } + + internal BlockFromToEvent(Block block, Block toBlock, BlockFace face) : base(block) + { + _to = toBlock; + _face = face; + _cancel = false; + } + + /// + /// Gets the BlockFace that the block is moving to. + /// + /// The BlockFace that the block is moving to. + public BlockFace getFace() => _face; + + /// + /// Convenience method for getting the faced Block. + /// + /// The faced Block. + public Block getToBlock() => _to; + + /// + public bool isCancelled() => _cancel; + + /// + public void setCancelled(bool cancel) + { + _cancel = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockGrowEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockGrowEvent.cs new file mode 100644 index 00000000..5c4905d1 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockGrowEvent.cs @@ -0,0 +1,44 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Called when a block grows naturally in the world. +/// +/// Examples: +/// +/// Wheat +/// Sugar Cane +/// Cactus +/// Watermelon +/// Pumpkin +/// +/// +/// If a Block Grow event is cancelled, the block will not grow. +/// +public class BlockGrowEvent : BlockEvent, Cancellable +{ + private bool _cancel; + private readonly BlockState _newState; + + internal BlockGrowEvent(Block block, BlockState newState) : base(block) + { + _cancel = false; + _newState = newState; + } + + /// + /// Gets the state of the block where it will form or spread to. + /// + /// The block state for this events block. + public BlockState getNewState() => _newState; + + /// + public bool isCancelled() => _cancel; + + /// + public void setCancelled(bool cancel) + { + _cancel = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockPistonEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockPistonEvent.cs new file mode 100644 index 00000000..a8e4d9b3 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockPistonEvent.cs @@ -0,0 +1,45 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Called when a piston block is triggered. +/// +public abstract class BlockPistonEvent : BlockEvent, Cancellable +{ + private bool _cancel; + private readonly BlockFace _direction; + + internal protected BlockPistonEvent(Block block, BlockFace direction) : base(block) + { + _direction = direction; + _cancel = false; + } + + + /// + public bool isCancelled() => _cancel; + + + /// + public void setCancelled(bool cancelled) + { + _cancel = cancelled; + } + + /// + /// Returns true if the Piston in the event is sticky. + /// + /// Stickiness of the piston. + public bool isSticky() + { + var type = getBlock().getType(); + return type == Material.PISTON_STICKY_BASE; + } + + /// + /// Return the direction in which the piston will operate. + /// + /// Direction of the piston. + public BlockFace getDirection() => _direction; +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockPistonExtendEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockPistonExtendEvent.cs new file mode 100644 index 00000000..f0e88110 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockPistonExtendEvent.cs @@ -0,0 +1,46 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Called when a piston extends. +/// +public class BlockPistonExtendEvent : BlockPistonEvent +{ + private readonly int _length; + + internal BlockPistonExtendEvent(Block block, int length, BlockFace direction) + : base(block, direction) + { + _length = length; + } + + /// + /// Get the amount of blocks which will be moved while extending. + /// + /// The amount of moving blocks. + public int getLength() => _length; + + /// + /// Get an immutable list of the blocks which will be moved by the extending. + /// + /// Immutable list of the moved blocks. + public List getBlocks() + { + var blocks = new List(); + var world = getBlock().getWorld(); + int x = getBlock().getX(); + int y = getBlock().getY(); + int z = getBlock().getZ(); + var dir = getDirection(); + + for (int i = 0; i < _length; i++) + { + x += dir.getModX(); + y += dir.getModY(); + z += dir.getModZ(); + blocks.Add(new Block(world, x, y, z)); + } + return blocks.AsReadOnly().ToList(); + } +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockPistonRetractEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockPistonRetractEvent.cs new file mode 100644 index 00000000..450f8c2c --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockPistonRetractEvent.cs @@ -0,0 +1,30 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Called when a piston retracts. +/// +public class BlockPistonRetractEvent : BlockPistonEvent +{ + internal BlockPistonRetractEvent(Block block, BlockFace direction) + : base(block, direction) + { + } + + /// + /// Gets the location where the possible moving block might be if the + /// retracting piston is sticky. + /// + /// The possible location of the possibly moving block. + public Location getRetractLocation() + { + var block = getBlock(); + var dir = getDirection(); + return new Location( + block.getWorld(), + block.getX() + dir.getModX() * 2, + block.getY() + dir.getModY() * 2, + block.getZ() + dir.getModZ() * 2); + } +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockPlaceEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockPlaceEvent.cs new file mode 100644 index 00000000..63de3f01 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockPlaceEvent.cs @@ -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; + +/// +/// Called when a block is placed by a player. +/// +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; + } + + /// + /// Gets the player who placed the block involved in this event. + /// + /// The Player who placed the block involved in this event. + public Player getPlayer() => player; + + /// + /// Clarity method for getting the placed block. Not really needed except + /// for reasons of clarity. + /// + /// The Block that was placed. + public Block getBlockPlaced() => getBlock(); + + /// + /// Gets the block that this block was placed against. + /// + /// Block the block that the new block was placed against. + public Block getBlockAgainst() => placedAgainst; + + /// + /// Gets the item in the player's hand when they placed the block. + /// + /// The ItemStack for the item in the player's hand when they placed the block. + public ItemStack getItemInHand() => itemInHand; + + /// + public bool isCancelled() => cancel; + + /// + public void setCancelled(bool cancel) => this.cancel = cancel; +} diff --git a/Minecraft.Server.FourKit/Event/Block/BlockSpreadEvent.cs b/Minecraft.Server.FourKit/Event/Block/BlockSpreadEvent.cs new file mode 100644 index 00000000..1f8f8d71 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/BlockSpreadEvent.cs @@ -0,0 +1,32 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; + +/// +/// Called when a block spreads based on world conditions. +/// Use to catch blocks that "randomly" form +/// instead of actually spread. +/// +/// Examples: +/// +/// Mushrooms spreading. +/// Fire spreading. +/// +/// +/// If a Block Spread event is cancelled, the block will not spread. +/// +public class BlockSpreadEvent : BlockFormEvent, Cancellable +{ + private readonly Block _source; + + internal BlockSpreadEvent(Block block, Block source, BlockState newState) : base(block, newState) + { + _source = source; + } + + /// + /// Gets the source block involved in this event. + /// + /// The Block for the source block involved in this event. + public Block getSource() => _source; +} diff --git a/Minecraft.Server.FourKit/Event/Block/SignChangeEvent.cs b/Minecraft.Server.FourKit/Event/Block/SignChangeEvent.cs new file mode 100644 index 00000000..4cc82712 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Block/SignChangeEvent.cs @@ -0,0 +1,65 @@ +namespace Minecraft.Server.FourKit.Event.Block; + +using Minecraft.Server.FourKit.Block; +using Minecraft.Server.FourKit.Entity; + +/// +/// Called when a sign is changed by a player. +/// +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; + } + + /// + /// Gets the player changing the sign involved in this event. + /// + /// The Player involved in this event. + public Player getPlayer() => _player; + + /// + /// Gets all of the lines of text from the sign involved in this event. + /// + /// The String array for the sign's lines new text. + public string[] getLines() => _lines; + + /// + /// Gets a single line of text from the sign involved in this event. + /// + /// Index of the line to get. + /// The String containing the line of text associated with the provided index. + /// Thrown when the provided index is > 3 or < 0. + 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]; + } + + /// + /// Sets a single line for the sign involved in this event. + /// + /// Index of the line to set. + /// Text to set. + /// Thrown when the provided index is > 3 or < 0. + 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; + } + + /// + public bool isCancelled() => _cancel; + + /// + public void setCancelled(bool cancel) => _cancel = cancel; +} diff --git a/Minecraft.Server.FourKit/Event/Cancellable.cs b/Minecraft.Server.FourKit/Event/Cancellable.cs new file mode 100644 index 00000000..d6e5890a --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Cancellable.cs @@ -0,0 +1,14 @@ +namespace Minecraft.Server.FourKit.Event; + +/// +/// Interface for events that can be cancelled by a plugin. +/// When cancelled, the server will skip the default action. +/// +public interface Cancellable +{ + /// Gets whether this event is cancelled. + bool isCancelled(); + + /// Sets whether this event is cancelled. + void setCancelled(bool cancel); +} diff --git a/Minecraft.Server.FourKit/Event/Entity/EntityDamageByEntityEvent.cs b/Minecraft.Server.FourKit/Event/Entity/EntityDamageByEntityEvent.cs new file mode 100644 index 00000000..873edcf9 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Entity/EntityDamageByEntityEvent.cs @@ -0,0 +1,22 @@ +namespace Minecraft.Server.FourKit.Event.Entity; + +using FourKitEntity = Minecraft.Server.FourKit.Entity; + +/// +/// Called when an entity is damaged by an entity. +/// +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; + } + + /// + /// Returns the entity that damaged the defender. + /// + /// The Entity that damaged the defender. + public FourKitEntity.Entity getDamager() => _damager; +} diff --git a/Minecraft.Server.FourKit/Event/Entity/EntityDamageEvent.cs b/Minecraft.Server.FourKit/Event/Entity/EntityDamageEvent.cs new file mode 100644 index 00000000..52a55375 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Entity/EntityDamageEvent.cs @@ -0,0 +1,112 @@ +namespace Minecraft.Server.FourKit.Event.Entity; + +using FourKitEntity = Minecraft.Server.FourKit.Entity; + +/// +/// Stores data for damage events. +/// +public class EntityDamageEvent : EntityEvent, Cancellable +{ + /// + /// An enum to specify the cause of the damage. + /// + public enum DamageCause + { + /// Damage caused by being in the area when a block explodes. + BLOCK_EXPLOSION, + /// Damage caused when an entity contacts a block such as a Cactus. + CONTACT, + /// Custom damage. + CUSTOM, + /// Damage caused by running out of air while in water. + DROWNING, + /// Damage caused when an entity attacks another entity. + ENTITY_ATTACK, + /// Damage caused by being in the area when an entity, such as a Creeper, explodes. + ENTITY_EXPLOSION, + /// Damage caused when an entity falls a distance greater than 3 blocks. + FALL, + /// Damage caused by being hit by a falling block which deals damage. + FALLING_BLOCK, + /// Damage caused by direct exposure to fire. + FIRE, + /// Damage caused due to burns caused by fire. + FIRE_TICK, + /// Damage caused by direct exposure to lava. + LAVA, + /// Damage caused by being struck by lightning. + LIGHTNING, + /// Damage caused by being hit by a damage potion or spell. + MAGIC, + /// Damage caused due to a snowman melting. + MELTING, + /// Damage caused due to an ongoing poison effect. + POISON, + /// Damage caused when attacked by a projectile. + PROJECTILE, + /// Damage caused by starving due to having an empty hunger bar. + STARVATION, + /// Damage caused by being put in a block. + SUFFOCATION, + /// Damage caused by committing suicide using the command "/kill". + SUICIDE, + /// Damage caused in retaliation to another attack by the Thorns enchantment. + THORNS, + /// Damage caused by falling into the void. + VOID, + /// Damage caused by Wither potion effect. + 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; + } + + /// + /// Gets the cause of the damage. + /// + /// A value detailing the cause of the damage. + public DamageCause getCause() => _cause; + + /// + /// Gets the raw amount of damage caused by the event. + /// + /// The raw amount of damage. + public double getDamage() => _damage; + + /// + /// Gets the amount of damage caused by the event after all damage + /// reduction is applied. + /// + /// The amount of damage after reduction. + public double getFinalDamage() => _finalDamage; + + + /// + public bool isCancelled() => _cancel; + + + /// + public void setCancelled(bool cancel) + { + _cancel = cancel; + } + + /// + /// Sets the raw amount of damage caused by the event. + /// + /// The raw amount of damage. + public void setDamage(double damage) + { + _damage = damage; + } +} diff --git a/Minecraft.Server.FourKit/Event/Entity/EntityDeathEvent.cs b/Minecraft.Server.FourKit/Event/Entity/EntityDeathEvent.cs new file mode 100644 index 00000000..4b6a7485 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Entity/EntityDeathEvent.cs @@ -0,0 +1,53 @@ +namespace Minecraft.Server.FourKit.Event.Entity; + +using FourKitEntity = Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Inventory; + +/// +/// Thrown whenever a LivingEntity dies. +/// +public class EntityDeathEvent : EntityEvent +{ + private readonly List _drops; + private int _droppedExp; + + internal EntityDeathEvent(FourKitEntity.LivingEntity entity, List drops) + : this(entity, drops, 0) + { + } + + internal EntityDeathEvent(FourKitEntity.LivingEntity what, List drops, int droppedExp) + : base(what) + { + _drops = drops; + _droppedExp = droppedExp; + } + + /// + /// Returns the Entity involved in this event. + /// + /// Entity who is involved in this event. + public new FourKitEntity.LivingEntity getEntity() => (FourKitEntity.LivingEntity)entity; + + /// + /// 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. + /// + /// Amount of EXP to drop. + public int getDroppedExp() => _droppedExp; + + /// + /// 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. + /// + /// Amount of EXP to drop. + public void setDroppedExp(int exp) => _droppedExp = exp; + + /// + /// Gets all the items which will drop when the entity dies. + /// + /// Items to drop when the entity dies. + public List getDrops() => _drops; +} diff --git a/Minecraft.Server.FourKit/Event/Entity/EntityEvent.cs b/Minecraft.Server.FourKit/Event/Entity/EntityEvent.cs new file mode 100644 index 00000000..a3822031 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Entity/EntityEvent.cs @@ -0,0 +1,28 @@ +namespace Minecraft.Server.FourKit.Event.Entity; + +using FourKitEntity = Minecraft.Server.FourKit.Entity; + +/// +/// Represents an Entity-related event. +/// +public abstract class EntityEvent : Event +{ + protected FourKitEntity.Entity entity; + + protected EntityEvent(FourKitEntity.Entity what) + { + entity = what; + } + + /// + /// Returns the Entity involved in this event. + /// + /// Entity who is involved in this event. + public FourKitEntity.Entity getEntity() => entity; + + /// + /// Gets the EntityType of the Entity involved in this event. + /// + /// EntityType of the Entity involved in this event. + public FourKitEntity.EntityType getEntityType() => entity.getType(); +} diff --git a/Minecraft.Server.FourKit/Event/Entity/PlayerDeathEvent.cs b/Minecraft.Server.FourKit/Event/Entity/PlayerDeathEvent.cs new file mode 100644 index 00000000..0e92d2cb --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Entity/PlayerDeathEvent.cs @@ -0,0 +1,110 @@ +namespace Minecraft.Server.FourKit.Event.Entity; + +using FourKitEntity = Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Inventory; + +/// +/// Thrown whenever a Player dies. +/// +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 drops, int droppedExp, string deathMessage) + : this(player, drops, droppedExp, 0, deathMessage) + { + } + + /// + /// Creates a new . + /// + /// The Player who died. + /// The items to drop when the player dies. + /// The amount of experience to drop. + /// The new EXP the Player should have at respawn. + /// The death message to display. + public PlayerDeathEvent(FourKitEntity.Player player, List drops, int droppedExp, int newExp, string deathMessage) + : base(player, drops, droppedExp) + { + _deathMessage = deathMessage; + _newExp = newExp; + _newLevel = 0; + _keepLevel = false; + _keepInventory = false; + } + + /// + /// Returns the Entity involved in this event. + /// + /// Entity who is involved in this event. + public new FourKitEntity.Player getEntity() => (FourKitEntity.Player)entity; + + /// + /// Get the death message that will appear to everyone on the server. + /// + /// Message to appear to other players on the server. + public string getDeathMessage() => _deathMessage; + + /// + /// Set the death message that will appear to everyone on the server. + /// + /// Message to appear to other players on the server. + public void setDeathMessage(string deathMessage) => _deathMessage = deathMessage; + + /// + /// Gets how much EXP the Player should have at respawn. + /// This does not indicate how much EXP should be dropped, please see + /// for that. + /// + /// New EXP of the respawned player. + public int getNewExp() => _newExp; + + /// + /// Sets how much EXP the Player should have at respawn. + /// This does not indicate how much EXP should be dropped, please see + /// for that. + /// + /// New EXP of the respawned player. + public void setNewExp(int exp) => _newExp = exp; + + /// + /// Gets the Level the Player should have at respawn. + /// + /// New Level of the respawned player. + public int getNewLevel() => _newLevel; + + /// + /// Sets the Level the Player should have at respawn. + /// + /// New Level of the respawned player. + public void setNewLevel(int level) => _newLevel = level; + + /// + /// Gets if the Player should keep all EXP at respawn. + /// This flag overrides other EXP settings. + /// + /// true if Player should keep all pre-death exp. + public bool getKeepLevel() => _keepLevel; + + /// + /// Sets if the Player should keep all EXP at respawn. + /// This overrides all other EXP settings. + /// + /// true to keep all current value levels. + public void setKeepLevel(bool keepLevel) => _keepLevel = keepLevel; + + /// + /// Gets if the Player keeps inventory on death. + /// + /// true if the player keeps inventory on death. + public bool getKeepInventory() => _keepInventory; + + /// + /// Sets if the Player keeps inventory on death. + /// + /// true to keep the inventory. + public void setKeepInventory(bool keepInventory) => _keepInventory = keepInventory; +} diff --git a/Minecraft.Server.FourKit/Event/Event.cs b/Minecraft.Server.FourKit/Event/Event.cs new file mode 100644 index 00000000..f40d2957 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Event.cs @@ -0,0 +1,10 @@ +namespace Minecraft.Server.FourKit.Event; + +/// +/// Base class for all events dispatched by the server. +/// +public abstract class Event +{ + /// Gets the name of this event (defaults to the class name). + public virtual string getEventName() => GetType().Name; +} diff --git a/Minecraft.Server.FourKit/Event/EventHandlerAttribute.cs b/Minecraft.Server.FourKit/Event/EventHandlerAttribute.cs new file mode 100644 index 00000000..e8f5db91 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/EventHandlerAttribute.cs @@ -0,0 +1,40 @@ +namespace Minecraft.Server.FourKit.Event; + +/// +/// Marks a method inside a as an event handler. +/// This class is not named "EventHandler" due to a naming conflict with the existing System.EventHandler +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class EventHandlerAttribute : Attribute +{ + /// + /// Priority of this handler. Lower values run first. + /// Default is . + /// + public EventPriority Priority { get; set; } = EventPriority.Normal; + + /// + /// Whether this handler should be skipped when the event is already + /// cancelled by a lower-priority handler. Default is false. + /// + public bool IgnoreCancelled { get; set; } = false; +} + +/// +/// Execution priority for event handlers. +/// +public enum EventPriority +{ + /// Event call is of very low importance and should be ran first, to allow other plugins to further customise the outcome + Lowest = 0, + /// Event call is of low importance + Low = 1, + /// Event call is neither important nor unimportant, and may be ran normally + Normal = 2, + /// Event call is of high importance + High = 3, + /// Event call is critical and must have the final say in what happens to the event + Highest = 4, + /// Event is listened to purely for monitoring the outcome of an event. Should not modify the event. + Monitor = 5 +} diff --git a/Minecraft.Server.FourKit/Event/Inventory/ClickType.cs b/Minecraft.Server.FourKit/Event/Inventory/ClickType.cs new file mode 100644 index 00000000..e0cedd39 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Inventory/ClickType.cs @@ -0,0 +1,93 @@ +namespace Minecraft.Server.FourKit.Event.Inventory; + +/// +/// What the client did to trigger this action (not the result). +/// +public enum ClickType +{ + /// The left (or primary) mouse button. + LEFT, + /// Holding shift while pressing the left mouse button. + SHIFT_LEFT, + /// The right mouse button. + RIGHT, + /// Holding shift while pressing the right mouse button. + SHIFT_RIGHT, + /// Clicking the left mouse button on the grey area around the inventory. + WINDOW_BORDER_LEFT, + /// Clicking the right mouse button on the grey area around the inventory. + WINDOW_BORDER_RIGHT, + /// The middle mouse button, or a "scrollwheel click". + MIDDLE, + /// One of the number keys 1-9, correspond to slots on the hotbar. + NUMBER_KEY, + /// Pressing the left mouse button twice in quick succession. + DOUBLE_CLICK, + /// The "Drop" key (defaults to Q). + DROP, + /// Holding Ctrl while pressing the "Drop" key (defaults to Q). + CONTROL_DROP, + /// Any action done with the Creative inventory open. + CREATIVE, + /// A type of inventory manipulation not yet recognized by Bukkit. + UNKNOWN, +} + +/// +/// Extension methods for . +/// +public static class ClickTypeExtensions +{ + /// + /// Gets whether this ClickType represents the pressing of a key on a keyboard. + /// + /// The click type. + /// true if this ClickType represents the pressing of a key. + public static bool isKeyboardClick(this ClickType click) + { + return click == ClickType.NUMBER_KEY || click == ClickType.DROP || click == ClickType.CONTROL_DROP; + } + + /// + /// Gets whether this ClickType represents an action that can only be performed + /// by a Player in creative mode. + /// + /// The click type. + /// true if this action requires Creative mode. + public static bool isCreativeAction(this ClickType click) + { + return click == ClickType.CREATIVE || click == ClickType.MIDDLE; + } + + /// + /// Gets whether this ClickType represents a right click. + /// + /// The click type. + /// true if this ClickType represents a right click. + public static bool isRightClick(this ClickType click) + { + return click == ClickType.RIGHT || click == ClickType.SHIFT_RIGHT; + } + + /// + /// Gets whether this ClickType represents a left click. + /// + /// The click type. + /// true if this ClickType represents a left click. + public static bool isLeftClick(this ClickType click) + { + return click == ClickType.LEFT || click == ClickType.SHIFT_LEFT + || click == ClickType.DOUBLE_CLICK || click == ClickType.CREATIVE; + } + + /// + /// Gets whether this ClickType indicates that the shift key was pressed + /// down when the click was made. + /// + /// The click type. + /// true if the action uses Shift. + public static bool isShiftClick(this ClickType click) + { + return click == ClickType.SHIFT_LEFT || click == ClickType.SHIFT_RIGHT; + } +} diff --git a/Minecraft.Server.FourKit/Event/Inventory/InventoryAction.cs b/Minecraft.Server.FourKit/Event/Inventory/InventoryAction.cs new file mode 100644 index 00000000..ec0404d5 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Inventory/InventoryAction.cs @@ -0,0 +1,46 @@ +namespace Minecraft.Server.FourKit.Event.Inventory; + +/// +/// An estimation of what the result will be. +/// +public enum InventoryAction +{ + /// Nothing will happen from the click. + NOTHING, + /// All of the items on the clicked slot are moved to the cursor. + PICKUP_ALL, + /// Some of the items on the clicked slot are moved to the cursor. + PICKUP_SOME, + /// Half of the items on the clicked slot are moved to the cursor. + PICKUP_HALF, + /// One of the items on the clicked slot are moved to the cursor. + PICKUP_ONE, + /// All of the items on the cursor are moved to the clicked slot. + PLACE_ALL, + /// Some of the items from the cursor are moved to the clicked slot (usually up to the max stack size). + PLACE_SOME, + /// A single item from the cursor is moved to the clicked slot. + PLACE_ONE, + /// The clicked item and the cursor are exchanged. + SWAP_WITH_CURSOR, + /// The entire cursor item is dropped. + DROP_ALL_CURSOR, + /// One item is dropped from the cursor. + DROP_ONE_CURSOR, + /// The entire clicked slot is dropped. + DROP_ALL_SLOT, + /// One item is dropped from the clicked slot. + DROP_ONE_SLOT, + /// The item is moved to the opposite inventory if a space is found. + MOVE_TO_OTHER_INVENTORY, + /// The clicked item is moved to the hotbar, and the item currently there is re-added to the player's inventory. + HOTBAR_MOVE_AND_READD, + /// The clicked slot and the picked hotbar slot are swapped. + HOTBAR_SWAP, + /// A max-size stack of the clicked item is put on the cursor. + CLONE_STACK, + /// The inventory is searched for the same material, and they are put on the cursor up to Material.getMaxStackSize(). + COLLECT_TO_CURSOR, + /// An unrecognized ClickType. + UNKNOWN, +} diff --git a/Minecraft.Server.FourKit/Event/Inventory/InventoryClickEvent.cs b/Minecraft.Server.FourKit/Event/Inventory/InventoryClickEvent.cs new file mode 100644 index 00000000..ea8b2dff --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Inventory/InventoryClickEvent.cs @@ -0,0 +1,157 @@ +namespace Minecraft.Server.FourKit.Event.Inventory; + +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Inventory; + +/// +/// This event is called when a player clicks a slot in an inventory. +/// +/// Because InventoryClickEvent occurs within a modification of the Inventory, +/// not all Inventory related methods are safe to use. +/// +/// +/// The following should never be invoked by an EventHandler for +/// InventoryClickEvent using the HumanEntity or InventoryView associated +/// with this event: +/// +/// +/// +/// +/// +/// +/// +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); + } + + /// + /// Gets the inventory that was clicked, or null if outside of window. + /// + /// The clicked inventory. + public Inventory? getClickedInventory() + { + if (_rawSlot == InventoryView.OUTSIDE) + return null; + int topSize = getView().getTopInventory().getSize(); + if (_rawSlot < topSize) + return getView().getTopInventory(); + return getView().getBottomInventory(); + } + + /// + /// Gets the type of slot that was clicked. + /// + /// The slot type. + public SlotType getSlotType() => _slotType; + + /// + /// Gets the current ItemStack on the cursor. + /// + /// The cursor ItemStack. + public ItemStack? getCursor() => getView().getCursor(); + + /// + /// Gets the ItemStack currently in the clicked slot. + /// + /// The item in the clicked slot. + public ItemStack? getCurrentItem() => _currentItem; + + /// + /// Gets whether or not the ClickType for this event represents a right click. + /// + /// true if the ClickType uses the right mouse button. + public bool isRightClick() => _click.isRightClick(); + + /// + /// Gets whether or not the ClickType for this event represents a left click. + /// + /// true if the ClickType uses the left mouse button. + public bool isLeftClick() => _click.isLeftClick(); + + /// + /// Gets whether the ClickType for this event indicates that the key was + /// pressed down when the click was made. + /// + /// true if the ClickType uses Shift or Ctrl. + public bool isShiftClick() => _click.isShiftClick(); + + /// + /// Sets the item on the cursor. + /// + /// The new cursor item. + [Obsolete("This changes the ItemStack in their hand before any calculations are applied to the Inventory.")] + public void setCursor(ItemStack? stack) => getView().setCursor(stack); + + /// + /// Sets the ItemStack currently in the clicked slot. + /// + /// The item to be placed in the current slot. + public void setCurrentItem(ItemStack? stack) + { + _currentItem = stack; + if (_rawSlot >= 0) + getView().setItem(_rawSlot, stack); + } + + /// + /// The slot number that was clicked, ready for passing to + /// . Note that there may be two slots + /// with the same slot number, since a view links two different inventories. + /// + /// The slot number. + public int getSlot() => _whichSlot; + + /// + /// The raw slot number clicked, ready for passing to + /// . This slot number is unique + /// for the view. + /// + /// The raw slot number. + public int getRawSlot() => _rawSlot; + + /// + /// If the ClickType is NUMBER_KEY, this method will return the index of + /// the pressed key (0-8). + /// + /// The number on the key minus 1 (range 0-8); or -1 if not a NUMBER_KEY action. + public int getHotbarButton() => _hotbarKey; + + /// + /// 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. + /// + /// The InventoryAction that triggered this event. + public InventoryAction getAction() => _action; + + /// + /// Gets the ClickType for this event. + /// This is insulated against changes to the inventory by other plugins. + /// + /// The type of inventory click. + public ClickType getClick() => _click; +} diff --git a/Minecraft.Server.FourKit/Event/Inventory/InventoryEvent.cs b/Minecraft.Server.FourKit/Event/Inventory/InventoryEvent.cs new file mode 100644 index 00000000..8a5f4781 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Inventory/InventoryEvent.cs @@ -0,0 +1,45 @@ +namespace Minecraft.Server.FourKit.Event.Inventory; + +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Inventory; + +/// +/// Represents a player related inventory event. +/// +public class InventoryEvent : Event +{ + /// The inventory view associated with this event. + protected readonly InventoryView transaction; + internal InventoryEvent(InventoryView transaction) + { + this.transaction = transaction; + } + + /// + /// Gets the primary Inventory involved in this transaction. + /// + /// The upper inventory. + public global::Minecraft.Server.FourKit.Inventory.Inventory getInventory() + { + return transaction.getTopInventory(); + } + + /// + /// Gets the list of players viewing the primary (upper) inventory + /// involved in this event. + /// + /// A list of people viewing. + public List getViewers() + { + return transaction.getTopInventory().getViewers(); + } + + /// + /// Gets the view object itself. + /// + /// The InventoryView. + public InventoryView getView() + { + return transaction; + } +} diff --git a/Minecraft.Server.FourKit/Event/Inventory/InventoryInteractEvent.cs b/Minecraft.Server.FourKit/Event/Inventory/InventoryInteractEvent.cs new file mode 100644 index 00000000..bdc3b89b --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Inventory/InventoryInteractEvent.cs @@ -0,0 +1,34 @@ +namespace Minecraft.Server.FourKit.Event.Inventory; + +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Inventory; + +/// +/// An abstract base class for events that describe an interaction between a +/// and the contents of an . +/// This is currently not emitted anywhere, use instead. +/// +public abstract class InventoryInteractEvent : InventoryEvent, Cancellable +{ + private bool _cancelled; + internal protected InventoryInteractEvent(InventoryView transaction) : base(transaction) + { + } + + /// + /// Gets the player who performed the click. + /// + /// The clicking player. + public HumanEntity getWhoClicked() + { + return transaction.getPlayer(); + } + + + /// + public bool isCancelled() => _cancelled; + + + /// + public void setCancelled(bool cancel) => _cancelled = cancel; +} diff --git a/Minecraft.Server.FourKit/Event/Inventory/InventoryOpenEvent.cs b/Minecraft.Server.FourKit/Event/Inventory/InventoryOpenEvent.cs new file mode 100644 index 00000000..5b2588f0 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Inventory/InventoryOpenEvent.cs @@ -0,0 +1,38 @@ +namespace Minecraft.Server.FourKit.Event.Inventory; + +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Inventory; + +/// +/// Called when a player opens an inventory. Cancelling this event will prevent +/// the inventory screen from showing. +/// +public class InventoryOpenEvent : InventoryEvent, Cancellable +{ + private bool _cancelled; + internal InventoryOpenEvent(InventoryView transaction) : base(transaction) + { + } + + /// + /// Returns the player involved in this event. + /// + /// Player who is involved in this event. + public HumanEntity getPlayer() => transaction.getPlayer(); + + /// + /// 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. + /// + /// true if this event is cancelled. + public bool isCancelled() => _cancelled; + + /// + /// 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. + /// + /// true if you wish to cancel this event. + public void setCancelled(bool cancel) => _cancelled = cancel; +} diff --git a/Minecraft.Server.FourKit/Event/Listener.cs b/Minecraft.Server.FourKit/Event/Listener.cs new file mode 100644 index 00000000..fcb7f726 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Listener.cs @@ -0,0 +1,9 @@ +namespace Minecraft.Server.FourKit.Event; + +/// +/// Simple interface for tagging all EventListeners +/// Register instances with . +/// +public interface Listener +{ +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerBedEnterEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerBedEnterEvent.cs new file mode 100644 index 00000000..5647da28 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerBedEnterEvent.cs @@ -0,0 +1,33 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Block; + +/// +/// This event is fired when the player is almost about to enter the bed. +/// +public class PlayerBedEnterEvent : PlayerEvent, Cancellable +{ + private readonly Block _bed; + private bool _cancelled; + + internal PlayerBedEnterEvent(Player player, Block bed) : base(player) + { + _bed = bed; + } + + /// + /// Returns the bed block involved in this event. + /// + /// the bed block involved in this event + public Block getBed() => _bed; + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) + { + _cancelled = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerBedLeaveEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerBedLeaveEvent.cs new file mode 100644 index 00000000..aae1a50b --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerBedLeaveEvent.cs @@ -0,0 +1,23 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Block; + +/// +/// This event is fired when the player is leaving a bed. +/// +public class PlayerBedLeaveEvent : PlayerEvent +{ + private readonly Block _bed; + + internal PlayerBedLeaveEvent(Player player, Block bed) : base(player) + { + _bed = bed; + } + + /// + /// Returns the bed block involved in this event. + /// + /// the bed block involved in this event + public Block getBed() => _bed; +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerChatEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerChatEvent.cs new file mode 100644 index 00000000..6888a2f2 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerChatEvent.cs @@ -0,0 +1,74 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Fired when a player sends a chat message. +/// +/// When the event finishes execution the server formats the final +/// output using the same format specifiers from Java. +/// %1$s is the player's display name and %2$s is the +/// message, exactly like Bukkits PlayerChatEvent. +/// +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"; + } + + /// + /// Gets the message that the player is attempting to send. + /// This message will be used with . + /// + /// Message the player is attempting to send. + public string getMessage() => _message; + + /// + /// Sets the message that the player will send. + /// This message will be used with . + /// + /// New message that the player will send. + public void setMessage(string message) + { + _message = message; + } + + /// + /// Gets the format used to display this chat message. + /// + /// When this event finishes execution, the first format parameter + /// (%1$s) is Player.getDisplayName() and the second + /// parameter (%2$s) is getMessage(). + /// + /// A Java-style positional format string compatible with Bukkit. + public string getFormat() => _format; + + /// + /// Sets the format used to display this chat message. + /// + /// When this event finishes execution, the first format parameter + /// (%1$s) is Player.getDisplayName() and the second + /// parameter (%2$s) is getMessage(). + /// + /// A Java-style positional format string (e.g. "<%1$s> %2$s"). + /// If format is null. + public void setFormat(string format) + { + ArgumentNullException.ThrowIfNull(format); + _format = format; + } + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) + { + _cancelled = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerCommandPreprocessEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerCommandPreprocessEvent.cs new file mode 100644 index 00000000..dd20a758 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerCommandPreprocessEvent.cs @@ -0,0 +1,53 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Called early in the command handling process. This event is only for very +/// exceptional cases and you should not normally use it. +/// +/// If a PlayerCommandPreprocessEvent is cancelled, the command will not +/// be executed in the server, but will still pass to other plugins. +/// +public class PlayerCommandPreprocessEvent : PlayerEvent, Cancellable +{ + private string _message; + private bool _cancel; + + internal PlayerCommandPreprocessEvent(Player player, string message) : base(player) + { + _message = message; + _cancel = false; + } + + /// + public bool isCancelled() => _cancel; + + /// + public void setCancelled(bool cancel) + { + _cancel = cancel; + } + + /// + /// Gets the command that the player is attempting to send. All commands + /// begin with a special character; implementations do not consider the + /// first character when executing the content. + /// + /// Message the player is attempting to send. + public string getMessage() => _message; + + /// + /// Sets the command that the player will send. All commands begin with a + /// special character; implementations do not consider the first character + /// when executing the content. + /// + /// New message that the player will send. + /// If command is null or empty. + public void setMessage(string command) + { + if (string.IsNullOrEmpty(command)) + throw new ArgumentException("Command may not be null or empty", nameof(command)); + _message = command; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerDropItemEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerDropItemEvent.cs new file mode 100644 index 00000000..b0d00468 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerDropItemEvent.cs @@ -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 + +/// +/// 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. +/// +public class PlayerDropItemEvent : PlayerEvent, Cancellable +{ + private ItemStack _itemDrop; + private bool _cancelled; + internal PlayerDropItemEvent(Player player, ItemStack drop) + : base(player) + { + _itemDrop = drop; + } + + /// + /// Gets the ItemDrop created by the player. + /// + /// The ItemStack being dropped. + public ItemStack getItemDrop() => _itemDrop; + + /// + /// Sets the item to be dropped. Plugins can modify which item + /// is actually dropped. + /// + /// The new item to drop. + public void setItemDrop(ItemStack item) + { + _itemDrop = item; + } + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) + { + _cancelled = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerEvent.cs new file mode 100644 index 00000000..733b1ed7 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerEvent.cs @@ -0,0 +1,19 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Base class for events related to a . +/// +public abstract class PlayerEvent : Event +{ + private readonly Player _player; + + internal protected PlayerEvent(Player player) + { + _player = player; + } + + /// Returns the player involved in this event. + public Player getPlayer() => _player; +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerInteractEntityEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerInteractEntityEvent.cs new file mode 100644 index 00000000..c4ab8356 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerInteractEntityEvent.cs @@ -0,0 +1,32 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Represents an event that is called when a player right clicks an entity. +/// +public class PlayerInteractEntityEvent : PlayerEvent, Cancellable +{ + /// The entity that was right-clicked. + protected Entity clickedEntity; + + private bool _cancelled; + + internal PlayerInteractEntityEvent(Player who, Entity clickedEntity) + : base(who) + { + this.clickedEntity = clickedEntity; + } + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) => _cancelled = cancel; + + /// + /// Gets the entity that was right-clicked by the player. + /// + /// entity right clicked by player + public Entity getRightClicked() => clickedEntity; +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerInteractEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerInteractEvent.cs new file mode 100644 index 00000000..5a3497b1 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerInteractEvent.cs @@ -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; + +/// +/// Called when a player interacts with an object or air. +/// +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; + } + + /// + /// Returns the action type. + /// + /// Action returns the type of interaction. + public Action getAction() => _action; + + /// + public bool isCancelled() => _cancelled; + + /// + /// 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). + /// + /// true if you wish to cancel this event. + public void setCancelled(bool cancel) => _cancelled = cancel; + + /// + /// Returns the item in hand represented by this event. + /// + /// ItemStack the item used. + public ItemStack? getItem() => _item; + + /// + /// Convenience method. Returns the material of the item represented by this event. + /// + /// Material the material of the item used. + public Material getMaterial() => _item?.getType() ?? Material.AIR; + + /// + /// Check if this event involved a block. + /// + /// true if it did. + public bool hasBlock() => _clickedBlock != null; + + /// + /// Check if this event involved an item. + /// + /// true if it did. + public bool hasItem() => _item != null; + + /// + /// Convenience method to inform the user whether this was a block placement event. + /// + /// true if the item in hand was a block. + public bool isBlockInHand() + { + if (_item == null) return false; + int id = (int)_item.getType(); + return id >= 1 && id <= 255; + } + + /// + /// Returns the clicked block. + /// + /// Block returns the block clicked with this item. + public FourKitBlock? getClickedBlock() => _clickedBlock; + + /// + /// Returns the face of the block that was clicked. + /// + /// BlockFace returns the face of the block that was clicked. + public BlockFace getBlockFace() => _clickedFace; + + /// + /// 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. + /// + /// the action to take with the item in hand. + public bool useItemInHand() => _useItemInHand; + + /// + /// Sets whether to use the item in hand. + /// + /// the action to take with the item in hand. + public void setUseItemInHand(bool useItemInHand) => _useItemInHand = useItemInHand; +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerJoinEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerJoinEvent.cs new file mode 100644 index 00000000..a0b5312b --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerJoinEvent.cs @@ -0,0 +1,30 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Called when a player joins a server +/// +public class PlayerJoinEvent : PlayerEvent +{ + private string _joinMessage; + internal PlayerJoinEvent(Player player) : base(player) + { + _joinMessage = $"{player.getName()} joined the game"; + } + + /// + /// Gets the join message to send to all online players + /// + /// string join message + public string getJoinMessage() => _joinMessage; + + /// + /// Sets the join message to send to all online players + /// + /// join message. + public void setJoinMessage(string? joinMessage) + { + _joinMessage = joinMessage ?? string.Empty; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerKickEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerKickEvent.cs new file mode 100644 index 00000000..b318f043 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerKickEvent.cs @@ -0,0 +1,62 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// 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. +/// +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; + } + + /// + /// Gets the reason why the player is getting kicked. + /// + /// The disconnect reason. + public DisconnectReason getReason() => _reason; + + /// + /// Sets the reason why the player is getting kicked. + /// + /// The new disconnect reason. + public void setReason(DisconnectReason kickReason) + { + _reason = kickReason; + } + + /// + /// Gets the leave message sent to all online players. + /// + /// The leave message. + public string getLeaveMessage() => _leaveMessage; + + /// + /// Sets the leave message sent to all online players. + /// + /// The new leave message. + public void setLeaveMessage(string leaveMessage) + { + _leaveMessage = leaveMessage; + } + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) + { + _cancelled = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerLoginEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerLoginEvent.cs new file mode 100644 index 00000000..87fa25f6 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerLoginEvent.cs @@ -0,0 +1,93 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Enums; +using Minecraft.Server.FourKit.Net; + +/// +/// Stores details for players attempting to log in. +/// +public class PlayerLoginEvent : Event, Cancellable +{ + private string name; + private InetSocketAddress ipAddress; //bukkit uses InetAddress but we expose port also + + private ulong onlineXuid; + private ulong offlineXuid; + private bool changedXuidValues; + + private LoginType loginType; + + private bool _cancelled; + + internal PlayerLoginEvent(string name, InetSocketAddress ipAddress, LoginType type, ulong onlineXuid, ulong offlineXuid) : base() + { + this.name = name; + this.ipAddress = ipAddress; + this.onlineXuid = onlineXuid; + this.offlineXuid = offlineXuid; + this.changedXuidValues = false; + + this.loginType = type; + } + + public LoginType getLoginType() => loginType; + + + /// + /// Experimental. Gets the online XUID (Xbox User ID), used for guests (splitscreen users). + /// + /// The online XUID value. + public ulong getOnlineXuid() => onlineXuid; + + /// + /// Experimental. Sets the online XUID (Xbox User ID). Marks XUID values as changed. + /// + /// The new online XUID value. + public void setOnlineXuid(ulong newXuid) + { + this.onlineXuid = newXuid; + this.changedXuidValues = true; + } + + /// + /// Experimental. Gets the offline XUID (Xbox User ID), which is the main XUID used by the client. + /// + /// The offline XUID value. + public ulong getOfflineXuid() => offlineXuid; + + /// + /// Experimental. Sets the offline XUID (Xbox User ID). Marks XUID values as changed. + /// + /// The new offline XUID value. + public void setOfflineXuid(ulong newXuid) + { + this.offlineXuid = newXuid; + this.changedXuidValues = true; + } + + /// + /// Experimental. Returns true if either XUID value has been changed via setters. + /// + /// True if XUID values have been changed; otherwise, false. + public bool hasChangedXuidValues() => changedXuidValues; + + + /// + /// Gets the player's name. + /// + /// The player's name. + public string getName() => name; + + + /// + /// Gets the player IP address. + /// + /// The IP address. + public InetSocketAddress getAddress() => ipAddress; + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) => _cancelled = cancel; +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerMoveEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerMoveEvent.cs new file mode 100644 index 00000000..b882336e --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerMoveEvent.cs @@ -0,0 +1,59 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Fired when a player moves. Plugins may modify the destination +/// or cancel the movement entirely. +/// +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; + } + + /// + /// Gets the location this player moved from. + /// + /// The from location. + public Location getFrom() => _from; + + /// + /// Gets the location this player moved to. + /// + /// The to location. + public Location getTo() => _to; + + /// + /// Sets the location to mark as where the player moved from. + /// + /// The new from location. + public void setFrom(Location from) + { + _from = from; + } + + /// + /// Sets the location that this player will move to. + /// + /// The new to location. + public void setTo(Location to) + { + _to = to; + } + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) + { + _cancelled = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerPickupItemEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerPickupItemEvent.cs new file mode 100644 index 00000000..a03b4638 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerPickupItemEvent.cs @@ -0,0 +1,41 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Thrown when a player picks an item up from the ground. +/// If cancelled the item will not be picked up. +/// +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; + } + + /// + /// Gets the Item picked up by the player. + /// + /// The entity. + public Item getItem() => _item; + + /// + /// Gets the amount remaining on the ground, if any. + /// + /// Amount remaining on the ground. + public int getRemaining() => _remaining; + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) + { + _cancelled = cancel; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerPortalEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerPortalEvent.cs new file mode 100644 index 00000000..8a4d543a --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerPortalEvent.cs @@ -0,0 +1,22 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Called when a player is about to teleport because it is in contact with a portal. +/// +public class PlayerPortalEvent : PlayerTeleportEvent +{ + internal PlayerPortalEvent(Player player, Location from, Location to) + : base(player, from, to, TeleportCause.UNKNOWN) { } + + /// + /// Constructs a new PlayerPortalEvent with the given cause. + /// + /// The player entering the portal. + /// The location the player is coming from. + /// The location the player is teleporting to. + /// The cause of this teleportation (should be a portal-related cause). + public PlayerPortalEvent(Player player, Location from, Location to, TeleportCause cause) + : base(player, from, to, cause) { } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerPreLoginEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerPreLoginEvent.cs new file mode 100644 index 00000000..a3015b1e --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerPreLoginEvent.cs @@ -0,0 +1,40 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Net; + +/// +/// Stores details for players attempting to log in. +/// +public class PlayerPreLoginEvent : Event, Cancellable +{ + private string name; + private InetSocketAddress ipAddress; //bukkit uses InetAddress but we expose port also + private bool _cancelled; + + + internal PlayerPreLoginEvent(string name, InetSocketAddress ipAddress) : base() + { + this.name = name; + this.ipAddress = ipAddress; + } + + + /// + /// Gets the player's name. + /// + /// The player's name. + public string getName() => name; + + + /// + /// Gets the player IP address. + /// + /// The IP address. + public InetSocketAddress getAddress() => ipAddress; + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) => _cancelled = cancel; +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerQuitEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerQuitEvent.cs new file mode 100644 index 00000000..dfd7326a --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerQuitEvent.cs @@ -0,0 +1,32 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Fired when a player disconnects from the server. +/// Plugins may read or modify the quit message that is broadcast to all +/// online players. +/// +public class PlayerQuitEvent : PlayerEvent +{ + private string _quitMessage; + internal PlayerQuitEvent(Player player) : base(player) + { + _quitMessage = $"{player.getName()} left the game"; + } + + /// + /// Gets the quit message to send to all online players. + /// + /// The quit message. + public string getQuitMessage() => _quitMessage; + + /// + /// Sets the quit message to send to all online players. + /// + /// The new quit message, or null to suppress it. + public void setQuitMessage(string? quitMessage) + { + _quitMessage = quitMessage ?? string.Empty; + } +} diff --git a/Minecraft.Server.FourKit/Event/Player/PlayerTeleportEvent.cs b/Minecraft.Server.FourKit/Event/Player/PlayerTeleportEvent.cs new file mode 100644 index 00000000..ce453c4f --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Player/PlayerTeleportEvent.cs @@ -0,0 +1,44 @@ +namespace Minecraft.Server.FourKit.Event.Player; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Holds information for player teleport events. +/// +public class PlayerTeleportEvent : PlayerMoveEvent +{ + /// + /// Represents the cause of a player teleportation. + /// + public enum TeleportCause + { + /// Indicates the teleportation was caused by a player throwing an Ender Pearl. + ENDER_PEARL, + /// Indicates the teleportation was caused by a player executing a command. + COMMAND, + /// Indicates the teleportation was caused by a plugin. + PLUGIN, + /// Indicates the teleportation was caused by a player entering a Nether portal. + NETHER_PORTAL, + /// Indicates the teleportation was caused by a player entering an End portal. + END_PORTAL, + /// Indicates the teleportation was caused by an event not covered by this enum. + 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; + } + + /// + /// Gets the cause of this teleportation event. + /// + /// The cause of the event. + public TeleportCause getCause() => _cause; +} diff --git a/Minecraft.Server.FourKit/Event/Server/PluginDisableEvent.cs b/Minecraft.Server.FourKit/Event/Server/PluginDisableEvent.cs new file mode 100644 index 00000000..6ef8ad0d --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Server/PluginDisableEvent.cs @@ -0,0 +1,11 @@ +namespace Minecraft.Server.FourKit.Event.Server; + +using Minecraft.Server.FourKit.Plugin; + +public class PluginDisableEvent : PluginEvent +{ + + internal PluginDisableEvent(ServerPlugin plugin) : base(plugin) + { + } +} diff --git a/Minecraft.Server.FourKit/Event/Server/PluginEnableEvent.cs b/Minecraft.Server.FourKit/Event/Server/PluginEnableEvent.cs new file mode 100644 index 00000000..9218bcc8 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Server/PluginEnableEvent.cs @@ -0,0 +1,11 @@ +namespace Minecraft.Server.FourKit.Event.Server; + +using Minecraft.Server.FourKit.Plugin; + +public class PluginEnableEvent : PluginEvent +{ + + internal PluginEnableEvent(ServerPlugin plugin) : base(plugin) + { + } +} diff --git a/Minecraft.Server.FourKit/Event/Server/PluginEvent.cs b/Minecraft.Server.FourKit/Event/Server/PluginEvent.cs new file mode 100644 index 00000000..ebc49294 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Server/PluginEvent.cs @@ -0,0 +1,16 @@ +namespace Minecraft.Server.FourKit.Event.Server; + +using Minecraft.Server.FourKit.Plugin; + +public abstract class PluginEvent : ServerEvent +{ + private readonly ServerPlugin _plugin; + + internal protected PluginEvent(ServerPlugin plugin) : base() + { + _plugin = plugin; + } + + /// Returns the plugin involved in this event. + public ServerPlugin getPlugin() => _plugin; +} diff --git a/Minecraft.Server.FourKit/Event/Server/PluginLoadFailedEvent.cs b/Minecraft.Server.FourKit/Event/Server/PluginLoadFailedEvent.cs new file mode 100644 index 00000000..a15ad97f --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Server/PluginLoadFailedEvent.cs @@ -0,0 +1,18 @@ +namespace Minecraft.Server.FourKit.Event.Server; + +using Minecraft.Server.FourKit.Plugin; + +public class PluginLoadFailedEvent : ServerEvent +{ + private readonly string _fileName; + private readonly string _message; + internal PluginLoadFailedEvent(string fileName, string message) : base() + { + _fileName = fileName; + _message = message; + } + + public string getFileName() => _fileName; + + public string getMessage() => _message; +} diff --git a/Minecraft.Server.FourKit/Event/Server/PluginsLoadedEvent.cs b/Minecraft.Server.FourKit/Event/Server/PluginsLoadedEvent.cs new file mode 100644 index 00000000..91c82089 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Server/PluginsLoadedEvent.cs @@ -0,0 +1,10 @@ +namespace Minecraft.Server.FourKit.Event.Server; + +using Minecraft.Server.FourKit.Plugin; + +public class PluginsLoadedEvent : ServerEvent +{ + internal PluginsLoadedEvent() : base() + { + } +} diff --git a/Minecraft.Server.FourKit/Event/Server/ServerEvent.cs b/Minecraft.Server.FourKit/Event/Server/ServerEvent.cs new file mode 100644 index 00000000..e3b437d8 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/Server/ServerEvent.cs @@ -0,0 +1,11 @@ +namespace Minecraft.Server.FourKit.Event.Server; + +using Minecraft.Server.FourKit.Plugin; + +public abstract class ServerEvent : Event +{ + + internal protected ServerEvent() : base() + { + } +} diff --git a/Minecraft.Server.FourKit/Event/World/StructureGrowEvent.cs b/Minecraft.Server.FourKit/Event/World/StructureGrowEvent.cs new file mode 100644 index 00000000..86567a93 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/World/StructureGrowEvent.cs @@ -0,0 +1,60 @@ +using Minecraft.Server.FourKit.Enums; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Minecraft.Server.FourKit.Event.World; + +/// +/// Event that is called when an organic structure attempts to grow (Sapling -> Tree), (Mushroom -> Huge Mushroom), naturally or using bonemeal. +/// +public class StructureGrowEvent : WorldEvent, Cancellable +{ + internal Location _location; + internal TreeType _treeType; + internal bool _wasBonemeal; + internal Minecraft.Server.FourKit.Entity.Player? _player; + + internal bool _cancelled; + internal StructureGrowEvent(Location location, TreeType treeType, bool wasBonemeal, Minecraft.Server.FourKit.Entity.Player? player) : base(location.getWorld()) + { + _location = location; + _treeType = treeType; + _wasBonemeal = wasBonemeal; + _player = player; + } + + + /// + /// Gets the species type (birch, normal, pine, red mushroom, brown mushroom). + /// + /// Structure species + public TreeType getSpecies() => _treeType; + + + /// + /// Gets the location of the structure. + /// + /// Location of the structure + public Location getLocation() => _location; + + + /// + /// Checks if structure was grown using bonemeal. + /// + /// True if the structure was grown using bonemeal. + public bool isFromBonemeal() => _wasBonemeal; + + + /// + /// Gets the player that created the structure. + /// + /// Player that created the structure, null if was not created manually + public Minecraft.Server.FourKit.Entity.Player? getPlayer() => _player; + + /// + public bool isCancelled() => _cancelled; + + /// + public void setCancelled(bool cancel) => _cancelled = cancel; +} diff --git a/Minecraft.Server.FourKit/Event/World/WorldEvent.cs b/Minecraft.Server.FourKit/Event/World/WorldEvent.cs new file mode 100644 index 00000000..ddbd4f97 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/World/WorldEvent.cs @@ -0,0 +1,13 @@ +namespace Minecraft.Server.FourKit.Event.World; + +using Minecraft.Server.FourKit; + +public class WorldEvent : Event +{ + internal World? _world; + + internal WorldEvent(World? world) : base() + { + _world = world; + } +} diff --git a/Minecraft.Server.FourKit/Event/World/WorldSaveEvent.cs b/Minecraft.Server.FourKit/Event/World/WorldSaveEvent.cs new file mode 100644 index 00000000..37c6be46 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/World/WorldSaveEvent.cs @@ -0,0 +1,11 @@ +namespace Minecraft.Server.FourKit.Event.World; + +using Minecraft.Server.FourKit; + +public class WorldSaveEvent : Event +{ + + internal WorldSaveEvent() : base() + { + } +} diff --git a/Minecraft.Server.FourKit/EventDispatcher.cs b/Minecraft.Server.FourKit/EventDispatcher.cs new file mode 100644 index 00000000..9c5bc2c1 --- /dev/null +++ b/Minecraft.Server.FourKit/EventDispatcher.cs @@ -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 + { + 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> _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(); + 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(); + _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? handlers; + lock (_lock) + { + if (!_handlers.TryGetValue(evt.GetType(), out handlers)) + return; + + handlers = new List(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}"); + } + } + } +} diff --git a/Minecraft.Server.FourKit/Experimental/PlayerConnection.cs b/Minecraft.Server.FourKit/Experimental/PlayerConnection.cs new file mode 100644 index 00000000..76b39aa1 --- /dev/null +++ b/Minecraft.Server.FourKit/Experimental/PlayerConnection.cs @@ -0,0 +1,35 @@ +using Minecraft.Server.FourKit.Entity; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace Minecraft.Server.FourKit.Experimental; + +public class PlayerConnection +{ + private Player _player; + + internal PlayerConnection(Player player) + { + this._player = player; + } + + /// + /// Sends raw packet data directly to the client over the player's connection. + /// The byte array must contain the complete packet including the packet ID as the first byte. The server automatically prepends the 4-byte big-endian size header before transmitting. + /// + /// The raw packet bytes to send, where data[0] is the packet ID. + public void send(byte[] data) + { + var gh = GCHandle.Alloc(data, GCHandleType.Pinned); + try + { + NativeBridge.SendRaw?.Invoke(_player.getEntityId(), gh.AddrOfPinnedObject(), data.Length); + } + finally + { + gh.Free(); + } + } +} diff --git a/Minecraft.Server.FourKit/FourKit.cs b/Minecraft.Server.FourKit/FourKit.cs new file mode 100644 index 00000000..7da6bcae --- /dev/null +++ b/Minecraft.Server.FourKit/FourKit.cs @@ -0,0 +1,391 @@ +namespace Minecraft.Server.FourKit; + +using Minecraft.Server.FourKit.Command; +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Event; +using Minecraft.Server.FourKit.Inventory; +using Minecraft.Server.FourKit.Plugin; + +/// +/// The main entry point for the FourKit plugin API. +/// +public static class FourKit +{ + private static readonly EventDispatcher _dispatcher = new(); + private static readonly Dictionary _players = new(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary _playersByEntityId = new(); + private static readonly object _playerLock = new(); + + internal const int MAX_CHAT_LENGTH = 123; + + private static readonly Dictionary _worldsByDimId = new(); + private static readonly Dictionary _worldNameToDimId = new(StringComparer.OrdinalIgnoreCase) + { + // grr + ["world"] = 0, + ["world_nether"] = -1, + ["world_the_end"] = 1, + }; + private static readonly object _worldLock = new(); + + /// + /// Gets a world by its name. Supported names: "world" (overworld), + /// "world_nether" (nether), "world_the_end" (the end). + /// + /// The name of the world to retrieve. + /// The world with the given name, or null if none exists. + public static World? getWorld(string name) + { + if (_worldNameToDimId.TryGetValue(name, out int dimId)) + return getWorld(dimId); + return null; + } + + /// + /// Gets a world by its dimension ID (0 = overworld, -1 = nether, 1 = the end). + /// + /// The dimension ID. + /// The world for that dimension, creating it if necessary. + 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; + } + } + + /// + /// Registers all the events in the given listener class + /// + 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 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 Entity.Entity? GetEntityByEntityId(int entityId) + { + var player = GetPlayerByEntityId(entityId); + if (player != null) return player; + + if (NativeBridge.GetEntityInfo == null) return null; + + IntPtr buf = System.Runtime.InteropServices.Marshal.AllocHGlobal(5 * sizeof(double)); + try + { + NativeBridge.GetEntityInfo(entityId, buf); + double[] data = new double[5]; + System.Runtime.InteropServices.Marshal.Copy(buf, data, 0, 5); + + int typeId = (int)data[0]; + if (typeId < 0) return null; + + var entityType = Enum.IsDefined(typeof(Entity.EntityType), typeId) + ? (Entity.EntityType)typeId + : Entity.EntityType.UNKNOWN; + int dimId = (int)data[4]; + + var entity = new Entity.Entity(); + entity.SetEntityIdInternal(entityId); + entity.SetEntityTypeInternal(entityType); + entity.SetDimensionInternal(dimId); + entity.SetLocation(new Location(getWorld(dimId), data[1], data[2], data[3])); + return entity; + } + finally + { + System.Runtime.InteropServices.Marshal.FreeHGlobal(buf); + } + } + + 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 _commands = new(StringComparer.OrdinalIgnoreCase); + private static readonly object _commandLock = new(); + + /// + /// Gets a with the given name, creating it + /// if it does not already exist. The returned command can be configured + /// with , + /// , etc. + /// + /// Name of the command. + /// The command for that name. + 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; + } + + /// + /// Broadcasts a message to all online players. + /// + /// The message to broadcast. + 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); + } + } + + /// + /// Creates a new with the specified size. + /// The inventory will be of type with + /// the default title. + /// + /// The size of the inventory (must be a multiple of 9). + /// A new Inventory. + public static Inventory.Inventory createInventory(int size) + { + return new Inventory.Inventory("Chest", InventoryType.CHEST, size); + } + + /// + /// Creates a new with the specified size + /// and custom title. + /// + /// The size of the inventory (must be a multiple of 9). + /// The title that will be shown to players. + /// A new Inventory. + public static Inventory.Inventory createInventory(int size, string title) + { + return new Inventory.Inventory(title, InventoryType.CHEST, size); + } + + /// + /// Creates a new of the specified + /// with the default title and size. + /// + /// The type of inventory to create. + /// A new Inventory. + public static Inventory.Inventory createInventory(InventoryType type) + { + return new Inventory.Inventory(type.getDefaultTitle(), type, type.getDefaultSize()); + } + + /// + /// Creates a new of the specified + /// with a custom title. + /// + /// The type of inventory to create. + /// The title that will be shown to players. + /// A new Inventory. + public static Inventory.Inventory createInventory(InventoryType type, string title) + { + return new Inventory.Inventory(title, type, type.getDefaultSize()); + } + + /// + /// Checks if the given plugin is loaded and returns it when applicable. + /// + /// Please note that the name of the plugin is case-sensitive. + /// + /// + /// Name of the plugin to check. + /// Plugin if it exists, otherwise null + public static ServerPlugin? getPlugin(string name) { + var loadedPlugins = FourKitHost.getLoadedPlugins().Where(x => x.name == name); + + if (loadedPlugins.Count() > 1) ServerLog.Warn("fourkit", $"More than one instance of a(n) '{name}' plugin."); + return loadedPlugins.FirstOrDefault(); + } + + /// + /// Gets a list of all currently loaded plugins. + /// + /// The array of plugins. + public static ServerPlugin[] getPlugins() => FourKitHost.getLoadedPlugins().ToArray(); // returns an array for better compatibility for bukkit->fourkit + + /// + /// Enables the specified plugin. + /// + /// Plugin to enable. + public static void enablePlugin(ServerPlugin plugin) => FourKitHost.s_loader?.EnablePlugin(plugin); + + /// + /// Disables the specified plugin. + /// + /// Plugin to disable. + public static void disablePlugin(ServerPlugin plugin) => FourKitHost.s_loader?.DisablePlugin(plugin); +} diff --git a/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs b/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs new file mode 100644 index 00000000..f4e17ed1 --- /dev/null +++ b/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs @@ -0,0 +1,124 @@ +using System.Runtime.InteropServices; + +namespace Minecraft.Server.FourKit; + +public static partial class FourKitHost +{ + [UnmanagedCallersOnly] + public static void SetNativeCallbacks(IntPtr damage, IntPtr setHealth, IntPtr teleport, IntPtr setGameMode, IntPtr broadcastMessage, IntPtr setFallDistance, IntPtr getPlayerSnapshot, IntPtr sendMessage, IntPtr setWalkSpeed, IntPtr teleportEntity) + { + try + { + NativeBridge.SetCallbacks(damage, setHealth, teleport, setGameMode, broadcastMessage, setFallDistance, getPlayerSnapshot, sendMessage, setWalkSpeed, teleportEntity); + ServerLog.Info("fourkit", "Native callbacks registered."); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetNativeCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetWorldCallbacks(IntPtr getTileId, IntPtr getTileData, IntPtr setTile, IntPtr setTileData, IntPtr breakBlock, IntPtr getHighestBlockY, IntPtr getWorldInfo, IntPtr setWorldTime, IntPtr setWeather, IntPtr createExplosion, IntPtr strikeLightning, IntPtr setSpawnLocation, IntPtr dropItem) + { + try + { + NativeBridge.SetWorldCallbacks(getTileId, getTileData, setTile, setTileData, breakBlock, getHighestBlockY, getWorldInfo, setWorldTime, setWeather, createExplosion, strikeLightning, setSpawnLocation, dropItem); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetWorldCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetPlayerCallbacks(IntPtr kickPlayer, IntPtr banPlayer, IntPtr banPlayerIp, IntPtr getPlayerAddress, IntPtr getPlayerLatency) + { + try + { + NativeBridge.SetPlayerCallbacks(kickPlayer, banPlayer, banPlayerIp, getPlayerAddress, getPlayerLatency); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetPlayerCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetPlayerConnectionCallbacks(IntPtr sendRaw) + { + try + { + NativeBridge.SetPlayerConnectionCallbacks(sendRaw); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetPlayerConnectionCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot) + { + try + { + NativeBridge.SetInventoryCallbacks(getPlayerInventory, setPlayerInventorySlot, getContainerContents, setContainerSlot, getContainerViewerEntityIds, closeContainer, openVirtualContainer, getItemMeta, setItemMeta, setHeldItemSlot); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetInventoryCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetEntityCallbacks(IntPtr setSneaking, IntPtr setVelocity, IntPtr setAllowFlight, IntPtr playSound, IntPtr setSleepingIgnored) + { + try + { + NativeBridge.SetEntityCallbacks(setSneaking, setVelocity, setAllowFlight, playSound, setSleepingIgnored); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetEntityCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetExperienceCallbacks(IntPtr setLevel, IntPtr setExp, IntPtr giveExp, IntPtr giveExpLevels, IntPtr setFoodLevel, IntPtr setSaturation, IntPtr setExhaustion) + { + try + { + NativeBridge.SetExperienceCallbacks(setLevel, setExp, giveExp, giveExpLevels, setFoodLevel, setSaturation, setExhaustion); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetExperienceCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetParticleCallbacks(IntPtr spawnParticle) + { + try + { + NativeBridge.SetParticleCallbacks(spawnParticle); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetParticleCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetVehicleCallbacks(IntPtr setPassenger, IntPtr leaveVehicle, IntPtr eject, IntPtr getVehicleId, IntPtr getPassengerId, IntPtr getEntityInfo) + { + try + { + NativeBridge.SetVehicleCallbacks(setPassenger, leaveVehicle, eject, getVehicleId, getPassengerId, getEntityInfo); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetVehicleCallbacks error: {ex}"); + } + } +} diff --git a/Minecraft.Server.FourKit/FourKitHost.Events.cs b/Minecraft.Server.FourKit/FourKitHost.Events.cs new file mode 100644 index 00000000..08329ebd --- /dev/null +++ b/Minecraft.Server.FourKit/FourKitHost.Events.cs @@ -0,0 +1,1253 @@ +using System.Runtime.InteropServices; +using Minecraft.Server.FourKit.Block; +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Event; +using Minecraft.Server.FourKit.Event.Block; +using Minecraft.Server.FourKit.Event.Entity; +using Minecraft.Server.FourKit.Event.Player; +using Minecraft.Server.FourKit.Event.Inventory; +using Minecraft.Server.FourKit.Inventory; +using Minecraft.Server.FourKit.Net; +using Minecraft.Server.FourKit.Event.World; +using Minecraft.Server.FourKit.Enums; + +namespace Minecraft.Server.FourKit; + +public static partial class FourKitHost +{ + [UnmanagedCallersOnly] + public static void FireWorldSave() + { + try + { + FourKit.FireEvent(new WorldSaveEvent()); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireWorldSave error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerPreLogin(IntPtr namePtr, int nameByteLen, IntPtr ipPtr, int ipByteLen, int port) + { + try + { + string name = nameByteLen > 0 + ? Marshal.PtrToStringUTF8(namePtr, nameByteLen) ?? string.Empty + : string.Empty; + + string ipStr = ipByteLen > 0 + ? Marshal.PtrToStringUTF8(ipPtr, ipByteLen) ?? string.Empty + : string.Empty; + + var evt = new PlayerPreLoginEvent(name, new InetSocketAddress(new InetAddress(ipStr), port)); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerPreLogin error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerLogin(IntPtr namePtr, int nameByteLen, IntPtr ipPtr, int ipByteLen, int port, int type, IntPtr offlineXUIDPtr, IntPtr onlineXUIDPtr) + { + try + { + string name = nameByteLen > 0 + ? Marshal.PtrToStringUTF8(namePtr, nameByteLen) ?? string.Empty + : string.Empty; + + string ipStr = ipByteLen > 0 + ? Marshal.PtrToStringUTF8(ipPtr, ipByteLen) ?? string.Empty + : string.Empty; + + var evt = new PlayerLoginEvent(name, new InetSocketAddress(new InetAddress(ipStr), port), (LoginType)type, unchecked((ulong)Marshal.ReadInt64(onlineXUIDPtr)), unchecked((ulong)Marshal.ReadInt64(offlineXUIDPtr))); + FourKit.FireEvent(evt); + + if (evt.hasChangedXuidValues()) + { + Marshal.WriteInt64(offlineXUIDPtr, unchecked((long)evt.getOfflineXuid())); + Marshal.WriteInt64(onlineXUIDPtr, unchecked((long)evt.getOnlineXuid())); + } + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerLogin error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static void FirePlayerJoin(int entityId, IntPtr namePtr, int nameByteLen, IntPtr uuidPtr, int uuidByteLen, ulong offlineXUIDPtr, ulong onlineXUIDPtr) + { + try + { + string name = nameByteLen > 0 + ? Marshal.PtrToStringUTF8(namePtr, nameByteLen) ?? string.Empty + : string.Empty; + + string uuidStr = uuidByteLen > 0 + ? Marshal.PtrToStringUTF8(uuidPtr, uuidByteLen) ?? string.Empty + : string.Empty; + + var player = FourKit.TrackPlayer(entityId, name); + player.SetPlayerUniqueIdInternal(ParseOrHashGuid(uuidStr)); + player.SetPlayerRawOnlineXUIDInternal(onlineXUIDPtr); + player.SetPlayerRawOfflineXUIDInternal(offlineXUIDPtr); + SyncPlayerFromNative(player); + var evt = new PlayerJoinEvent(player); + FourKit.FireEvent(evt); + BroadcastNativeMessage(evt.getJoinMessage()); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerJoin error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void FirePlayerQuit(int entityId) + { + try + { + var player = FourKit.UntrackPlayer(entityId); + if (player != null) + { + SyncPlayerFromNative(player); + var evt = new PlayerQuitEvent(player); + FourKit.FireEvent(evt); + BroadcastNativeMessage(evt.getQuitMessage()); + } + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerQuit error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerKick(int entityId, int disconnectReason, + IntPtr reasonPtr, int reasonByteLen, + IntPtr outBuf, int outBufSize, IntPtr outLenPtr) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + Marshal.WriteInt32(outLenPtr, 0); + return 0; + } + + SyncPlayerFromNative(player); + + var reason = Enum.IsDefined(typeof(DisconnectReason), disconnectReason) + ? (DisconnectReason)disconnectReason + : DisconnectReason.NONE; + + string defaultLeave = $"{player.getName()} was kicked from the game"; + var evt = new PlayerKickEvent(player, reason, defaultLeave); + FourKit.FireEvent(evt); + + if (evt.isCancelled()) + { + Marshal.WriteInt32(outLenPtr, 0); + return 1; + } + + string leaveMessage = evt.getLeaveMessage(); + if (!string.IsNullOrEmpty(leaveMessage)) + { + byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(leaveMessage); + if (utf8Bytes.Length < outBufSize) + { + Marshal.Copy(utf8Bytes, 0, outBuf, utf8Bytes.Length); + Marshal.WriteInt32(outLenPtr, utf8Bytes.Length); + } + else + { + Marshal.WriteInt32(outLenPtr, 0); + } + } + else + { + Marshal.WriteInt32(outLenPtr, 0); + } + + return 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerKick error: {ex}"); + Marshal.WriteInt32(outLenPtr, 0); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerMove(int entityId, + double fromX, double fromY, double fromZ, + double toX, double toY, double toZ, + IntPtr outCoords) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + Marshal.Copy(new double[] { toX, toY, toZ }, 0, outCoords, 3); + return 0; + } + + SyncPlayerFromNative(player); + + var from = new Location(fromX, fromY, fromZ); + var to = new Location(toX, toY, toZ); + var evt = new PlayerMoveEvent(player, from, to); + FourKit.FireEvent(evt); + + var finalTo = evt.getTo(); + Marshal.Copy(new double[] { finalTo.X, finalTo.Y, finalTo.Z }, 0, outCoords, 3); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerMove error: {ex}"); + Marshal.Copy(new double[] { toX, toY, toZ }, 0, outCoords, 3); + return 0; + } + } + + [UnmanagedCallersOnly] + public static void UpdatePlayerEntityId(int oldEntityId, int newEntityId) + { + try + { + FourKit.UpdatePlayerEntityId(oldEntityId, newEntityId); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"UpdatePlayerEntityId error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static int FireStructureGrow(int dimId, int x, int y, int z, int treeType, int wasBonemeal, int entityId) + { + try + { + Location location = new Location(FourKit.getWorld(dimId), x, y, z); + + StructureGrowEvent evt = new StructureGrowEvent(location, (TreeType)treeType, wasBonemeal == 1, (wasBonemeal == 1 && entityId != -1 ? FourKit.GetPlayerByEntityId(entityId) : null)); + FourKit.FireEvent(evt); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireStructureGrow error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerChat(int entityId, IntPtr msgPtr, int msgByteLen, + IntPtr outBuf, int outBufSize, IntPtr outLenPtr) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + Marshal.WriteInt32(outLenPtr, 0); + return 0; + } + + SyncPlayerFromNative(player); + + string message = msgByteLen > 0 + ? Marshal.PtrToStringUTF8(msgPtr, msgByteLen) ?? string.Empty + : string.Empty; + + var evt = new PlayerChatEvent(player, message); + FourKit.FireEvent(evt); + + if (evt.isCancelled()) + { + Marshal.WriteInt32(outLenPtr, 0); + return 1; + } + + string formatted = JavaFormat(evt.getFormat(), player.getDisplayName(), evt.getMessage()); + byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(formatted); + if (utf8Bytes.Length < outBufSize) + { + Marshal.Copy(utf8Bytes, 0, outBuf, utf8Bytes.Length); + Marshal.WriteInt32(outLenPtr, utf8Bytes.Length); + } + else + { + Marshal.WriteInt32(outLenPtr, 0); + } + + return 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerChat error: {ex}"); + Marshal.WriteInt32(outLenPtr, 0); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireBlockPlace(int entityId, int dimId, + int placedX, int placedY, int placedZ, + int againstX, int againstY, int againstZ, + int itemId, int itemCount, int canBuild) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + return 0; + + SyncPlayerFromNative(player); + + var world = FourKit.getWorld(dimId); + var placedBlock = new Block.Block(world, placedX, placedY, placedZ); + var againstBlock = new Block.Block(world, againstX, againstY, againstZ); + + Material mat = Enum.IsDefined(typeof(Material), itemId) ? (Material)itemId : Material.AIR; + var itemInHand = new ItemStack(mat, itemCount); + + var evt = new BlockPlaceEvent(placedBlock, againstBlock, itemInHand, player, canBuild != 0); + FourKit.FireEvent(evt); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBlockPlace error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireBlockBreak(int entityId, int dimId, + int x, int y, int z, int tileId, int data, int exp) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + return exp; + + SyncPlayerFromNative(player); + + var world = FourKit.getWorld(dimId); + var block = new Block.Block(world, x, y, z); + + var evt = new BlockBreakEvent(block, player, exp); + FourKit.FireEvent(evt); + + if (evt.isCancelled()) + return -1; + + return evt.getExpToDrop(); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBlockBreak error: {ex}"); + return exp; + } + } + + [UnmanagedCallersOnly] + public static int FireEntityDamage(int entityId, int entityTypeId, int dimId, + double x, double y, double z, int causeId, double damage, IntPtr outDamage, + int damagerEntityId, int damagerEntityTypeId, + double damagerX, double damagerY, double damagerZ) + { + try + { + Entity.Entity? entity = FourKit.GetPlayerByEntityId(entityId); + if (entity is Player player) + { + SyncPlayerFromNative(player); + } + else + { + var entityType = Enum.IsDefined(typeof(EntityType), entityTypeId) + ? (EntityType)entityTypeId + : EntityType.UNKNOWN; + entity = new LivingEntity(entityId, entityType, dimId, x, y, z); + } + + var cause = Enum.IsDefined(typeof(EntityDamageEvent.DamageCause), causeId) + ? (EntityDamageEvent.DamageCause)causeId + : EntityDamageEvent.DamageCause.CUSTOM; + + EntityDamageByEntityEvent? byEntityEvt = null; + if (damagerEntityId >= 0) + { + Entity.Entity? damager = FourKit.GetPlayerByEntityId(damagerEntityId); + if (damager is Player damagerPlayer) + { + SyncPlayerFromNative(damagerPlayer); + } + else + { + var damagerType = Enum.IsDefined(typeof(EntityType), damagerEntityTypeId) + ? (EntityType)damagerEntityTypeId + : EntityType.UNKNOWN; + damager = new LivingEntity(damagerEntityId, damagerType, dimId, damagerX, damagerY, damagerZ); + } + byEntityEvt = new EntityDamageByEntityEvent(damager, entity!, cause, damage); + FourKit.FireEvent(byEntityEvt); + damage = byEntityEvt.getDamage(); + } + + var evt = new EntityDamageEvent(entity!, cause, damage); + if (byEntityEvt != null && byEntityEvt.isCancelled()) + evt.setCancelled(true); + FourKit.FireEvent(evt); + + Marshal.Copy(new double[] { evt.getDamage() }, 0, outDamage, 1); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireEntityDamage error: {ex}"); + Marshal.Copy(new double[] { damage }, 0, outDamage, 1); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireSignChange(int entityId, int dimId, + int x, int y, int z, + IntPtr line0Ptr, int line0Len, + IntPtr line1Ptr, int line1Len, + IntPtr line2Ptr, int line2Len, + IntPtr line3Ptr, int line3Len, + IntPtr outBuf, int outBufSize, IntPtr outLensPtr) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + WriteSignOutLens(outLensPtr, [0, 0, 0, 0]); + return 0; + } + + SyncPlayerFromNative(player); + + string[] lines = + [ + line0Len > 0 ? Marshal.PtrToStringUTF8(line0Ptr, line0Len) ?? string.Empty : string.Empty, + line1Len > 0 ? Marshal.PtrToStringUTF8(line1Ptr, line1Len) ?? string.Empty : string.Empty, + line2Len > 0 ? Marshal.PtrToStringUTF8(line2Ptr, line2Len) ?? string.Empty : string.Empty, + line3Len > 0 ? Marshal.PtrToStringUTF8(line3Ptr, line3Len) ?? string.Empty : string.Empty, + ]; + + var world = FourKit.getWorld(dimId); + var block = new Block.Block(world, x, y, z); + var evt = new Event.Block.SignChangeEvent(block, player, lines); + FourKit.FireEvent(evt); + + int offset = 0; + int[] lens = new int[4]; + for (int i = 0; i < 4; i++) + { + byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(evt.getLine(i)); + if (offset + utf8.Length <= outBufSize) + { + Marshal.Copy(utf8, 0, outBuf + offset, utf8.Length); + lens[i] = utf8.Length; + offset += utf8.Length; + } + } + WriteSignOutLens(outLensPtr, lens); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireSignChange error: {ex}"); + WriteSignOutLens(outLensPtr, [0, 0, 0, 0]); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireEntityDeath(int entityId, int entityTypeId, int dimId, + double x, double y, double z, int exp) + { + try + { + var entityType = Enum.IsDefined(typeof(EntityType), entityTypeId) + ? (EntityType)entityTypeId + : EntityType.UNKNOWN; + var entity = new LivingEntity(entityId, entityType, dimId, x, y, z); + + var drops = new List(); + var evt = new EntityDeathEvent(entity, drops, exp); + FourKit.FireEvent(evt); + + return evt.getDroppedExp(); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireEntityDeath error: {ex}"); + return exp; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerDeath(int entityId, + IntPtr deathMsgPtr, int deathMsgByteLen, int exp, + IntPtr outMsgBuf, int outMsgBufSize, IntPtr outMsgLenPtr, IntPtr outKeepInventoryPtr, + IntPtr outNewExpPtr, IntPtr outNewLevelPtr, IntPtr outKeepLevelPtr) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + Marshal.WriteInt32(outMsgLenPtr, 0); + Marshal.WriteInt32(outKeepInventoryPtr, 0); + Marshal.WriteInt32(outNewExpPtr, 0); + Marshal.WriteInt32(outNewLevelPtr, 0); + Marshal.WriteInt32(outKeepLevelPtr, 0); + return exp; + } + + SyncPlayerFromNative(player); + + string deathMessage = deathMsgByteLen > 0 + ? Marshal.PtrToStringUTF8(deathMsgPtr, deathMsgByteLen) ?? string.Empty + : string.Empty; + + var drops = new List(); + + var playerEvt = new PlayerDeathEvent(player, drops, exp, deathMessage); + FourKit.FireEvent(playerEvt); + + var entityEvt = new EntityDeathEvent(player, playerEvt.getDrops(), playerEvt.getDroppedExp()); + FourKit.FireEvent(entityEvt); + + int finalExp = entityEvt.getDroppedExp(); + + string finalMsg = playerEvt.getDeathMessage(); + byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(finalMsg); + if (utf8Bytes.Length < outMsgBufSize) + { + Marshal.Copy(utf8Bytes, 0, outMsgBuf, utf8Bytes.Length); + Marshal.WriteInt32(outMsgLenPtr, utf8Bytes.Length); + } + else + { + Marshal.WriteInt32(outMsgLenPtr, 0); + } + + Marshal.WriteInt32(outKeepInventoryPtr, playerEvt.getKeepInventory() ? 1 : 0); + Marshal.WriteInt32(outNewExpPtr, playerEvt.getNewExp()); + Marshal.WriteInt32(outNewLevelPtr, playerEvt.getNewLevel()); + Marshal.WriteInt32(outKeepLevelPtr, playerEvt.getKeepLevel() ? 1 : 0); + + return finalExp; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerDeath error: {ex}"); + Marshal.WriteInt32(outMsgLenPtr, 0); + Marshal.WriteInt32(outKeepInventoryPtr, 0); + Marshal.WriteInt32(outNewExpPtr, 0); + Marshal.WriteInt32(outNewLevelPtr, 0); + Marshal.WriteInt32(outKeepLevelPtr, 0); + return exp; + } + } + + [UnmanagedCallersOnly] + public static long FirePlayerDropItem(int entityId, int itemId, int itemCount, int itemAux, + IntPtr outItemIdPtr, IntPtr outItemCountPtr, IntPtr outItemAuxPtr) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + Marshal.WriteInt32(outItemIdPtr, itemId); + Marshal.WriteInt32(outItemCountPtr, itemCount); + Marshal.WriteInt32(outItemAuxPtr, itemAux); + return 0; + } + + SyncPlayerFromNative(player); + + Material mat = Enum.IsDefined(typeof(Material), itemId) ? (Material)itemId : Material.AIR; + var itemStack = new ItemStack(mat, itemCount, (short)itemAux); + + var evt = new PlayerDropItemEvent(player, itemStack); + FourKit.FireEvent(evt); + + var result = evt.getItemDrop(); + Marshal.WriteInt32(outItemIdPtr, result.getTypeId()); + Marshal.WriteInt32(outItemCountPtr, result.getAmount()); + Marshal.WriteInt32(outItemAuxPtr, result.getDurability()); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerDropItem error: {ex}"); + Marshal.WriteInt32(outItemIdPtr, itemId); + Marshal.WriteInt32(outItemCountPtr, itemCount); + Marshal.WriteInt32(outItemAuxPtr, itemAux); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerInteract(int entityId, int action, + int itemId, int itemCount, int itemAux, + int clickedX, int clickedY, int clickedZ, + int blockFace, int dimId, + IntPtr outUseItemInHandPtr) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + Marshal.WriteInt32(outUseItemInHandPtr, 1); + return 0; + } + + SyncPlayerFromNative(player); + + var actionEnum = Enum.IsDefined(typeof(Block.Action), action) + ? (Block.Action)action + : Block.Action.RIGHT_CLICK_AIR; + + var faceEnum = Enum.IsDefined(typeof(Block.BlockFace), blockFace) + ? (Block.BlockFace)blockFace + : Block.BlockFace.SELF; + + ItemStack? itemStack = null; + if (itemId > 0) + itemStack = new ItemStack(itemId, itemCount, (short)itemAux); + + Block.Block? clickedBlock = null; + bool hasBlock = actionEnum == Block.Action.LEFT_CLICK_BLOCK + || actionEnum == Block.Action.RIGHT_CLICK_BLOCK + || actionEnum == Block.Action.PHYSICAL; + if (hasBlock) + { + var world = FourKit.getWorld(dimId); + if (world != null) + clickedBlock = new Block.Block(world, clickedX, clickedY, clickedZ); + } + + var evt = new PlayerInteractEvent( + player, actionEnum, itemStack, clickedBlock, faceEnum); + FourKit.FireEvent(evt); + + Marshal.WriteInt32(outUseItemInHandPtr, evt.useItemInHand() ? 1 : 0); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerInteract error: {ex}"); + Marshal.WriteInt32(outUseItemInHandPtr, 1); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerInteractEntity(int playerEntityId, + int targetEntityId, int targetEntityTypeId, + int dimId, double targetX, double targetY, double targetZ, + float targetHealth, float targetMaxHealth, float targetEyeHeight) + { + try + { + var player = FourKit.GetPlayerByEntityId(playerEntityId); + if (player == null) + return 0; + + SyncPlayerFromNative(player); + + Entity.Entity? target = FourKit.GetPlayerByEntityId(targetEntityId); + if (target is Player targetPlayer) + { + SyncPlayerFromNative(targetPlayer); + } + else + { + var entityType = Enum.IsDefined(typeof(EntityType), targetEntityTypeId) + ? (EntityType)targetEntityTypeId + : EntityType.UNKNOWN; + var living = new LivingEntity(targetEntityId, entityType, dimId, targetX, targetY, targetZ, + targetHealth, targetMaxHealth); + living.SetEyeHeightInternal(targetEyeHeight); + target = living; + } + + var evt = new PlayerInteractEntityEvent(player, target); + FourKit.FireEvent(evt); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerInteractEntity error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerPickupItem(int playerEntityId, + int itemEntityId, int dimId, double itemX, double itemY, double itemZ, + int itemId, int itemCount, int itemAux, int remaining, + IntPtr outItemIdPtr, IntPtr outItemCountPtr, IntPtr outItemAuxPtr) + { + try + { + var player = FourKit.GetPlayerByEntityId(playerEntityId); + if (player == null) + { + Marshal.WriteInt32(outItemIdPtr, itemId); + Marshal.WriteInt32(outItemCountPtr, itemCount); + Marshal.WriteInt32(outItemAuxPtr, itemAux); + // todo: fix + return 1; + } + + SyncPlayerFromNative(player); + + var itemStack = new Inventory.ItemStack(itemId, itemCount, (short)itemAux); + var item = new Entity.Item(itemEntityId, dimId, itemX, itemY, itemZ, itemStack); + var evt = new PlayerPickupItemEvent(player, item, remaining); + FourKit.FireEvent(evt); + + var result = evt.getItem().getItemStack(); + Marshal.WriteInt32(outItemIdPtr, result.getTypeId()); + Marshal.WriteInt32(outItemCountPtr, result.getAmount()); + Marshal.WriteInt32(outItemAuxPtr, result.getDurability()); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerPickupItem error: {ex}"); + Marshal.WriteInt32(outItemIdPtr, itemId); + Marshal.WriteInt32(outItemCountPtr, itemCount); + Marshal.WriteInt32(outItemAuxPtr, itemAux); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireInventoryOpen(int entityId, int nativeContainerType, + IntPtr titlePtr, int titleByteLen, int containerSize) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) return 0; + + SyncPlayerFromNative(player); + + string title = titleByteLen > 0 + ? Marshal.PtrToStringUTF8(titlePtr, titleByteLen) ?? string.Empty + : string.Empty; + + InventoryType invType = MapNativeContainerType(nativeContainerType); + Inventory.Inventory topInv = CreateContainerInventory(invType, nativeContainerType, title, containerSize, entityId); + var bottomInv = player.getInventory(); + + var view = new InventoryView(topInv, bottomInv, player, invType); + var evt = new InventoryOpenEvent(view); + FourKit.FireEvent(evt); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireInventoryOpen error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireInventoryClick(int entityId, int slot, int button, int clickType, + int nativeContainerType, int containerSize, IntPtr titleUtf8Ptr, int titleByteLen) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) return 0; + + SyncPlayerFromNative(player); + + ClickType click = MapNativeClickType(clickType, button); + InventoryAction action = DetermineInventoryAction(click, slot); + + InventoryType invType; + Inventory.Inventory topInv; + if (nativeContainerType < 0) + { + invType = InventoryType.PLAYER; + topInv = player.getInventory(); + } + else + { + invType = MapNativeContainerType(nativeContainerType); + int size = containerSize > 0 ? containerSize : invType.getDefaultSize(); + string title = titleByteLen > 0 && titleUtf8Ptr != IntPtr.Zero + ? Marshal.PtrToStringUTF8(titleUtf8Ptr, titleByteLen) ?? invType.getDefaultTitle() + : invType.getDefaultTitle(); + topInv = CreateContainerInventory(invType, nativeContainerType, title, size, entityId); + } + + var bottomInv = player.getInventory(); + var view = new InventoryView(topInv, bottomInv, player, invType); + + SlotType slotType = SlotType.CONTAINER; + if (slot == InventoryView.OUTSIDE) + slotType = SlotType.OUTSIDE; + + Inventory.Inventory._slotModifiedByPlugin = false; + int hotbarKey = click == ClickType.NUMBER_KEY ? button : -1; + var evt = new InventoryClickEvent(view, slotType, slot, click, action, hotbarKey); + FourKit.FireEvent(evt); + + if (evt.isCancelled()) return 1; + if (Inventory.Inventory._slotModifiedByPlugin) return 2; + return 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireInventoryClick error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int HandlePlayerCommand(int entityId, IntPtr cmdUtf8, int cmdByteLen) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + return 0; + + SyncPlayerFromNative(player); + + string commandLine = cmdByteLen > 0 + ? Marshal.PtrToStringUTF8(cmdUtf8, cmdByteLen) ?? string.Empty + : string.Empty; + + if (string.IsNullOrEmpty(commandLine)) + return 0; + + return FourKit.DispatchCommand(player, commandLine) ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"HandlePlayerCommand error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireCommandPreprocess(int entityId, IntPtr cmdUtf8, int cmdByteLen, IntPtr outBuf, int outBufSize, IntPtr outLenPtr) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + return 0; + + SyncPlayerFromNative(player); + + string commandLine = cmdByteLen > 0 + ? Marshal.PtrToStringUTF8(cmdUtf8, cmdByteLen) ?? string.Empty + : string.Empty; + + if (string.IsNullOrEmpty(commandLine)) + return 0; + + var preEvt = new Event.Player.PlayerCommandPreprocessEvent(player, commandLine); + FourKit.FireEvent(preEvt); + + if (preEvt.isCancelled()) + return 1; + + string modified = preEvt.getMessage(); + if (modified != commandLine && outBuf != IntPtr.Zero && outBufSize > 0) + { + byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(modified); + if (utf8Bytes.Length < outBufSize) + { + Marshal.Copy(utf8Bytes, 0, outBuf, utf8Bytes.Length); + if (outLenPtr != IntPtr.Zero) + Marshal.WriteInt32(outLenPtr, utf8Bytes.Length); + } + } + + return 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireCommandPreprocess error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int HandleConsoleCommand(IntPtr cmdUtf8, int cmdByteLen) + { + try + { + string commandLine = cmdByteLen > 0 + ? Marshal.PtrToStringUTF8(cmdUtf8, cmdByteLen) ?? string.Empty + : string.Empty; + + if (string.IsNullOrEmpty(commandLine)) + return 0; + + string trimmed = commandLine.StartsWith('/') ? commandLine[1..] : commandLine; + string[] parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + return 0; + + if (!FourKit.HasCommand(parts[0])) + return 0; + + FourKit.DispatchCommand(FourKit.getConsoleSender(), commandLine); + return 1; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"HandleConsoleCommand error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int GetPluginCommandHelp(IntPtr outBuf, int outBufSize, IntPtr outLenPtr) + { + try + { + var entries = FourKit.GetRegisteredCommandHelp(); + if (entries.Count == 0) + { + if (outLenPtr != IntPtr.Zero) + Marshal.WriteInt32(outLenPtr, 0); + return 0; + } + + var sb = new System.Text.StringBuilder(); + foreach (var (usage, description) in entries) + { + sb.Append(usage); + sb.Append('\0'); + sb.Append(description); + sb.Append('\0'); + } + + byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(sb.ToString()); + int copyLen = Math.Min(utf8.Length, outBufSize); + if (outBuf != IntPtr.Zero && copyLen > 0) + Marshal.Copy(utf8, 0, outBuf, copyLen); + if (outLenPtr != IntPtr.Zero) + Marshal.WriteInt32(outLenPtr, copyLen); + + return entries.Count; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"GetPluginCommandHelp error: {ex}"); + if (outLenPtr != IntPtr.Zero) + Marshal.WriteInt32(outLenPtr, 0); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerTeleport(int entityId, + double fromX, double fromY, double fromZ, int fromDimId, + double toX, double toY, double toZ, int toDimId, + int cause, IntPtr outCoords) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + Marshal.Copy(new double[] { toX, toY, toZ }, 0, outCoords, 3); + return 0; + } + + SyncPlayerFromNative(player); + + var fromWorld = FourKit.getWorld(fromDimId); + var toWorld = FourKit.getWorld(toDimId); + var from = new Location(fromWorld, fromX, fromY, fromZ); + var to = new Location(toWorld, toX, toY, toZ); + var teleportCause = cause >= 0 && cause <= (int)PlayerTeleportEvent.TeleportCause.UNKNOWN + ? (PlayerTeleportEvent.TeleportCause)cause + : PlayerTeleportEvent.TeleportCause.UNKNOWN; + + var evt = new PlayerTeleportEvent(player, from, to, teleportCause); + FourKit.FireEvent(evt); + + var finalTo = evt.getTo(); + Marshal.Copy(new double[] { finalTo.X, finalTo.Y, finalTo.Z }, 0, outCoords, 3); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerTeleport error: {ex}"); + Marshal.Copy(new double[] { toX, toY, toZ }, 0, outCoords, 3); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePlayerPortal(int entityId, + double fromX, double fromY, double fromZ, int fromDimId, + double toX, double toY, double toZ, int toDimId, + int cause, IntPtr outCoords) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) + { + Marshal.Copy(new double[] { toX, toY, toZ }, 0, outCoords, 3); + return 0; + } + + SyncPlayerFromNative(player); + + var fromWorld = FourKit.getWorld(fromDimId); + var toWorld = FourKit.getWorld(toDimId); + var from = new Location(fromWorld, fromX, fromY, fromZ); + var to = new Location(toWorld, toX, toY, toZ); + var teleportCause = cause >= 0 && cause <= (int)PlayerTeleportEvent.TeleportCause.UNKNOWN + ? (PlayerTeleportEvent.TeleportCause)cause + : PlayerTeleportEvent.TeleportCause.UNKNOWN; + + var evt = new PlayerPortalEvent(player, from, to, teleportCause); + FourKit.FireEvent(evt); + + var finalTo = evt.getTo(); + Marshal.Copy(new double[] { finalTo.X, finalTo.Y, finalTo.Z }, 0, outCoords, 3); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePlayerPortal error: {ex}"); + Marshal.Copy(new double[] { toX, toY, toZ }, 0, outCoords, 3); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireBedEnter(int entityId, int dimId, int bedX, int bedY, int bedZ) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) return 0; + + SyncPlayerFromNative(player); + + var world = FourKit.getWorld(dimId); + var bed = new Block.Block(world, bedX, bedY, bedZ); + var evt = new Event.Player.PlayerBedEnterEvent(player, bed); + FourKit.FireEvent(evt); + + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBedEnter error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static void FireBedLeave(int entityId, int dimId, int bedX, int bedY, int bedZ) + { + try + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player == null) return; + + SyncPlayerFromNative(player); + + var world = FourKit.getWorld(dimId); + var bed = new Block.Block(world, bedX, bedY, bedZ); + var evt = new Event.Player.PlayerBedLeaveEvent(player, bed); + FourKit.FireEvent(evt); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBedLeave error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static int FireBlockGrow(int dimId, int x, int y, int z, int newTileId, int newTileData) + { + try + { + var world = FourKit.getWorld(dimId); + var block = new Block.Block(world, x, y, z); + var newState = new Block.BlockState(world, x, y, z, newTileId, newTileData); + var evt = new Event.Block.BlockGrowEvent(block, newState); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBlockGrow error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireBlockForm(int dimId, int x, int y, int z, int newTileId, int newTileData) + { + try + { + var world = FourKit.getWorld(dimId); + var block = new Block.Block(world, x, y, z); + var newState = new Block.BlockState(world, x, y, z, newTileId, newTileData); + var evt = new Event.Block.BlockFormEvent(block, newState); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBlockForm error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireBlockBurn(int dimId, int x, int y, int z) + { + try + { + var world = FourKit.getWorld(dimId); + var block = new Block.Block(world, x, y, z); + var evt = new Event.Block.BlockBurnEvent(block); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBlockBurn error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireBlockSpread(int dimId, int x, int y, int z, int srcX, int srcY, int srcZ, int newTileId, int newTileData) + { + try + { + var world = FourKit.getWorld(dimId); + var block = new Block.Block(world, x, y, z); + var source = new Block.Block(world, srcX, srcY, srcZ); + var newState = new Block.BlockState(world, x, y, z, newTileId, newTileData); + var evt = new Event.Block.BlockSpreadEvent(block, source, newState); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBlockSpread error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePistonExtend(int dimId, int x, int y, int z, int direction, int length) + { + try + { + var world = FourKit.getWorld(dimId); + var block = new Block.Block(world, x, y, z); + var face = Enum.IsDefined(typeof(Block.BlockFace), direction) + ? (Block.BlockFace)direction + : Block.BlockFace.SELF; + var evt = new Event.Block.BlockPistonExtendEvent(block, length, face); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePistonExtend error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FirePistonRetract(int dimId, int x, int y, int z, int direction) + { + try + { + var world = FourKit.getWorld(dimId); + var block = new Block.Block(world, x, y, z); + var face = Enum.IsDefined(typeof(Block.BlockFace), direction) + ? (Block.BlockFace)direction + : Block.BlockFace.SELF; + var evt = new Event.Block.BlockPistonRetractEvent(block, face); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FirePistonRetract error: {ex}"); + return 0; + } + } + + [UnmanagedCallersOnly] + public static int FireBlockFromTo(int dimId, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, int face) + { + try + { + var world = FourKit.getWorld(dimId); + var from = new Block.Block(world, fromX, fromY, fromZ); + var to = new Block.Block(world, toX, toY, toZ); + var blockFace = Enum.IsDefined(typeof(Block.BlockFace), face) + ? (Block.BlockFace)face + : Block.BlockFace.SELF; + var evt = new Event.Block.BlockFromToEvent(from, to, blockFace); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireBlockFromTo error: {ex}"); + return 0; + } + } +} diff --git a/Minecraft.Server.FourKit/FourKitHost.cs b/Minecraft.Server.FourKit/FourKitHost.cs new file mode 100644 index 00000000..15f49637 --- /dev/null +++ b/Minecraft.Server.FourKit/FourKitHost.cs @@ -0,0 +1,276 @@ +using System.Runtime.InteropServices; +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Event.Inventory; +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit; + +public static partial class FourKitHost +{ + internal static PluginLoader? s_loader; + + public static IReadOnlyList getLoadedPlugins() => s_loader?.Plugins ?? []; + + [UnmanagedCallersOnly] + public static void Initialize() + { + try + { + ServerLog.Info("fourkit", "Initializing plugin system..."); + + // Resolve plugins relative to the host process (Minecraft.Server.exe), + // NOT the FourKit assembly. AppContext.BaseDirectory points at the + // "runtime/" subfolder where the self-contained .NET payload lives, + // so we'd otherwise create plugins inside runtime/plugins/. Use the + // host exe's directory instead so end users see a top-level plugins/. + string hostExePath = Environment.ProcessPath ?? AppContext.BaseDirectory; + string serverRoot = Path.GetDirectoryName(hostExePath) ?? AppContext.BaseDirectory; + + // Redirect AppContext.BaseDirectory to the server root so that + // plugins using AppContext.BaseDirectory get the exe directory + // instead of the runtime/ subfolder. + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", serverRoot + Path.DirectorySeparatorChar); + + string pluginsDir = Path.Combine(serverRoot, "plugins"); + s_loader = new PluginLoader(); + s_loader.LoadPlugins(pluginsDir, serverRoot); + s_loader.EnableAll(); + + ServerLog.Info("fourkit", "Plugin system ready."); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"Initialize failed: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void Shutdown() + { + try + { + ServerLog.Info("fourkit", "Shutting down plugin system..."); + s_loader?.DisableAll(); + s_loader = null; + ServerLog.Info("fourkit", "Plugin system shut down."); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"Shutdown error: {ex}"); + } + } + + + private static Guid ParseOrHashGuid(string s) + { + if (Guid.TryParse(s, out var g)) return g; + if (string.IsNullOrEmpty(s)) return Guid.NewGuid(); + return new Guid(System.Security.Cryptography.MD5.HashData(System.Text.Encoding.UTF8.GetBytes(s))); + } + + static double[] s_playerSnapshotBuffer = new double[27]; + static GCHandle? s_playerSnapshotBuffer_Handle = null; + + // double[27] = { x, y, z, health, maxHealth, fallDistance, gameMode, walkSpeed, yaw, pitch, dimension, isSleeping, sleepTimer, sneaking, sprinting, onGround, velocityX, velocityY, velocityZ, allowFlight, sleepingIgnored, experienceLevel, experienceProgress, totalExperience, foodLevel, saturation, exhaustion } + internal static void SyncPlayerFromNative(Player player) + { + if (NativeBridge.GetPlayerSnapshot == null) + return; + + if (s_playerSnapshotBuffer_Handle == null) + { + s_playerSnapshotBuffer_Handle = GCHandle.Alloc(s_playerSnapshotBuffer, GCHandleType.Pinned); + } + + NativeBridge.GetPlayerSnapshot(player.getEntityId(), s_playerSnapshotBuffer_Handle.GetValueOrDefault().AddrOfPinnedObject()); + + int dimId = (int)s_playerSnapshotBuffer[10]; + player.SetDimensionInternal(dimId); + var world = FourKit.getWorld(dimId); + player.SetLocation(new Location(world, s_playerSnapshotBuffer[0], s_playerSnapshotBuffer[1], s_playerSnapshotBuffer[2], (float)s_playerSnapshotBuffer[8], (float)s_playerSnapshotBuffer[9])); + player.SetHealthInternal(s_playerSnapshotBuffer[3]); + player.SetMaxHealthInternal(s_playerSnapshotBuffer[4]); + player.SetFallDistanceInternal((float)s_playerSnapshotBuffer[5]); + player.SetGameModeInternal((GameMode)(int)s_playerSnapshotBuffer[6]); + player.SetWalkSpeedInternal((float)s_playerSnapshotBuffer[7]); + player.SetSleepingInternal(s_playerSnapshotBuffer[11] != 0.0); + player.SetSleepTicksInternal((int)s_playerSnapshotBuffer[12]); + player.SetSneakingInternal(s_playerSnapshotBuffer[13] != 0.0); + player.SetSprintingInternal(s_playerSnapshotBuffer[14] != 0.0); + player.SetOnGroundInternal(s_playerSnapshotBuffer[15] != 0.0); + player.SetVelocityInternal(s_playerSnapshotBuffer[16], s_playerSnapshotBuffer[17], s_playerSnapshotBuffer[18]); + player.SetAllowFlightInternal(s_playerSnapshotBuffer[19] != 0.0); + player.SetSleepingIgnoredInternal(s_playerSnapshotBuffer[20] != 0.0); + player.SetLevelInternal((int)s_playerSnapshotBuffer[21]); + player.SetExpInternal((float)s_playerSnapshotBuffer[22]); + player.SetTotalExperienceInternal((int)s_playerSnapshotBuffer[23]); + player.SetFoodLevelInternal((int)s_playerSnapshotBuffer[24]); + player.SetSaturationInternal((float)s_playerSnapshotBuffer[25]); + player.SetExhaustionInternal((float)s_playerSnapshotBuffer[26]); + } + + internal static void BroadcastNativeMessage(string message) + { + if (string.IsNullOrEmpty(message) || NativeBridge.BroadcastMessage == null) + return; + IntPtr ptr = Marshal.StringToCoTaskMemUTF8(message); + try + { + NativeBridge.BroadcastMessage(ptr, System.Text.Encoding.UTF8.GetByteCount(message)); + } + finally + { + Marshal.FreeCoTaskMem(ptr); + } + } + + private static void WriteSignOutLens(IntPtr ptr, int[] lens) + { + Marshal.Copy(lens, 0, ptr, 4); + } + + private static string JavaFormat(string format, params string[] args) + { + var sb = new System.Text.StringBuilder(format.Length + 64); + int seqIndex = 0; + + for (int i = 0; i < format.Length; i++) + { + char c = format[i]; + if (c == '%' && i + 1 < format.Length) + { + char next = format[i + 1]; + + if (next == '%') + { + sb.Append('%'); + i++; + continue; + } + + if (next == 's') + { + if (seqIndex < args.Length) + sb.Append(args[seqIndex]); + seqIndex++; + i++; + continue; + } + + if (char.IsDigit(next)) + { + int numStart = i + 1; + int j = numStart; + while (j < format.Length && char.IsDigit(format[j])) + j++; + + if (j + 1 < format.Length && format[j] == '$' && format[j + 1] == 's') + { + int argIndex = int.Parse(format.AsSpan(numStart, j - numStart)) - 1; + if (argIndex >= 0 && argIndex < args.Length) + sb.Append(args[argIndex]); + i = j + 1; + continue; + } + } + + sb.Append(c); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } + + + private static InventoryType MapNativeContainerType(int nativeType) + { + return nativeType switch + { + 0 => InventoryType.CHEST, // CONTAINER + 1 => InventoryType.WORKBENCH, // WORKBENCH + 2 => InventoryType.FURNACE, // FURNACE + 3 => InventoryType.DISPENSER, // TRAP (dispenser) + 4 => InventoryType.ENCHANTING, // ENCHANTMENT + 5 => InventoryType.BREWING, // BREWING_STAND + 6 => InventoryType.MERCHANT, // TRADER_NPC + 7 => InventoryType.BEACON, // BEACON + 8 => InventoryType.ANVIL, // REPAIR_TABLE + 9 => InventoryType.HOPPER, // HOPPER + 10 => InventoryType.DROPPER, // DROPPER + 11 => InventoryType.CHEST, // HORSE + 12 => InventoryType.WORKBENCH, // FIREWORKS + 13 => InventoryType.CHEST, // BONUS_CHEST + 14 => InventoryType.CHEST, // LARGE_CHEST + 15 => InventoryType.ENDER_CHEST,// ENDER_CHEST + 16 => InventoryType.CHEST, // MINECART_CHEST + 17 => InventoryType.HOPPER, // MINECART_HOPPER + _ => InventoryType.CHEST, + }; + } + + private static Inventory.Inventory CreateContainerInventory(InventoryType invType, int nativeType, string title, int containerSize, int entityId) + { + string name = string.IsNullOrEmpty(title) ? invType.getDefaultTitle() : title; + int size = containerSize > 0 ? containerSize : invType.getDefaultSize(); + + return invType switch + { + InventoryType.FURNACE => new Inventory.FurnaceInventory(name, size, entityId), + InventoryType.BEACON => new Inventory.BeaconInventory(name, size, entityId), + InventoryType.ENCHANTING => new Inventory.EnchantingInventory(name, size, entityId), + _ => nativeType switch + { + 11 => new Inventory.HorseInventory(name, size, entityId), // HORSE + 14 => new Inventory.DoubleChestInventory(name, size, entityId), // LARGE_CHEST + _ => new Inventory.Inventory(name, invType, size, entityId), + } + }; + } + + private static ClickType MapNativeClickType(int nativeClickType, int button) + { + return nativeClickType switch + { + 0 => button == 0 ? ClickType.LEFT : ClickType.RIGHT, + 1 => button == 0 ? ClickType.SHIFT_LEFT : ClickType.SHIFT_RIGHT, + 2 => ClickType.NUMBER_KEY, + 3 => ClickType.MIDDLE, + 4 => button == 1 ? ClickType.CONTROL_DROP : ClickType.DROP, + 5 => ClickType.UNKNOWN, + 6 => ClickType.DOUBLE_CLICK, + _ => ClickType.UNKNOWN, + }; + } + + private static InventoryAction DetermineInventoryAction(ClickType click, int slot) + { + if (slot == InventoryView.OUTSIDE) + { + return click switch + { + ClickType.LEFT => InventoryAction.DROP_ALL_CURSOR, + ClickType.RIGHT => InventoryAction.DROP_ONE_CURSOR, + ClickType.WINDOW_BORDER_LEFT => InventoryAction.DROP_ALL_CURSOR, + ClickType.WINDOW_BORDER_RIGHT => InventoryAction.DROP_ONE_CURSOR, + _ => InventoryAction.NOTHING, + }; + } + + return click switch + { + ClickType.LEFT => InventoryAction.PICKUP_ALL, + ClickType.RIGHT => InventoryAction.PICKUP_HALF, + ClickType.SHIFT_LEFT or ClickType.SHIFT_RIGHT => InventoryAction.MOVE_TO_OTHER_INVENTORY, + ClickType.NUMBER_KEY => InventoryAction.HOTBAR_SWAP, + ClickType.MIDDLE => InventoryAction.CLONE_STACK, + ClickType.DROP => InventoryAction.DROP_ONE_SLOT, + ClickType.CONTROL_DROP => InventoryAction.DROP_ALL_SLOT, + ClickType.DOUBLE_CLICK => InventoryAction.COLLECT_TO_CURSOR, + _ => InventoryAction.UNKNOWN, + }; + } +} diff --git a/Minecraft.Server.FourKit/GameMode.cs b/Minecraft.Server.FourKit/GameMode.cs new file mode 100644 index 00000000..fd76d649 --- /dev/null +++ b/Minecraft.Server.FourKit/GameMode.cs @@ -0,0 +1,24 @@ +namespace Minecraft.Server.FourKit; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Represents the various type of game modes that s may have +/// +public enum GameMode +{ + /// + /// Survival mode is the "normal" gameplay type, with no special features. + /// + SURVIVAL = 0, + + /// + /// Creative mode may fly, build instantly, become invulnerable and create free items. + /// + CREATIVE = 1, + + /// + /// Adventure mode cannot break blocks without the correct tools. + /// + ADVENTURE = 2, +} diff --git a/Minecraft.Server.FourKit/Inventory/BeaconInventory.cs b/Minecraft.Server.FourKit/Inventory/BeaconInventory.cs new file mode 100644 index 00000000..788b2873 --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/BeaconInventory.cs @@ -0,0 +1,25 @@ +namespace Minecraft.Server.FourKit.Inventory; + +/// +/// Represents the inventory of a Beacon. +/// Single slot at index 0. +/// +public class BeaconInventory : Inventory +{ + internal BeaconInventory(string title, int size, int entityId) + : base(title, InventoryType.BEACON, size, entityId) + { + } + + /// + /// Get the item in the beacon's single slot. + /// + /// The item. + public ItemStack? getBeaconItem() => getItem(0); + + /// + /// Set the item in the beacon's single slot. + /// + /// The item to set. + public void setBeaconItem(ItemStack? item) => setItem(0, item); +} diff --git a/Minecraft.Server.FourKit/Inventory/DoubleChestInventory.cs b/Minecraft.Server.FourKit/Inventory/DoubleChestInventory.cs new file mode 100644 index 00000000..4e4ce833 --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/DoubleChestInventory.cs @@ -0,0 +1,56 @@ +namespace Minecraft.Server.FourKit.Inventory; + +/// +/// Represents the inventory of a Double Chest. +/// 54 slots total — left side is slots 0–26, right side is slots 27–53. +/// +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); + } + + /// + /// Get the left half of this double chest. + /// + /// The left side inventory. + public Inventory getLeftSide() => _left; + + /// + /// Get the right half of this double chest. + /// + /// The right side inventory. + 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); + } + } +} diff --git a/Minecraft.Server.FourKit/Inventory/EnchantingInventory.cs b/Minecraft.Server.FourKit/Inventory/EnchantingInventory.cs new file mode 100644 index 00000000..e033eb86 --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/EnchantingInventory.cs @@ -0,0 +1,25 @@ +namespace Minecraft.Server.FourKit.Inventory; + +/// +/// Represents the inventory of an Enchanting Table. +/// Single slot at index 0. +/// +public class EnchantingInventory : Inventory +{ + internal EnchantingInventory(string title, int size, int entityId) + : base(title, InventoryType.ENCHANTING, size, entityId) + { + } + + /// + /// Get the item being enchanted. + /// + /// The item. + public ItemStack? getEnchantItem() => getItem(0); + + /// + /// Set the item being enchanted. + /// + /// The item to set. + public void setEnchantItem(ItemStack? item) => setItem(0, item); +} diff --git a/Minecraft.Server.FourKit/Inventory/FurnaceInventory.cs b/Minecraft.Server.FourKit/Inventory/FurnaceInventory.cs new file mode 100644 index 00000000..9a99ecdc --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/FurnaceInventory.cs @@ -0,0 +1,49 @@ +namespace Minecraft.Server.FourKit.Inventory; + +/// +/// Represents the inventory of a Furnace. +/// Slot layout: 0 = smelting input, 1 = fuel, 2 = result. +/// +public class FurnaceInventory : Inventory +{ + internal FurnaceInventory(string title, int size, int entityId) + : base(title, InventoryType.FURNACE, size, entityId) + { + } + + /// + /// Get the current item in the result slot. + /// + /// The item. + public ItemStack? getResult() => getItem(2); + + /// + /// Get the current fuel. + /// + /// The item. + public ItemStack? getFuel() => getItem(1); + + /// + /// Get the item currently smelting. + /// + /// The item. + public ItemStack? getSmelting() => getItem(0); + + /// + /// Set the current fuel. + /// + /// The item. + public void setFuel(ItemStack? stack) => setItem(1, stack); + + /// + /// Set the current item in the result slot. + /// + /// The item. + public void setResult(ItemStack? stack) => setItem(2, stack); + + /// + /// Set the item currently smelting. + /// + /// The item. + public void setSmelting(ItemStack? stack) => setItem(0, stack); +} diff --git a/Minecraft.Server.FourKit/Inventory/HorseInventory.cs b/Minecraft.Server.FourKit/Inventory/HorseInventory.cs new file mode 100644 index 00000000..bdb0f5e2 --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/HorseInventory.cs @@ -0,0 +1,37 @@ +namespace Minecraft.Server.FourKit.Inventory; + +/// +/// Represents the inventory of a Horse. +/// Slot layout: 0 = saddle, 1 = armor, 2+ = chest slots. +/// +public class HorseInventory : Inventory +{ + internal HorseInventory(string title, int size, int entityId) + : base(title, InventoryType.CHEST, size < 2 ? 2 : size, entityId) + { + } + + /// + /// Gets the item in the horse's saddle slot. + /// + /// The saddle item. + public ItemStack? getSaddle() => getItem(0); + + /// + /// Sets the item in the horse's saddle slot. + /// + /// The saddle item. + public void setSaddle(ItemStack? stack) => setItem(0, stack); + + /// + /// Gets the item in the horse's armor slot. + /// + /// The armor item. + public ItemStack? getArmor() => getItem(1); + + /// + /// Sets the item in the horse's armor slot. + /// + /// The armor item. + public void setArmor(ItemStack? stack) => setItem(1, stack); +} diff --git a/Minecraft.Server.FourKit/Inventory/Inventory.cs b/Minecraft.Server.FourKit/Inventory/Inventory.cs new file mode 100644 index 00000000..52fda5fc --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/Inventory.cs @@ -0,0 +1,606 @@ +namespace Minecraft.Server.FourKit.Inventory; + +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Minecraft.Server.FourKit.Entity; + +/// +/// Represents an inventory containing items. Behavior relating to +/// is unspecified. +/// +public class Inventory : IEnumerable +{ + 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 + 0]; + int aux = buf[i * 3 + 1]; + int packed = buf[i * 3 + 2]; + + ushort count = (ushort)((packed >> 8) & 0xFFFF); + + //byte flags = (byte)((packed >> 24) & 0xFF); + //bool hasMetadata = (flags & 0x1) != 0; //unused here + + _items[i]?.UnbindFromInventory(); + if (id > 0 && count > 0) + { + if (_items[i] == null) + { + _items[i] = new ItemStack(id, count, (short)aux); + } + else + { + _items[i]!.setTypeId(id); + _items[i]!.setAmount(count); + _items[i]!.setDurability((short)aux); + } + _items[i]!.BindToInventory(this, i); //should we unbind and rebind or just keep the bind? + } + else + { + _items[i] = null; + } + } + } + + /// + /// Returns the size of the inventory. + /// + /// The size of the inventory. + public int getSize() => _items.Length; + + /// + /// Returns the name of the inventory. + /// + /// The String with the name of the inventory. + public string getName() => _name; + + /// + /// Returns the ItemStack found in the slot at the given index. + /// + /// The index of the Slot's ItemStack to return. + /// The ItemStack in the slot. + public virtual ItemStack? getItem(int index) + { + EnsureSynced(); + if (index < 0 || index >= _items.Length) return null; + var item = _items[index]; + item?.BindToInventory(this, index); + return item; + } + + /// + /// Stores the ItemStack at the given index of the inventory. + /// + /// The index where to put the ItemStack. + /// The ItemStack to set. + public virtual void setItem(int index, ItemStack? item) + { + if (index >= 0 && index < _items.Length) + { + var old = _items[index]; + if (old != item) + { + old?.UnbindFromInventory(); + item?.BindToInventory(this, index); + } + _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); + } + } + + /// + /// 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. + /// + /// The ItemStacks to add. + /// A Dictionary containing items that didn't fit. + public Dictionary addItem(params ItemStack[] items) + { + EnsureSynced(); + var leftover = new Dictionary(); + 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; + } + + /// + /// 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. + /// + /// The ItemStacks to remove. + /// A Dictionary containing items that couldn't be removed. + public Dictionary removeItem(params ItemStack[] items) + { + EnsureSynced(); + var leftover = new Dictionary(); + 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; + } + + /// + /// Returns all ItemStacks from the inventory. + /// + /// An array of ItemStacks from the inventory. + public ItemStack?[] getContents() + { + EnsureSynced(); + return (ItemStack?[])_items.Clone(); + } + + /// + /// Completely replaces the inventory's contents. Removes all existing + /// contents and replaces it with the ItemStacks given in the array. + /// + /// A complete replacement for the contents; the length must + /// be less than or equal to . + 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); + } + + /// + /// Checks if the inventory contains any ItemStacks with the given material id. + /// + /// The material id to check for. + /// true if an ItemStack in this inventory contains the material id. + public bool contains(int materialId) + { + EnsureSynced(); + foreach (var item in _items) + if (item != null && item.getTypeId() == materialId) return true; + return false; + } + + /// + /// Checks if the inventory contains any ItemStacks with the given material. + /// + /// The material to check for. + /// true if an ItemStack is found with the given Material. + public bool contains(Material material) + { + EnsureSynced(); + foreach (var item in _items) + if (item != null && item.getType() == material) return true; + return false; + } + + /// + /// 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. + /// + /// The ItemStack to match against. + /// false if item is null, true if any exactly matching ItemStacks were found. + 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; + } + + /// + /// Checks if the inventory contains any ItemStacks with the given material id, + /// adding to at least the minimum amount specified. + /// + /// The material id to check for. + /// The minimum amount to look for. + /// true if this contains any matching ItemStack with the given material id and amount. + 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; + } + + /// + /// Checks if the inventory contains any ItemStacks with the given material, + /// adding to at least the minimum amount specified. + /// + /// The material to check for. + /// The minimum amount. + /// true if enough ItemStacks were found to add to the given amount. + 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; + } + + /// + /// 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. + /// + /// The ItemStack to match against. + /// How many identical stacks to check for. + /// false if item is null, true if amount of exactly matching ItemStacks were found. + 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; + } + + /// + /// Checks if the inventory contains ItemStacks matching the given ItemStack + /// whose amounts sum to at least the minimum amount specified. + /// + /// The ItemStack to match against. + /// The minimum amount. + /// false if item is null, true if enough ItemStacks were found to add to the given amount. + 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; + } + + /// + /// Returns a Dictionary with all slots and ItemStacks in the inventory with given material id. + /// + /// The material id to look for. + /// A Dictionary containing the slot index, ItemStack pairs. + public Dictionary all(int materialId) + { + EnsureSynced(); + var result = new Dictionary(); + for (int i = 0; i < _items.Length; i++) + if (_items[i] != null && _items[i]!.getTypeId() == materialId) + result[i] = _items[i]!; + return result; + } + + /// + /// Returns a Dictionary with all slots and ItemStacks in the inventory with the given Material. + /// + /// The material to look for. + /// A Dictionary containing the slot index, ItemStack pairs. + public Dictionary all(Material material) + { + EnsureSynced(); + var result = new Dictionary(); + for (int i = 0; i < _items.Length; i++) + if (_items[i] != null && _items[i]!.getType() == material) + result[i] = _items[i]!; + return result; + } + + /// + /// 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. + /// + /// The ItemStack to match against. + /// A dictionary from slot indexes to item at index. + public Dictionary all(ItemStack? item) + { + var result = new Dictionary(); + 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; + } + + /// + /// Finds the first slot in the inventory containing an ItemStack with the given material id. + /// + /// The material id to look for. + /// The slot index of the given material id or -1 if not found. + 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; + } + + /// + /// Finds the first slot in the inventory containing an ItemStack with the given material. + /// + /// The material to look for. + /// The slot index of the given Material or -1 if not found. + 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; + } + + /// + /// 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. + /// + /// The ItemStack to match against. + /// The slot index of the given ItemStack or -1 if not found. + 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; + } + + /// + /// Returns the first empty Slot. + /// + /// The first empty Slot found, or -1 if no empty slots. + public int firstEmpty() + { + EnsureSynced(); + for (int i = 0; i < _items.Length; i++) + if (_items[i] == null) return i; + return -1; + } + + /// + /// Removes all stacks in the inventory matching the given material id. + /// + /// The material to remove. + 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); + } + + /// + /// Removes all stacks in the inventory matching the given material. + /// + /// The material to remove. + 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); + } + + /// + /// 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. + /// + /// The ItemStack to match against. + 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); + } + + /// + /// Clears out a particular slot in the index. + /// + /// The index to empty. + public void clear(int index) + { + setItem(index, null); + } + + /// + /// Clears out the whole Inventory. + /// + public void clear() + { + for (int i = 0; i < _items.Length; i++) + setItem(i, null); + } + + /// + /// Returns the title of this inventory. + /// + /// A String with the title. + public string getTitle() => _name; + + /// + /// Returns what type of inventory this is. + /// + /// The InventoryType representing the type of inventory. + public InventoryType getType() => _type; + + /// + /// Gets a list of players viewing the inventory. + /// + /// A list of HumanEntities who are viewing this Inventory. + public virtual List getViewers() + { + if (_nativeEntityId < 0 || NativeBridge.GetContainerViewerEntityIds == null) + return new List(); + + 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(); + for (int i = 0; i < count; i++) + { + var player = FourKit.GetPlayerByEntityId(ids[i]); + if (player != null) + viewers.Add(player); + } + return viewers; + } + + /// + /// Returns an iterator starting at the given index. + /// + /// The index. + /// An enumerator. + public IEnumerator iterator(int index) + { + EnsureSynced(); + int start = index >= 0 ? index : _items.Length + index; + return GetEnumeratorFrom(start); + } + + /// + public IEnumerator GetEnumerator() + { + EnsureSynced(); + return GetEnumeratorFrom(0); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IEnumerator GetEnumeratorFrom(int start) + { + for (int i = start; i < _items.Length; i++) + if (_items[i] != null) + { + _items[i]!.BindToInventory(this, i); + yield return _items[i]!; + } + } +} diff --git a/Minecraft.Server.FourKit/Inventory/InventoryHolder.cs b/Minecraft.Server.FourKit/Inventory/InventoryHolder.cs new file mode 100644 index 00000000..27228463 --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/InventoryHolder.cs @@ -0,0 +1,10 @@ +namespace Minecraft.Server.FourKit.Inventory; + +public interface InventoryHolder +{ + /// + /// Get the object's inventory. + /// + /// The inventory. + Inventory getInventory(); +} diff --git a/Minecraft.Server.FourKit/Inventory/InventoryType.cs b/Minecraft.Server.FourKit/Inventory/InventoryType.cs new file mode 100644 index 00000000..0ce02b99 --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/InventoryType.cs @@ -0,0 +1,115 @@ +namespace Minecraft.Server.FourKit.Inventory; + +/// +/// Represents the different types of inventories available. +/// +public enum InventoryType +{ + /// A chest inventory, with 0, 9, 18, 27, 36, 45, or 54 slots of type CONTAINER. + CHEST, + /// A dispenser inventory, with 9 slots of type CONTAINER. + DISPENSER, + /// A dropper inventory, with 9 slots of type CONTAINER. + DROPPER, + /// A furnace inventory, with a RESULT slot, a CRAFTING slot, and a FUEL slot. + FURNACE, + /// A workbench inventory, with 9 CRAFTING slots and a RESULT slot. + WORKBENCH, + /// A player's crafting inventory, with 4 CRAFTING slots and a RESULT slot. + CRAFTING, + /// An enchantment table inventory, with one CRAFTING slot and three enchanting buttons. + ENCHANTING, + /// A brewing stand inventory, with one FUEL slot and three CRAFTING slots. + BREWING, + /// A player's inventory, with 9 QUICKBAR slots, 27 CONTAINER slots, and 4 ARMOR slots. + PLAYER, + /// The creative mode inventory, with only 9 QUICKBAR slots and nothing else. + CREATIVE, + /// The merchant inventory, with 2 TRADE-IN slots, and 1 RESULT slot. + MERCHANT, + /// The ender chest inventory, with 27 slots. + ENDER_CHEST, + /// An anvil inventory, with 2 CRAFTING slots and 1 RESULT slot. + ANVIL, + /// A beacon inventory, with 1 CRAFTING slot. + BEACON, + /// A hopper inventory, with 5 slots of type CONTAINER. + HOPPER +} + +/// +/// Represents a slot type within an inventory. +/// +public enum SlotType +{ + /// A result slot in a furnace or crafting inventory. + RESULT, + /// 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. + CRAFTING, + /// An armour slot in the player's inventory. + ARMOR, + /// A regular slot in the container or the player's inventory. + CONTAINER, + /// A slot in the bottom row or quickbar. + QUICKBAR, + /// A pseudo-slot representing the area outside the inventory window. + OUTSIDE, + /// The fuel slot in a furnace inventory, or the ingredient slot in a brewing stand inventory. + FUEL, +} + +/// +/// Provides default size and title information for each . +/// +public static class InventoryTypeExtensions +{ + /// + /// Gets the default size for this inventory type. + /// + /// The inventory type. + /// The default number of slots. + 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, + }; + + /// + /// Gets the default title for this inventory type. + /// + /// The inventory type. + /// The default title string. + 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", + }; +} diff --git a/Minecraft.Server.FourKit/Inventory/InventoryView.cs b/Minecraft.Server.FourKit/Inventory/InventoryView.cs new file mode 100644 index 00000000..917aee84 --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/InventoryView.cs @@ -0,0 +1,225 @@ +namespace Minecraft.Server.FourKit.Inventory; + +using Minecraft.Server.FourKit.Entity; + +/// +/// Represents a view linking two inventories and a single player +/// (whose inventory may or may not be one of the two). +/// +public class InventoryView +{ + /// + /// Represents the raw slot ID for clicks outside the inventory window. + /// + public static readonly int OUTSIDE = -999; + + private readonly Inventory _topInventory; + private readonly Inventory _bottomInventory; + private readonly HumanEntity _player; + private readonly InventoryType _type; + + /// + /// Creates a new InventoryView linking two inventories and a player. + /// + /// The upper inventory. + /// The lower inventory. + /// The player viewing. + /// The inventory type. + public InventoryView(Inventory topInventory, Inventory bottomInventory, HumanEntity player, InventoryType type) + { + _topInventory = topInventory; + _bottomInventory = bottomInventory; + _player = player; + _type = type; + } + + /// + /// Get the upper inventory involved in this transaction. + /// + /// The inventory. + public virtual Inventory getTopInventory() => _topInventory; + + /// + /// Get the lower inventory involved in this transaction. + /// + /// The inventory. + public virtual Inventory getBottomInventory() => _bottomInventory; + + /// + /// Get the player viewing. + /// + /// The player. + public virtual HumanEntity getPlayer() => _player; + + /// + /// 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. + /// + /// The inventory type. + public virtual InventoryType getType() => _type; + + /// + /// Sets one item in this inventory view by its raw slot ID. + /// + /// + /// If slot ID -999 is chosen, it may be expected that the item is dropped + /// on the ground. This is not required behaviour, however. + /// + /// The raw slot ID. + /// The new item to put in the slot, or null to clear it. + 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); + } + + /// + /// Gets one item in this inventory view by its raw slot ID. + /// + /// The raw slot ID. + /// The item currently in the slot. + 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); + } + + /// + /// Sets the item on the cursor of one of the viewing players. + /// + /// The item to put on the cursor, or null to remove it. + public void setCursor(ItemStack? item) + { + _player.setItemOnCursor(item); + } + + /// + /// Get the item on the cursor of one of the viewing players. + /// + /// The item on the player's cursor, or null if they aren't holding one. + public ItemStack? getCursor() + { + return _player.getItemOnCursor(); + } + + /// + /// Converts a raw slot ID into its local slot ID into whichever of the two + /// inventories the slot points to. + /// + /// The raw slot ID. + /// The converted slot ID. + public int convertSlot(int rawSlot) + { + int topSize = _topInventory.getSize(); + if (rawSlot < topSize) + return rawSlot; + return rawSlot - topSize; + } + + /// + /// Closes the inventory view. + /// + public void close() + { + } + + /// + /// Check the total number of slots in this view, combining the upper + /// and lower inventories. + /// + /// The total size. + public int countSlots() + { + return _topInventory.getSize() + _bottomInventory.getSize(); + } + + /// + /// Sets an extra property of this inventory if supported by that inventory, + /// for example the state of a progress bar. + /// + /// The window property to update. + /// The new value for the window property. + /// true if the property was updated successfully. + public bool setProperty(Property prop, int value) + { + return false; + } + + /// + /// Get the title of this inventory window. + /// + /// The title. + public string getTitle() + { + return _topInventory.getTitle(); + } + + /// + /// Represents various extra properties of certain inventory windows. + /// + public enum Property + { + /// The progress of the down-pointing arrow in a brewing inventory. + BREW_TIME, + /// The progress of the flame in a furnace inventory. + BURN_TIME, + /// The progress of the right-pointing arrow in a furnace inventory. + COOK_TIME, + /// In an enchanting inventory, the top button's experience level value. + ENCHANT_BUTTON1, + /// In an enchanting inventory, the middle button's experience level value. + ENCHANT_BUTTON2, + /// In an enchanting inventory, the bottom button's experience level value. + ENCHANT_BUTTON3, + /// How many total ticks the current fuel should last. + TICKS_FOR_CURRENT_FUEL + } +} + +/// +/// Extension methods for . +/// +public static class InventoryViewPropertyExtensions +{ + /// + /// Gets the that this property belongs to. + /// + /// The property. + /// The inventory type. + 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, + }; + + /// + /// Gets the window-property id for this . + /// + /// The property. + /// The id. + 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, + }; +} diff --git a/Minecraft.Server.FourKit/Inventory/ItemStack.cs b/Minecraft.Server.FourKit/Inventory/ItemStack.cs new file mode 100644 index 00000000..5af48d3f --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/ItemStack.cs @@ -0,0 +1,154 @@ +namespace Minecraft.Server.FourKit.Inventory; + +using Minecraft.Server.FourKit.Inventory.Meta; + +/// +/// Represents a stack of items. +/// +public class ItemStack +{ + private Material _type; + private int _amount; + private short _durability; + private ItemMeta? _meta; + internal Inventory? _ownerInventory; + internal int _ownerSlot = -1; + + /// + /// Creates a new ItemStack of the specified material with amount 1. + /// + public ItemStack(Material type) : this(type, 1) { } + + /// + /// Creates a new ItemStack of the specified material and amount. + /// + public ItemStack(Material type, int amount) : this(type, amount, 0) { } + + /// + /// Creates a new ItemStack of the specified material, amount and durability. + /// + public ItemStack(Material type, int amount, short durability) + { + _type = type; + _amount = amount; + _durability = durability; + } + + /// + /// Creates a new ItemStack from a raw type ID with amount 1. + /// + public ItemStack(int typeId) : this(typeId, 1) { } + + /// + /// Creates a new ItemStack from a raw type ID and amount. + /// + public ItemStack(int typeId, int amount) : this(typeId, amount, 0) { } + + /// + /// Creates a new ItemStack from a raw type ID, amount and durability. + /// + public ItemStack(int typeId, int amount, short durability) + { + _type = Enum.IsDefined(typeof(Material), typeId) ? (Material)typeId : Material.AIR; + _amount = amount; + _durability = durability; + } + + /// + /// Gets the type of this item. + /// + /// Type of the items in this stack. + public Material getType() => _type; + + /// + /// Sets the type of this item. + /// + /// New type to set the items in this stack to. + public void setType(Material type) { _type = type; SyncToOwner(); } + + /// + /// Gets the type id for this item. + /// + /// Type Id of the items in this stack. + public int getTypeId() => (int)_type; + + /// + /// Sets the type id for this item. + /// + /// New type id to set the items in this stack to. + public void setTypeId(int type) + { + _type = Enum.IsDefined(typeof(Material), type) ? (Material)type : Material.AIR; + SyncToOwner(); + } + + /// + /// Gets the amount of items in this stack. + /// + /// Amount of items in this stack. + public int getAmount() => _amount; + + /// + /// Sets the amount of items in this stack. + /// + /// New amount of items in this stack. + public void setAmount(int amount) { _amount = amount; SyncToOwner(); } + + /// + /// Gets the durability of this item. + /// + /// Durability of this item. + public short getDurability() => _durability; + + /// + /// Sets the durability of this item. + /// + /// Durability of this item. + public void setDurability(short durability) { _durability = durability; SyncToOwner(); } + + /// + /// Get a copy of this ItemStack's ItemMeta. + /// + /// A copy of the current ItemStack's ItemMeta. + public ItemMeta getItemMeta() => _meta?.clone() ?? new ItemMeta(); + + /// + /// Checks to see if any meta data has been defined. + /// + /// Returns true if some meta data has been set for this item. + public bool hasItemMeta() => _meta != null && !_meta.isEmpty(); + + /// + /// Set the ItemMeta of this ItemStack. + /// + /// New ItemMeta, or null to indicate meta data be cleared. + /// True if successfully applied ItemMeta. + public bool setItemMeta(ItemMeta? itemMeta) + { + _meta = itemMeta?.clone(); + SyncToOwner(); + return true; + } + + internal ItemMeta? getItemMetaInternal() => _meta; + + internal void setItemMetaInternal(ItemMeta? meta) => _meta = meta; + + internal void BindToInventory(Inventory inventory, int slot) + { + _ownerInventory = inventory; + _ownerSlot = slot; + } + + internal void UnbindFromInventory() + { + _ownerInventory = null; + _ownerSlot = -1; + } + + private void SyncToOwner() + { + if (_ownerInventory != null && _ownerSlot >= 0) + _ownerInventory.setItem(_ownerSlot, this); + } +} diff --git a/Minecraft.Server.FourKit/Inventory/Meta/ItemMeta.cs b/Minecraft.Server.FourKit/Inventory/Meta/ItemMeta.cs new file mode 100644 index 00000000..60ba224c --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/Meta/ItemMeta.cs @@ -0,0 +1,174 @@ +namespace Minecraft.Server.FourKit.Inventory.Meta; + +using Minecraft.Server.FourKit.Enchantments; + +/// +/// Represents the metadata of an , including display name and lore. +/// +public class ItemMeta +{ + private string? _displayName; + private Dictionary? _enchants; + private List? _lore; + + /// + /// Checks for existence of a display name. + /// + /// true if this has a display name. + public bool hasDisplayName() => _displayName != null; + + /// + /// Gets the display name that is set. + /// Plugins should check that hasDisplayName() returns true before calling this method. + /// + /// The display name that is set. + public string getDisplayName() => _displayName ?? string.Empty; + + /// + /// Sets the display name. + /// + /// The name to set. + public void setDisplayName(string? name) => _displayName = name; + + /// + /// Checks for existence of lore. + /// + /// true if this has lore. + public bool hasLore() => _lore != null && _lore.Count > 0; + + /// + /// Gets the lore that is set. + /// Plugins should check if hasLore() returns true before calling this method. + /// + /// A list of lore that is set. + public List getLore() => _lore != null ? new List(_lore) : new List(); + + /// + /// Sets the lore for this item. Removes lore when given null. + /// + /// The lore that will be set. + public void setLore(List? lore) + { + _lore = lore != null ? new List(lore) : null; + } + + /// + /// Adds the specified enchantment to this item meta. + /// + /// Enchantment to add + /// Level for the enchantment + /// Indicates the enchantment should be applied, ignoring the level limit + /// true if the item meta changed as a result of this call, false otherwise + public bool addEnchant(EnchantmentType enchantment, int level, bool ignoreLevelRestriction) + { + if (_enchants == null) + _enchants = new Dictionary(); + + if (!ignoreLevelRestriction) + { + Enchantment enchant = Enchantment.getByType(enchantment); + + if (enchant.getMaxLevel() < level) return false; + } + + try + { + _enchants.Add(enchantment, level); + return true; + } catch { } + + return false; + } + + /// + /// Removes the specified enchantment from this item meta. + /// + /// Enchantment to remove + /// true if the item meta changed as a result of this call, false otherwise + public bool removeEnchant(EnchantmentType enchantment) + { + if (!hasEnchant(enchantment)) return false; + + return _enchants.Remove(enchantment); + } + + /// + /// Returns a copy of the enchantments in this ItemMeta. Returns an empty map if none. + /// + /// An immutable copy of the enchantments + public Dictionary getEnchants() => _enchants != null ? new Dictionary(_enchants) { } : new Dictionary(); + + /// + /// Checks for the level of the specified enchantment. + /// + /// Enchantment to check + /// The level that the specified enchantment has, or 0 if none + public int getEnchantLevel(EnchantmentType enchantment) + { + if (!hasEnchant(enchantment)) return 0; + + return _enchants[enchantment]; //this cant be invalid, we check above + } + + /// + /// Checks if the specified enchantment conflicts with any enchantments in this ItemMeta. + /// + /// Enchantment to test + /// true if the enchantment conflicts, false otherwise + public bool hasConflictingEnchant(EnchantmentType enchantment) + { + Enchantment enchantmentClass = Enchantment.getByType(enchantment); + if (enchantmentClass == null) return false; //this should never happen + + foreach (KeyValuePair ench in _enchants) + { + Enchantment enchClass = Enchantment.getByType(ench.Key); + + if (enchClass == null) continue; //this should never happen + + if (enchClass.conflictsWith(enchantmentClass)) + { + return true; + } + } + + return false; + } + + /// + /// Checks for existence of the specified enchantment. + /// + /// Enchantment to check + /// true if this enchantment exists for this meta + public bool hasEnchant(EnchantmentType enchantment) => hasEnchants() && _enchants.ContainsKey(enchantment); + + /// + /// Checks for the existence of any enchantments. + /// + /// true if an enchantment exists on this meta + public bool hasEnchants() => _enchants != null && _enchants.Count > 0; + + /// + /// Sets the enchantments for this item meta. + /// + /// The enchantments to set. + public void setEnchants(Dictionary? enchants) + { + enchants = enchants != null ? new Dictionary(enchants) : null; + } + + + public ItemMeta clone() + { + var copy = new ItemMeta(); + copy._displayName = _displayName; + copy._enchants = _enchants != null ? new Dictionary(_enchants) : null; + copy._lore = _lore != null ? new List(_lore) : null; + return copy; + } + + internal bool isEmpty() + { + return _displayName == null && (_lore == null || _lore.Count == 0) && (_enchants == null || _enchants.Count == 0); + } +} diff --git a/Minecraft.Server.FourKit/Inventory/PlayerInventory.cs b/Minecraft.Server.FourKit/Inventory/PlayerInventory.cs new file mode 100644 index 00000000..e5941146 --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/PlayerInventory.cs @@ -0,0 +1,413 @@ +namespace Minecraft.Server.FourKit.Inventory; + +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Minecraft.Server.FourKit.Enchantments; +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Inventory.Meta; + +/// +/// Represents a player's inventory, including armor slots and the held item. +/// +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; + + private int[] syncBuffer; + private GCHandle syncBufferHandle; + + internal PlayerInventory() + : base("Player", InventoryType.PLAYER, INVENTORY_SIZE) + { + this.syncBuffer = new int[121]; + this.syncBufferHandle = GCHandle.Alloc(this.syncBuffer, GCHandleType.Pinned); + } + + protected internal override void EnsureSynced() + { + if (_holder == null || NativeBridge.GetPlayerInventory == null) + return; + + int entityId = _holder.getEntityId(); + + NativeBridge.GetPlayerInventory(entityId, this.syncBufferHandle.AddrOfPinnedObject()); + + byte[]? metadataBuffer = null; + GCHandle? metadataBufferHandle = null; + + for (int i = 0; i < INVENTORY_SIZE; i++) + { + int id = this.syncBuffer[i * 3 + 0]; + int aux = this.syncBuffer[i * 3 + 1]; + int packed = this.syncBuffer[i * 3 + 2]; + + ushort count = (ushort)((packed >> 8) & 0xFFFF); + + byte flags = (byte)((packed >> 24) & 0xFF); + bool hasMetadata = (flags & 0x1) != 0; + + _items[i]?.UnbindFromInventory(); + if (id > 0 && count > 0) + { + if (_items[i] == null) + { + _items[i] = new ItemStack(id, count, (short)aux); + } + else + { + _items[i]!.setTypeId(id); + _items[i]!.setAmount(count); + _items[i]!.setDurability((short)aux); + } + + if (hasMetadata) + { + var meta = ReadMetaFromNative(entityId, i, metadataBuffer, metadataBufferHandle); + if (meta != null) + { + _items[i]!.setItemMetaInternal(meta); + } + + } + _items[i]!.BindToInventory(this, i); + } + else + { + _items[i] = null; + } + } + _heldItemSlot = this.syncBuffer[120]; + + if (metadataBufferHandle.HasValue) + metadataBufferHandle.Value.Free(); + } + + /// + 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); + WriteMetaToNative(_holder.getEntityId(), index, item?.getItemMetaInternal()); + } + } + + /// + /// Returns all ItemStacks from the armor slots. + /// + /// An array of ItemStacks for the armor slots. + public ItemStack?[] getArmorContents() + { + EnsureSynced(); + var armor = new ItemStack?[4]; + for (int i = 0; i < 4; i++) + armor[i] = _items[ARMOR_START + i]; + return armor; + } + + /// + /// Gets the ItemStack in the helmet slot. + /// + /// The helmet ItemStack. + public ItemStack? getHelmet() => getItem(ARMOR_START + 3); + + /// + /// Gets the ItemStack in the chestplate slot. + /// + /// The chestplate ItemStack. + public ItemStack? getChestplate() => getItem(ARMOR_START + 2); + + /// + /// Gets the ItemStack in the leggings slot. + /// + /// The leggings ItemStack. + public ItemStack? getLeggings() => getItem(ARMOR_START + 1); + + /// + /// Gets the ItemStack in the boots slot. + /// + /// The boots ItemStack. + public ItemStack? getBoots() => getItem(ARMOR_START); + + /// + /// Sets all four armor slots at once. + /// + /// An array of ItemStacks for the armor slots. + 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]); + } + + /// + /// Sets the helmet slot. + /// + /// The ItemStack to set. + public void setHelmet(ItemStack? helmet) => setItem(ARMOR_START + 3, helmet); + + /// + /// Sets the chestplate slot. + /// + /// The ItemStack to set. + public void setChestplate(ItemStack? chestplate) => setItem(ARMOR_START + 2, chestplate); + + /// + /// Sets the leggings slot. + /// + /// The ItemStack to set. + public void setLeggings(ItemStack? leggings) => setItem(ARMOR_START + 1, leggings); + + /// + /// Sets the boots slot. + /// + /// The ItemStack to set. + public void setBoots(ItemStack? boots) => setItem(ARMOR_START, boots); + + /// + /// Gets the item the player is currently holding. + /// + /// The held ItemStack. + public ItemStack? getItemInHand() { + EnsureSynced(); + return getItem(_heldItemSlot); + } + + /// + /// Sets the item in the player's hand. + /// + /// The ItemStack to set. + public void setItemInHand(ItemStack? stack) + { + EnsureSynced(); //we need to sync the current held slot, hate doing this here during a set call + setItem(_heldItemSlot, stack); + } + + /// + /// Gets the slot number of the currently held item. + /// + /// The held item slot index (0-8). + public int getHeldItemSlot() + { + EnsureSynced(); + return _heldItemSlot; + } + + /// + /// Sets the slot number of the currently held item. + /// + /// The slot index (0-8). + 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; + if (_holder != null) + NativeBridge.SetHeldItemSlot?.Invoke(_holder.getEntityId(), slot); + } + + /// + /// 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. + /// + /// The material id to match, or -1 for any. + /// The data value to match, or -1 for any. + /// The number of stacks cleared. + 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; + } + + /// + /// Gets the holder of this inventory. + /// + /// The HumanEntity that owns this inventory. + public HumanEntity? getHolder() => _holder; + + private static ItemMeta? ReadMetaFromNative(int entityId, int slot, byte[]? buffer, GCHandle? bufferHandle) + { + if (NativeBridge.GetItemMeta == null) + return null; + + if (buffer == null) + { + buffer = new byte[4096]; + bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + } + + int bytesWritten = NativeBridge.GetItemMeta(entityId, slot, bufferHandle.GetValueOrDefault().AddrOfPinnedObject(), buffer!.Length); + + if (bytesWritten <= 0) + return null; + + int offset = 0; + int nameLen = BitConverter.ToInt32(buffer, offset); + offset += 4; + + string? displayName = null; + if (nameLen > 0) + { + displayName = Encoding.UTF8.GetString(buffer, offset, nameLen); + offset += nameLen; + } + + int loreCount = 0; + if (offset + 4 <= bytesWritten) + { + loreCount = BitConverter.ToInt32(buffer, offset); + offset += 4; + } + + List? lore = null; + if (loreCount > 0) + { + lore = new List(loreCount); + for (int i = 0; i < loreCount; i++) + { + if (offset + 4 > bytesWritten) break; + int lineLen = BitConverter.ToInt32(buffer, offset); + offset += 4; + if (lineLen > 0 && offset + lineLen <= bytesWritten) + { + lore.Add(Encoding.UTF8.GetString(buffer, offset, lineLen)); + offset += lineLen; + } + else + { + lore.Add(string.Empty); + } + } + } + + int enchantCount = 0; + if (offset + 4 <= bytesWritten) + { + enchantCount = BitConverter.ToInt32(buffer, offset); + offset += 4; + } + + Dictionary? enchants = null; + if (enchantCount > 0) + { + enchants = new Dictionary(enchantCount); + for (int i = 0; i < enchantCount; i++) + { + if (offset + (4 + 4) > bytesWritten) + break; + + int type = BitConverter.ToInt32(buffer, offset); + offset += 4; + + int level = BitConverter.ToInt32(buffer, offset); + offset += 4; + + enchants.Add((EnchantmentType)type, level); + } + } + + if (displayName == null && (lore == null || lore.Count == 0) && (enchants == null || enchants.Count == 0)) + return null; + + var meta = new ItemMeta(); + if (displayName != null) + meta.setDisplayName(displayName); + if (lore != null && lore.Count > 0) + meta.setLore(lore); + if (enchants != null && enchants.Count > 0) + meta.setEnchants(enchants); + return meta; + } + + private static void WriteMetaToNative(int entityId, int slot, ItemMeta? meta) + { + if (NativeBridge.SetItemMeta == null) + return; + + if (meta == null || meta.isEmpty()) + { + NativeBridge.SetItemMeta(entityId, slot, IntPtr.Zero, 0); + return; + } + + using var ms = new System.IO.MemoryStream(512); + using var bw = new System.IO.BinaryWriter(ms); + + if (meta.hasDisplayName()) + { + byte[] nameBytes = Encoding.UTF8.GetBytes(meta.getDisplayName()); + bw.Write(nameBytes.Length); + bw.Write(nameBytes); + } + else + { + bw.Write(0); + } + + if (meta.hasLore()) + { + var lore = meta.getLore(); + bw.Write(lore.Count); + foreach (var line in lore) + { + byte[] lineBytes = Encoding.UTF8.GetBytes(line ?? string.Empty); + bw.Write(lineBytes.Length); + bw.Write(lineBytes); + } + } + else + { + bw.Write(0); + } + + if (meta.hasEnchants()) + { + var enchants = meta.getEnchants(); + bw.Write(enchants.Count); + foreach (var enchant in enchants) + { + bw.Write((int)enchant.Key); + bw.Write(enchant.Value); + } + } + else + { + bw.Write(0); + } + + bw.Flush(); + byte[] data = ms.ToArray(); + var gh = GCHandle.Alloc(data, GCHandleType.Pinned); + try + { + NativeBridge.SetItemMeta(entityId, slot, gh.AddrOfPinnedObject(), data.Length); + } + finally + { + gh.Free(); + } + } +} diff --git a/Minecraft.Server.FourKit/Location.cs b/Minecraft.Server.FourKit/Location.cs new file mode 100644 index 00000000..6b8e314a --- /dev/null +++ b/Minecraft.Server.FourKit/Location.cs @@ -0,0 +1,191 @@ +namespace Minecraft.Server.FourKit; + +/// +/// Represents a 3-dimensional position in a world. +/// +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; } + + /// + /// Constructs a new Location with the given coordinates and direction. + /// + /// The world in which this location resides. + /// The x-coordinate. + /// The y-coordinate. + /// The z-coordinate. + /// The absolute rotation on the x-plane, in degrees. + /// The absolute rotation on the y-plane, in degrees. + 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; + } + + /// + /// Constructs a new Location with the given coordinates. + /// + /// The world in which this location resides. + /// The x-coordinate. + /// The y-coordinate. + /// The z-coordinate. + public Location(World? world, double x, double y, double z) : this(world, x, y, z, 0f, 0f) { } + + /// + /// Creates a new with the given coordinates and no world. + /// + /// The x-coordinate. + /// The y-coordinate. + /// The z-coordinate. + 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) { } + + /// + /// Gets the x-coordinate of this location. + /// + /// The x-coordinate. + public double getX() => X; + + /// + /// Sets the x-coordinate of this location. + /// + /// The new x-coordinate. + public void setX(double x) => X = x; + + /// + /// Gets the y-coordinate of this location. + /// + /// The y-coordinate. + public double getY() => Y; + + /// + /// Sets the y-coordinate of this location. + /// + /// The new y-coordinate. + public void setY(double y) => Y = y; + + /// + /// Gets the z-coordinate of this location. + /// + /// The z-coordinate. + public double getZ() => Z; + + /// + /// Sets the z-coordinate of this location. + /// + /// The new z-coordinate. + public void setZ(double z) => Z = z; + + /// + /// Gets the yaw of this location, measured in degrees. + /// + /// The yaw. + public float getYaw() => Yaw; + + /// + /// Sets the yaw of this location, measured in degrees. + /// + /// The new yaw. + public void setYaw(float yaw) => Yaw = yaw; + + /// + /// Gets the pitch of this location, measured in degrees. + /// + /// The pitch. + public float getPitch() => Pitch; + + /// + /// Sets the pitch of this location, measured in degrees. + /// + /// The new pitch. + public void setPitch(float pitch) => Pitch = pitch; + + /// + /// Gets the world that this location resides in. + /// + /// The world, or null if not set. + public World? getWorld() => LocationWorld; + + /// + /// Sets the world that this location resides in. + /// + /// The new world. + public void setWorld(World? world) => LocationWorld = world; + + /// + /// Gets the floored value of the X component, indicating the block that + /// this location is contained with. + /// + /// The block X. + public int getBlockX() => (int)Math.Floor(X); + + /// + /// Gets the floored value of the Y component, indicating the block that + /// this location is contained with. + /// + /// The block Y. + public int getBlockY() => (int)Math.Floor(Y); + + /// + /// Gets the floored value of the Z component, indicating the block that + /// this location is contained with. + /// + /// The block Z. + public int getBlockZ() => (int)Math.Floor(Z); + + /// + /// Gets the magnitude of the location, defined as sqrt(x^2+y^2+z^2). + /// + /// The magnitude. + public double length() => Math.Sqrt(X * X + Y * Y + Z * Z); + + /// + /// Gets the magnitude of the location squared. + /// + /// The magnitude squared. + public double lengthSquared() => X * X + Y * Y + Z * Z; + + /// + /// Adds the location by another. + /// + /// The x-coordinate to add. + /// The y-coordinate to add. + /// The z-coordinate to add. + /// This location, for chaining. + public Location add(double x, double y, double z) + { + X += x; + Y += y; + Z += z; + return this; + } + + /// + /// Adds the location by another. + /// + /// The location to add. + /// This location, for chaining. + public Location add(Location vec) + { + X += vec.X; + Y += vec.Y; + Z += vec.Z; + return this; + } + + public Location clone() => new Location(LocationWorld, X, Y, Z, Yaw, Pitch); + + /// + public override string ToString() => $"Location(world={LocationWorld.getName()}, x={X}, y={Y}, z={Z}, yaw={Yaw}, pitch={Pitch})"; +} diff --git a/Minecraft.Server.FourKit/Material.cs b/Minecraft.Server.FourKit/Material.cs new file mode 100644 index 00000000..2f06eb3d --- /dev/null +++ b/Minecraft.Server.FourKit/Material.cs @@ -0,0 +1,343 @@ +namespace Minecraft.Server.FourKit; + +/// +/// An enum of all material IDs accepted by the official server and client. +/// +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, +} diff --git a/Minecraft.Server.FourKit/Minecraft.Server.FourKit.csproj b/Minecraft.Server.FourKit/Minecraft.Server.FourKit.csproj new file mode 100644 index 00000000..ac1fff0a --- /dev/null +++ b/Minecraft.Server.FourKit/Minecraft.Server.FourKit.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + Minecraft.Server.FourKit + Minecraft.Server.FourKit + true + bin + + diff --git a/Minecraft.Server.FourKit/NativeBridge.cs b/Minecraft.Server.FourKit/NativeBridge.cs new file mode 100644 index 00000000..2a38c788 --- /dev/null +++ b/Minecraft.Server.FourKit/NativeBridge.cs @@ -0,0 +1,337 @@ +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 int NativeGetTileDataDelegate(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 int NativeGetPlayerLatencyDelegate(int entityId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeSendRawDelegate(int entityId, IntPtr dataBuf, int dataBufSize); + + [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); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetItemMetaDelegate(int entityId, int slot, IntPtr outBuf, int bufSize); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetItemMetaDelegate(int entityId, int slot, IntPtr inBuf, int bufSize); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetHeldItemSlotDelegate(int entityId, int slot); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetSneakingDelegate(int entityId, int sneak); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetVelocityDelegate(int entityId, double x, double y, double z); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetAllowFlightDelegate(int entityId, int allowFlight); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativePlaySoundDelegate(int entityId, int soundId, double x, double y, double z, float volume, float pitch); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetSleepingIgnoredDelegate(int entityId, int ignored); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetLevelDelegate(int entityId, int level); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetExpDelegate(int entityId, float exp); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeGiveExpDelegate(int entityId, int amount); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeGiveExpLevelsDelegate(int entityId, int amount); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetFoodLevelDelegate(int entityId, int foodLevel); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetSaturationDelegate(int entityId, float saturation); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetExhaustionDelegate(int entityId, float exhaustion); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSpawnParticleDelegate(int entityId, int particleId, float x, float y, float z, float offsetX, float offsetY, float offsetZ, float speed, int count); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeSetPassengerDelegate(int entityId, int passengerEntityId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeLeaveVehicleDelegate(int entityId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeEjectDelegate(int entityId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetVehicleIdDelegate(int entityId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetPassengerIdDelegate(int entityId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeGetEntityInfoDelegate(int entityId, IntPtr outBuf); + + + 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 NativeGetTileDataDelegate? GetTileData; + 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 NativeGetPlayerLatencyDelegate? GetPlayerLatency; + internal static NativeSendRawDelegate? SendRaw; + 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 NativeGetItemMetaDelegate? GetItemMeta; + internal static NativeSetItemMetaDelegate? SetItemMeta; + internal static NativeSetHeldItemSlotDelegate? SetHeldItemSlot; + internal static NativeSetSneakingDelegate? SetSneaking; + internal static NativeSetVelocityDelegate? SetVelocity; + internal static NativeSetAllowFlightDelegate? SetAllowFlight; + internal static NativePlaySoundDelegate? PlaySound; + internal static NativeSetSleepingIgnoredDelegate? SetSleepingIgnored; + internal static NativeSetLevelDelegate? SetLevel; + internal static NativeSetExpDelegate? SetExp; + internal static NativeGiveExpDelegate? GiveExp; + internal static NativeGiveExpLevelsDelegate? GiveExpLevels; + internal static NativeSetFoodLevelDelegate? SetFoodLevel; + internal static NativeSetSaturationDelegate? SetSaturation; + internal static NativeSetExhaustionDelegate? SetExhaustion; + internal static NativeSpawnParticleDelegate? SpawnParticle; + internal static NativeSetPassengerDelegate? SetPassenger; + internal static NativeLeaveVehicleDelegate? LeaveVehicle; + internal static NativeEjectDelegate? Eject; + internal static NativeGetVehicleIdDelegate? GetVehicleId; + internal static NativeGetPassengerIdDelegate? GetPassengerId; + internal static NativeGetEntityInfoDelegate? GetEntityInfo; + + 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(damage); + SetPlayerHealth = Marshal.GetDelegateForFunctionPointer(setHealth); + TeleportPlayer = Marshal.GetDelegateForFunctionPointer(teleport); + SetPlayerGameMode = Marshal.GetDelegateForFunctionPointer(setGameMode); + BroadcastMessage = Marshal.GetDelegateForFunctionPointer(broadcastMessage); + SetFallDistance = Marshal.GetDelegateForFunctionPointer(setFallDistance); + GetPlayerSnapshot = Marshal.GetDelegateForFunctionPointer(getPlayerSnapshot); + SendMessage = Marshal.GetDelegateForFunctionPointer(sendMessage); + SetWalkSpeed = Marshal.GetDelegateForFunctionPointer(setWalkSpeed); + TeleportEntity = Marshal.GetDelegateForFunctionPointer(teleportEntity); + } + + internal static void SetWorldCallbacks(IntPtr getTileId, IntPtr getTileData, 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(getTileId); + GetTileData = Marshal.GetDelegateForFunctionPointer(getTileData); + SetTile = Marshal.GetDelegateForFunctionPointer(setTile); + SetTileData = Marshal.GetDelegateForFunctionPointer(setTileData); + BreakBlock = Marshal.GetDelegateForFunctionPointer(breakBlock); + GetHighestBlockY = Marshal.GetDelegateForFunctionPointer(getHighestBlockY); + GetWorldInfo = Marshal.GetDelegateForFunctionPointer(getWorldInfo); + SetWorldTime = Marshal.GetDelegateForFunctionPointer(setWorldTime); + SetWeather = Marshal.GetDelegateForFunctionPointer(setWeather); + CreateExplosion = Marshal.GetDelegateForFunctionPointer(createExplosion); + StrikeLightning = Marshal.GetDelegateForFunctionPointer(strikeLightning); + SetSpawnLocation = Marshal.GetDelegateForFunctionPointer(setSpawnLocation); + DropItem = Marshal.GetDelegateForFunctionPointer(dropItem); + } + + internal static void SetPlayerCallbacks(IntPtr kickPlayer, IntPtr banPlayer, IntPtr banPlayerIp, IntPtr getPlayerAddress, IntPtr getPlayerLatency) + { + KickPlayer = Marshal.GetDelegateForFunctionPointer(kickPlayer); + BanPlayer = Marshal.GetDelegateForFunctionPointer(banPlayer); + BanPlayerIp = Marshal.GetDelegateForFunctionPointer(banPlayerIp); + GetPlayerAddress = Marshal.GetDelegateForFunctionPointer(getPlayerAddress); + GetPlayerLatency = Marshal.GetDelegateForFunctionPointer(getPlayerLatency); + } + + internal static void SetPlayerConnectionCallbacks(IntPtr sendRaw) + { + SendRaw = Marshal.GetDelegateForFunctionPointer(sendRaw); + } + + internal static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot) + { + GetPlayerInventory = Marshal.GetDelegateForFunctionPointer(getPlayerInventory); + SetPlayerInventorySlot = Marshal.GetDelegateForFunctionPointer(setPlayerInventorySlot); + GetContainerContents = Marshal.GetDelegateForFunctionPointer(getContainerContents); + SetContainerSlot = Marshal.GetDelegateForFunctionPointer(setContainerSlot); + GetContainerViewerEntityIds = Marshal.GetDelegateForFunctionPointer(getContainerViewerEntityIds); + CloseContainer = Marshal.GetDelegateForFunctionPointer(closeContainer); + OpenVirtualContainer = Marshal.GetDelegateForFunctionPointer(openVirtualContainer); + GetItemMeta = Marshal.GetDelegateForFunctionPointer(getItemMeta); + SetItemMeta = Marshal.GetDelegateForFunctionPointer(setItemMeta); + SetHeldItemSlot = Marshal.GetDelegateForFunctionPointer(setHeldItemSlot); + } + + internal static void SetEntityCallbacks(IntPtr setSneaking, IntPtr setVelocity, IntPtr setAllowFlight, IntPtr playSound, IntPtr setSleepingIgnored) + { + SetSneaking = Marshal.GetDelegateForFunctionPointer(setSneaking); + SetVelocity = Marshal.GetDelegateForFunctionPointer(setVelocity); + SetAllowFlight = Marshal.GetDelegateForFunctionPointer(setAllowFlight); + PlaySound = Marshal.GetDelegateForFunctionPointer(playSound); + SetSleepingIgnored = Marshal.GetDelegateForFunctionPointer(setSleepingIgnored); + } + + internal static void SetExperienceCallbacks(IntPtr setLevel, IntPtr setExp, IntPtr giveExp, IntPtr giveExpLevels, IntPtr setFoodLevel, IntPtr setSaturation, IntPtr setExhaustion) + { + SetLevel = Marshal.GetDelegateForFunctionPointer(setLevel); + SetExp = Marshal.GetDelegateForFunctionPointer(setExp); + GiveExp = Marshal.GetDelegateForFunctionPointer(giveExp); + GiveExpLevels = Marshal.GetDelegateForFunctionPointer(giveExpLevels); + SetFoodLevel = Marshal.GetDelegateForFunctionPointer(setFoodLevel); + SetSaturation = Marshal.GetDelegateForFunctionPointer(setSaturation); + SetExhaustion = Marshal.GetDelegateForFunctionPointer(setExhaustion); + } + + internal static void SetParticleCallbacks(IntPtr spawnParticle) + { + SpawnParticle = Marshal.GetDelegateForFunctionPointer(spawnParticle); + } + + internal static void SetVehicleCallbacks(IntPtr setPassenger, IntPtr leaveVehicle, IntPtr eject, IntPtr getVehicleId, IntPtr getPassengerId, IntPtr getEntityInfo) + { + SetPassenger = Marshal.GetDelegateForFunctionPointer(setPassenger); + LeaveVehicle = Marshal.GetDelegateForFunctionPointer(leaveVehicle); + Eject = Marshal.GetDelegateForFunctionPointer(eject); + GetVehicleId = Marshal.GetDelegateForFunctionPointer(getVehicleId); + GetPassengerId = Marshal.GetDelegateForFunctionPointer(getPassengerId); + GetEntityInfo = Marshal.GetDelegateForFunctionPointer(getEntityInfo); + } +} diff --git a/Minecraft.Server.FourKit/Net/InetAddress.cs b/Minecraft.Server.FourKit/Net/InetAddress.cs new file mode 100644 index 00000000..d9c987cc --- /dev/null +++ b/Minecraft.Server.FourKit/Net/InetAddress.cs @@ -0,0 +1,106 @@ +namespace Minecraft.Server.FourKit.Net; + +/// +/// Represents an Internet Protocol (IP) address. +/// +public class InetAddress +{ + private readonly string _hostAddress; + + internal InetAddress(string hostAddress) + { + _hostAddress = hostAddress ?? string.Empty; + } + + /// + /// Returns the IP address string in textual presentation. + /// + /// The IP address as a string. + public string getHostAddress() => _hostAddress; + + /// + /// Gets the host name for this IP address. + /// For this implementation, returns the IP address string. + /// + /// The host name. + public string getHostName() => _hostAddress; + + /// + /// Returns the raw IP address of this InetAddress object as bytes. + /// + /// The raw IP address bytes, or an empty array if parsing fails. + public byte[] getAddress() + { + if (System.Net.IPAddress.TryParse(_hostAddress, out var ip)) + return ip.GetAddressBytes(); + return []; + } + + /// + /// Checks whether this is a loopback address (127.x.x.x or ::1). + /// + /// true if this is a loopback address. + public bool isLoopbackAddress() + { + if (System.Net.IPAddress.TryParse(_hostAddress, out var ip)) + return System.Net.IPAddress.IsLoopback(ip); + return false; + } + + /// + /// Checks whether this is a site-local (private) address. + /// + /// true if this is a site-local address. + 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; + } + + /// + /// Checks whether this is a link-local address (169.254.x.x). + /// + /// true if this is a link-local address. + 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; + } + + /// + /// Checks whether this is a multicast address (224-239.x.x.x). + /// + /// true if this is a multicast address. + 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; + } + + /// + /// Checks whether this is the wildcard (any) address (0.0.0.0). + /// + /// true if this is the wildcard address. + 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; + } + + /// + public override string ToString() => _hostAddress; +} diff --git a/Minecraft.Server.FourKit/Net/InetSocketAddress.cs b/Minecraft.Server.FourKit/Net/InetSocketAddress.cs new file mode 100644 index 00000000..e723a2e2 --- /dev/null +++ b/Minecraft.Server.FourKit/Net/InetSocketAddress.cs @@ -0,0 +1,79 @@ +namespace Minecraft.Server.FourKit.Net; + +/// +/// Represents an IP Socket Address (IP address + port number). +/// +public class InetSocketAddress +{ + private readonly InetAddress _address; + private readonly string _hostname; + private readonly int _port; + + /// + /// Creates a socket address from an IP address and a port number. + /// + /// The IP address. + /// The port number. + public InetSocketAddress(InetAddress addr, int port) + { + _address = addr; + _hostname = addr.getHostAddress(); + _port = port; + } + + /// + /// Creates a socket address from a hostname and a port number. + /// + /// The hostname or IP address string. + /// The port number. + public InetSocketAddress(string hostname, int port) + { + _hostname = hostname ?? string.Empty; + _address = new InetAddress(_hostname); + _port = port; + } + + /// + /// Creates a socket address where the IP address is the wildcard address + /// and the port number a specified value. + /// + /// The port number. + public InetSocketAddress(int port) + { + _hostname = "0.0.0.0"; + _address = new InetAddress(_hostname); + _port = port; + } + + /// + /// Gets the InetAddress. + /// + /// The InetAddress. + public InetAddress getAddress() => _address; + + /// + /// Gets the hostname. + /// + /// The hostname, or the IP address string if created from an address. + public string getHostName() => _hostname; + + /// + /// Gets the port number. + /// + /// The port number. + public int getPort() => _port; + + /// + public override int GetHashCode() => HashCode.Combine(_hostname, _port); + + /// + public override bool Equals(object? obj) + { + if (obj is InetSocketAddress other) + return _hostname == other._hostname && _port == other._port; + return false; + } + + /// + public override string ToString() => _hostname + ":" + _port; +} diff --git a/Minecraft.Server.FourKit/Particle.cs b/Minecraft.Server.FourKit/Particle.cs new file mode 100644 index 00000000..7df6c196 --- /dev/null +++ b/Minecraft.Server.FourKit/Particle.cs @@ -0,0 +1,45 @@ +namespace Minecraft.Server.FourKit; + +/// +/// Enum of particle effects that can be spawned for a player. +/// +public enum Particle +{ + WATER_BUBBLE = 0, + SMOKE_NORMAL = 1, + NOTE = 2, + PORTAL = 3, + EXPLOSION_NORMAL = 5, + FLAME = 6, + LAVA = 7, + FOOTSTEP = 8, + WATER_SPLASH = 9, + SMOKE_LARGE = 10, + REDSTONE = 11, + SNOWBALL = 12, + SNOW_SHOVEL = 13, + SLIME = 14, + HEART = 15, + SUSPENDED = 16, + SUSPENDED_DEPTH = 17, + CRIT = 18, + EXPLOSION_HUGE = 19, + EXPLOSION_LARGE = 20, + TOWN_AURA = 21, + SPELL = 22, + SPELL_WITCH = 23, + SPELL_MOB = 24, + SPELL_MOB_AMBIENT = 25, + SPELL_INSTANT = 26, + CRIT_MAGIC = 27, + DRIP_WATER = 28, + DRIP_LAVA = 29, + ENCHANTMENT_TABLE = 30, + DRAGON_BREATH = 31, + END_ROD = 32, + VILLAGER_ANGRY = 33, + VILLAGER_HAPPY = 34, + FIREWORKS_SPARK = 35, + ITEM_CRACK = 0x100000, + BLOCK_CRACK = 0x200000, +} diff --git a/Minecraft.Server.FourKit/Plugin/ServerPlugin.cs b/Minecraft.Server.FourKit/Plugin/ServerPlugin.cs new file mode 100644 index 00000000..c3358039 --- /dev/null +++ b/Minecraft.Server.FourKit/Plugin/ServerPlugin.cs @@ -0,0 +1,56 @@ +namespace Minecraft.Server.FourKit.Plugin; + +/// +/// Base class that every plugin must extend. +/// +/// public string name => "MyPlugin"; +/// public string version => "1.0.0"; +/// public string author => "Me"; +/// +/// public void onEnable() { /* startup logic */ } +/// public void onDisable() { /* shutdown logic */ } +/// +/// +public abstract class ServerPlugin +{ + /// + /// The name of this plugin. Must be declared in your plugin class. + /// + public virtual string name { get; } = string.Empty; + + /// + /// The version of this plugin. + /// + public virtual string version { get; } = "1.0.0"; + + /// + /// The author of this plugin. + /// + public virtual string author { get; } = "Unknown"; + + /// + /// The server's root directory (where the server executable lives). + /// Use this instead of AppContext.BaseDirectory which points + /// to the .NET runtime subfolder. Set automatically before + /// is called. + /// + public string serverDirectory { get; internal set; } = string.Empty; + + /// + /// A per-plugin data directory (plugins/<PluginName>/). + /// Created automatically if it does not exist. Use this for config + /// files, logs, databases, or any plugin-specific storage. + /// Set automatically before is called. + /// + public string dataDirectory { get; internal set; } = string.Empty; + + /// + /// Called when this plugin is enabled + /// + public virtual void onEnable() { } + + /// + /// Called when this plugin is disabled + /// + public virtual void onDisable() { } +} diff --git a/Minecraft.Server.FourKit/PluginLoadContext.cs b/Minecraft.Server.FourKit/PluginLoadContext.cs new file mode 100644 index 00000000..b7429e56 --- /dev/null +++ b/Minecraft.Server.FourKit/PluginLoadContext.cs @@ -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; + } +} diff --git a/Minecraft.Server.FourKit/PluginLoader.cs b/Minecraft.Server.FourKit/PluginLoader.cs new file mode 100644 index 00000000..9dd89af9 --- /dev/null +++ b/Minecraft.Server.FourKit/PluginLoader.cs @@ -0,0 +1,227 @@ +using Minecraft.Server.FourKit.Event.Server; +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 _plugins = new(); + private string _serverRoot = string.Empty; + private string _pluginsDirectory = string.Empty; + + public IReadOnlyList Plugins => _plugins.AsReadOnly(); + + public void LoadPlugins(string pluginsDirectory, string serverRoot) + { + _serverRoot = serverRoot; + _pluginsDirectory = 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}"); + FourKit.FireEvent(new PluginLoadFailedEvent(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}"); + FourKit.FireEvent(new PluginLoadFailedEvent(dll, ex.Message)); + + } + } + } + } + FourKit.FireEvent(new PluginsLoadedEvent()); + } + + 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) + { + EnablePlugin(plugin); + } + } + + public void DisableAll() + { + foreach (var plugin in _plugins) + { + DisablePlugin(plugin); + } + } + + public void EnablePlugin(ServerPlugin plugin) + { + try + { + string pName = GetPluginString(plugin, "name", "getName", "GetName", plugin.GetType().Name); + + plugin.serverDirectory = _serverRoot; + string dataDir = Path.Combine(_pluginsDirectory, pName); + if (!Directory.Exists(dataDir)) + Directory.CreateDirectory(dataDir); + plugin.dataDirectory = dataDir; + + InvokePluginMethod(plugin, "onEnable", "OnEnable"); + ServerLog.Info("fourkit", $"Enabled: {pName}"); + + FourKit.FireEvent(new PluginEnableEvent(plugin)); + } + catch (Exception ex) + { + string pName = GetPluginString(plugin, "name", "getName", "GetName", plugin.GetType().Name); + ServerLog.Error("fourkit", $"Error enabling {pName}: {ex.Message}"); + } + } + + public void DisablePlugin(ServerPlugin plugin) + { + try + { + InvokePluginMethod(plugin, "onDisable", "OnDisable"); + string pName = GetPluginString(plugin, "name", "getName", "GetName", plugin.GetType().Name); + ServerLog.Info("fourkit", $"Disabled: {pName}"); + + FourKit.FireEvent(new PluginDisableEvent(plugin)); + } + catch (Exception ex) + { + string pName = GetPluginString(plugin, "name", "getName", "GetName", plugin.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; + } +} diff --git a/Minecraft.Server.FourKit/ServerLog.cs b/Minecraft.Server.FourKit/ServerLog.cs new file mode 100644 index 00000000..6b8d1db0 --- /dev/null +++ b/Minecraft.Server.FourKit/ServerLog.cs @@ -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}"); +} diff --git a/Minecraft.Server.FourKit/Sound.cs b/Minecraft.Server.FourKit/Sound.cs new file mode 100644 index 00000000..ea0698e4 --- /dev/null +++ b/Minecraft.Server.FourKit/Sound.cs @@ -0,0 +1,208 @@ +namespace Minecraft.Server.FourKit; + +/// +/// An Enum of Sounds the server is able to send to players. +/// +public enum Sound +{ + AMBIENCE_CAVE = 75, + AMBIENCE_RAIN = 73, + AMBIENCE_THUNDER = 74, + ANVIL_BREAK = 110, + ANVIL_LAND = 111, + ANVIL_USE = 112, + ARROW_HIT = 56, + BAT_DEATH = 140, + BAT_HURT = 139, + BAT_IDLE = 138, + BAT_LOOP = 138, + BAT_TAKEOFF = 141, + BLAZE_BREATH = 15, + BLAZE_DEATH = 17, + BLAZE_HIT = 16, + BREATH = 81, + BURP = 63, + CAT_HISS = 54, + CAT_HIT = 54, + CAT_MEOW = 53, + CAT_PURR = 51, + CAT_PURREOW = 52, + CHEST_CLOSE = 70, + CHEST_OPEN = 69, + CHICKEN_EGG_POP = 2, + CHICKEN_HURT = 1, + CHICKEN_IDLE = 0, + CHICKEN_WALK = 148, + CLICK = 65, + COW_HURT = 4, + COW_IDLE = 3, + COW_WALK = 147, + CREEPER_DEATH = 42, + CREEPER_HISS = 41, + DIG_GRASS = 125, + DIG_GRAVEL = 126, + DIG_SAND = 127, + DIG_SNOW = 128, + DIG_STONE = 129, + DIG_WOOD = 130, + DIG_WOOL = 124, + DONKEY_ANGRY = 175, + DONKEY_DEATH = 165, + DONKEY_HIT = 169, + DONKEY_IDLE = 173, + DOOR_CLOSE = 72, + DOOR_OPEN = 71, + DRINK = 61, + EAT = 62, + ENDERDRAGON_DEATH = 101, + ENDERDRAGON_GROWL = 102, + ENDERDRAGON_HIT = 103, + ENDERDRAGON_WINGS = 104, + ENDERMAN_DEATH = 25, + ENDERMAN_HIT = 24, + ENDERMAN_IDLE = 23, + ENDERMAN_SCREAM = 151, + ENDERMAN_STARE = 150, + ENDERMAN_TELEPORT = 26, + EXPLODE = 57, + FALL_BIG = 83, + FALL_SMALL = 82, + FIRE = 80, + FIRE_IGNITE = 79, + FIREWORK_BLAST = 132, + FIREWORK_BLAST2 = 133, + FIREWORK_LARGE_BLAST = 134, + FIREWORK_LARGE_BLAST2 = 135, + FIREWORK_LAUNCH = 131, + FIREWORK_TWINKLE = 136, + FIREWORK_TWINKLE2 = 137, + FIZZ = 58, + FUSE = 60, + GHAST_CHARGE = 22, + GHAST_DEATH = 20, + GHAST_FIREBALL = 21, + GHAST_MOAN = 18, + GHAST_SCREAM = 19, + GHAST_SCREAM2 = 19, + GLASS = 66, + HORSE_ANGRY = 176, + HORSE_ARMOR = 161, + HORSE_BREATHE = 178, + HORSE_DEATH = 166, + HORSE_GALLOP = 177, + HORSE_HIT = 170, + HORSE_IDLE = 174, + HORSE_JUMP = 181, + HORSE_LAND = 160, + HORSE_SADDLE = 162, + HORSE_SKELETON_DEATH = 164, + HORSE_SKELETON_HIT = 168, + HORSE_SKELETON_IDLE = 172, + HORSE_SOFT = 180, + HORSE_WOOD = 179, + HORSE_ZOMBIE_DEATH = 163, + HORSE_ZOMBIE_HIT = 167, + HORSE_ZOMBIE_IDLE = 171, + HURT = 81, + HURT_FLESH = 81, + IRONGOLEM_DEATH = 107, + IRONGOLEM_HIT = 106, + IRONGOLEM_THROW = 105, + IRONGOLEM_WALK = 108, + ITEM_BREAK = 68, + ITEM_PICKUP = 59, + LAVA = 93, + LAVA_POP = 92, + LEVEL_UP = 188, + MAGMACUBE_JUMP = 49, + MAGMACUBE_WALK = 50, + MAGMACUBE_WALK2 = 49, + MINECART_BASE = 94, + MINECART_INSIDE = 95, + NOTE_BASS = 88, + NOTE_BASS_DRUM = 85, + NOTE_BASS_GUITAR = 88, + NOTE_PIANO = 84, + NOTE_PLING = 84, + NOTE_SNARE_DRUM = 86, + NOTE_STICKS = 87, + ORB_PICKUP = 67, + PIG_DEATH = 6, + PIG_IDLE = 5, + PIG_WALK = 149, + PISTON_EXTEND = 90, + PISTON_RETRACT = 89, + PORTAL = 76, + PORTAL_TRAVEL = 78, + PORTAL_TRIGGER = 77, + SHEEP_IDLE = 7, + SHEEP_SHEAR = 152, + SHEEP_WALK = 153, + SHOOT_ARROW = 55, + SILVERFISH_HIT = 32, + SILVERFISH_IDLE = 31, + SILVERFISH_KILL = 33, + SILVERFISH_WALK = 34, + SKELETON_DEATH = 154, + SKELETON_HURT = 36, + SKELETON_IDLE = 35, + SKELETON_WALK = 155, + SLIME_ATTACK = 40, + SLIME_WALK = 185, + SLIME_WALK2 = 186, + SPIDER_DEATH = 38, + SPIDER_IDLE = 37, + SPIDER_WALK = 156, + SPLASH = 64, + SPLASH2 = 64, + STEP_GRASS = 97, + STEP_GRAVEL = 96, + STEP_LADDER = 123, + STEP_SAND = 100, + STEP_SNOW = 122, + STEP_STONE = 94, + STEP_WOOD = 95, + STEP_WOOL = 99, + SUCCESSFUL_HIT = 56, + SWIM = 159, + THORNS = 109, + VILLAGER_DEATH = 116, + VILLAGER_HAGGLE = 113, + VILLAGER_HIT = 115, + VILLAGER_IDLE = 114, + VILLAGER_NO = 118, + VILLAGER_YES = 117, + WATER = 91, + WITCH_DEATH = 184, + WITCH_HURT = 183, + WITCH_IDLE = 182, + WITHER_DEATH = 145, + WITHER_HURT = 144, + WITHER_IDLE = 143, + WITHER_SHOOT = 146, + WITHER_SPAWN = 142, + WOLF_BARK = 11, + WOLF_DEATH = 13, + WOLF_GROWL = 8, + WOLF_HOWL = 8, + WOLF_HURT = 12, + WOLF_PANT = 10, + WOLF_SHAKE = 14, + WOLF_WALK = 157, + WOLF_WHINE = 9, + WOOD_CLICK = 65, + ZOMBIE_DEATH = 45, + ZOMBIE_HURT = 44, + ZOMBIE_IDLE = 43, + ZOMBIE_INFECT = 119, + ZOMBIE_METAL = 48, + ZOMBIE_PIG_ANGRY = 30, + ZOMBIE_PIG_DEATH = 29, + ZOMBIE_PIG_HURT = 28, + ZOMBIE_PIG_IDLE = 27, + ZOMBIE_REMEDY = 121, + ZOMBIE_STEP = 158, + ZOMBIE_UNFECT = 120, + ZOMBIE_WOOD = 46, + ZOMBIE_WOODBREAK = 47, +} diff --git a/Minecraft.Server.FourKit/Util/Vector.cs b/Minecraft.Server.FourKit/Util/Vector.cs new file mode 100644 index 00000000..e6778f5e --- /dev/null +++ b/Minecraft.Server.FourKit/Util/Vector.cs @@ -0,0 +1,579 @@ +namespace Minecraft.Server.FourKit.Util; + +/// +/// Represents a mutable vector. Because the components of Vectors are mutable, +/// storing Vectors long term may be dangerous if passing code modifies the +/// Vector later. If you want to keep around a Vector, it may be wise to call +/// in order to get a copy. +/// +public class Vector +{ + private static readonly double EPSILON = 0.000001; + private static readonly Random _random = new(); + + protected double x; + protected double y; + protected double z; + + public Vector() + { + x = 0; + y = 0; + z = 0; + } + + /// + /// Construct the vector with provided integer components. + /// + /// X component. + /// Y component. + /// Z component. + public Vector(int x, int y, int z) + { + this.x = x; + this.y = y; + this.z = z; + } + + /// + /// Construct the vector with provided double components. + /// + /// X component. + /// Y component. + /// Z component. + public Vector(double x, double y, double z) + { + this.x = x; + this.y = y; + this.z = z; + } + + /// + /// Construct the vector with provided float components. + /// + /// X component. + /// Y component. + /// Z component. + public Vector(float x, float y, float z) + { + this.x = x; + this.y = y; + this.z = z; + } + + /// + /// Adds a vector to this one. + /// + /// The other vector. + /// The same vector. + public Vector add(Vector vec) + { + x += vec.x; + y += vec.y; + z += vec.z; + return this; + } + + /// + /// Subtracts a vector from this one. + /// + /// The other vector. + /// The same vector. + public Vector subtract(Vector vec) + { + x -= vec.x; + y -= vec.y; + z -= vec.z; + return this; + } + + /// + /// Multiplies the vector by another. + /// + /// The other vector. + /// The same vector. + public Vector multiply(Vector vec) + { + x *= vec.x; + y *= vec.y; + z *= vec.z; + return this; + } + + /// + /// Divides the vector by another. + /// + /// The other vector. + /// The same vector. + public Vector divide(Vector vec) + { + x /= vec.x; + y /= vec.y; + z /= vec.z; + return this; + } + + /// + /// Copies another vector. + /// + /// The other vector. + /// The same vector. + public Vector copy(Vector vec) + { + x = vec.x; + y = vec.y; + z = vec.z; + return this; + } + + /// + /// Gets the magnitude of the vector, defined as sqrt(x^2+y^2+z^2). + /// The value of this method is not cached and uses a costly square-root + /// function, so do not repeatedly call this method to get the vector's + /// magnitude. NaN will be returned if the inner result of the sqrt() + /// function overflows, which will be caused if the length is too long. + /// + /// The magnitude. + public double length() + { + return Math.Sqrt(x * x + y * y + z * z); + } + + /// + /// Gets the magnitude of the vector squared. + /// + /// The magnitude squared. + public double lengthSquared() + { + return x * x + y * y + z * z; + } + + /// + /// Get the distance between this vector and another. The value of this + /// method is not cached and uses a costly square-root function, so do not + /// repeatedly call this method to get the vector's magnitude. NaN will be + /// returned if the inner result of the sqrt() function overflows, which + /// will be caused if the distance is too long. + /// + /// The other vector. + /// The distance. + public double distance(Vector o) + { + return Math.Sqrt(distanceSquared(o)); + } + + /// + /// Get the squared distance between this vector and another. + /// + /// The other vector. + /// The distance squared. + public double distanceSquared(Vector o) + { + double dx = x - o.x; + double dy = y - o.y; + double dz = z - o.z; + return dx * dx + dy * dy + dz * dz; + } + + /// + /// Gets the angle between this vector and another in radians. + /// + /// The other vector. + /// Angle in radians. + public float angle(Vector other) + { + double dot = this.dot(other) / (length() * other.length()); + return (float)Math.Acos(dot); + } + + /// + /// Sets this vector to the midpoint between this vector and another. + /// + /// The other vector. + /// This same vector (now a midpoint). + public Vector midpoint(Vector other) + { + x = (x + other.x) / 2.0; + y = (y + other.y) / 2.0; + z = (z + other.z) / 2.0; + return this; + } + + /// + /// Gets a new midpoint vector between this vector and another. + /// + /// The other vector. + /// A new midpoint vector. + public Vector getMidpoint(Vector other) + { + double mx = (x + other.x) / 2.0; + double my = (y + other.y) / 2.0; + double mz = (z + other.z) / 2.0; + return new Vector(mx, my, mz); + } + + /// + /// Performs scalar multiplication, multiplying all components with a scalar. + /// + /// The factor. + /// The same vector. + public Vector multiply(int m) + { + x *= m; + y *= m; + z *= m; + return this; + } + + /// + /// Performs scalar multiplication, multiplying all components with a scalar. + /// + /// The factor. + /// The same vector. + public Vector multiply(double m) + { + x *= m; + y *= m; + z *= m; + return this; + } + + /// + /// Performs scalar multiplication, multiplying all components with a scalar. + /// + /// The factor. + /// The same vector. + public Vector multiply(float m) + { + x *= m; + y *= m; + z *= m; + return this; + } + + /// + /// Calculates the dot product of this vector with another. The dot product + /// is defined as x1*x2+y1*y2+z1*z2. The returned value is a scalar. + /// + /// The other vector. + /// Dot product. + public double dot(Vector other) + { + return x * other.x + y * other.y + z * other.z; + } + + /// + /// Calculates the cross product of this vector with another. + /// The cross product is defined as: + /// + /// x = y1 * z2 - y2 * z1 + /// y = z1 * x2 - z2 * x1 + /// z = x1 * y2 - x2 * y1 + /// + /// + /// The other vector. + /// The same vector. + public Vector crossProduct(Vector o) + { + double newX = y * o.z - o.y * z; + double newY = z * o.x - o.z * x; + double newZ = x * o.y - o.x * y; + x = newX; + y = newY; + z = newZ; + return this; + } + + /// + /// Converts this vector to a unit vector (a vector with length of 1). + /// + /// The same vector. + public Vector normalize() + { + double mag = length(); + x /= mag; + y /= mag; + z /= mag; + return this; + } + + /// + /// Zero this vector's components. + /// + /// The same vector. + public Vector zero() + { + x = 0; + y = 0; + z = 0; + return this; + } + + /// + /// Returns whether this vector is in an axis-aligned bounding box. + /// The minimum and maximum vectors given must be truly the minimum and + /// maximum X, Y and Z components. + /// + /// Minimum vector. + /// Maximum vector. + /// Whether this vector is in the AABB. + public bool isInAABB(Vector min, Vector max) + { + return x >= min.x && x <= max.x && + y >= min.y && y <= max.y && + z >= min.z && z <= max.z; + } + + /// + /// Returns whether this vector is within a sphere. + /// + /// Sphere origin. + /// Sphere radius. + /// Whether this vector is in the sphere. + public bool isInSphere(Vector origin, double radius) + { + return distanceSquared(origin) <= radius * radius; + } + + /// + /// Gets the X component. + /// + /// The X component. + public double getX() + { + return x; + } + + /// + /// Gets the floored value of the X component, indicating the block that + /// this vector is contained with. + /// + /// Block X. + public int getBlockX() + { + return (int)Math.Floor(x); + } + + /// + /// Gets the Y component. + /// + /// The Y component. + public double getY() + { + return y; + } + + /// + /// Gets the floored value of the Y component, indicating the block that + /// this vector is contained with. + /// + /// Block Y. + public int getBlockY() + { + return (int)Math.Floor(y); + } + + /// + /// Gets the Z component. + /// + /// The Z component. + public double getZ() + { + return z; + } + + /// + /// Gets the floored value of the Z component, indicating the block that + /// this vector is contained with. + /// + /// Block Z. + public int getBlockZ() + { + return (int)Math.Floor(z); + } + + /// + /// Set the X component. + /// + /// The new X component. + /// This vector. + public Vector setX(int x) + { + this.x = x; + return this; + } + + /// + /// Set the X component. + /// + /// The new X component. + /// This vector. + public Vector setX(double x) + { + this.x = x; + return this; + } + + /// + /// Set the X component. + /// + /// The new X component. + /// This vector. + public Vector setX(float x) + { + this.x = x; + return this; + } + + /// + /// Set the Y component. + /// + /// The new Y component. + /// This vector. + public Vector setY(int y) + { + this.y = y; + return this; + } + + /// + /// Set the Y component. + /// + /// The new Y component. + /// This vector. + public Vector setY(double y) + { + this.y = y; + return this; + } + + /// + /// Set the Y component. + /// + /// The new Y component. + /// This vector. + public Vector setY(float y) + { + this.y = y; + return this; + } + + /// + /// Set the Z component. + /// + /// The new Z component. + /// This vector. + public Vector setZ(int z) + { + this.z = z; + return this; + } + + /// + /// Set the Z component. + /// + /// The new Z component. + /// This vector. + public Vector setZ(double z) + { + this.z = z; + return this; + } + + /// + /// Set the Z component. + /// + /// The new Z component. + /// This vector. + public Vector setZ(float z) + { + this.z = z; + return this; + } + + /// + /// Get a new vector. + /// + /// A clone of this vector. + public Vector clone() + { + return new Vector(x, y, z); + } + + /// + /// Gets a Location version of this vector with yaw and pitch being 0. + /// + /// The world to link the location to. + /// The location. + public Location toLocation(World world) + { + return new Location(world, x, y, z); + } + + /// + /// Gets a Location version of this vector. + /// + /// The world to link the location to. + /// The desired yaw. + /// The desired pitch. + /// The location. + public Location toLocation(World world, float yaw, float pitch) + { + return new Location(world, x, y, z, yaw, pitch); + } + + /// + /// Get the threshold used for equals(). + /// + /// The epsilon. + public static double getEpsilon() + { + return EPSILON; + } + + /// + /// Gets the minimum components of two vectors. + /// + /// The first vector. + /// The second vector. + /// Minimum. + public static Vector getMinimum(Vector v1, Vector v2) + { + return new Vector(Math.Min(v1.x, v2.x), Math.Min(v1.y, v2.y), Math.Min(v1.z, v2.z)); + } + + /// + /// Gets the maximum components of two vectors. + /// + /// The first vector. + /// The second vector. + /// Maximum. + public static Vector getMaximum(Vector v1, Vector v2) + { + return new Vector(Math.Max(v1.x, v2.x), Math.Max(v1.y, v2.y), Math.Max(v1.z, v2.z)); + } + + /// + /// Gets a random vector with components having a random value between 0 and 1. + /// + /// A random vector. + public static Vector getRandom() + { + return new Vector(_random.NextDouble(), _random.NextDouble(), _random.NextDouble()); + } + + /// + public override bool Equals(object? obj) + { + if (obj is not Vector other) return false; + return Math.Abs(x - other.x) < EPSILON && + Math.Abs(y - other.y) < EPSILON && + Math.Abs(z - other.z) < EPSILON; + } + + /// + public override string ToString() + { + return $"{x},{y},{z}"; + } +} diff --git a/Minecraft.Server.FourKit/World.cs b/Minecraft.Server.FourKit/World.cs new file mode 100644 index 00000000..1d85fe9c --- /dev/null +++ b/Minecraft.Server.FourKit/World.cs @@ -0,0 +1,377 @@ +using System.Runtime.InteropServices; +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Inventory; + +namespace Minecraft.Server.FourKit; + +/// +/// Represents a world, which may contain entities, chunks and blocks. +/// +public class World +{ + private readonly int _dimensionId; + private readonly string _name; + + internal World(int dimensionId, string name) + { + _dimensionId = dimensionId; + _name = name; + } + + /// + /// Gets the dimension ID of this world. + /// + /// Dimension ID of this world. + public int getDimensionId() => _dimensionId; + + /// + /// Gets the unique name of this world. + /// + /// Name of this world. + public string getName() => _name; + + /// + /// Gets the Block at the given coordinates. + /// + /// X-coordinate of the block. + /// Y-coordinate of the block. + /// Z-coordinate of the block. + /// Block at the given coordinates. + public Block.Block getBlockAt(int x, int y, int z) + { + return new Block.Block(this, x, y, z); + } + + /// + /// Gets the Block at the given Location. + /// + /// Location of the block. + /// Block at the given location. + public Block.Block getBlockAt(Location location) + { + return getBlockAt(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + } + + /// + /// Gets the block type ID at the given coordinates. + /// + /// X-coordinate of the block. + /// Y-coordinate of the block. + /// Z-coordinate of the block. + /// Type ID of the block. + public int getBlockTypeIdAt(int x, int y, int z) + { + if (NativeBridge.GetTileId != null) + return NativeBridge.GetTileId(_dimensionId, x, y, z); + return 0; + } + + /// + /// Gets the block type ID at the given Location. + /// + /// Location of the block. + /// Type ID of the block. + public int getBlockTypeIdAt(Location location) + { + return getBlockTypeIdAt(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + } + + /// + /// Gets the highest non-air coordinate at the given coordinates. + /// + /// X-coordinate. + /// Z-coordinate. + /// The Y-coordinate of the highest non-air block. + public int getHighestBlockYAt(int x, int z) + { + if (NativeBridge.GetHighestBlockY != null) + return NativeBridge.GetHighestBlockY(_dimensionId, x, z); + return 0; + } + + /// + /// Gets the highest non-air coordinate at the given Location. + /// + /// Location to check. + /// The Y-coordinate of the highest non-air block. + public int getHighestBlockYAt(Location location) + { + return getHighestBlockYAt(location.getBlockX(), location.getBlockZ()); + } + + /// + /// Gets the highest non-empty block at the given coordinates. + /// + /// X-coordinate. + /// Z-coordinate. + /// Highest non-empty block. + public Block.Block getHighestBlockAt(int x, int z) + { + int y = getHighestBlockYAt(x, z); + return getBlockAt(x, y, z); + } + + /// + /// Gets the highest non-empty block at the given Location. + /// + /// Coordinates to get the highest block at. + /// Highest non-empty block. + 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; + } + + /// + /// Gets the default spawn Location of this world. + /// + /// The spawn location of this world. + public Location getSpawnLocation() + { + double[] info = GetWorldInfoSnapshot(); + return new Location(this, info[0], info[1], info[2], 0f, 0f); + } + + /// + /// Sets the spawn location of the world. + /// + /// X-coordinate. + /// Y-coordinate. + /// Z-coordinate. + /// True if the spawn was set successfully. + public bool setSpawnLocation(int x, int y, int z) + { + if (NativeBridge.SetSpawnLocation != null) + return NativeBridge.SetSpawnLocation(_dimensionId, x, y, z) != 0; + return false; + } + + /// + /// Gets the Seed for this world. + /// + /// This world's Seed. + public long getSeed() + { + double[] info = GetWorldInfoSnapshot(); + return (long)info[3]; + } + + /// + /// Gets the relative in-game time of this world. + /// + /// The current relative time. + public long getTime() + { + double[] info = GetWorldInfoSnapshot(); + return (long)info[4]; + } + + /// + /// Sets the relative in-game time on the server. + /// + /// The new relative time to set the in-game time to. + public void setTime(long time) + { + NativeBridge.SetWorldTime?.Invoke(_dimensionId, time); + } + + /// + /// Sets the in-game time on the server. + /// + /// The new absolute time to set this world to. + public void setFullTime(long time) + { + NativeBridge.SetWorldTime?.Invoke(_dimensionId, time); + } + + /// + /// Set whether there is a storm. + /// + /// Whether there is rain and snow. + public void setStorm(bool hasStorm) + { + NativeBridge.SetWeather?.Invoke(_dimensionId, hasStorm ? 1 : 0, -1, -1); + } + + /// + /// Set whether it is thundering. + /// + /// Whether it is thundering. + public void setThundering(bool thundering) + { + NativeBridge.SetWeather?.Invoke(_dimensionId, -1, thundering ? 1 : 0, -1); + } + + /// + /// Set the thundering duration. + /// + /// Duration in ticks. + public void setThunderDuration(int duration) + { + NativeBridge.SetWeather?.Invoke(_dimensionId, -1, -1, duration); + } + + /// + /// Get a list of all players in this World. + /// + /// A list of all Players currently residing in this world. + public List getPlayers() + { + var all = FourKit.getOnlinePlayers(); + var result = new List(); + foreach (var p in all) + { + var loc = p.getLocation(); + if (loc?.LocationWorld == this) + result.Add(p); + } + return result; + } + + /// + /// Creates explosion at given coordinates with given power. + /// + /// X-coordinate. + /// Y-coordinate. + /// Z-coordinate. + /// The power of explosion, where 4F is TNT. + /// false if explosion was canceled, otherwise true. + public bool createExplosion(double x, double y, double z, float power) + { + return createExplosion(x, y, z, power, false, true); + } + + /// + /// Creates explosion at given coordinates with given power and optionally + /// setting blocks on fire. + /// + /// X-coordinate. + /// Y-coordinate. + /// Z-coordinate. + /// The power of explosion, where 4F is TNT. + /// Whether or not to set blocks on fire. + /// false if explosion was canceled, otherwise true. + public bool createExplosion(double x, double y, double z, float power, bool setFire) + { + return createExplosion(x, y, z, power, setFire, true); + } + + /// + /// Creates explosion at given coordinates with given power and optionally + /// setting blocks on fire or breaking blocks. + /// + /// X-coordinate. + /// Y-coordinate. + /// Z-coordinate. + /// The power of explosion, where 4F is TNT. + /// Whether or not to set blocks on fire. + /// Whether or not to have blocks be destroyed. + /// false if explosion was canceled, otherwise true. + 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; + } + + /// + /// Creates explosion at given coordinates with given power and optionally + /// setting blocks on fire or breaking blocks. + /// + /// Location to blow up. + /// The power of explosion, where 4F is TNT. + /// Whether or not to set blocks on fire. + /// Whether or not to have blocks be destroyed. + /// false if explosion was canceled, otherwise true. + 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; + } + + /// + /// Creates explosion at given coordinates with given power. + /// + /// Location to blow up. + /// The power of explosion, where 4F is TNT. + /// false if explosion was canceled, otherwise true. + public bool createExplosion(Location loc, float power) + { + return createExplosion(loc.X, loc.Y, loc.Z, power); + } + + /// + /// Creates explosion at given coordinates with given power and optionally + /// setting blocks on fire. + /// + /// Location to blow up. + /// The power of explosion, where 4F is TNT. + /// Whether or not to set blocks on fire. + /// false if explosion was canceled, otherwise true. + public bool createExplosion(Location loc, float power, bool setFire) + { + return createExplosion(loc.X, loc.Y, loc.Z, power, setFire); + } + + /// + /// Strikes lightning at the given Location. + /// + /// The location to strike lightning. + /// true if lightning was successfully summoned. + public bool strikeLightning(Location loc) + { + if (NativeBridge.StrikeLightning != null) + return NativeBridge.StrikeLightning(_dimensionId, loc.X, loc.Y, loc.Z, 0) != 0; + return false; + } + + /// + /// Strikes lightning at the given Location without doing damage. + /// + /// The location to strike lightning. + /// true if lightning was successfully summoned. + public bool strikeLightningEffect(Location loc) + { + if (NativeBridge.StrikeLightning != null) + return NativeBridge.StrikeLightning(_dimensionId, loc.X, loc.Y, loc.Z, 1) != 0; + return false; + } + + /// + /// Drops an item at the specified Location. + /// + /// Location to drop the item. + /// ItemStack to drop. + public void dropItem(Location location, ItemStack item) + { + NativeBridge.DropItem?.Invoke(_dimensionId, location.X, location.Y, location.Z, item.getTypeId(), item.getAmount(), item.getDurability(), 0); + } + + /// + /// Drops an item at the specified Location with a random offset. + /// + /// Location to drop the item. + /// ItemStack to drop. + public void dropItemNaturally(Location location, ItemStack item) + { + NativeBridge.DropItem?.Invoke(_dimensionId, location.X, location.Y, location.Z, item.getTypeId(), item.getAmount(), item.getDurability(), 1); + } +} diff --git a/Minecraft.Server.FourKit/docs/install.md b/Minecraft.Server.FourKit/docs/install.md new file mode 100644 index 00000000..e3328ff7 --- /dev/null +++ b/Minecraft.Server.FourKit/docs/install.md @@ -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 \ No newline at end of file diff --git a/Minecraft.Server.FourKit/docs/main.md b/Minecraft.Server.FourKit/docs/main.md new file mode 100644 index 00000000..b2f40468 --- /dev/null +++ b/Minecraft.Server.FourKit/docs/main.md @@ -0,0 +1,9 @@ +@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). + +You can join the discord server for support [here](https://discord.gg/n5PrRTy3TJ). Report issues and make suggestions on the [github](https://github.com/sylvessa/MinecraftConsoles) + +@ref install \ No newline at end of file diff --git a/Minecraft.Server.FourKit/docs/plugin-creation.md b/Minecraft.Server.FourKit/docs/plugin-creation.md new file mode 100644 index 00000000..a1d4cf1e --- /dev/null +++ b/Minecraft.Server.FourKit/docs/plugin-creation.md @@ -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. + +Note: 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. + +**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 "); +``` + +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 - 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: + + + +
    +
  1. + Install Fody Costura +

    This can be through NuGet, or through anything you wish to use for getting dependencies.

    +
  2. + +
  3. + Create a FodyWeavers.xml in your project root: +```xml + + + + + Minecraft.Server.FourKit + + + +``` +

    This will exclude bundling fourkit in the DLL too.

    +
  4. + +
  5. + Make csproj copy dependency DLL files over to build dir +

    You can do this by adding <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> to the property group.

    +
  6. + +
  7. + Build +

    After you've done all this, it should build and put all dependencies into one DLL in your output folder.

    +
  8. +
\ No newline at end of file diff --git a/Minecraft.Server.FourKit/docs/sending-packets.md b/Minecraft.Server.FourKit/docs/sending-packets.md new file mode 100644 index 00000000..ec1d83e4 --- /dev/null +++ b/Minecraft.Server.FourKit/docs/sending-packets.md @@ -0,0 +1,1189 @@ +@page sending-packets Sending Packets + +This page covers how to manually send raw packets to clients using the \ref Minecraft.Server.FourKit.Experimental.PlayerConnection "PlayerConnection" API. This is an **experimental** feature for advanced use cases where you need to send data that FourKit doesnt yet wrap in its API. + +> **This API is experimental and may change. You are responsible for constructing valid packets!!! malformed data can crash or disconnect the client.** + +Also please keep in mind, some of this info may not be accurate! Feel free to improve this by contributing on the Github. + +--- + +@section packet_overview Overview + +Every Minecraft packet on the wire looks like this: + +| Field | Size | Description | +|-------|------|-------------| +| Size | 4 bytes (big-endian) | Total length of the remaining data (packet ID + payload). **Written automatically by the server.. you do NOT include it.** | +| Packet ID | 1 byte | Which packet this is. Only the low byte matters on the wire even though IDs are `int` in source. | +| Payload | variable | The rest of the packet data, format depends on the packet ID. | + +When you call `PlayerConnection.send(byte[] data)`, the server prepends the 4-byte big-endian size header for you. Your byte array should start with the packet ID byte followed by the payload. + +--- + +@section wire_data_types Wire Data Types + +All multi-byte values are **big-endian** (most significant byte first). + +| Type | Size | Description | +|------|------|-------------| +| `byte` | 1 | Unsigned 8-bit integer (0-255). | +| `bool` | 1 | 0 = false, non-zero = true. | +| `short` | 2 | Signed 16-bit integer. | +| `int` | 4 | Signed 32-bit integer. | +| `long` | 8 | Signed 64-bit integer. | +| `float` | 4 | IEEE 754 single-precision float. | +| `utf` | 2 + n | Modified UTF-8 string: `short` byte-length prefix followed by that many bytes of data. See \ref string_encoding "String Encoding". | +| `item` | variable | Item data (item ID, count, damage). See \ref item_data "Item Data". | +| `metadata` | variable | Entity metadata (SynchedEntityData). See \ref metadata_encoding "Metadata Encoding". | + +--- + +@section string_encoding String Encoding + +Strings use modified UTF-8 encoding: + +1. A `short` (2 bytes, big-endian) containing the byte length of the string data. +2. That many bytes of modified-UTF8 characters. + +For most ASCII text (chat messages, names, etc.) this is identical to standard UTF-8. A helper to write a string: + +```csharp +static int WriteUTF(byte[] buffer, int offset, string text) +{ + byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(text); + WriteShort(buffer, offset, (short)utf8.Length); + Buffer.BlockCopy(utf8, 0, buffer, offset + 2, utf8.Length); + return 2 + utf8.Length; // total bytes written +} +``` + +--- + +@section item_data Item Data + +Several container packets include serialized item data. The wire format for a single item slot is: + +| Field | Type | Description | +|-------|------|-------------| +| id | `short` | Item ID. `-1` means the slot is empty (no further fields follow). | +| count | `byte` | Stack size. | +| damage | `short` | Damage/metadata value. | + +If the item ID is `-1`, only the `short` is written (2 bytes for an empty slot). Otherwise all three fields are written (5 bytes). + +--- + +@section metadata_encoding Metadata Encoding (SynchedEntityData) + +Entity metadata is used in packets like AddMobPacket (24), AddPlayerPacket (20), and SetEntityDataPacket (40). It's a list of typed key-value entries terminated by `0x7F`. + +Each entry starts with a 1-byte header: +- **Bits 5-7** (mask `0xE0`): Type ID, shifted right by 5. +- **Bits 0-4** (mask `0x1F`): Data slot ID (max 31). + +So the header byte is `(type << 5) | (id & 0x1F)`. + +The value immediately follows, sized based on the type: + +| Type ID | Name | Value Size | +|---------|------|------------| +| 0 | Byte | 1 byte | +| 1 | Short | 2 bytes | +| 2 | Int | 4 bytes | +| 3 | Float | 4 bytes | +| 4 | String | `utf` (2-byte length prefix + string data) | +| 5 | ItemInstance | `item` (see \ref item_data "Item Data") | + +After all entries, write `0x7F` as the EOF marker. + +@subsubsection metadata_data_ids Common Data Slot IDs + +These are the data slot IDs used by the entity class hierarchy. When constructing metadata, define these in order and use the correct type. + +**Entity (base):** +| ID | Type | Name | Description | +|----|------|------|-------------| +| 0 | Byte | DATA_SHARED_FLAGS | Bitfield: bit 0=on fire, 1=sneaking, 3=sprinting, 4=using item, 5=invisible, 6=idle anim | +| 1 | Short | DATA_AIR_SUPPLY | Air supply (default 300, max 300) | + +**LivingEntity:** +| ID | Type | Name | Description | +|----|------|------|-------------| +| 6 | Float | DATA_HEALTH | Health value (default 1.0) | +| 7 | Int | DATA_EFFECT_COLOR | Potion effect color (default 0) | +| 8 | Byte | DATA_EFFECT_AMBIENCE | Potion effect ambience (default 0) | +| 9 | Byte | DATA_ARROW_COUNT | Number of arrows stuck in entity (default 0) | + +**Mob:** +| ID | Type | Name | Description | +|----|------|------|-------------| +| 10 | String | DATA_CUSTOM_NAME | Custom name tag (default "") | +| 11 | Byte | DATA_CUSTOM_NAME_VISIBLE | Show name tag (0 or 1, default 0) | + +For example, to write metadata for a basic mob with a custom name: + +```csharp +// helper to write a metadata entry header +static void WriteMetaHeader(byte[] buffer, int offset, int type, int id) +{ + buffer[offset] = (byte)((type << 5) | (id & 0x1F)); +} + +// build metadata for a mob with a visible custom name "booty" +// enity base: flags=0, air=300 +// livingentity: health=20.0, effectColor=0, effectAmbience=0, arrowCount=0 +// mob: customName="booty", customNameVisible=1 + +byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes("booty"); +int metaSize = (1+1) + (1+2) + (1+4) + (1+4) + (1+1) + (1+1) + + (1 + 2 + nameBytes.Length) + (1+1) + 1; // +1 for EOF +byte[] meta = new byte[metaSize]; +int pos = 0; + +// Entity +WriteMetaHeader(meta, pos, 0, 0); pos++; meta[pos++] = 0; // flags = 0 +WriteMetaHeader(meta, pos, 1, 1); pos++; WriteShort(meta, pos, 300); pos += 2; // air supply + +// LivingEntity +WriteMetaHeader(meta, pos, 3, 6); pos++; WriteFloat(meta, pos, 20.0f); pos += 4; // health +WriteMetaHeader(meta, pos, 2, 7); pos++; WriteInt(meta, pos, 0); pos += 4; // effect color +WriteMetaHeader(meta, pos, 0, 8); pos++; meta[pos++] = 0; // effect ambience +WriteMetaHeader(meta, pos, 0, 9); pos++; meta[pos++] = 0; // arrow count + +// Mob +WriteMetaHeader(meta, pos, 4, 10); pos++; +pos += WriteUTF(meta, pos, "booty"); // custom name +WriteMetaHeader(meta, pos, 0, 11); pos++; meta[pos++] = 1; // name visible + +meta[pos] = 0x7F; // EOF marker +``` + +--- + +@section writing_data Writing Packet Data + +Helper methods for writing big-endian values into a `byte[]` buffer: + +```csharp +static void WriteByte(byte[] buffer, int offset, int value) +{ + buffer[offset] = (byte)(value & 0xFF); +} + +static void WriteBool(byte[] buffer, int offset, bool value) +{ + buffer[offset] = (byte)(value ? 1 : 0); +} + +static void WriteShort(byte[] buffer, int offset, short value) +{ + buffer[offset] = (byte)((value >> 8) & 0xFF); + buffer[offset + 1] = (byte)(value & 0xFF); +} + +static void WriteInt(byte[] buffer, int offset, int value) +{ + buffer[offset] = (byte)((value >> 24) & 0xFF); + buffer[offset + 1] = (byte)((value >> 16) & 0xFF); + buffer[offset + 2] = (byte)((value >> 8) & 0xFF); + buffer[offset + 3] = (byte)(value & 0xFF); +} + +static void WriteFloat(byte[] buffer, int offset, float value) +{ + byte[] bytes = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); + Buffer.BlockCopy(bytes, 0, buffer, offset, 4); +} + +static void WriteLong(byte[] buffer, int offset, long value) +{ + buffer[offset] = (byte)((value >> 56) & 0xFF); + buffer[offset + 1] = (byte)((value >> 48) & 0xFF); + buffer[offset + 2] = (byte)((value >> 40) & 0xFF); + buffer[offset + 3] = (byte)((value >> 32) & 0xFF); + buffer[offset + 4] = (byte)((value >> 24) & 0xFF); + buffer[offset + 5] = (byte)((value >> 16) & 0xFF); + buffer[offset + 6] = (byte)((value >> 8) & 0xFF); + buffer[offset + 7] = (byte)(value & 0xFF); +} + +static void WriteEmptyItem(byte[] buffer, int offset) +{ + WriteShort(buffer, offset, (short)-1); +} + +static void WriteItem(byte[] buffer, int offset, short id, byte count, short damage) +{ + WriteShort(buffer, offset, id); + buffer[offset + 2] = count; + WriteShort(buffer, offset + 3, damage); +} +``` + +--- + +@section precision_scaling Precision Scaling + +Some fields use fixed-point encoding integers on the wire representing floats/doubles with a multiplier applied: + +| Data Type | Multiplier | Wire Type | Description | +|-----------|------------|-----------|-------------| +| Entity position | x 32 | `int` | `(int)(position * 32)`. 1/32 block precision. | +| Sound position | x 8 | `int` | `(int)(position * 8)`. 1/8 block precision. | +| Rotation | x 256 / 360 | `byte` | `(byte)(angle * 256.0 / 360.0)`. | +| Velocity | x 8000 | `short` | `(short)(velocity * 8000.0)`, clamped to +/-3.9 blocks/tick before encoding. | + +```csharp +int wireX = (int)(player.getLocation().getX() * 32); +int wireY = (int)(player.getLocation().getY() * 32); +int wireZ = (int)(player.getLocation().getZ() * 32); + +byte wireYaw = (byte)(player.getLocation().getYaw() * 256.0 / 360.0); +byte wirePitch = (byte)(player.getLocation().getPitch() * 256.0 / 360.0); +``` + +--- + +@section example_sound Example: Playing a Sound Effect + +Sends a LevelSoundPacket (ID 62) to play a ghast scream at the player's location whenever they chat. + +```csharp +[EventHandler] +public void onPlayerChat(PlayerChatEvent e) +{ + // LevelSoundPacket layout: + // [0] byte packet ID (62) + // [1..4] int sound type + // [5..8] int x * 8 + // [9..12] int y * 8 + // [13..16] int z * 8 + // [17..20] float volume + // [21..24] float pitch + byte[] buffer = new byte[25]; + buffer[0] = (byte)62; // packet id + + WriteInt(buffer, 1, (int)19); // eSoundType_MOB_GHAST_SCREAM (Sound.GHAST_SCREAM) + + WriteInt(buffer, 5, (int)(e.getPlayer().getLocation().getX() * 8)); + WriteInt(buffer, 9, (int)(e.getPlayer().getLocation().getY() * 8)); + WriteInt(buffer, 13, (int)(e.getPlayer().getLocation().getZ() * 8)); + + WriteFloat(buffer, 17, 10); // volume + WriteFloat(buffer, 21, 1); // pitch + + e.getPlayer().getConnection().send(buffer); +} +``` + +Sound type IDs correspond to the \ref Minecraft.Server.FourKit.Sound "Sound" enum. Cast any `Sound` value to `int` to get the wire value, for example `(int)Sound.GHAST_SCREAM` is `19`. + +--- + +@section example_gamee Example: Changing the Game Mode + +GameEventPacket (ID 70) notifies the client of game state changes. + +```csharp +[EventHandler] +public void onJoin(PlayerJoinEvent e) +{ + // layout: + // [0] byte packet ID (70) + // [1] byte event type + // [2] byte parameter + byte[] buffer = new byte[3]; + buffer[0] = (byte)70; // packet id + buffer[1] = (byte)3; // CHANGE_GAME_MODE + buffer[2] = (byte)1; // 0 = Survival, 1 = Creative, 2 = Adventure + e.getPlayer().getConnection().send(buffer); +} +``` + +--- + +@section example_entity_teleport Example: Teleporting an Entity (Client-Side) + +TeleportEntityPacket (ID 34) moves an entity to an absolute position on the client. + +```csharp +// Teleport entity 42 to (100.5, 64.0, -200.25) +byte[] buffer = new byte[17] +buffer[0] = (byte)34; // packet id + +WriteShort(buffer, 1, (short)42); // entity id +WriteInt(buffer, 3, (int)(100.5 * 32)); // x * 32 +WriteInt(buffer, 7, (int)(64.0 * 32)); // y * 32 +WriteInt(buffer, 11, (int)(-200.25 * 32)); // z * 32 +buffer[15] = (byte)(90.0 * 256.0 / 360.0); // yRot +buffer[16] = (byte)(0.0 * 256.0 / 360.0); // xRot + +player.getConnection().send(buffer); +``` + +--- + +@section example_set_time Example: Setting the World Time + +SetTimePacket (ID 4) updates the client's world time. + +```csharp +// set time to noon (6000 ticks) +byte[] buffer = new byte[17] +buffer[0] = (byte)4; // packet id + +WriteLong(buffer, 1, 6000L); // game time (total ticks elapsed) +WriteLong(buffer, 9, 6000L); // day time (time of day, 0-24000) + +player.getConnection().send(buffer); +``` + +--- + +@section example_health Example: Updating Health Display + +SetHealthPacket (ID 8) updates the client's health, food, and saturation display. + +```csharp +// set health to 20 (full), food to 20 (full), saturation to 5.0 +byte[] buffer = new byte[12]; +buffer[0] = (byte)8; // packet id + +WriteFloat(buffer, 1, 20.0f); // health +WriteShort(buffer, 5, (short)20); // food +WriteFloat(buffer, 7, 5.0f); // saturation +buffer[11] = (byte)0; // damage source (0 = unknown) + +player.getConnection().send(buffer); +``` + +--- + +@section example_abilities Example: Setting Player Abilities + +PlayerAbilitiesPacket (ID 202) updates the player's ability flags and speeds. + +```csharp +// enable flying for the player +byte[] buffer = new byte[10]; +buffer[0] = (byte)202; // packet id + +// 0x01=invulnerable, 0x02=flying, 0x04=canFly, 0x08=instabuild (creative) +buffer[1] = (byte)(0x02 | 0x04); // flying + canFly + +WriteFloat(buffer, 2, 0.05f); // fly speed (default 0.05) +WriteFloat(buffer, 6, 0.1f); // walk speed (default 0.1) + +player.getConnection().send(buffer); +``` + +--- + +@section example_explosion Example: Creating an Explosion Effect + +ExplodePacket (ID 60) creates a client-side explosion with optional block destruction and knockback. + +```csharp +// create an explosion at (100.0, 64.0, 200.0) with radius 4.0 and no destroyed blocks +byte[] buffer = new byte[46]; +buffer[0] = (byte)60; // packet id + +buffer[1] = (byte)0; // knockbackOnly = false (full explosion with position data) + +// position (double precision, 8 bytes each) +byte[] xBytes = BitConverter.GetBytes(100.0); +byte[] yBytes = BitConverter.GetBytes(64.0); +byte[] zBytes = BitConverter.GetBytes(200.0); +Buffer.BlockCopy(xBytes, 0, buffer, 2, 8); +Buffer.BlockCopy(yBytes, 0, buffer, 10, 8); +Buffer.BlockCopy(zBytes, 0, buffer, 18, 8); + +WriteFloat(buffer, 26, 4.0f); // radius +WriteInt(buffer, 30, 0); // destroyed block count (0 = no blocks) + +// knockback velocity applied to player +WriteFloat(buffer, 34, 0.0f); // knockback X +WriteFloat(buffer, 38, 0.5f); // knockback Y (push player up) +WriteFloat(buffer, 42, 0.0f); // knockback Z + +player.getConnection().send(buffer); +``` + +--- + +@section example_xp Example: Setting the XP Bar + +SetExperiencePacket (ID 43) updates the XP bar. + +```csharp +// Set XP bar to 50% progress, level 10, total 300 XP +byte[] buffer = new byte[1 + 4 + 2 + 2]; // 9 bytes +buffer[0] = (byte)43; // packet id + +WriteFloat(buffer, 1, 0.5f); // bar progress (0.0 to 1.0) +WriteShort(buffer, 5, (short)10); // level +WriteShort(buffer, 7, (short)300); // total XP points + +player.getConnection().send(buffer); +``` + +--- + +@section packet_reference Complete packet reference + +All server-to-client packet layouts. The packet ID byte is always `buffer[0]` and is not listed in the field tables below. only the payload after the ID is shown. + +@subsection packet_ref_general General / Connection Packets + +@subsubsection pkt_0 Packet 0 - KeepAlivePacket + +Connection keepalive. The client echoes this back. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | id | Keepalive ID. The client should respond with the same value. | + +**Total payload: 4 bytes.** + +@subsubsection pkt_3 Packet 3 - ChatPacket + +Send a chat/system message to the client. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | messageType | Message type enum value. | +| 2 | `short` | packedCounts | Packed field: high nibble = string arg count, low nibble = int arg count. Computed as `(stringCount << 4) \| intCount`. | +| 4 | `utf[]` | stringArgs | Variable number of `utf` strings (the message text, source name, item name, etc). | +| ... | `int[]` | intArgs | Variable number of `int` values (for example source entity type). | + +**Total payload: variable.** For a simple chat message, `messageType` = 0, one string arg (the message), zero int args. The packed counts would be `(1 << 4) | 0` = `0x0010`. + +@subsubsection pkt_255 Packet 255 - DisconnectPacket + +Disconnect the client. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | reason | Disconnect reason code. | + +**Total payload: 4 bytes.** + +@subsection packet_ref_world World & Environment Packets + +@subsubsection pkt_4 Packet 4 - SetTimePacket + +Set world time. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `long` | gameTime | Total game time in ticks (monotonically increasing). | +| 8 | `long` | dayTime | Time of day in ticks (0-24000 range). | + +**Total payload: 16 bytes.** + +@subsubsection pkt_6 Packet 6 - SetSpawnPositionPacket + +Set the world spawn point. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | x | Spawn X coordinate. | +| 4 | `int` | y | Spawn Y coordinate. | +| 8 | `int` | z | Spawn Z coordinate. | + +**Total payload: 12 bytes.** + +@subsubsection pkt_9 Packet 9 - RespawnPacket + +Sent on respawn or dimension change. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | dimension | Dimension ID (0=Overworld, -1=Nether, 1=End). | +| 1 | `byte` | gameType | Game mode ID (0=Survival, 1=Creative, 2=Adventure). | +| 2 | `short` | mapHeight | World height. | +| 4 | `utf` | levelType | Level type name (for example "DEFAULT", "FLAT"). | +| ... | `long` | mapSeed | World seed. | +| ... | `byte` | difficulty | Difficulty (0=Peaceful, 1=Easy, 2=Normal, 3=Hard). | +| ... | `bool` | newSeaLevel | Whether the new sea level is active. | +| ... | `short` | newEntityId | The player's new entity ID. | +| ... | `short` | xzSize | World XZ size. | +| ... | `byte` | hellScale | Nether scale factor. | + +**Total payload: variable** (depends on level type string length). + +@subsubsection pkt_53 Packet 53 - TileUpdatePacket + +Update a single block. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | x | Block X coordinate. | +| 4 | `byte` | y | Block Y coordinate (0-255). | +| 5 | `int` | z | Block Z coordinate. | +| 9 | `short` | block | Block type ID. | +| 11 | `byte` | dataLevel | Block data/metadata value. | + +**Total payload: 12 bytes.** + +@subsubsection pkt_54 Packet 54 - TileEventPacket + +Trigger a block action (note blocks, pistons, chests). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | x | Block X coordinate. | +| 4 | `short` | y | Block Y coordinate. | +| 6 | `int` | z | Block Z coordinate. | +| 10 | `byte` | b0 | Action parameter 1 (depends on block type). | +| 11 | `byte` | b1 | Action parameter 2 (depends on block type). | +| 12 | `short` | tile | Block type ID. | + +**Total payload: 14 bytes.** + +@subsubsection pkt_55 Packet 55 - TileDestructionPacket + +Show a block breaking animation (crack overlay). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | id | Breaker entity ID. | +| 4 | `int` | x | Block X coordinate. | +| 8 | `int` | y | Block Y coordinate. | +| 12 | `int` | z | Block Z coordinate. | +| 16 | `byte` | state | Destroy stage (0-9). Any other value removes the overlay. | + +**Total payload: 17 bytes.** + +@subsubsection pkt_60 Packet 60 - ExplodePacket + +Explosion with optional block destruction and knockback. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `bool` | knockbackOnly | If true, only knockback fields follow (no position/radius/blocks). | + +If `knockbackOnly` is **false** (typical explosion): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 1 | `double` | x | Explosion center X. | +| 9 | `double` | y | Explosion center Y. | +| 17 | `double` | z | Explosion center Z. | +| 25 | `float` | radius | Explosion radius. | +| 29 | `int` | count | Number of destroyed block offsets. | +| 33 | `byte[count*3]` | offsets | For each block: 3 signed bytes (dx, dy, dz) relative to the center. | +| ... | `float` | knockbackX | Player knockback velocity X. | +| ... | `float` | knockbackY | Player knockback velocity Y. | +| ... | `float` | knockbackZ | Player knockback velocity Z. | + +If `knockbackOnly` is **true** (just apply knockback, no visual explosion): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 1 | `float` | knockbackX | Player knockback velocity X. | +| 5 | `float` | knockbackY | Player knockback velocity Y. | +| 9 | `float` | knockbackZ | Player knockback velocity Z. | + +**Total payload: variable.** + +@subsubsection pkt_61 Packet 61 - LevelEventPacket + +World event (sounds, particles, door effects, etc). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | type | Event type ID (for example 1000=click sound, 1005=bow sound, 2000=smoke, 2001=break block). | +| 4 | `int` | x | Block X coordinate. | +| 8 | `byte` | y | Block Y coordinate. | +| 9 | `int` | z | Block Z coordinate. | +| 13 | `int` | data | Event-specific data (for example block ID for break effect, direction for smoke). | +| 17 | `bool` | globalEvent | If true, event is global (all players hear it regardless of distance). | + +**Total payload: 18 bytes.** + +@subsubsection pkt_62 Packet 62 - LevelSoundPacket + +Play a sound at a position. Positions use x8 scaling. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | soundId | Sound type ID. Maps to the \ref Minecraft.Server.FourKit.Sound "Sound" enum. | +| 4 | `int` | x | X coordinate * 8. | +| 8 | `int` | y | Y coordinate * 8. | +| 12 | `int` | z | Z coordinate * 8. | +| 16 | `float` | volume | Sound volume (1.0 = normal). | +| 20 | `float` | pitch | Sound pitch (1.0 = normal). | + +**Total payload: 24 bytes.** + +@subsubsection pkt_63 Packet 63 - LevelParticlesPacket + +Spawn particles at a position. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `utf` | name | Particle name string (for example "flame", "smoke", "heart"). | +| ... | `float` | x | Center X coordinate. | +| ... | `float` | y | Center Y coordinate. | +| ... | `float` | z | Center Z coordinate. | +| ... | `float` | xDist | Random X offset range. | +| ... | `float` | yDist | Random Y offset range. | +| ... | `float` | zDist | Random Z offset range. | +| ... | `float` | maxSpeed | Maximum particle speed. | +| ... | `int` | count | Number of particles to spawn. | + +**Total payload: variable** (depends on particle name string length) **+ 32 bytes** for the fixed fields. + +@subsubsection pkt_70 Packet 70 - GameEventPacket + +Game state change notification. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | event | Event type. | +| 1 | `byte` | param | Event parameter. | + +Event types: +| Value | Name | Parameter | +|-------|------|-----------| +| 0 | No Bed | (unused) | +| 1 | Start Rain | (unused) | +| 2 | Stop Rain | (unused) | +| 3 | Change Game Mode | 0=Survival, 1=Creative, 2=Adventure | +| 4 | Win Game | 0=show credits, 1=just respawn | + +**Total payload: 2 bytes.** + +@subsubsection pkt_130 Packet 130 - SignUpdatePacket + +Update sign text. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | x | Sign X coordinate. | +| 4 | `short` | y | Sign Y coordinate. | +| 6 | `int` | z | Sign Z coordinate. | +| 10 | `bool` | verified | Whether the sign text has been verified. | +| 11 | `bool` | censored | Whether the sign text has been censored. | +| 12 | `utf` | line1 | First line of text. | +| ... | `utf` | line2 | Second line of text. | +| ... | `utf` | line3 | Third line of text. | +| ... | `utf` | line4 | Fourth line of text. | + +**Total payload: variable** (12 bytes fixed + 4 * `utf` strings). + +@subsection packet_ref_entity Entity Packets + +@subsubsection pkt_8 Packet 8 - SetHealthPacket + +Update health, food, and saturation. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `float` | health | Current health (0.0-20.0, 20 = full). | +| 4 | `short` | food | Food level (0-20). | +| 6 | `float` | saturation | Food saturation level. | +| 10 | `byte` | damageSource | Damage source type (for telemetry). | + +**Total payload: 11 bytes.** + +@subsubsection pkt_18 Packet 18 - AnimatePacket + +Play an entity animation. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | action | Animation type (1=swing arm, 2=damage, 3=leave bed, 104=crouch, 105=uncrouch). | + +**Total payload: 5 bytes.** + +@subsubsection pkt_20 Packet 20 - AddPlayerPacket + +Spawn a named player entity. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `utf` | name | Player name. | +| ... | `int` | x | X coordinate * 32 (fixed-point). | +| ... | `int` | y | Y coordinate * 32 (fixed-point). | +| ... | `int` | z | Z coordinate * 32 (fixed-point). | +| ... | `byte` | yRot | Yaw (angle * 256 / 360). | +| ... | `byte` | xRot | Pitch (angle * 256 / 360). | +| ... | `byte` | yHeadRot | Head yaw (angle * 256 / 360). | +| ... | `short` | carriedItem | Item ID of held item. | +| ... | `playerUID` | xuid | Player XUID (8 bytes). | +| ... | `playerUID` | onlineXuid | Online XUID for splitscreen guests (8 bytes). | +| ... | `byte` | playerIndex | Local player index. | +| ... | `int` | skinId | Custom skin ID. | +| ... | `int` | capeId | Custom cape ID. | +| ... | `int` | gamePrivileges | Player game privileges bitfield. | +| ... | `metadata` | entityData | Entity metadata (SynchedEntityData). | + +**Total payload: variable** (includes entity metadata). + +@subsubsection pkt_23 Packet 23 - AddEntityPacket + +Spawn a non-mob entity (minecart, arrow, falling block, etc). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | type | Entity type ID (for example 1=boat, 10=minecart, 50=TNT, 60=arrow, 70=falling block). | +| 3 | `int` | x | X coordinate * 32 (fixed-point). | +| 7 | `int` | y | Y coordinate * 32 (fixed-point). | +| 11 | `int` | z | Z coordinate * 32 (fixed-point). | +| 15 | `byte` | yRot | Yaw (angle * 256 / 360). | +| 16 | `byte` | xRot | Pitch (angle * 256 / 360). | +| 17 | `int` | data | Entity-specific data (for example block ID for falling blocks, owner entity ID for projectiles). | + +If `data` is non-zero, three additional velocity fields follow: + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 21 | `short` | xVel | X velocity * 8000. | +| 23 | `short` | yVel | Y velocity * 8000. | +| 25 | `short` | zVel | Z velocity * 8000. | + +**Total payload: 21 bytes** (no velocity) or **27 bytes** (with velocity). + +@subsubsection pkt_24 Packet 24 - AddMobPacket + +Spawn a mob. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | mobType | Mob type ID (for example 50=Creeper, 51=Skeleton, 52=Spider, 54=Zombie, 90=Pig, 91=Sheep). | +| 3 | `int` | x | X coordinate * 32 (fixed-point). | +| 7 | `int` | y | Y coordinate * 32 (fixed-point). | +| 11 | `int` | z | Z coordinate * 32 (fixed-point). | +| 15 | `byte` | yRot | Yaw (angle * 256 / 360). | +| 16 | `byte` | xRot | Pitch (angle * 256 / 360). | +| 17 | `byte` | yHeadRot | Head yaw (angle * 256 / 360). | +| 18 | `short` | xVel | X velocity * 8000. | +| 20 | `short` | yVel | Y velocity * 8000. | +| 22 | `short` | zVel | Z velocity * 8000. | +| 24 | `metadata` | entityData | Entity metadata (SynchedEntityData). | + +**Total payload: 24 bytes + variable metadata.** + +@subsubsection pkt_26 Packet 26 - AddExperienceOrbPacket + +Spawn an XP orb. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `int` | x | X coordinate * 32 (fixed-point). | +| 8 | `int` | y | Y coordinate * 32 (fixed-point). | +| 12 | `int` | z | Z coordinate * 32 (fixed-point). | +| 16 | `short` | value | XP value of the orb. | + +**Total payload: 18 bytes.** + +@subsubsection pkt_28 Packet 28 - SetEntityMotionPacket + +Set entity velocity. Has two encoding modes based on a flag bit in the entity ID field. + +The first field is a `short` combining the entity ID and a flag: +- **Bits 0-10** (mask `0x07FF`): Entity ID (max 2047). +- **Bit 11** (mask `0x0800`): If set, velocity uses 3 bytes (lower precision). If clear, 3 shorts (full precision). + +**Full precision mode** (flag clear): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | idAndFlag | Entity ID (low 11 bits), flag=0. | +| 2 | `short` | xVel | X velocity * 8000. | +| 4 | `short` | yVel | Y velocity * 8000. | +| 6 | `short` | zVel | Z velocity * 8000. | + +**Total payload: 8 bytes.** + +**Compact mode** (flag set, bit 11 = 1): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | idAndFlag | Entity ID (low 11 bits) OR `0x0800`. | +| 2 | `byte` | xVel | X velocity / 16 (sign-extended, then multiplied by 16 on read). | +| 3 | `byte` | yVel | Y velocity / 16. | +| 4 | `byte` | zVel | Z velocity / 16. | + +**Total payload: 5 bytes.** The server automatically picks compact mode when velocity values fit. + +@subsubsection pkt_29 Packet 29 - RemoveEntitiesPacket + +Despawn one or more entities. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | count | Number of entities to remove. | +| 1 | `int[count]` | entityIds | Array of entity IDs (4 bytes each). | + +**Total payload: 1 + (count * 4) bytes.** + +@subsubsection pkt_30_33 Packets 30-33 - MoveEntityPacket + +Relative entity movement/rotation updates. Four sub-types: + +**Packet 30 - MoveEntityPacket** (base, no movement): `short` entityId only. **Payload: 2 bytes.** + +**Packet 31 - MoveEntityPacket.Pos** (position only): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | dx | X delta (signed, in 1/32 block units). | +| 3 | `byte` | dy | Y delta. | +| 4 | `byte` | dz | Z delta. | + +**Payload: 5 bytes.** + +**Packet 32 - MoveEntityPacket.Rot** (rotation only): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | yRot | New yaw (angle * 256 / 360). | +| 3 | `byte` | xRot | New pitch (angle * 256 / 360). | + +**Payload: 4 bytes.** + +**Packet 33 - MoveEntityPacket.PosRot** (position + rotation): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `byte` | dx | X delta. | +| 3 | `byte` | dy | Y delta. | +| 4 | `byte` | dz | Z delta. | +| 5 | `byte` | yRot | New yaw. | +| 6 | `byte` | xRot | New pitch. | + +**Payload: 7 bytes.** + +@subsubsection pkt_34 Packet 34 - TeleportEntityPacket + +Teleport an entity to an absolute position. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `short` | entityId | Entity ID. | +| 2 | `int` | x | X coordinate * 32 (fixed-point). | +| 6 | `int` | y | Y coordinate * 32 (fixed-point). | +| 10 | `int` | z | Z coordinate * 32 (fixed-point). | +| 14 | `byte` | yRot | Yaw (angle * 256 / 360). | +| 15 | `byte` | xRot | Pitch (angle * 256 / 360). | + +**Total payload: 16 bytes.** + +@subsubsection pkt_35 Packet 35 - RotateHeadPacket + +Update an entity's head rotation. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | yHeadRot | Head yaw (angle * 256 / 360). | + +**Total payload: 5 bytes.** + +@subsubsection pkt_38 Packet 38 - EntityEventPacket + +Trigger an entity event (hurt, death, eating, etc). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | eventId | Event type (2=hurt, 3=death, 9=eating finished). | + +**Total payload: 5 bytes.** + +@subsubsection pkt_39 Packet 39 - SetEntityLinkPacket + +Attach or detach entities (leash, riding). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | sourceId | Entity being attached (the rider/leashed entity). | +| 4 | `int` | destId | Entity being attached to (the vehicle/fence). `-1` to detach. | +| 8 | `byte` | type | Link type. | + +**Total payload: 9 bytes.** + +@subsubsection pkt_40 Packet 40 - SetEntityDataPacket + +Update entity metadata. See \ref metadata_encoding "Metadata Encoding" for how to construct the metadata blob. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `metadata` | data | Packed entity metadata entries. | + +**Total payload: 4 bytes + variable metadata.** + +@subsubsection pkt_41 Packet 41 - UpdateMobEffectPacket + +Apply or update a potion effect. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | effectId | Effect ID (1=speed, 2=slowness, 3=haste, 4=mining fatigue, 5=strength, ...). | +| 5 | `byte` | amplifier | Effect level (0 = level I, 1 = level II, etc). | +| 6 | `short` | duration | Duration in ticks. | + +**Total payload: 8 bytes.** + +@subsubsection pkt_42 Packet 42 - RemoveMobEffectPacket + +Remove a potion effect. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | effectId | Effect ID to remove. | + +**Total payload: 5 bytes.** + +@subsubsection pkt_43 Packet 43 - SetExperiencePacket + +Update the XP bar. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `float` | progress | XP bar fill amount (0.0-1.0). | +| 4 | `short` | level | Current level. | +| 6 | `short` | totalXP | Total experience points. | + +**Total payload: 8 bytes.** + +@subsubsection pkt_71 Packet 71 - AddGlobalEntityPacket + +Spawn a global entity (lightning bolt). + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `byte` | type | Entity type (1 = lightning bolt). | +| 5 | `int` | x | X coordinate * 32 (fixed-point). | +| 9 | `int` | y | Y coordinate * 32 (fixed-point). | +| 13 | `int` | z | Z coordinate * 32 (fixed-point). | + +**Total payload: 17 bytes.** + +@subsection packet_ref_player Player Packets + +@subsubsection pkt_5 Packet 5 - SetEquippedItemPacket + +Change the visible held item for an entity. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | entityId | Entity ID. | +| 4 | `short` | slot | Equipment slot (0=held). | +| 6 | `item` | item | Item data (see \ref item_data "Item Data"). | + +**Total payload: 6 bytes + item data** (2 bytes if empty, 5 bytes if present). + +@subsubsection pkt_200 Packet 200 - AwardStatPacket + +Award a statistic or achievement. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `int` | statId | Statistic/achievement ID. | +| 4 | `int` | length | Length of parameter data blob in bytes. | +| 8 | `byte[length]` | data | Parameter data (typically a 4-byte `int` count). | + +**Total payload: 8 bytes + length bytes.** For a simple stat increment, `length` = 4 and `data` contains an `int` count. + +@subsubsection pkt_202 Packet 202 - PlayerAbilitiesPacket + +Update player abilities. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | flags | Bitfield: `0x01`=invulnerable, `0x02`=flying, `0x04`=canFly, `0x08`=instabuild (creative). | +| 1 | `float` | flySpeed | Fly speed (default 0.05). | +| 5 | `float` | walkSpeed | Walk speed (default 0.1). | + +**Total payload: 9 bytes.** + +@subsection packet_ref_container Container Packets + +@subsubsection pkt_100 Packet 100 - ContainerOpenPacket + +Open a container window. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | containerId | Window ID. | +| 1 | `byte` | type | Container type (0=chest, 1=workbench, 2=furnace, 3=dispenser, 4=enchanting table). | +| 2 | `byte` | size | Number of slots. | +| 3 | `bool` | customName | Whether a custom title follows. | + +If `type` == HORSE (type 12): + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 4 | `int` | entityId | Horse entity ID. | + +If `customName` is true: + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| ... | `utf` | title | Custom container title. | + +**Total payload: 4 bytes minimum**, variable with conditionals. + +@subsubsection pkt_101 Packet 101 - ContainerClosePacket + +Close a container window. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | containerId | Window ID. | + +**Total payload: 1 byte.** + +@subsubsection pkt_103 Packet 103 - ContainerSetSlotPacket + +Set a single slot in a container. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | containerId | Window ID. | +| 1 | `short` | slot | Slot index. | +| 3 | `item` | item | Item data (see \ref item_data "Item Data"). | + +**Total payload: 3 bytes + item data** (2 bytes if empty, 5 bytes if present). + +@subsubsection pkt_104 Packet 104 - ContainerSetContentPacket + +Set all slots in a container. + +| Offset | Type | Field | Description | +|--------|------|-------|-------------| +| 0 | `byte` | containerId | Window ID. | +| 1 | `short` | count | Number of item slots. | +| 3 | `item[count]` | items | Array of item data entries. | + +**Total payload: 3 bytes + count * item data.** + +@subsection packet_ref_all_ids All Packet IDs + +Every registered packet. **S-C** = server to client, **C-S** = client to server. + +Just know that some of these packets have no function (such as scoreboards), most should work. + +| ID | Name | S-C | C-S | Notes | +|----|------|:---:|:---:|-------| +| 0 | KeepAlivePacket | ✓ | ✓ | Connection keepalive. | +| 1 | LoginPacket | ✓ | ✓ | Login handshake. | +| 2 | PreLoginPacket | ✓ | ✓ | Pre-login handshake. | +| 3 | ChatPacket | ✓ | ✓ | Chat messages. | +| 4 | SetTimePacket | ✓ | | World time. | +| 5 | SetEquippedItemPacket | ✓ | | Held item display. | +| 6 | SetSpawnPositionPacket | ✓ | | World spawn point. | +| 7 | InteractPacket | | ✓ | Player interact with entity. | +| 8 | SetHealthPacket | ✓ | | Health/food/saturation. | +| 9 | RespawnPacket | ✓ | ✓ | Respawn/dimension change. | +| 10 | MovePlayerPacket | ✓ | ✓ | Player position (base). | +| 11 | MovePlayerPacket.Pos | ✓ | ✓ | Player position only. | +| 12 | MovePlayerPacket.Rot | ✓ | ✓ | Player rotation only. | +| 13 | MovePlayerPacket.PosRot | ✓ | ✓ | Player position + rotation. | +| 14 | PlayerActionPacket | | ✓ | Block breaking, item dropping. | +| 15 | UseItemPacket | | ✓ | Place block / use item. | +| 16 | SetCarriedItemPacket | ✓ | ✓ | Hotbar slot selection. | +| 17 | EntityActionAtPositionPacket | ✓ | | Sleep in bed. | +| 18 | AnimatePacket | ✓ | ✓ | Entity animations. | +| 19 | PlayerCommandPacket | | ✓ | Sneak, sprint, etc. | +| 20 | AddPlayerPacket | ✓ | | Spawn named player. | +| 22 | TakeItemEntityPacket | ✓ | | Item pickup animation. | +| 23 | AddEntityPacket | ✓ | | Spawn non-mob entity. | +| 24 | AddMobPacket | ✓ | | Spawn mob. | +| 25 | AddPaintingPacket | ✓ | | Spawn painting. | +| 26 | AddExperienceOrbPacket | ✓ | | Spawn XP orb. | +| 27 | PlayerInputPacket | | ✓ | Vehicle steering input. | +| 28 | SetEntityMotionPacket | ✓ | | Entity velocity. | +| 29 | RemoveEntitiesPacket | ✓ | | Despawn entities. | +| 30 | MoveEntityPacket | ✓ | | Entity movement (base). | +| 31 | MoveEntityPacket.Pos | ✓ | | Entity position delta. | +| 32 | MoveEntityPacket.Rot | ✓ | | Entity rotation. | +| 33 | MoveEntityPacket.PosRot | ✓ | | Entity pos + rot delta. | +| 34 | TeleportEntityPacket | ✓ | | Entity absolute position. | +| 35 | RotateHeadPacket | ✓ | | Entity head rotation. | +| 38 | EntityEventPacket | ✓ | | Entity events (hurt, death). | +| 39 | SetEntityLinkPacket | ✓ | | Leash / riding. | +| 40 | SetEntityDataPacket | ✓ | | Entity metadata update. | +| 41 | UpdateMobEffectPacket | ✓ | | Apply potion effect. | +| 42 | RemoveMobEffectPacket | ✓ | | Remove potion effect. | +| 43 | SetExperiencePacket | ✓ | | XP bar update. | +| 44 | UpdateAttributesPacket | ✓ | | Entity attributes. | +| 50 | ChunkVisibilityPacket | ✓ | | Chunk visibility. | +| 51 | BlockRegionUpdatePacket | ✓ | | Chunk data. | +| 52 | ChunkTilesUpdatePacket | ✓ | | Multi-block change. | +| 53 | TileUpdatePacket | ✓ | | Single block change. | +| 54 | TileEventPacket | ✓ | | Block action. | +| 55 | TileDestructionPacket | ✓ | | Block breaking animation. | +| 60 | ExplodePacket | ✓ | | Explosion. | +| 61 | LevelEventPacket | ✓ | | World event (sounds, particles). | +| 62 | LevelSoundPacket | ✓ | | Sound effect. | +| 63 | LevelParticlesPacket | ✓ | | Particle effect. | +| 70 | GameEventPacket | ✓ | | Game state change. | +| 71 | AddGlobalEntityPacket | ✓ | | Lightning bolt. | +| 100 | ContainerOpenPacket | ✓ | | Open container. | +| 101 | ContainerClosePacket | ✓ | ✓ | Close container. | +| 102 | ContainerClickPacket | | ✓ | Click container slot. | +| 103 | ContainerSetSlotPacket | ✓ | ✓ | Set container slot. | +| 104 | ContainerSetContentPacket | ✓ | | Set all container slots. | +| 105 | ContainerSetDataPacket | ✓ | | Container progress bar data. | +| 106 | ContainerAckPacket | ✓ | ✓ | Transaction acknowledgement. | +| 107 | SetCreativeModeSlotPacket | ✓ | ✓ | Creative inventory action. | +| 108 | ContainerButtonClickPacket | | ✓ | Enchanting / other button. | +| 130 | SignUpdatePacket | ✓ | ✓ | Sign text update. | +| 131 | ComplexItemDataPacket | ✓ | | Map data. | +| 132 | TileEntityDataPacket | ✓ | | Tile entity NBT data. | +| 133 | TileEditorOpenPacket | ✓ | | Open tile entity editor. | +| 200 | AwardStatPacket | ✓ | | Award statistic. | +| 201 | PlayerInfoPacket | ✓ | ✓ | Player list info. | +| 202 | PlayerAbilitiesPacket | ✓ | ✓ | Player abilities. | +| 206 | SetObjectivePacket | ✓ | | Scoreboard objective. | +| 207 | SetScorePacket | ✓ | | Scoreboard score. | +| 208 | SetDisplayObjectivePacket | ✓ | | Scoreboard display slot. | +| 209 | SetPlayerTeamPacket | ✓ | | Scoreboard team. | +| 255 | DisconnectPacket | ✓ | ✓ | Disconnect. | + +--- + +@section where_to_look Where to Look + +- **\ref Minecraft.Server.FourKit.Experimental.PlayerConnection "PlayerConnection"** - Get it via `player.getConnection()`. This is what you call `send()` on. +- **\ref Minecraft.Server.FourKit.Sound "Sound"** - All sound type IDs for LevelSoundPacket (62). Cast to `int` for the wire value. +- **\ref Minecraft.Server.FourKit.GameMode "GameMode"** - Game mode constants for GameEventPacket (70) and PlayerAbilitiesPacket (202). +- **\ref Minecraft.Server.FourKit.Location "Location"** - Get world coordinates from `player.getLocation()` for position encoding. +- **Packet source files** - The definitive reference is the `write()` method of each packet class in thre code (for example `LevelSoundPacket.cpp`, `TeleportEntityPacket.cpp`). + +--- + +@section extra-info Extra info + +- **Packet IDs** are a single byte. Only the low byte matters on the wire. +- **Position values** in entity packets use *32 fixed-point. Sound event positions use *8. +- **Rotation angles** are a single byte: `(byte)(angle * 256.0 / 360.0)`. +- **Velocity** is `(short)(velocity * 8000.0)`, clamped to +/-3.9 blocks/tick before encoding. +- **Strings** use modified UTF-8 with a 2-byte length prefix. +- **Item data** writes `-1` as a short for empty slots (2 bytes), or short ID + byte count + short damage for occupied slots (5 bytes). +- The 4-byte size header is written by the server automatically - don't include it in your byte array. +- FourKit's \ref Minecraft.Server.FourKit.Sound "Sound" enum values map directly to the sound IDs used in LevelSoundPacket. VERY USEFUL!!!! +- If you send malformed data, the client will likely disconnect or crash, or do some funny weird stuff. Test carefully. +- Some packets have conditional fields (ExplodePacket, ContainerOpenPacket, SetEntityMotionPacket). Read the wire format tables above carefully. +- **Entity metadata** - see \ref metadata_encoding "Metadata Encoding" for how to construct SynchedEntityData blobs by hand. diff --git a/Minecraft.Server.FourKit/docs/setup.md b/Minecraft.Server.FourKit/docs/setup.md new file mode 100644 index 00000000..c4f186ca --- /dev/null +++ b/Minecraft.Server.FourKit/docs/setup.md @@ -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 + + + ..\Minecraft.Server.FourKit.dll + + +``` + +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 + + + + net10.0 + enable + enable + + + + + ..\Minecraft.Server.FourKit.dll + + + + +``` + +@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 \ No newline at end of file diff --git a/Minecraft.Server.FourKit/docs/usage-of-all-events.md b/Minecraft.Server.FourKit/docs/usage-of-all-events.md new file mode 100644 index 00000000..66c27212 --- /dev/null +++ b/Minecraft.Server.FourKit/docs/usage-of-all-events.md @@ -0,0 +1,1135 @@ +@page usage-of-all-events Usage of all Events + +This page covers every event that FourKit provides, with descriptions of what they do and code examples showing how to use them. Every event handler method must be `public`, return `void`, take a single event parameter, and be annotated with `[EventHandler]` inside a class that implements \ref Minecraft.Server.FourKit.Event.Listener "Listener". + +Events that implement \ref Minecraft.Server.FourKit.Event.Cancellable "Cancellable" can be cancelled by calling `setCancelled(true)`. A cancelled event will not execute its default server action (such as preventing chatting), but will still be passed to other plugins (unless IgnoreCancelled is set to true for a certain event handler) + +> **Theres no guarantee this will be fully up to date as new events get added. Be sure to check the actual API documentation. Also some of these functions may not work as intended, as we are still in the process of rewriting everything. If something doesnt work, let us know on github.** + +--- + +@section player_events Player Events + +@subsection playerloginevent PlayerLoginEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerLoginEvent "PlayerLoginEvent" is fired when a player is about to log in, after pre-login checks but before the join event. You can inspect or modify the player's XUIDs, name, and IP address, and cancel the login. The XUID API is **experimental**: you can set and get the raw XUID values. The offline XUID is the main XUID used by the client; the online XUID is used for guests (splitscreen users). + +```csharp +[EventHandler] +public void onLogin(PlayerLoginEvent e) +{ + // block a specific XUID + if (e.getOfflineXuid() == 12345678901234567UL) + { + e.setCancelled(true); + } + + // change the XUID (experimental) + e.setOfflineXuid(98765432109876543UL); +} +``` + +| Method | Description | +|--------|-------------| +| `getName()` | The player's username attempting to log in. | +| `getAddress()` | The \ref Minecraft.Server.FourKit.Net.InetSocketAddress "InetSocketAddress" of the connection. | +| `getLoginType()` | The login type (e.g. online, offline). | +| `getOfflineXuid()` | **Experimental.** The main XUID used by the client. | +| `setOfflineXuid(ulong)` | **Experimental.** Set the offline XUID. | +| `getOnlineXuid()` | **Experimental.** The XUID used for guests (splitscreen users). | +| `setOnlineXuid(ulong)` | **Experimental.** Set the online XUID. | +| `hasChangedXuidValues()` | **Experimental.** True if XUID values were changed. | +| `isCancelled()` | Whether the login is cancelled. | +| `setCancelled(bool)` | Cancel or allow the login. | + +> **Cancellable:** Yes + +@subsection playerpreloginevent PlayerPreLoginEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerPreLoginEvent "PlayerPreLoginEvent" is fired before a player is allowed to join the server. You can inspect the players name and IP address, and cancel the event to prevent them from joining (such as for bans, whitelists, etc). + +```csharp +[EventHandler] +public void onPreLogin(PlayerPreLoginEvent e) +{ + // block by name + if (e.getName() == "Dumb") + { + e.setCancelled(true); + } + + // block by ip + if (e.getAddress().getHostAddress() == "127.0.0.1") + { + e.setCancelled(true); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getName()` | The player's username attempting to join. | +| `getAddress()` | The \ref Minecraft.Server.FourKit.Net.InetSocketAddress "InetSocketAddress" of the connection. | +| `isCancelled()` | Whether the login is cancelled. | +| `setCancelled(bool)` | Cancel or allow the login. | + +> **Cancellable:** Yes + +@subsection playerjoinevent PlayerJoinEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerJoinEvent "PlayerJoinEvent" is fired when a player joins the server. You can read or change the join message that is broadcast to all online players. + +```csharp +[EventHandler] +public void onJoin(PlayerJoinEvent e) +{ + e.setJoinMessage("Welcome, " + e.getPlayer().getName() + "!"); +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who joined. | +| `getJoinMessage()` | The join message that will be broadcast. | +| `setJoinMessage(string)` | Change or suppress the join message. Pass `null` to suppress. | + +> **Cancellable:** No + +--- + +@subsection playerquitevent PlayerQuitEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerQuitEvent "PlayerQuitEvent" is fired when a player disconnects from the server. You can change the quit message or suppress it. + +```csharp +[EventHandler] +public void onQuit(PlayerQuitEvent e) +{ + e.setQuitMessage(e.getPlayer().getName() + " has left."); +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who left. | +| `getQuitMessage()` | The quit message that will be broadcast. | +| `setQuitMessage(string)` | Change or suppress the quit message. Pass `null` to suppress. | + +> **Cancellable:** No + +--- + +@subsection playerkickevent PlayerKickEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerKickEvent "PlayerKickEvent" is fired when a player is about to be kicked. Cancelling this event keeps the player connected. + +```csharp +[EventHandler] +public void onKick(PlayerKickEvent e) +{ + // Prevent all kicks + e.setCancelled(true); +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player being kicked. | +| `getReason()` | The \ref Minecraft.Server.FourKit.Entity.DisconnectReason "DisconnectReason". | +| `setReason(DisconnectReason)` | Change the kick reason. | +| `getLeaveMessage()` | The message broadcast to other players. | +| `setLeaveMessage(string)` | Change the broadcast message. | + +> **Cancellable:** Yes + +--- + +@subsection playerchatevent PlayerChatEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerChatEvent "PlayerChatEvent" is fired when a player sends a chat message. You can modify the message, change the format, or cancel it entirely. + +Format is the same as bukkit, with `%1$s` being the player name and ` %2$s` being the message. + +```csharp +[EventHandler] +public void onChat(PlayerChatEvent e) +{ + // [PlayerName] message + e.setFormat("[%1$s] %2$s"); + + // or modify the message itself + e.setMessage(e.getMessage().ToUpper()); // ALL CAPS +} +``` + +```csharp +[EventHandler] +public void onMute(PlayerChatEvent e) +{ + // Mute a specific player + if (e.getPlayer().getName() == "DumbLoserFatty123") + { + e.setCancelled(true); + e.getPlayer().sendMessage("You are muted!"); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who sent the message. | +| `getMessage()` | The chat message. | +| `setMessage(string)` | Change the chat message. | +| `getFormat()` | The format string (e.g. `"<%1$s> %2$s"`). | +| `setFormat(string)` | Change the format string. | + +> **Cancellable:** Yes + +--- + +@subsection playermoveevent PlayerMoveEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerMoveEvent "PlayerMoveEvent" is fired whenever a player moves. You can redirect the player or cancel the movement. + +```csharp +[EventHandler] +public void onMove(PlayerMoveEvent e) +{ + // Freeze players in place + e.setCancelled(true); +} +``` + +```csharp +[EventHandler] +public void onMove(PlayerMoveEvent e) +{ + // prevent players from going above Y=100 + Location to = e.getTo(); + if (to.getY() > 100) + { + to.setY(100); + e.setTo(to); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who moved. | +| `getFrom()` | The location the player moved from. | +| `getTo()` | The location the player is moving to. | +| `setFrom(Location)` | Override the from location. | +| `setTo(Location)` | Redirect the player to a different destination. | + +> **Cancellable:** Yes + +--- + +@subsection playerteleportevent PlayerTeleportEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerTeleportEvent "PlayerTeleportEvent" extends `PlayerMoveEvent` and is fired when a player teleports. It includes a \ref Minecraft.Server.FourKit.Event.Player.PlayerTeleportEvent.TeleportCause "TeleportCause" describing why the teleport happened. + +```csharp +[EventHandler] +public void onTeleport(PlayerTeleportEvent e) +{ + if (e.getCause() == PlayerTeleportEvent.TeleportCause.ENDER_PEARL) + { + e.setCancelled(true); + e.getPlayer().sendMessage("ender pearls are a big nono!"); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who teleported. | +| `getFrom()` | The origin location. | +| `getTo()` | The destination location. | +| `setTo(Location)` | Redirect the teleport destination. | +| `getCause()` | The `TeleportCause` (`ENDER_PEARL`, `COMMAND`, `PLUGIN`, `NETHER_PORTAL`, `END_PORTAL`, `UNKNOWN`). | + +> **Cancellable:** Yes (inherited from `PlayerMoveEvent`) + +--- + +@subsection playerportalevent PlayerPortalEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerPortalEvent "PlayerPortalEvent" extends `PlayerTeleportEvent` and is fired when a player enters a portal (nether or end). You can cancel it to prevent dimension travel. + +```csharp +[EventHandler] +public void onPortal(PlayerPortalEvent e) +{ + // no nether travel + if (e.getCause() == PlayerTeleportEvent.TeleportCause.NETHER_PORTAL) + { + e.setCancelled(true); + e.getPlayer().sendMessage("you cannot go to the nether!!!"); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player entering the portal. | +| `getFrom()` | Where the player is coming from. | +| `getTo()` | Where the player will arrive. | +| `setTo(Location)` | Override the destination. | +| `getCause()` | The teleport cause. | + +> **Cancellable:** Yes (inherited) + +--- + +@subsection playerdropitemevent PlayerDropItemEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerDropItemEvent "PlayerDropItemEvent" is fired when a player drops an item from their inventory. You can cancel it or change what item is dropped. + +```csharp +[EventHandler] +public void onDrop(PlayerDropItemEvent e) +{ + // do not let a user drop diamonds + if (e.getItemDrop().getType() == Material.DIAMOND) + { + e.setCancelled(true); + e.getPlayer().sendMessage("You can't drop that!"); + } +} +``` + +```csharp +[EventHandler] +public void onDrop(PlayerDropItemEvent e) +{ + // replace dropped item with dirt + e.setItemDrop(new ItemStack(Material.DIRT, 1)); +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who dropped the item. | +| `getItemDrop()` | The `ItemStack` being dropped. | +| `setItemDrop(ItemStack)` | Change which item is dropped. | + +> **Cancellable:** Yes + +--- + +@subsection playerpickupitemevent PlayerPickupItemEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerPickupItemEvent "PlayerPickupItemEvent" is fired when a player picks up an item from the ground. + +```csharp +[EventHandler] +public void onPickup(PlayerPickupItemEvent e) +{ + // disable picking up items + e.setCancelled(true); +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player picking up the item. | +| `getItem()` | The \ref Minecraft.Server.FourKit.Entity.Item "Item" entity on the ground. | + +> **Cancellable:** Yes + +--- + +@subsection playerinteractevent PlayerInteractEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerInteractEvent "PlayerInteractEvent" is fired when a player interacts with an object, block, or air. The \ref Minecraft.Server.FourKit.Block.Action "Action" enum tells you what kind of click it was. + +```csharp +[EventHandler] +public void onInteract(PlayerInteractEvent e) +{ + if (e.getAction() == Action.RIGHT_CLICK_BLOCK && e.hasBlock()) + { + e.getPlayer().sendMessage("You clicked on " + e.getClickedBlock().getType()); + } +} +``` + +```csharp +[EventHandler] +public void onInteract(PlayerInteractEvent e) +{ + // no using flint and steel + if (e.getMaterial() == Material.FLINT_AND_STEEL) + { + e.setCancelled(true); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who interacted. | +| `getAction()` | The `Action` (`LEFT_CLICK_AIR`, `LEFT_CLICK_BLOCK`, `RIGHT_CLICK_AIR`, `RIGHT_CLICK_BLOCK`, `PHYSICAL`). | +| `getItem()` | The `ItemStack` in the player's hand, or `null`. | +| `getMaterial()` | CONVENIENCE. the `Material` of the held item, or `AIR`. | +| `hasBlock()` | Whether a block was involved. | +| `hasItem()` | Whether an item was in hand. | +| `isBlockInHand()` | Whether the held item is a block (id 1-255). | +| `getClickedBlock()` | The `Block` that was clicked, or `null`. | +| `getBlockFace()` | The `BlockFace` that was clicked. | +| `useItemInHand()` | Whether the item in hand should be used. | +| `setUseItemInHand(bool)` | Override whether the item in hand is used. | + +> **Cancellable:** Yes + +--- + +@subsection playerinteractentityevent PlayerInteractEntityEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerInteractEntityEvent "PlayerInteractEntityEvent" is fired when a player right-clicks an entity. + +```csharp +[EventHandler] +public void onInteractEntity(PlayerInteractEntityEvent e) +{ + e.getPlayer().sendMessage("You interacted with entity id " + e.getRightClicked().getEntityId()); +} +``` + +```csharp +[EventHandler] +public void onInteractEntity(PlayerInteractEntityEvent e) +{ + // do not let user interact with a villager + if (e.getRightClicked().getType() == EntityType.VILLAGER) + { + e.setCancelled(true); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who interacted. | +| `getRightClicked()` | The entity that was right-clicked. | + +> **Cancellable:** Yes + +--- + +@subsection playerbedenterevent PlayerBedEnterEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerBedEnterEvent "PlayerBedEnterEvent" is fired when a player is about to enter a bed. + +```csharp +[EventHandler] +public void onBedEnter(PlayerBedEnterEvent e) +{ + e.setCancelled(true); + e.getPlayer().sendMessage("Sleeping is disabled on this server."); +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player entering the bed. | +| `getBed()` | The bed `Block`. | + +> **Cancellable:** Yes + +--- + +@subsection playerbedleaveevent PlayerBedLeaveEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerBedLeaveEvent "PlayerBedLeaveEvent" is fired when a player leaves a bed. + +```csharp +[EventHandler] +public void onBedLeave(PlayerBedLeaveEvent e) +{ + e.getPlayer().sendMessage("Bro left the bed"); +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player leaving the bed. | +| `getBed()` | The bed `Block`. | + +> **Cancellable:** No + +--- + +@subsection playercommandpreprocessevent PlayerCommandPreprocessEvent + +\ref Minecraft.Server.FourKit.Event.Player.PlayerCommandPreprocessEvent "PlayerCommandPreprocessEvent" is fired early in the command handling process when a player sends a command. You can modify the command, cancel it, or use it for logging. This fires before the command is dispatched to any command handler. + +```csharp +[EventHandler] +public void onCommand(PlayerCommandPreprocessEvent e) +{ + // log all commands + Console.WriteLine(e.getPlayer().getName() + " issued command: " + e.getMessage()); +} +``` + +```csharp +[EventHandler] +public void onCommand(PlayerCommandPreprocessEvent e) +{ + // block a specific command + if (e.getMessage().StartsWith("/secret")) + { + e.setCancelled(true); + e.getPlayer().sendMessage("That command is disabled!"); + } +} +``` + +```csharp +[EventHandler] +public void onCommand(PlayerCommandPreprocessEvent e) +{ + // redirect a command alias + if (e.getMessage().StartsWith("/gm ")) + { + e.setMessage("/gamemode " + e.getMessage().Substring(4)); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The player who issued the command. | +| `getMessage()` | The full command string the player is sending. | +| `setMessage(string)` | Change the command that will be processed. | +| `isCancelled()` | Whether the command is cancelled. | +| `setCancelled(bool)` | Cancel or allow the command. | + +> **Cancellable:** Yes + +--- + +@section entity_events Entity Events + +@subsection entitydamageevent EntityDamageEvent + +\ref Minecraft.Server.FourKit.Event.Entity.EntityDamageEvent "EntityDamageEvent" is fired when any entity takes damage. The entity can be a `Player` or any `LivingEntity`. Use `getEntityType()` to check before casting. + +```csharp +[EventHandler] +public void onDamage(EntityDamageEvent e) +{ + // halve damage + e.setDamage(e.getDamage() / 2); +} +``` + +```csharp +[EventHandler] +public void onDamage(EntityDamageEvent e) +{ + // no fall damage + if (e.getEntityType() == EntityType.PLAYER && + e.getCause() == EntityDamageEvent.DamageCause.FALL) + { + e.setCancelled(true); + } + + // another method for negating fall damage is in playermoveevent and using player.setFallDistance(0) +} +``` + +```csharp +[EventHandler] +public void onDamage(EntityDamageEvent e) +{ + // cast to living entity + LivingEntity entity = (LivingEntity)e.getEntity(); + if (entity.getHealth() - e.getFinalDamage() <= 0) + { + // entity is going to die + } +} +``` + +| Method | Description | +|--------|-------------| +| `getEntity()` | The entity that was damaged. | +| `getEntityType()` | The `EntityType` of the damaged entity. | +| `getCause()` | The `DamageCause` (e.g. `FALL`, `FIRE`, `ENTITY_ATTACK`, `VOID`, `DROWNING`, etc.). | +| `getDamage()` | The raw damage amount. | +| `setDamage(double)` | Change the raw damage amount. | +| `getFinalDamage()` | The damage after reductions. | + +> **Cancellable:** Yup. cancelling prevents any damage. + +--- + +@subsection entitydamagebyentityevent EntityDamageByEntityEvent + +\ref Minecraft.Server.FourKit.Event.Entity.EntityDamageByEntityEvent "EntityDamageByEntityEvent" extends `EntityDamageEvent` and is fired when an entity is damaged by another entity (e.g. player hits a mob, or mob hits a player). + +```csharp +[EventHandler] +public void onPvP(EntityDamageByEntityEvent e) +{ + // no PVP + if (e.getEntity().getType() == EntityType.PLAYER && + e.getDamager().getType() == EntityType.PLAYER) + { + e.setCancelled(true); + Player attacker = (Player)e.getDamager(); + attacker.sendMessage("PvP is disabled!"); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getEntity()` | The entity that was damaged. | +| `getDamager()` | The entity that dealt the damage. | +| `getCause()` | The `DamageCause`. | +| `getDamage()` / `setDamage(double)` | Read/modify the damage. | + +> **Cancellable:** Yes + +--- + +@subsection playerdeathevent PlayerDeathEvent + +\ref Minecraft.Server.FourKit.Event.Entity.PlayerDeathEvent "PlayerDeathEvent" extends `EntityDeathEvent` and is fired when a player dies. It adds control over the death message, respawn experience, and keep-inventory. + +```csharp +[EventHandler] +public void onPlayerDeath(PlayerDeathEvent e) +{ + e.setDeathMessage(e.getEntity().getName() + " was eliminated!"); + e.setKeepInventory(true); // player keeps their items + e.setKeepLevel(true); // player keeps their XP level + e.setDroppedExp(0); // dont drop any XP orbs +} +``` + +```csharp +[EventHandler] +public void onPlayerDeath(PlayerDeathEvent e) +{ + // clear all item drops and set new respawn XP + e.getDrops().Clear(); + e.setNewExp(0); + e.setNewLevel(0); +} +``` + +| Method | Description | +|--------|-------------| +| `getEntity()` | The `Player` who died. | +| `getDeathMessage()` | The death message shown to all players. | +| `setDeathMessage(string)` | Change the death message. | +| `getDroppedExp()` / `setDroppedExp(int)` | XP orbs dropped on death. | +| `getDrops()` | Mutable list of item drops. | +| `getNewExp()` / `setNewExp(int)` | XP the player has after respawning. | +| `getNewLevel()` / `setNewLevel(int)` | Level the player has after respawning. | +| `getKeepLevel()` / `setKeepLevel(bool)` | Whether the player keeps their XP level. Overrides other XP settings. | +| `getKeepInventory()` / `setKeepInventory(bool)` | Whether the player keeps their inventory on death. | + +> **Cancellable:** No + +--- + + +@section world_events World Events + +@subsection structuregrowevent StructureGrowEvent + +\ref Minecraft.Server.FourKit.Event.World.StructureGrowEvent "StructureGrowEvent" is fired when an organic structure attempts to grow (Sapling -> Tree), (Mushroom -> Huge Mushroom), either naturally or using bonemeal. You can cancel it, inspect the location, species, and optionally the player who caused it. + +```csharp +[EventHandler] +public void onStructureGrow(StructureGrowEvent e) +{ + // prevent huge mushrooms from growing + if (e.getSpecies() == TreeType.BROWN_MUSHROOM || e.getSpecies() == TreeType.RED_MUSHROOM) + { + e.setCancelled(true); + } + + // send a message if grown by player + var player = e.getPlayer(); + if (player != null) + { + player.sendMessage($"You grew a {e.getSpecies()} at {e.getLocation()}"); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getLocation()` | The location of the structure. | +| `getSpecies()` | The species type (birch, normal, pine, red mushroom, brown mushroom). | +| `isFromBonemeal()` | True if the structure was grown using bonemeal. | +| `getPlayer()` | The player that created the structure, null if not created manually. | +| `isCancelled()` | Whether the event is cancelled. | +| `setCancelled(bool)` | Cancel or allow the structure growth. | + +> **Cancellable:** Yes + +--- +@section block_events Block Events + +@subsection blockbreakevent BlockBreakEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockBreakEvent "BlockBreakEvent" is fired when a player breaks a block. You can cancel it, change the XP dropped, or inspect the block. + +```csharp +[EventHandler] +public void onBreak(BlockBreakEvent e) +{ + // protect stone + if (e.getBlock().getType() == Material.STONE) + { + e.setCancelled(true); + e.getPlayer().sendMessage("You can't break stone!"); + } +} +``` + +```csharp +[EventHandler] +public void onBreak(BlockBreakEvent e) +{ + // drop 15 XP for any block broken in survival + e.setExpToDrop(15); +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The `Block` being broken. | +| `getPlayer()` | The player who broke the block. | +| `getExpToDrop()` | The XP that will be dropped. | +| `setExpToDrop(int)` | Set the XP dropped (1+ to drop, 0 for none). | + +> **Cancellable:** Yes + +--- + +@subsection blockplaceevent BlockPlaceEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockPlaceEvent "BlockPlaceEvent" is fired when a player places a block. You can cancel the placement or change the placed block. + +```csharp +[EventHandler] +public void onPlace(BlockPlaceEvent e) +{ + // replace everything placed with stone + e.getBlock().setType(Material.STONE); +} +``` + +```csharp +[EventHandler] +public void onPlace(BlockPlaceEvent e) +{ + // NO TNT + if (e.getBlock().getType() == Material.TNT) + { + e.setCancelled(true); + e.getPlayer().sendMessage("TNT is NOT ALLOWED!"); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` / `getBlockPlaced()` | The `Block` that was placed. | +| `getBlockAgainst()` | The block this was placed against. | +| `getItemInHand()` | The `ItemStack` used to place the block. | +| `getPlayer()` | The player who placed the block. | + +> **Cancellable:** Yes + +--- + +@subsection signchangeevent SignChangeEvent + +\ref Minecraft.Server.FourKit.Event.Block.SignChangeEvent "SignChangeEvent" is fired when a player edits a sign. You can read, modify, or cancel the sign text. + +```csharp +[EventHandler] +public void onSign(SignChangeEvent e) +{ + // log what was written in the sign + for (int i = 0; i < 4; i++) + { + Console.WriteLine($"Line {i}: {e.getLine(i)}"); + } +} +``` + +```csharp +[EventHandler] +public void onSign(SignChangeEvent e) +{ + // censor a word + for (int i = 0; i < 4; i++) + { + if (e.getLine(i).Contains("shit")) + e.setLine(i, "****"); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The sign `Block`. | +| `getPlayer()` | The player who edited the sign. | +| `getLines()` | All 4 lines as a `string[]`. | +| `getLine(int)` | A single line (0-3). | +| `setLine(int, string)` | Change a single line (0-3). | + +> **Cancellable:** Yes + +--- + + +--- + +@subsection blockgrowevent BlockGrowEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockGrowEvent "BlockGrowEvent" is fired when a block grows naturally in the world. This includes crops (wheat, nether wart, cocoa beans), sugar cane, cactus, and melon/pumpkin fruit placement. + +```csharp +[EventHandler] +public void onGrow(BlockGrowEvent e) +{ + // prevent all crop growth + e.setCancelled(true); +} +``` + +```csharp +[EventHandler] +public void onGrow(BlockGrowEvent e) +{ + // log growth events + var b = e.getBlock(); + Console.WriteLine($"Block grew at {b.getX()}, {b.getY()}, {b.getZ()} type={b.getType()}"); +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The `Block` that is growing. | +| `getNewState()` | The `BlockState` representing what the block will become. | +| `isCancelled()` | Whether the growth is cancelled. | +| `setCancelled(bool)` | Cancel or allow the growth. | + +> **Cancellable:** Yes + +--- + +@subsection blockformevent BlockFormEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockFormEvent "BlockFormEvent" extends `BlockGrowEvent` and is fired when a block forms due to world conditions. Examples include snow forming during a storm and ice forming in cold biomes. Use \ref Minecraft.Server.FourKit.Event.Block.BlockSpreadEvent "BlockSpreadEvent" to catch blocks that actually spread instead of randomly forming. + +```csharp +[EventHandler] +public void onForm(BlockFormEvent e) +{ + // prevent snow and ice from forming + e.setCancelled(true); +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The `Block` that is forming. | +| `getNewState()` | The `BlockState` representing what the block will become. | +| `isCancelled()` | Whether the formation is cancelled. | +| `setCancelled(bool)` | Cancel or allow the formation. | + +> **Cancellable:** Yes (inherited from `BlockGrowEvent`) + +--- + +@subsection blockspreadevent BlockSpreadEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockSpreadEvent "BlockSpreadEvent" extends `BlockFormEvent` and is fired when a block spreads from one location to another. Examples include fire spreading, mushrooms spreading, and grass spreading to dirt. + +```csharp +[EventHandler] +public void onSpread(BlockSpreadEvent e) +{ + // prevent fire from spreading + if (e.getSource().getType() == Material.FIRE) + { + e.setCancelled(true); + } +} +``` + +```csharp +[EventHandler] +public void onSpread(BlockSpreadEvent e) +{ + // prevent grass from spreading + if (e.getBlock().getType() == Material.GRASS) + { + e.setCancelled(true); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The `Block` where the spread is occurring (destination). | +| `getSource()` | The source `Block` that is spreading. | +| `getNewState()` | The `BlockState` representing what the block will become. | +| `isCancelled()` | Whether the spread is cancelled. | +| `setCancelled(bool)` | Cancel or allow the spread. | + +> **Cancellable:** Yes (inherited) + +--- + +@subsection blockburnevent BlockBurnEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockBurnEvent "BlockBurnEvent" is fired when a block is destroyed as a result of being burnt by fire. + +```csharp +[EventHandler] +public void onBurn(BlockBurnEvent e) +{ + // prevent wooden planks from burning + if (e.getBlock().getType() == Material.WOOD) + { + e.setCancelled(true); + } +} +``` + +```csharp +[EventHandler] +public void onBurn(BlockBurnEvent e) +{ + // prevent all fire destruction + e.setCancelled(true); +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The `Block` that is being burnt. | +| `isCancelled()` | Whether the burn is cancelled. | +| `setCancelled(bool)` | Cancel or allow the burn. | + +> **Cancellable:** Yes + +--- + +@subsection blockfromtoevent BlockFromToEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockFromToEvent "BlockFromToEvent" is fired when a block moves from one location to another. This currently only applies to liquid flow (lava and water) and teleporting dragon eggs. + +```csharp +[EventHandler] +public void onFromTo(BlockFromToEvent e) +{ + // prevent water from flowing + if (e.getBlock().getType() == Material.WATER || e.getBlock().getType() == Material.STATIONARY_WATER) + { + e.setCancelled(true); + } +} +``` + +```csharp +[EventHandler] +public void onFromTo(BlockFromToEvent e) +{ + // log liquid flow + var from = e.getBlock(); + var to = e.getToBlock(); + Console.WriteLine($"Block moving from {from.getX()},{from.getY()},{from.getZ()} to {to.getX()},{to.getY()},{to.getZ()}"); +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The source `Block` that is moving. | +| `getToBlock()` | The destination `Block`. | +| `getFace()` | The `BlockFace` direction the block is moving to. | +| `isCancelled()` | Whether the move is cancelled. | +| `setCancelled(bool)` | Cancel or allow the move. | + +> **Cancellable:** Yes + +--- + +@subsection blockpistonextendevent BlockPistonExtendEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockPistonExtendEvent "BlockPistonExtendEvent" extends `BlockPistonEvent` and is fired when a piston extends. You can inspect the piston direction, stickiness, number of blocks being pushed, and cancel the extension. + +```csharp +[EventHandler] +public void onPiston(BlockPistonExtendEvent e) +{ + // prevent sticky pistons from extending + if (e.isSticky()) + { + e.setCancelled(true); + } +} +``` + +```csharp +[EventHandler] +public void onPiston(BlockPistonExtendEvent e) +{ + // log piston activity + Console.WriteLine($"Piston extending {e.getDirection()} pushing {e.getLength()} blocks"); +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The piston `Block`. | +| `getDirection()` | The `BlockFace` direction the piston is extending. | +| `isSticky()` | Whether the piston is a sticky piston. | +| `getLength()` | The number of blocks being pushed. | +| `getBlocks()` | List of `Block` objects that will be moved. | +| `isCancelled()` | Whether the extension is cancelled. | +| `setCancelled(bool)` | Cancel or allow the extension. | + +> **Cancellable:** Yes + +--- + +@subsection blockpistonretractevent BlockPistonRetractEvent + +\ref Minecraft.Server.FourKit.Event.Block.BlockPistonRetractEvent "BlockPistonRetractEvent" extends `BlockPistonEvent` and is fired when a piston retracts. For sticky pistons, the retract location indicates where the block being pulled is located. + +```csharp +[EventHandler] +public void onPiston(BlockPistonRetractEvent e) +{ + // prevent all sticky piston retractions + if (e.isSticky()) + { + e.setCancelled(true); + } +} +``` + +```csharp +[EventHandler] +public void onPiston(BlockPistonRetractEvent e) +{ + // log where the retract is pulling from + var loc = e.getRetractLocation(); + Console.WriteLine($"Piston retracting, pull location: {loc.getBlockX()}, {loc.getBlockY()}, {loc.getBlockZ()}"); +} +``` + +| Method | Description | +|--------|-------------| +| `getBlock()` | The piston `Block`. | +| `getDirection()` | The `BlockFace` direction the piston is retracting. | +| `isSticky()` | Whether the piston is a sticky piston. | +| `getRetractLocation()` | The `Location` of the block that may be pulled (for sticky pistons). | +| `isCancelled()` | Whether the retraction is cancelled. | +| `setCancelled(bool)` | Cancel or allow the retraction. | + +> **Cancellable:** Yes + +--- + +@section inventory_events Inventory Events + +@subsection inventoryopenevent InventoryOpenEvent + +\ref Minecraft.Server.FourKit.Event.Inventory.InventoryOpenEvent "InventoryOpenEvent" is fired when a player opens an inventory (chest, furnace, crafting table, etc.). Cancelling this prevents the inventory screen from showing. + +```csharp +[EventHandler] +public void onOpen(InventoryOpenEvent e) +{ + e.getPlayer().sendMessage("You opened: " + e.getInventory().getName()); +} +``` + +```csharp +[EventHandler] +public void onOpen(InventoryOpenEvent e) +{ + // Lock all chests + if (e.getInventory().getType() == InventoryType.CHEST) + { + e.setCancelled(true); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getPlayer()` | The `HumanEntity` who opened the inventory. | +| `getInventory()` | The top `Inventory` that was opened. | +| `getView()` | The full `InventoryView`. | +| `getViewers()` | List of players viewing this inventory. | + +> **Cancellable:** Yes + +--- + +@subsection inventoryclickevent InventoryClickEvent + +\ref Minecraft.Server.FourKit.Event.Inventory.InventoryClickEvent "InventoryClickEvent" is fired when a player clicks a slot in any open inventory. It provides detailed information about the click type, slot, and item. + +> **Warning:** Do not call `closeInventory()` or `openInventory()` from inside this event handler. The inventory is mid-modification. You should do this in a seperate thread fired later on. + +```csharp +[EventHandler] +public void onClick(InventoryClickEvent e) +{ + // do not let user take item from first slot + if (e.getRawSlot() == 0) + { + e.setCancelled(true); + } +} +``` + +```csharp +[EventHandler] +public void onClick(InventoryClickEvent e) +{ + // log every click + Player player = (Player)e.getWhoClicked(); + Console.WriteLine($"{player.getName()} clicked slot {e.getSlot()} with {e.getClick()}"); +} +``` + +| Method | Description | +|--------|-------------| +| `getWhoClicked()` | The `HumanEntity` who clicked. | +| `getClickedInventory()` | The `Inventory` that was clicked, or `null` if outside the window. | +| `getSlotType()` | The `SlotType` of the clicked slot. | +| `getCurrentItem()` | The `ItemStack` in the clicked slot. | +| `setCurrentItem(ItemStack)` | Change the item in the clicked slot. | +| `getCursor()` | The `ItemStack` on the cursor. | +| `getSlot()` | The slot index relative to the clicked inventory. | +| `getRawSlot()` | The raw slot number unique to the view. | +| `getClick()` | The `ClickType` (e.g. `LEFT`, `RIGHT`, `SHIFT_LEFT`). | +| `getAction()` | The `InventoryAction` describing the outcome. | +| `isLeftClick()` / `isRightClick()` / `isShiftClick()` | Convenience click-type checks. | +| `getHotbarButton()` | The hotbar key pressed (0-8), or -1 if not a `NUMBER_KEY` click. | +| `getInventory()` | The top inventory of the view. | +| `getView()` | The full `InventoryView`. | + +> **Cancellable:** Yes + +

Page currently under construction

\ No newline at end of file diff --git a/Minecraft.Server/Access/OpManager.cpp b/Minecraft.Server/Access/OpManager.cpp index b8b76d1e..03cac70f 100644 --- a/Minecraft.Server/Access/OpManager.cpp +++ b/Minecraft.Server/Access/OpManager.cpp @@ -2,11 +2,11 @@ #include "OpManager.h" -#include "..\Common\AccessStorageUtils.h" -#include "..\Common\FileUtils.h" -#include "..\Common\StringUtils.h" -#include "..\ServerLogger.h" -#include "..\vendor\nlohmann\json.hpp" +#include "../Common/AccessStorageUtils.h" +#include "../Common/FileUtils.h" +#include "../Common/StringUtils.h" +#include "../ServerLogger.h" +#include "../vendor/nlohmann/json.hpp" #include diff --git a/Minecraft.Server/CMakeLists.txt b/Minecraft.Server/CMakeLists.txt index a384f7b8..e0bb6a42 100644 --- a/Minecraft.Server/CMakeLists.txt +++ b/Minecraft.Server/CMakeLists.txt @@ -1,86 +1,17 @@ -# Note: A lot of this file is the same as the client due to the shared code +# Vanilla dedicated server (no plugin support). +# +# The FourKit-enabled flavour is built by Minecraft.Server.FourKit/CMakeLists.txt +# from the same shared source list. Both targets use configure_lce_server_target() +# from cmake/ServerTarget.cmake to apply identical compile/link/asset settings. include("${CMAKE_CURRENT_LIST_DIR}/cmake/sources/Common.cmake") - include("${CMAKE_SOURCE_DIR}/cmake/CommonSources.cmake") +include("${CMAKE_SOURCE_DIR}/cmake/ServerTarget.cmake") -# Combine all source files into a single variable for the target -# We cant use CMAKE_CONFIGURE_PRESET here as VS doesn't set it, so just rely on the folder set(MINECRAFT_SERVER_SOURCES ${MINECRAFT_SERVER_COMMON} ${SOURCES_COMMON} ) add_executable(Minecraft.Server ${MINECRAFT_SERVER_SOURCES}) - -target_include_directories(Minecraft.Server PRIVATE - "${CMAKE_BINARY_DIR}/generated/" # This is for the generated BuildVer.h - "${CMAKE_SOURCE_DIR}/Minecraft.Client/" - "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/Iggy/include" - "${CMAKE_CURRENT_SOURCE_DIR}" - "${CMAKE_SOURCE_DIR}/include/" -) -target_compile_definitions(Minecraft.Server PRIVATE - ${MINECRAFT_SHARED_DEFINES} - MINECRAFT_SERVER_BUILD -) -target_precompile_headers(Minecraft.Server PRIVATE "$<$:stdafx.h>") -set_source_files_properties("${CMAKE_SOURCE_DIR}/Minecraft.Client/compat_shims.cpp" PROPERTIES SKIP_PRECOMPILE_HEADERS ON) # This redefines internal MSVC CRT symbols which will cause an issue with PCH - -configure_compiler_target(Minecraft.Server) - -set_target_properties(Minecraft.Server PROPERTIES - OUTPUT_NAME "Minecraft.Server" - VS_DEBUGGER_WORKING_DIRECTORY "$" - VS_DEBUGGER_COMMAND_ARGUMENTS "-port 25565 -bind 0.0.0.0 -name DedicatedServer" -) - -target_link_libraries(Minecraft.Server PRIVATE - Minecraft.World - d3d11 - dxgi - d3dcompiler - XInput9_1_0 - wsock32 - legacy_stdio_definitions - $<$: # Debug 4J libraries - "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Input_d.lib" - "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Storage_d.lib" - "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Render_PC_d.lib" - > - $<$>: # Release 4J libraries - "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Input.lib" - "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Storage.lib" - "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Render_PC.lib" - > -) - -# Iggy libs -foreach(lib IN LISTS IGGY_LIBS) - target_link_libraries(Minecraft.Server PRIVATE "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/Iggy/lib/${lib}") -endforeach() - -# --- -# Asset / redist copy -# --- -include("${CMAKE_SOURCE_DIR}/cmake/CopyAssets.cmake") - -# Copy res -set(ASSET_FOLDER_PAIRS - "${CMAKE_SOURCE_DIR}/Minecraft.Client/Common/res" "Common/res" -) -setup_asset_folder_copy(Minecraft.Server "${ASSET_FOLDER_PAIRS}") - -# Copy arc media -set(ASSET_FILES_PAIRS - "${CMAKE_SOURCE_DIR}/Minecraft.Client/Common/Media/MediaWindows64.arc" "Common/Media/" -) -setup_asset_file_copy(Minecraft.Server "${ASSET_FILES_PAIRS}") - -# Copy redist files -add_copyredist_target(Minecraft.Server) - -# Make sure GameHDD exists on Windows -if(PLATFORM_NAME STREQUAL "Windows64") - add_gamehdd_target(Minecraft.Server) -endif() +configure_lce_server_target(Minecraft.Server) diff --git a/Minecraft.Server/Console/ServerCliEngine.cpp b/Minecraft.Server/Console/ServerCliEngine.cpp index f1569607..037c7a8c 100644 --- a/Minecraft.Server/Console/ServerCliEngine.cpp +++ b/Minecraft.Server/Console/ServerCliEngine.cpp @@ -27,6 +27,7 @@ #include "../Common/StringUtils.h" #include "../ServerShutdown.h" #include "../ServerLogger.h" +#include "../FourKitBridge.h" #include "../../Minecraft.Client/MinecraftServer.h" #include "../../Minecraft.Client/PlayerList.h" #include "../../Minecraft.Client/ServerPlayer.h" @@ -163,6 +164,10 @@ namespace ServerRuntime IServerCliCommand *command = m_registry->FindMutable(parsed.tokens[0]); if (command == NULL) { + if (FourKitBridge::HandleConsoleCommand(normalizedLine)) + { + return true; + } LogWarn("Unknown command: " + parsed.tokens[0]); return false; } @@ -209,6 +214,38 @@ namespace ServerRuntime } m_registry->SuggestCommandNames(prefix, linePrefix, out); + + char buf[4096]; + int len = 0; + int count = FourKitBridge::GetPluginCommandHelp(buf, sizeof(buf), &len); + if (count > 0 && len > 0) + { + const char *ptr = buf; + const char *end = buf + len; + for (int i = 0; i < count && ptr < end; ++i) + { + std::string usage(ptr); + ptr += usage.size() + 1; + if (ptr >= end) + { + break; + } + std::string description(ptr); + ptr += description.size() + 1; + + std::string name = usage; + if (!name.empty() && name[0] == '/') + { + name = name.substr(1); + } + + if (name.size() >= prefix.size() && + name.compare(0, prefix.size(), prefix) == 0) + { + out->push_back(linePrefix + name); + } + } + } } else { diff --git a/Minecraft.Server/Console/commands/help/CliCommandHelp.cpp b/Minecraft.Server/Console/commands/help/CliCommandHelp.cpp index af525ded..8dcdf355 100644 --- a/Minecraft.Server/Console/commands/help/CliCommandHelp.cpp +++ b/Minecraft.Server/Console/commands/help/CliCommandHelp.cpp @@ -4,6 +4,11 @@ #include "../../ServerCliEngine.h" #include "../../ServerCliRegistry.h" +#include "../../ServerCliEngine.h" +#include "../../ServerCliRegistry.h" +#include "../../../FourKitBridge.h" + +#include namespace ServerRuntime { @@ -40,6 +45,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; } } diff --git a/Minecraft.Server/Console/commands/revoketoken/CliCommandRevokeToken.cpp b/Minecraft.Server/Console/commands/revoketoken/CliCommandRevokeToken.cpp index 9388b184..81537c38 100644 --- a/Minecraft.Server/Console/commands/revoketoken/CliCommandRevokeToken.cpp +++ b/Minecraft.Server/Console/commands/revoketoken/CliCommandRevokeToken.cpp @@ -2,11 +2,11 @@ #include "CliCommandRevokeToken.h" -#include "..\..\ServerCliEngine.h" -#include "..\..\ServerCliParser.h" -#include "..\..\..\Access\Access.h" -#include "..\..\..\Security\IdentityTokenManager.h" -#include "..\..\..\ServerLogManager.h" +#include "../../ServerCliEngine.h" +#include "../../ServerCliParser.h" +#include "../../../Access/Access.h" +#include "../../../Security/IdentityTokenManager.h" +#include "../../../ServerLogManager.h" namespace ServerRuntime { diff --git a/Minecraft.Server/FourKitBridge.cpp b/Minecraft.Server/FourKitBridge.cpp new file mode 100644 index 00000000..4d1fab80 --- /dev/null +++ b/Minecraft.Server/FourKitBridge.cpp @@ -0,0 +1,1017 @@ +#include "FourKitBridge.h" +#include "FourKitNatives.h" +#include "FourKitRuntime.h" +#include "Common/StringUtils.h" +#include "ServerLogger.h" +#include "stdafx.h" + +#include +#include +#include + +using ServerRuntime::LogDebugf; +using ServerRuntime::LogError; +using ServerRuntime::LogInfo; + +namespace FourKitBridge +{ + +typedef void(__stdcall *fn_initialize)(); +typedef void(__stdcall *fn_shutdown)(); +typedef void(__stdcall *fn_fire_world_save)(); +typedef int(__stdcall *fn_fire_player_prelogin)(const char* nameUtf8, int nameByteLen, const char* ipUtf8, int ipByteLen, int port); +typedef int(__stdcall *fn_fire_player_login)(const char* nameUtf8, int nameByteLen, const char* ipUtf8, int ipByteLen, int port, int type, unsigned long long* offlineXUID, unsigned long long* onlineXUID); +typedef void(__stdcall *fn_fire_player_join)(int entityId, const char *nameUtf8, int nameByteLen, const char *uuidUtf8, int uuidByteLen, unsigned long long offlineXUID, unsigned long long onlineXUID); +typedef void(__stdcall *fn_fire_player_quit)(int entityId); +typedef int(__stdcall *fn_fire_player_kick)(int entityId, int disconnectReason, + const char *reasonUtf8, int reasonByteLen, + char *outBuf, int outBufSize, int *outLen); +typedef int(__stdcall *fn_fire_player_move)(int entityId, + double fromX, double fromY, double fromZ, + double toX, double toY, double toZ, + double *outCoords); +typedef void(__stdcall *fn_set_native_callbacks)(void *damage, void *setHealth, void *teleport, void *setGameMode, void *broadcastMessage, void *setFallDistance, void *getPlayerSnapshot, void *sendMessage, void *setWalkSpeed, void *teleportEntity); +typedef void(__stdcall *fn_set_world_callbacks)(void *getTileId, void *getTileData, void *setTile, void *setTileData, void *breakBlock, void *getHighestBlockY, void *getWorldInfo, void *setWorldTime, void *setWeather, void *createExplosion, void *strikeLightning, void *setSpawnLocation, void *dropItem); +typedef void(__stdcall *fn_update_entity_id)(int oldEntityId, int newEntityId); +typedef int(__stdcall *fn_fire_structure_grow)(int dimId, int x, int y, int z, int species, int wasBonemeal, int entityId); +typedef int(__stdcall *fn_fire_player_chat)(int entityId, const char *msgUtf8, int msgByteLen, char *outBuf, int outBufSize, int *outLen); +typedef int(__stdcall *fn_fire_block_place)(int entityId, int dimId, + int placedX, int placedY, int placedZ, + int againstX, int againstY, int againstZ, + int itemId, int itemCount, int canBuild); +typedef int(__stdcall *fn_fire_block_break)(int entityId, int dimId, + int x, int y, int z, int tileId, int data, int exp); +typedef int(__stdcall *fn_fire_entity_damage)(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); +typedef int(__stdcall *fn_fire_sign_change)(int entityId, int dimId, + int x, int y, int z, + const char *line0, int line0Len, + const char *line1, int line1Len, + const char *line2, int line2Len, + const char *line3, int line3Len, + char *outBuf, int outBufSize, int *outLens); +typedef int(__stdcall *fn_fire_entity_death)(int entityId, int entityTypeId, int dimId, + double x, double y, double z, int exp); +typedef int(__stdcall *fn_fire_player_death)(int entityId, + const char *deathMsgUtf8, int deathMsgByteLen, int exp, + char *outMsgBuf, int outMsgBufSize, int *outMsgLen, int *outKeepInventory, + int *outNewExp, int *outNewLevel, int *outKeepLevel); +typedef void(__stdcall *fn_set_player_callbacks)(void *kickPlayer, void *banPlayer, void *banPlayerIp, void *getPlayerAddress, void *getPlayerLatency); +typedef void(__stdcall *fn_set_player_connection_callbacks)(void *sendRaw); +typedef long long(__stdcall *fn_fire_player_drop_item)(int entityId, + int itemId, int itemCount, int itemAux, + int *outItemId, int *outItemCount, int *outItemAux); +typedef void(__stdcall *fn_set_inventory_callbacks)(void *getPlayerInventory, void *setPlayerInventorySlot, void *getContainerContents, void *setContainerSlot, void *getContainerViewerEntityIds, void *closeContainer, void *openVirtualContainer, void *getItemMeta, void *setItemMeta, void *setHeldItemSlot); +typedef int(__stdcall *fn_fire_player_interact)(int entityId, int action, + int itemId, int itemCount, int itemAux, + int clickedX, int clickedY, int clickedZ, + int blockFace, int dimId, + int *outUseItemInHand); +typedef int(__stdcall *fn_fire_player_interact_entity)(int playerEntityId, + int targetEntityId, int targetEntityTypeId, + int dimId, double targetX, double targetY, double targetZ, + float targetHealth, float targetMaxHealth, float targetEyeHeight); +typedef int(__stdcall *fn_fire_player_pickup_item)(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); +typedef int(__stdcall *fn_fire_inventory_open)(int entityId, int nativeContainerType, + const char *titleUtf8, int titleByteLen, int containerSize); +typedef int(__stdcall *fn_handle_player_command)(int entityId, + const char *cmdUtf8, int cmdByteLen); +typedef int(__stdcall *fn_handle_console_command)(const char *cmdUtf8, int cmdByteLen); +typedef int(__stdcall *fn_get_plugin_command_help)(char *outBuf, int outBufSize, int *outLen); +typedef int(__stdcall *fn_fire_player_teleport)(int entityId, + double fromX, double fromY, double fromZ, int fromDimId, + double toX, double toY, double toZ, int toDimId, + int cause, double *outCoords); +typedef int(__stdcall *fn_fire_inventory_click)(int entityId, + int slot, int button, int clickType, int nativeContainerType, int containerSize, + const char *titleUtf8, int titleByteLen); +typedef int(__stdcall *fn_fire_bed_enter)(int entityId, int dimId, int bedX, int bedY, int bedZ); +typedef void(__stdcall *fn_fire_bed_leave)(int entityId, int dimId, int bedX, int bedY, int bedZ); +typedef void(__stdcall *fn_set_entity_callbacks)(void *setSneaking, void *setVelocity, void *setAllowFlight, void *playSound, void *setSleepingIgnored); +typedef void(__stdcall *fn_set_experience_callbacks)(void *setLevel, void *setExp, void *giveExp, void *giveExpLevels, void *setFoodLevel, void *setSaturation, void *setExhaustion); +typedef void(__stdcall *fn_set_particle_callbacks)(void *spawnParticle); +typedef void(__stdcall *fn_set_vehicle_callbacks)(void *setPassenger, void *leaveVehicle, void *eject, void *getVehicleId, void *getPassengerId, void *getEntityInfo); +typedef int(__stdcall *fn_fire_block_grow)(int dimId, int x, int y, int z, int newTileId, int newTileData); +typedef int(__stdcall *fn_fire_block_form)(int dimId, int x, int y, int z, int newTileId, int newTileData); +typedef int(__stdcall *fn_fire_block_burn)(int dimId, int x, int y, int z); +typedef int(__stdcall *fn_fire_block_spread)(int dimId, int x, int y, int z, int srcX, int srcY, int srcZ, int newTileId, int newTileData); +typedef int(__stdcall *fn_fire_piston_extend)(int dimId, int x, int y, int z, int direction, int length); +typedef int(__stdcall *fn_fire_piston_retract)(int dimId, int x, int y, int z, int direction); +typedef int(__stdcall *fn_fire_command_preprocess)(int entityId, const char *cmdUtf8, int cmdByteLen, char *outBuf, int outBufSize, int *outLen); +typedef int(__stdcall *fn_fire_block_from_to)(int dimId, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, int face); + +struct OpenContainerInfo +{ + int type; + int size; + std::wstring title; +}; +static std::unordered_map s_openContainerInfo; + +static fn_initialize s_managedInit = nullptr; +static fn_shutdown s_managedShutdown = nullptr; +static fn_fire_world_save s_managedFireWorldSave = nullptr; +static fn_fire_player_prelogin s_managedFirePreLogin = nullptr; +static fn_fire_player_login s_managedFireLogin = nullptr; +static fn_fire_player_join s_managedFireJoin = nullptr; +static fn_update_entity_id s_managedUpdateEntityId = nullptr; +static fn_fire_player_quit s_managedFireQuit = nullptr; +static fn_fire_player_kick s_managedFireKick = nullptr; +static fn_fire_player_move s_managedFireMove = nullptr; +static fn_set_native_callbacks s_managedSetCallbacks = nullptr; +static fn_set_world_callbacks s_managedSetWorldCallbacks = nullptr; +static fn_fire_structure_grow s_managedFireStructureGrow = nullptr; +static fn_fire_player_chat s_managedFireChat = nullptr; +static fn_fire_block_place s_managedFireBlockPlace = nullptr; +static fn_fire_block_break s_managedFireBlockBreak = nullptr; +static fn_fire_entity_damage s_managedFireEntityDamage = nullptr; +static fn_fire_sign_change s_managedFireSignChange = nullptr; +static fn_fire_entity_death s_managedFireEntityDeath = nullptr; +static fn_fire_player_death s_managedFirePlayerDeath = nullptr; +static fn_set_player_callbacks s_managedSetPlayerCallbacks = nullptr; +static fn_set_player_connection_callbacks s_managedSetPlayerConnectionCallbacks = nullptr; +static fn_fire_player_drop_item s_managedFirePlayerDropItem = nullptr; +static fn_set_inventory_callbacks s_managedSetInventoryCallbacks = nullptr; +static fn_fire_player_interact s_managedFirePlayerInteract = nullptr; +static fn_fire_player_interact_entity s_managedFirePlayerInteractEntity = nullptr; +static fn_fire_player_pickup_item s_managedFirePlayerPickupItem = nullptr; +static fn_fire_inventory_open s_managedFireInventoryOpen = nullptr; +static fn_handle_player_command s_managedHandlePlayerCommand = nullptr; +static fn_handle_console_command s_managedHandleConsoleCommand = nullptr; +static fn_get_plugin_command_help s_managedGetPluginCommandHelp = nullptr; +static fn_fire_player_teleport s_managedFirePlayerTeleport = nullptr; +static fn_fire_player_teleport s_managedFirePlayerPortal = nullptr; +static fn_fire_inventory_click s_managedFireInventoryClick = nullptr; +static fn_fire_bed_enter s_managedFireBedEnter = nullptr; +static fn_fire_bed_leave s_managedFireBedLeave = nullptr; +static fn_set_entity_callbacks s_managedSetEntityCallbacks = nullptr; +static fn_set_experience_callbacks s_managedSetExperienceCallbacks = nullptr; +static fn_set_particle_callbacks s_managedSetParticleCallbacks = nullptr; +static fn_set_vehicle_callbacks s_managedSetVehicleCallbacks = nullptr; +static fn_fire_block_grow s_managedFireBlockGrow = nullptr; +static fn_fire_block_form s_managedFireBlockForm = nullptr; +static fn_fire_block_burn s_managedFireBlockBurn = nullptr; +static fn_fire_block_spread s_managedFireBlockSpread = nullptr; +static fn_fire_piston_extend s_managedFirePistonExtend = nullptr; +static fn_fire_piston_retract s_managedFirePistonRetract = nullptr; +static fn_fire_command_preprocess s_managedFireCommandPreprocess = nullptr; +static fn_fire_block_from_to s_managedFireBlockFromTo = nullptr; + +static bool s_initialized = false; + +void Initialize() +{ + LogInfo("fourkit", "FourKit initializing..."); + + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + + std::wstring exeDir(exePath); + size_t lastSlash = exeDir.find_last_of(L"\\/"); + if (lastSlash != std::wstring::npos) + { + exeDir = exeDir.substr(0, lastSlash); + } + + // FourKit's self-contained .NET runtime + managed assembly live in a "runtime/" + // subfolder to keep the server's root directory tidy. See FourKitRuntime.cpp's + // LoadHostfxr() for the matching subfolder lookup. The runtimeconfig.json + // sits next to the DLL but is consumed implicitly by hostfxr's command-line + // init API rather than passed in directly. + std::wstring assemblyPath = exeDir + L"\\runtime\\Minecraft.Server.FourKit.dll"; + + load_assembly_fn loadAssembly = nullptr; + if (!LoadManagedRuntime(assemblyPath.c_str(), exePath, &loadAssembly)) + { + return; + } + + const wchar_t *typeName = L"Minecraft.Server.FourKit.FourKitHost, Minecraft.Server.FourKit"; + const wchar_t *asmPath = assemblyPath.c_str(); + + struct { const wchar_t *name; void **target; } entries[] = { + {L"Initialize", (void **)&s_managedInit}, + {L"Shutdown", (void **)&s_managedShutdown}, + {L"FireWorldSave", (void **)&s_managedFireWorldSave}, + {L"FirePlayerPreLogin", (void **)&s_managedFirePreLogin}, + {L"FirePlayerLogin", (void **)&s_managedFireLogin}, + {L"FirePlayerJoin", (void **)&s_managedFireJoin}, + {L"FirePlayerQuit", (void **)&s_managedFireQuit}, + {L"FirePlayerKick", (void **)&s_managedFireKick}, + {L"FirePlayerMove", (void **)&s_managedFireMove}, + {L"SetNativeCallbacks", (void **)&s_managedSetCallbacks}, + {L"SetWorldCallbacks", (void **)&s_managedSetWorldCallbacks}, + {L"UpdatePlayerEntityId", (void **)&s_managedUpdateEntityId}, + {L"FirePlayerChat", (void**)&s_managedFireChat}, + {L"FireStructureGrow", (void **)&s_managedFireStructureGrow}, + {L"FireBlockPlace", (void **)&s_managedFireBlockPlace}, + {L"FireBlockBreak", (void **)&s_managedFireBlockBreak}, + {L"FireEntityDamage", (void **)&s_managedFireEntityDamage}, + {L"FireSignChange", (void **)&s_managedFireSignChange}, + {L"FireEntityDeath", (void **)&s_managedFireEntityDeath}, + {L"FirePlayerDeath", (void **)&s_managedFirePlayerDeath}, + {L"SetPlayerCallbacks", (void**)&s_managedSetPlayerCallbacks}, + {L"SetPlayerConnectionCallbacks", (void **)&s_managedSetPlayerConnectionCallbacks}, + {L"FirePlayerDropItem", (void **)&s_managedFirePlayerDropItem}, + {L"SetInventoryCallbacks", (void **)&s_managedSetInventoryCallbacks}, + {L"FirePlayerInteract", (void **)&s_managedFirePlayerInteract}, + {L"FirePlayerInteractEntity", (void **)&s_managedFirePlayerInteractEntity}, + {L"FirePlayerPickupItem", (void **)&s_managedFirePlayerPickupItem}, + {L"FireInventoryOpen", (void **)&s_managedFireInventoryOpen}, + {L"HandlePlayerCommand", (void **)&s_managedHandlePlayerCommand}, + {L"HandleConsoleCommand", (void **)&s_managedHandleConsoleCommand}, + {L"GetPluginCommandHelp", (void **)&s_managedGetPluginCommandHelp}, + {L"FirePlayerTeleport", (void **)&s_managedFirePlayerTeleport}, + {L"FirePlayerPortal", (void **)&s_managedFirePlayerPortal}, + {L"FireInventoryClick", (void **)&s_managedFireInventoryClick}, + {L"FireBedEnter", (void **)&s_managedFireBedEnter}, + {L"FireBedLeave", (void **)&s_managedFireBedLeave}, + {L"SetEntityCallbacks", (void **)&s_managedSetEntityCallbacks}, + {L"SetExperienceCallbacks", (void **)&s_managedSetExperienceCallbacks}, + {L"SetParticleCallbacks", (void **)&s_managedSetParticleCallbacks}, + {L"SetVehicleCallbacks", (void **)&s_managedSetVehicleCallbacks}, + {L"FireBlockGrow", (void **)&s_managedFireBlockGrow}, + {L"FireBlockForm", (void **)&s_managedFireBlockForm}, + {L"FireBlockBurn", (void **)&s_managedFireBlockBurn}, + {L"FireBlockSpread", (void **)&s_managedFireBlockSpread}, + {L"FirePistonExtend", (void **)&s_managedFirePistonExtend}, + {L"FirePistonRetract", (void **)&s_managedFirePistonRetract}, + {L"FireCommandPreprocess", (void **)&s_managedFireCommandPreprocess}, + {L"FireBlockFromTo", (void **)&s_managedFireBlockFromTo}, + }; + + bool ok = true; + for (const auto &e : entries) + { + ok = ok && GetManagedEntryPoint(loadAssembly, asmPath, typeName, e.name, e.target); + } + + if (!ok) + { + LogError("fourkit", "Not all managed entry points resolved. FourKit will be disabled."); + return; + } + + s_initialized = true; + + s_managedInit(); + + s_managedSetCallbacks( + (void *)&NativeDamagePlayer, + (void *)&NativeSetPlayerHealth, + (void *)&NativeTeleportPlayer, + (void *)&NativeSetPlayerGameMode, + (void *)&NativeBroadcastMessage, + (void *)&NativeSetFallDistance, + (void *)&NativeGetPlayerSnapshot, + (void *)&NativeSendMessage, + (void *)&NativeSetWalkSpeed, + (void *)&NativeTeleportEntity); + + s_managedSetWorldCallbacks( + (void *)&NativeGetTileId, + (void *)&NativeGetTileData, + (void *)&NativeSetTile, + (void *)&NativeSetTileData, + (void *)&NativeBreakBlock, + (void *)&NativeGetHighestBlockY, + (void *)&NativeGetWorldInfo, + (void *)&NativeSetWorldTime, + (void *)&NativeSetWeather, + (void *)&NativeCreateExplosion, + (void *)&NativeStrikeLightning, + (void *)&NativeSetSpawnLocation, + (void *)&NativeDropItem); + + s_managedSetPlayerCallbacks( + (void *)&NativeKickPlayer, + (void *)&NativeBanPlayer, + (void *)&NativeBanPlayerIp, + (void *)&NativeGetPlayerAddress, + (void *)&NativeGetPlayerLatency); + + s_managedSetPlayerConnectionCallbacks( + (void*)&NativeSendRaw); + + s_managedSetInventoryCallbacks( + (void *)&NativeGetPlayerInventory, + (void *)&NativeSetPlayerInventorySlot, + (void *)&NativeGetContainerContents, + (void *)&NativeSetContainerSlot, + (void *)&NativeGetContainerViewerEntityIds, + (void *)&NativeCloseContainer, + (void *)&NativeOpenVirtualContainer, + (void *)&NativeGetItemMeta, + (void *)&NativeSetItemMeta, + (void *)&NativeSetHeldItemSlot); + + s_managedSetEntityCallbacks( + (void *)&NativeSetSneaking, + (void *)&NativeSetVelocity, + (void *)&NativeSetAllowFlight, + (void *)&NativePlaySound, + (void *)&NativeSetSleepingIgnored); + + s_managedSetExperienceCallbacks( + (void *)&NativeSetLevel, + (void *)&NativeSetExp, + (void *)&NativeGiveExp, + (void *)&NativeGiveExpLevels, + (void *)&NativeSetFoodLevel, + (void *)&NativeSetSaturation, + (void *)&NativeSetExhaustion); + + s_managedSetParticleCallbacks( + (void *)&NativeSpawnParticle); + + s_managedSetVehicleCallbacks( + (void *)&NativeSetPassenger, + (void *)&NativeLeaveVehicle, + (void *)&NativeEject, + (void *)&NativeGetVehicleId, + (void *)&NativeGetPassengerId, + (void *)&NativeGetEntityInfo); + + LogInfo("fourkit", "FourKit initialized successfully."); +} + +void Shutdown() +{ + if (!s_initialized) + { + return; + } + + LogInfo("fourkit", "FourKit shutting down..."); + s_managedShutdown(); + s_initialized = false; + LogInfo("fourkit", "FourKit shut down."); +} + +void FireWorldSave() +{ + if (!s_initialized || !s_managedFireWorldSave) + { + return; + } + + s_managedFireWorldSave(); + + LogDebugf("fourkit", "Fired WorldSave"); +} + +bool FirePlayerPreLogin(const std::wstring& name, const std::string& ip, int port) +{ + if (!s_initialized || !s_managedFirePreLogin) + { + return true; + } + + std::string nameUtf8 = ServerRuntime::StringUtils::WideToUtf8(name); + int canceled = s_managedFirePreLogin( + nameUtf8.empty() ? "" : nameUtf8.data(), (int)nameUtf8.size(), + ip.empty() ? "" : ip.data(), (int)ip.size(), + port); + + LogDebugf("fourkit", "Fired PlayerPreLogin: %s", nameUtf8.data()); + + return canceled != 0; +} + +bool FirePlayerLogin(const std::wstring& name, const std::string& ip, int port, int type, unsigned long long* onlineXUID, unsigned long long* offlineXUID) +{ + if (!s_initialized || !s_managedFireLogin) + { + return true; + } + + std::string nameUtf8 = ServerRuntime::StringUtils::WideToUtf8(name); + + int canceled = s_managedFireLogin( + nameUtf8.empty() ? "" : nameUtf8.data(), (int)nameUtf8.size(), + ip.empty() ? "" : ip.data(), (int)ip.size(), + port, type, offlineXUID, onlineXUID); + + LogDebugf("fourkit", "Fired PlayerLogin: %s", nameUtf8.data()); + + return canceled != 0; +} + +void FirePlayerJoin(int entityId, const std::wstring &name, const std::wstring &uuid, unsigned long long onlineXUID, unsigned long long offlineXUID) +{ + if (!s_initialized || !s_managedFireJoin) + { + return; + } + + std::string nameUtf8 = ServerRuntime::StringUtils::WideToUtf8(name); + std::string uuidUtf8 = ServerRuntime::StringUtils::WideToUtf8(uuid); + s_managedFireJoin(entityId, + nameUtf8.empty() ? "" : nameUtf8.data(), (int)nameUtf8.size(), + uuidUtf8.empty() ? "" : uuidUtf8.data(), (int)uuidUtf8.size(), + offlineXUID, onlineXUID); + LogDebugf("fourkit", "Fired PlayerJoin: entityId=%d", entityId); +} + +bool FirePlayerQuit(int entityId) +{ + s_openContainerInfo.erase(entityId); + + if (!s_initialized || !s_managedFireQuit) + { + return false; + } + + s_managedFireQuit(entityId); + LogDebugf("fourkit", "Fired PlayerQuit: entityId=%d", entityId); + return true; +} + +bool FirePlayerKick(int entityId, int disconnectReason, + std::wstring &outLeaveMessage) +{ + if (!s_initialized || !s_managedFireKick) + { + return false; + } + + std::string reasonUtf8 = std::to_string(disconnectReason); + + const int kBufSize = 2048; + char outBuf[kBufSize] = {}; + int outLen = 0; + + int cancelled = s_managedFireKick(entityId, disconnectReason, + reasonUtf8.empty() ? "" : reasonUtf8.data(), (int)reasonUtf8.size(), + outBuf, kBufSize, &outLen); + + if (outLen > 0 && outLen < kBufSize) + { + std::string resultUtf8(outBuf, outLen); + outLeaveMessage = ServerRuntime::StringUtils::Utf8ToWide(resultUtf8); + } + + LogDebugf("fourkit", "Fired PlayerKick: entityId=%d cancelled=%d", entityId, cancelled); + return cancelled != 0; +} + +void UpdatePlayerEntityId(int oldEntityId, int newEntityId) +{ + auto it = s_openContainerInfo.find(oldEntityId); + if (it != s_openContainerInfo.end()) + { + s_openContainerInfo[newEntityId] = it->second; + s_openContainerInfo.erase(it); + } + + if (!s_initialized || !s_managedUpdateEntityId) + { + return; + } + s_managedUpdateEntityId(oldEntityId, newEntityId); + LogDebugf("fourkit", "UpdatePlayerEntityId: %d -> %d", oldEntityId, newEntityId); +} + +bool FirePlayerMove(int entityId, + double fromX, double fromY, double fromZ, + double toX, double toY, double toZ, + double *outToX, double *outToY, double *outToZ) +{ + if (!s_initialized || !s_managedFireMove) + { + return false; + } + + double outCoords[3] = {toX, toY, toZ}; + int cancelled = s_managedFireMove(entityId, + fromX, fromY, fromZ, + toX, toY, toZ, + outCoords); + + *outToX = outCoords[0]; + *outToY = outCoords[1]; + *outToZ = outCoords[2]; + return cancelled != 0; +} + +bool FireStructureGrow(int dimId, int x, int y, int z, int treeType, bool wasBonemeal, int entityId) +{ + if (!s_initialized || !s_managedFireStructureGrow) + { + return false; + } + + int cancelled = s_managedFireStructureGrow(dimId, x, y, z, treeType, wasBonemeal ? 1 : 0, entityId); + + if (cancelled != 0) + { + return true; + } + + return false; +} + +bool FirePlayerChat(int entityId, + const std::wstring &message, + std::wstring &outFormatted) +{ + if (!s_initialized || !s_managedFireChat) + { + return false; + } + + std::string msgUtf8 = ServerRuntime::StringUtils::WideToUtf8(message); + + const int kBufSize = 2048; + char outBuf[kBufSize] = {}; + int outLen = 0; + + int cancelled = s_managedFireChat(entityId, + msgUtf8.empty() ? "" : msgUtf8.data(), (int)msgUtf8.size(), + outBuf, kBufSize, &outLen); + + if (cancelled != 0) + { + return true; + } + + if (outLen > 0 && outLen < kBufSize) + { + std::string resultUtf8(outBuf, outLen); + outFormatted = ServerRuntime::StringUtils::Utf8ToWide(resultUtf8); + } + + return false; +} + +bool FireBlockPlace(int entityId, int dimId, + int placedX, int placedY, int placedZ, + int againstX, int againstY, int againstZ, + int itemId, int itemCount, bool canBuild) +{ + if (!s_initialized || !s_managedFireBlockPlace) + { + return false; + } + + int cancelled = s_managedFireBlockPlace(entityId, dimId, + placedX, placedY, placedZ, + againstX, againstY, againstZ, + itemId, itemCount, canBuild ? 1 : 0); + return cancelled != 0; +} + +int FireBlockBreak(int entityId, int dimId, + int x, int y, int z, int tileId, int data, int exp) +{ + if (!s_initialized || !s_managedFireBlockBreak) + { + return exp; + } + + return s_managedFireBlockBreak(entityId, dimId, x, y, z, tileId, data, 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) +{ + if (!s_initialized || !s_managedFireEntityDamage) + { + *outDamage = damage; + return false; + } + + double outDmg = damage; + int cancelled = s_managedFireEntityDamage(entityId, entityTypeId, dimId, + x, y, z, causeId, damage, &outDmg, + damagerEntityId, damagerEntityTypeId, damagerX, damagerY, damagerZ); + *outDamage = outDmg; + return cancelled != 0; +} + +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]) +{ + if (!s_initialized || !s_managedFireSignChange) + { + return false; + } + + std::string l0 = ServerRuntime::StringUtils::WideToUtf8(line0); + std::string l1 = ServerRuntime::StringUtils::WideToUtf8(line1); + std::string l2 = ServerRuntime::StringUtils::WideToUtf8(line2); + std::string l3 = ServerRuntime::StringUtils::WideToUtf8(line3); + + const int kBufSize = 512; + char outBuf[kBufSize] = {}; + int outLens[4] = {}; + + int cancelled = s_managedFireSignChange(entityId, dimId, + x, y, z, + l0.empty() ? "" : l0.data(), (int)l0.size(), + l1.empty() ? "" : l1.data(), (int)l1.size(), + l2.empty() ? "" : l2.data(), (int)l2.size(), + l3.empty() ? "" : l3.data(), (int)l3.size(), + outBuf, kBufSize, outLens); + + int offset = 0; + for (int i = 0; i < 4; i++) + { + if (outLens[i] > 0 && offset + outLens[i] <= kBufSize) + { + std::string s(outBuf + offset, outLens[i]); + outLines[i] = ServerRuntime::StringUtils::Utf8ToWide(s); + } + else + { + outLines[i] = (i == 0 ? line0 : (i == 1 ? line1 : (i == 2 ? line2 : line3))); + } + offset += outLens[i]; + } + + return cancelled != 0; +} + +int FireEntityDeath(int entityId, int entityTypeId, int dimId, + double x, double y, double z, int exp) +{ + if (!s_initialized || !s_managedFireEntityDeath) + { + return exp; + } + + return s_managedFireEntityDeath(entityId, entityTypeId, dimId, x, y, z, exp); +} + +int FirePlayerDeath(int entityId, const std::wstring &deathMessage, int exp, + std::wstring &outDeathMessage, int *outKeepInventory, + int *outNewExp, int *outNewLevel, int *outKeepLevel) +{ + if (!s_initialized || !s_managedFirePlayerDeath) + { + outDeathMessage = deathMessage; + *outKeepInventory = 0; + *outNewExp = 0; + *outNewLevel = 0; + *outKeepLevel = 0; + return exp; + } + + std::string msgUtf8 = ServerRuntime::StringUtils::WideToUtf8(deathMessage); + + const int kBufSize = 2048; + char outMsgBuf[kBufSize] = {}; + int outMsgLen = 0; + int keepInv = 0; + int newExp = 0, newLevel = 0, keepLevel = 0; + + int outExp = s_managedFirePlayerDeath(entityId, + msgUtf8.empty() ? "" : msgUtf8.data(), (int)msgUtf8.size(), exp, + outMsgBuf, kBufSize, &outMsgLen, &keepInv, + &newExp, &newLevel, &keepLevel); + + if (outMsgLen > 0 && outMsgLen < kBufSize) + { + std::string resultUtf8(outMsgBuf, outMsgLen); + outDeathMessage = ServerRuntime::StringUtils::Utf8ToWide(resultUtf8); + } + else + { + outDeathMessage = deathMessage; + } + + *outKeepInventory = keepInv; + *outNewExp = newExp; + *outNewLevel = newLevel; + *outKeepLevel = keepLevel; + return outExp; +} + +bool FirePlayerDropItem(int entityId, int itemId, int itemCount, int itemAux, + int *outItemId, int *outItemCount, int *outItemAux) +{ + *outItemId = itemId; + *outItemCount = itemCount; + *outItemAux = itemAux; + + if (!s_initialized || !s_managedFirePlayerDropItem) + { + return false; + } + + int newId = itemId, newCount = itemCount, newAux = itemAux; + long long result = s_managedFirePlayerDropItem(entityId, itemId, itemCount, itemAux, + &newId, &newCount, &newAux); + + *outItemId = newId; + *outItemCount = newCount; + *outItemAux = newAux; + + return result != 0; +} + +bool FirePlayerInteract(int entityId, int action, + int itemId, int itemCount, int itemAux, + int clickedX, int clickedY, int clickedZ, + int blockFace, int dimId, + int *outUseItemInHand) +{ + *outUseItemInHand = 1; + + if (!s_initialized || !s_managedFirePlayerInteract) + { + return false; + } + + int useItem = 1; + int result = s_managedFirePlayerInteract(entityId, action, + itemId, itemCount, itemAux, + clickedX, clickedY, clickedZ, + blockFace, dimId, + &useItem); + + *outUseItemInHand = useItem; + return result != 0; +} + +bool FirePlayerInteractEntity(int playerEntityId, + int targetEntityId, int targetEntityTypeId, + int dimId, double targetX, double targetY, double targetZ, + float targetHealth, float targetMaxHealth, float targetEyeHeight) +{ + if (!s_initialized || !s_managedFirePlayerInteractEntity) + { + return false; + } + + int result = s_managedFirePlayerInteractEntity(playerEntityId, + targetEntityId, targetEntityTypeId, + dimId, targetX, targetY, targetZ, + targetHealth, targetMaxHealth, targetEyeHeight); + + return result != 0; +} + +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) +{ + if (!s_initialized || !s_managedFirePlayerPickupItem) + { + *outItemId = itemId; + *outItemCount = itemCount; + *outItemAux = itemAux; + return false; + } + + *outItemId = itemId; + *outItemCount = itemCount; + *outItemAux = itemAux; + + int result = s_managedFirePlayerPickupItem(playerEntityId, + itemEntityId, dimId, itemX, itemY, itemZ, + itemId, itemCount, itemAux, remaining, + outItemId, outItemCount, outItemAux); + + return result != 0; +} + +bool FireInventoryOpen(int entityId, int nativeContainerType, + const std::wstring &title, int containerSize) +{ + if (!s_initialized || !s_managedFireInventoryOpen) + { + return false; + } + + s_openContainerInfo[entityId] = {nativeContainerType, containerSize, title}; + + std::string titleUtf8 = ServerRuntime::StringUtils::WideToUtf8(title); + + int cancelled = s_managedFireInventoryOpen(entityId, nativeContainerType, + titleUtf8.empty() ? "" : titleUtf8.data(), (int)titleUtf8.size(), + containerSize); + + return cancelled != 0; +} + +bool HandlePlayerCommand(int entityId, const std::wstring &commandLine) +{ + if (!s_initialized || !s_managedHandlePlayerCommand) + { + return false; + } + + std::string cmdUtf8 = ServerRuntime::StringUtils::WideToUtf8(commandLine); + + int handled = s_managedHandlePlayerCommand(entityId, + cmdUtf8.empty() ? "" : cmdUtf8.data(), (int)cmdUtf8.size()); + + return handled != 0; +} + +bool HandleConsoleCommand(const std::string &commandLine) +{ + if (!s_initialized || !s_managedHandleConsoleCommand) + { + return false; + } + + int handled = s_managedHandleConsoleCommand( + commandLine.empty() ? "" : commandLine.data(), (int)commandLine.size()); + + return handled != 0; +} + +int GetPluginCommandHelp(char *outBuf, int outBufSize, int *outLen) +{ + if (!s_initialized || !s_managedGetPluginCommandHelp) + { + return 0; + } + + return s_managedGetPluginCommandHelp(outBuf, outBufSize, outLen); +} + +static bool FireSpatialEvent(fn_fire_player_teleport managedFn, + 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) +{ + if (!s_initialized || !managedFn) + { + return false; + } + + double outCoords[3] = {toX, toY, toZ}; + int cancelled = managedFn(entityId, + fromX, fromY, fromZ, fromDimId, + toX, toY, toZ, toDimId, + cause, outCoords); + + *outToX = outCoords[0]; + *outToY = outCoords[1]; + *outToZ = outCoords[2]; + return cancelled != 0; +} + +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) +{ + return FireSpatialEvent(s_managedFirePlayerTeleport, entityId, + fromX, fromY, fromZ, fromDimId, + toX, toY, toZ, toDimId, + cause, outToX, outToY, 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) +{ + return FireSpatialEvent(s_managedFirePlayerPortal, entityId, + fromX, fromY, fromZ, fromDimId, + toX, toY, toZ, toDimId, + cause, outToX, outToY, outToZ); +} + +int FireInventoryClick(int entityId, int slot, int button, int clickType) +{ + if (!s_initialized || !s_managedFireInventoryClick) + { + return 0; + } + + int nativeContainerType = -1; + int containerSize = 0; + std::string titleUtf8; + auto it = s_openContainerInfo.find(entityId); + if (it != s_openContainerInfo.end()) + { + nativeContainerType = it->second.type; + containerSize = it->second.size; + if (!it->second.title.empty()) + { + titleUtf8 = ServerRuntime::StringUtils::WideToUtf8(it->second.title); + } + } + + return s_managedFireInventoryClick(entityId, slot, button, clickType, nativeContainerType, containerSize, + titleUtf8.empty() ? nullptr : titleUtf8.data(), (int)titleUtf8.size()); +} + +bool FireBedEnter(int entityId, int dimId, int bedX, int bedY, int bedZ) +{ + if (!s_initialized || !s_managedFireBedEnter) + return false; + return s_managedFireBedEnter(entityId, dimId, bedX, bedY, bedZ) != 0; +} + +void FireBedLeave(int entityId, int dimId, int bedX, int bedY, int bedZ) +{ + if (!s_initialized || !s_managedFireBedLeave) + return; + s_managedFireBedLeave(entityId, dimId, bedX, bedY, bedZ); +} + +bool FireBlockGrow(int dimId, int x, int y, int z, int newTileId, int newTileData) +{ + if (!s_initialized || !s_managedFireBlockGrow) + return false; + return s_managedFireBlockGrow(dimId, x, y, z, newTileId, newTileData) != 0; +} + +bool FireBlockForm(int dimId, int x, int y, int z, int newTileId, int newTileData) +{ + if (!s_initialized || !s_managedFireBlockForm) + return false; + return s_managedFireBlockForm(dimId, x, y, z, newTileId, newTileData) != 0; +} + +bool FireBlockBurn(int dimId, int x, int y, int z) +{ + if (!s_initialized || !s_managedFireBlockBurn) + return false; + return s_managedFireBlockBurn(dimId, x, y, z) != 0; +} + +bool FireBlockSpread(int dimId, int x, int y, int z, int srcX, int srcY, int srcZ, int newTileId, int newTileData) +{ + if (!s_initialized || !s_managedFireBlockSpread) + return false; + return s_managedFireBlockSpread(dimId, x, y, z, srcX, srcY, srcZ, newTileId, newTileData) != 0; +} + +bool FirePistonExtend(int dimId, int x, int y, int z, int direction, int length) +{ + if (!s_initialized || !s_managedFirePistonExtend) + return false; + return s_managedFirePistonExtend(dimId, x, y, z, direction, length) != 0; +} + +bool FirePistonRetract(int dimId, int x, int y, int z, int direction) +{ + if (!s_initialized || !s_managedFirePistonRetract) + return false; + return s_managedFirePistonRetract(dimId, x, y, z, direction) != 0; +} + +bool FireCommandPreprocess(int entityId, const std::wstring &commandLine, std::wstring &outCommand) +{ + if (!s_initialized || !s_managedFireCommandPreprocess) + { + return false; + } + + std::string cmdUtf8 = ServerRuntime::StringUtils::WideToUtf8(commandLine); + + const int kBufSize = 2048; + char outBuf[kBufSize] = {}; + int outLen = 0; + + int cancelled = s_managedFireCommandPreprocess(entityId, + cmdUtf8.empty() ? "" : cmdUtf8.data(), (int)cmdUtf8.size(), + outBuf, kBufSize, &outLen); + + if (cancelled != 0) + { + return true; + } + + if (outLen > 0 && outLen < kBufSize) + { + std::string resultUtf8(outBuf, outLen); + outCommand = ServerRuntime::StringUtils::Utf8ToWide(resultUtf8); + } + else + { + outCommand = commandLine; + } + + return false; +} + +bool FireBlockFromTo(int dimId, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, int face) +{ + if (!s_initialized || !s_managedFireBlockFromTo) + return false; + return s_managedFireBlockFromTo(dimId, fromX, fromY, fromZ, toX, toY, toZ, face) != 0; +} +} // namespace FourKitBridge diff --git a/Minecraft.Server/FourKitBridge.h b/Minecraft.Server/FourKitBridge.h new file mode 100644 index 00000000..3349cc64 --- /dev/null +++ b/Minecraft.Server/FourKitBridge.h @@ -0,0 +1,215 @@ +#pragma once +#include + +// FourKit plugin host bridge. +// +// In the FourKit-enabled server build, MINECRAFT_SERVER_FOURKIT_BUILD is +// defined and the real implementations live in FourKitBridge.cpp / +// FourKitNatives.cpp / FourKitRuntime.cpp / FourKitMappers.cpp. +// +// In the standalone server build (no plugin support), the macro is undefined +// and every entry point becomes an inline no-op stub. This lets gameplay code +// in Minecraft.Server / Minecraft.Client / Minecraft.World call into the +// bridge unconditionally without per-call-site #ifdefs. Cancellable hooks +// return false (do not cancel) so vanilla behaviour is preserved. + +namespace FourKitBridge +{ +#ifdef MINECRAFT_SERVER_FOURKIT_BUILD + void Initialize(); + void Shutdown(); + void FireWorldSave(); + bool FirePlayerPreLogin(const std::wstring& name, const std::string& ip, int port); + bool FirePlayerLogin(const std::wstring& name, const std::string& ip, int port, int type, unsigned long long* onlineXUID, unsigned long long* offlineXUID); + void FirePlayerJoin(int entityId, const std::wstring& name, const std::wstring& uuid, unsigned long long onlineXUID, unsigned long long offlineXUID); + bool FirePlayerQuit(int entityId); + bool FirePlayerKick(int entityId, int disconnectReason, + std::wstring &outLeaveMessage); + bool FirePlayerMove(int entityId, + double fromX, double fromY, double fromZ, + double toX, double toY, double toZ, + double *outToX, double *outToY, double *outToZ); + bool FireStructureGrow(int dimId, int x, int y, int z, int treeType, bool wasBonemeal = false, int entityId = -1); + bool FirePlayerChat(int entityId, + const std::wstring &message, + std::wstring &outFormatted); + void UpdatePlayerEntityId(int oldEntityId, int newEntityId); + + int GetTileId(int dimId, int x, int y, int z); + int GetTileData(int dimId, int x, int y, int z); + void SetTile(int dimId, int x, int y, int z, int tileId, int data); + void SetTileData(int dimId, int x, int y, int z, int data); + int BreakBlock(int dimId, int x, int y, int z); + int GetHighestBlockY(int dimId, int x, int z); + bool FireBlockPlace(int entityId, int dimId, + int placedX, int placedY, int placedZ, + int againstX, int againstY, int againstZ, + int itemId, int itemCount, bool canBuild); + int FireBlockBreak(int entityId, int dimId, + int x, int y, int z, int tileId, int data, int exp); + bool FireEntityDamage(int entityId, int entityTypeId, int dimId, + double x, double y, double z, int causeId, double damage, + double *outDamage, + int damagerEntityId, int damagerEntityTypeId, + double damagerX, double damagerY, double damagerZ); + bool FireSignChange(int entityId, int dimId, + int x, int y, int z, + const std::wstring &line0, const std::wstring &line1, + const std::wstring &line2, const std::wstring &line3, + std::wstring outLines[4]); + int FireEntityDeath(int entityId, int entityTypeId, int dimId, + double x, double y, double z, int exp); + int FirePlayerDeath(int entityId, const std::wstring &deathMessage, int exp, + std::wstring &outDeathMessage, int *outKeepInventory, + int *outNewExp, int *outNewLevel, int *outKeepLevel); + int MapEntityType(int nativeType); + int MapDamageCause(void *source); + bool FirePlayerDropItem(int entityId, int itemId, int itemCount, int itemAux, + int *outItemId, int *outItemCount, int *outItemAux); + bool FirePlayerInteract(int entityId, int action, + int itemId, int itemCount, int itemAux, + int clickedX, int clickedY, int clickedZ, + int blockFace, int dimId, + int *outUseItemInHand); + bool FirePlayerInteractEntity(int playerEntityId, + int targetEntityId, int targetEntityTypeId, + int dimId, double targetX, double targetY, double targetZ, + float targetHealth, float targetMaxHealth, float targetEyeHeight); + bool FirePlayerPickupItem(int playerEntityId, + int itemEntityId, int dimId, double itemX, double itemY, double itemZ, + int itemId, int itemCount, int itemAux, int remaining, + int *outItemId, int *outItemCount, int *outItemAux); + bool FireInventoryOpen(int entityId, int nativeContainerType, + const std::wstring &title, int containerSize); + bool HandlePlayerCommand(int entityId, const std::wstring &commandLine); + bool HandleConsoleCommand(const std::string &commandLine); + int GetPluginCommandHelp(char *outBuf, int outBufSize, int *outLen); + bool FirePlayerTeleport(int entityId, + double fromX, double fromY, double fromZ, int fromDimId, + double toX, double toY, double toZ, int toDimId, + int cause, + double *outToX, double *outToY, double *outToZ); + bool FirePlayerPortal(int entityId, + double fromX, double fromY, double fromZ, int fromDimId, + double toX, double toY, double toZ, int toDimId, + int cause, + double *outToX, double *outToY, double *outToZ); + int FireInventoryClick(int entityId, int slot, int button, int clickType); + bool FireBedEnter(int entityId, int dimId, int bedX, int bedY, int bedZ); + void FireBedLeave(int entityId, int dimId, int bedX, int bedY, int bedZ); + bool FireBlockGrow(int dimId, int x, int y, int z, int newTileId, int newTileData); + bool FireBlockForm(int dimId, int x, int y, int z, int newTileId, int newTileData); + bool FireBlockBurn(int dimId, int x, int y, int z); + bool FireBlockSpread(int dimId, int x, int y, int z, int srcX, int srcY, int srcZ, int newTileId, int newTileData); + bool FirePistonExtend(int dimId, int x, int y, int z, int direction, int length); + bool FirePistonRetract(int dimId, int x, int y, int z, int direction); + bool FireCommandPreprocess(int entityId, const std::wstring &commandLine, std::wstring &outCommand); + bool FireBlockFromTo(int dimId, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, int face); +#else + // Standalone build: every hook is an inline no-op. Cancellable hooks + // return false so vanilla code paths run unmodified, AND every out + // parameter is defaulted to its corresponding input value (or to a + // benign default like 0 / empty string) so callers do not read + // uninitialized stack memory. Reading uninitialized out-params would + // pass garbage into anti-cheat / damage / inventory checks and break + // the vanilla server build. + inline void Initialize() {} + inline void Shutdown() {} + inline void FireWorldSave() {} + inline bool FirePlayerPreLogin(const std::wstring&, const std::string&, int) { return false; } + inline bool FirePlayerLogin(const std::wstring&, const std::string&, int, int, unsigned long long*, unsigned long long*) { return false; } + inline void FirePlayerJoin(int, const std::wstring&, const std::wstring&, unsigned long long, unsigned long long) {} + inline bool FirePlayerQuit(int) { return false; } + inline bool FirePlayerKick(int, int, std::wstring &outLeaveMessage) { outLeaveMessage.clear(); return false; } + inline bool FirePlayerMove(int, double, double, double, double toX, double toY, double toZ, double *outToX, double *outToY, double *outToZ) + { + if (outToX) *outToX = toX; + if (outToY) *outToY = toY; + if (outToZ) *outToZ = toZ; + return false; + } + inline bool FireStructureGrow(int, int, int, int, int, bool = false, int = -1) { return false; } + inline bool FirePlayerChat(int, const std::wstring&, std::wstring &outFormatted) { outFormatted.clear(); return false; } + inline void UpdatePlayerEntityId(int, int) {} + + inline int GetTileId(int, int, int, int) { return 0; } + inline int GetTileData(int, int, int, int) { return 0; } + inline void SetTile(int, int, int, int, int, int) {} + inline void SetTileData(int, int, int, int, int) {} + inline int BreakBlock(int, int, int, int) { return 0; } + inline int GetHighestBlockY(int, int, int) { return 0; } + inline bool FireBlockPlace(int, int, int, int, int, int, int, int, int, int, bool) { return false; } + inline int FireBlockBreak(int, int, int, int, int, int, int, int exp) { return exp; } + inline bool FireEntityDamage(int, int, int, double, double, double, int, double damage, double *outDamage, int, int, double, double, double) + { + if (outDamage) *outDamage = damage; + return false; + } + inline bool FireSignChange(int, int, int, int, int, const std::wstring &line0, const std::wstring &line1, const std::wstring &line2, const std::wstring &line3, std::wstring outLines[4]) + { + if (outLines) { outLines[0] = line0; outLines[1] = line1; outLines[2] = line2; outLines[3] = line3; } + return false; + } + inline int FireEntityDeath(int, int, int, double, double, double, int exp) { return exp; } + inline int FirePlayerDeath(int, const std::wstring&, int exp, std::wstring &outDeathMessage, int *outKeepInventory, int *outNewExp, int *outNewLevel, int *outKeepLevel) + { + outDeathMessage.clear(); + if (outKeepInventory) *outKeepInventory = 0; + if (outNewExp) *outNewExp = 0; + if (outNewLevel) *outNewLevel = 0; + if (outKeepLevel) *outKeepLevel = 0; + return exp; + } + inline int MapEntityType(int nativeType) { return nativeType; } + inline int MapDamageCause(void*) { return 0; } + inline bool FirePlayerDropItem(int, int itemId, int itemCount, int itemAux, int *outItemId, int *outItemCount, int *outItemAux) + { + if (outItemId) *outItemId = itemId; + if (outItemCount) *outItemCount = itemCount; + if (outItemAux) *outItemAux = itemAux; + return false; + } + inline bool FirePlayerInteract(int, int, int, int, int, int, int, int, int, int, int *outUseItemInHand) + { + if (outUseItemInHand) *outUseItemInHand = 1; + return false; + } + inline bool FirePlayerInteractEntity(int, int, int, int, double, double, double, float, float, float) { return false; } + inline bool FirePlayerPickupItem(int, int, int, double, double, double, int itemId, int itemCount, int itemAux, int, int *outItemId, int *outItemCount, int *outItemAux) + { + if (outItemId) *outItemId = itemId; + if (outItemCount) *outItemCount = itemCount; + if (outItemAux) *outItemAux = itemAux; + return false; + } + inline bool FireInventoryOpen(int, int, const std::wstring&, int) { return false; } + inline bool HandlePlayerCommand(int, const std::wstring&) { return false; } + inline bool HandleConsoleCommand(const std::string&) { return false; } + inline int GetPluginCommandHelp(char*, int, int *outLen) { if (outLen) *outLen = 0; return 0; } + inline bool FirePlayerTeleport(int, double, double, double, int, double toX, double toY, double toZ, int, int, double *outToX, double *outToY, double *outToZ) + { + if (outToX) *outToX = toX; + if (outToY) *outToY = toY; + if (outToZ) *outToZ = toZ; + return false; + } + inline bool FirePlayerPortal(int, double, double, double, int, double toX, double toY, double toZ, int, int, double *outToX, double *outToY, double *outToZ) + { + if (outToX) *outToX = toX; + if (outToY) *outToY = toY; + if (outToZ) *outToZ = toZ; + return false; + } + inline int FireInventoryClick(int, int, int, int) { return 0; } + inline bool FireBedEnter(int, int, int, int, int) { return false; } + inline void FireBedLeave(int, int, int, int, int) {} + inline bool FireBlockGrow(int, int, int, int, int, int) { return false; } + inline bool FireBlockForm(int, int, int, int, int, int) { return false; } + inline bool FireBlockBurn(int, int, int, int) { return false; } + inline bool FireBlockSpread(int, int, int, int, int, int, int, int, int) { return false; } + inline bool FirePistonExtend(int, int, int, int, int, int) { return false; } + inline bool FirePistonRetract(int, int, int, int, int) { return false; } + inline bool FireCommandPreprocess(int, const std::wstring &commandLine, std::wstring &outCommand) { outCommand = commandLine; return false; } + inline bool FireBlockFromTo(int, int, int, int, int, int, int, int) { return false; } +#endif +} diff --git a/Minecraft.Server/FourKitMappers.cpp b/Minecraft.Server/FourKitMappers.cpp new file mode 100644 index 00000000..776c43e5 --- /dev/null +++ b/Minecraft.Server/FourKitMappers.cpp @@ -0,0 +1,251 @@ +#include "FourKitBridge.h" +#include "stdafx.h" + +#include "../Minecraft.World/DamageSource.h" +#include "../Minecraft.World/EntityDamageSource.h" + +namespace FourKitBridge +{ +int MapEntityType(int nativeType) +{ + eINSTANCEOF type = (eINSTANCEOF)nativeType; + const int ARROW = 0, BAT = 1, BLAZE = 2, BOAT = 3, CAVE_SPIDER = 4; + const int CHICKEN = 5, COW = 7, CREEPER = 8, DROPPED_ITEM = 9; + const int EGG = 10, ENDER_CRYSTAL = 11, ENDER_DRAGON = 12; + const int ENDER_PEARL = 13, ENDER_SIGNAL = 14, ENDERMAN = 15; + const int EXPERIENCE_ORB = 16, FALLING_BLOCK = 17, FIREBALL = 18; + const int FIREWORK = 19, FISHING_HOOK = 20, GHAST = 21, GIANT = 22; + const int HORSE = 23, IRON_GOLEM = 24, ITEM_FRAME = 25; + const int LEASH_HITCH = 26, LIGHTNING = 27, MAGMA_CUBE = 28; + const int MINECART = 29, MINECART_CHEST = 30, MINECART_FURNACE = 32; + const int MINECART_HOPPER = 33, MINECART_MOB_SPAWNER = 34; + const int MINECART_TNT = 35, MUSHROOM_COW = 36, OCELOT = 37; + const int PAINTING = 38, PIG = 39, PIG_ZOMBIE = 40, PLAYER = 41; + const int PRIMED_TNT = 42, SHEEP = 43, SILVERFISH = 44; + const int SKELETON = 45, SLIME = 46, SMALL_FIREBALL = 47; + const int SNOWBALL = 48, SNOWMAN = 49, SPIDER = 50; + const int SPLASH_POTION = 51, SQUID = 52, THROWN_EXP_BOTTLE = 53; + const int UNKNOWN = 54, VILLAGER = 55, WITCH = 57; + const int WITHER = 58, WITHER_SKULL = 59, WOLF = 60, ZOMBIE = 61; + + switch (type) + { + case eTYPE_ARROW: + return ARROW; + case eTYPE_BAT: + return BAT; + case eTYPE_BLAZE: + return BLAZE; + case eTYPE_BOAT: + return BOAT; + case eTYPE_CAVESPIDER: + return CAVE_SPIDER; + case eTYPE_CHICKEN: + return CHICKEN; + case eTYPE_COW: + return COW; + case eTYPE_CREEPER: + return CREEPER; + case eTYPE_ITEMENTITY: + return DROPPED_ITEM; + case eTYPE_THROWNEGG: + return EGG; + case eTYPE_NETHER_SPHERE: + return ENDER_CRYSTAL; + case eTYPE_ENDERDRAGON: + return ENDER_DRAGON; + case eTYPE_THROWNENDERPEARL: + return ENDER_PEARL; + case eTYPE_EYEOFENDERSIGNAL: + return ENDER_SIGNAL; + case eTYPE_ENDERMAN: + return ENDERMAN; + case eTYPE_EXPERIENCEORB: + return EXPERIENCE_ORB; + case eTYPE_FALLINGTILE: + return FALLING_BLOCK; + case eTYPE_LARGE_FIREBALL: + return FIREBALL; + case eTYPE_FIREWORKS_ROCKET: + return FIREWORK; + case eTYPE_FISHINGHOOK: + return FISHING_HOOK; + case eTYPE_GHAST: + return GHAST; + case eTYPE_GIANT: + return GIANT; + case eTYPE_HORSE: + return HORSE; + case eTYPE_VILLAGERGOLEM: + return IRON_GOLEM; + case eTYPE_ITEM_FRAME: + return ITEM_FRAME; + case eTYPE_LEASHFENCEKNOT: + return LEASH_HITCH; + case eTYPE_LIGHTNINGBOLT: + return LIGHTNING; + case eTYPE_LAVASLIME: + return MAGMA_CUBE; + case eTYPE_MINECART_RIDEABLE: + return MINECART; + case eTYPE_MINECART_CHEST: + return MINECART_CHEST; + case eTYPE_MINECART_FURNACE: + return MINECART_FURNACE; + case eTYPE_MINECART_HOPPER: + return MINECART_HOPPER; + case eTYPE_MINECART_SPAWNER: + return MINECART_MOB_SPAWNER; + case eTYPE_MINECART_TNT: + return MINECART_TNT; + case eTYPE_MUSHROOMCOW: + return MUSHROOM_COW; + case eTYPE_OCELOT: + return OCELOT; + case eTYPE_PAINTING: + return PAINTING; + case eTYPE_PIG: + return PIG; + case eTYPE_PIGZOMBIE: + return PIG_ZOMBIE; + case eTYPE_PLAYER: + return PLAYER; + case eTYPE_SERVERPLAYER: + return PLAYER; + case eTYPE_REMOTEPLAYER: + return PLAYER; + case eTYPE_LOCALPLAYER: + return PLAYER; + case eTYPE_PRIMEDTNT: + return PRIMED_TNT; + case eTYPE_SHEEP: + return SHEEP; + case eTYPE_SILVERFISH: + return SILVERFISH; + case eTYPE_SKELETON: + return SKELETON; + case eTYPE_SLIME: + return SLIME; + case eTYPE_SMALL_FIREBALL: + return SMALL_FIREBALL; + case eTYPE_SNOWBALL: + return SNOWBALL; + case eTYPE_SNOWMAN: + return SNOWMAN; + case eTYPE_SPIDER: + return SPIDER; + case eTYPE_THROWNPOTION: + return SPLASH_POTION; + case eTYPE_SQUID: + return SQUID; + case eTYPE_THROWNEXPBOTTLE: + return THROWN_EXP_BOTTLE; + case eTYPE_VILLAGER: + return VILLAGER; + case eTYPE_WITCH: + return WITCH; + case eTYPE_WITHERBOSS: + return WITHER; + case eTYPE_WITHER_SKULL: + return WITHER_SKULL; + case eTYPE_WOLF: + return WOLF; + case eTYPE_ZOMBIE: + return ZOMBIE; + default: + return UNKNOWN; + } +} + +int MapDamageCause(void *sourcePtr) +{ + DamageSource *source = (DamageSource *)sourcePtr; + const int CONTACT = 1, CUSTOM = 2, DROWNING = 3; + const int ENTITY_ATTACK = 4, ENTITY_EXPLOSION = 5; + const int FALL = 6, FALLING_BLOCK = 7, FIRE = 8, FIRE_TICK = 9; + const int LAVA = 10, MAGIC = 12; + const int PROJECTILE = 15, STARVATION = 16, SUFFOCATION = 17; + const int CAUSE_VOID = 20, CAUSE_WITHER = 21; + + if (source == nullptr) + { + return CUSTOM; + } + if (source == DamageSource::inFire) + { + return FIRE; + } + if (source == DamageSource::onFire) + { + return FIRE_TICK; + } + if (source == DamageSource::lava) + { + return LAVA; + } + if (source == DamageSource::inWall) + { + return SUFFOCATION; + } + if (source == DamageSource::drown) + { + return DROWNING; + } + if (source == DamageSource::starve) + { + return STARVATION; + } + if (source == DamageSource::cactus) + { + return CONTACT; + } + if (source == DamageSource::fall) + { + return FALL; + } + if (source == DamageSource::outOfWorld) + { + return CAUSE_VOID; + } + if (source == DamageSource::genericSource) + { + return CUSTOM; + } + if (source == DamageSource::magic) + { + return MAGIC; + } + if (source == DamageSource::wither) + { + return CAUSE_WITHER; + } + if (source == DamageSource::anvil) + { + return FALLING_BLOCK; + } + if (source == DamageSource::fallingBlock) + { + return FALLING_BLOCK; + } + + if (source->isExplosion()) + { + return ENTITY_EXPLOSION; + } + if (source->isProjectile()) + { + return PROJECTILE; + } + if (source->isMagic()) + { + return MAGIC; + } + + if (dynamic_cast(source) != nullptr) + { + return ENTITY_ATTACK; + } + + return CUSTOM; +} +} diff --git a/Minecraft.Server/FourKitNatives.cpp b/Minecraft.Server/FourKitNatives.cpp new file mode 100644 index 00000000..f634e825 --- /dev/null +++ b/Minecraft.Server/FourKitNatives.cpp @@ -0,0 +1,1271 @@ +#include "FourKitNatives.h" +#include "FourKitBridge.h" +#include "Common/StringUtils.h" +#include "stdafx.h" + +#include +#include + +#include "../Minecraft.Client/MinecraftServer.h" +#include "../Minecraft.Client/PlayerConnection.h" +#include "../Minecraft.Client/PlayerList.h" +#include "../Minecraft.Client/ServerConnection.h" +#include "../Minecraft.Client/ServerLevel.h" +#include "../Minecraft.Client/ServerPlayer.h" +#include "../Minecraft.Client/ServerPlayerGameMode.h" +#include "../Minecraft.Client/Windows64/Network/WinsockNetLayer.h" +#include "../Minecraft.World/AbstractContainerMenu.h" +#include "../Minecraft.World/AddGlobalEntityPacket.h" +#include "../Minecraft.World/ArrayWithLength.h" +#include "../Minecraft.World/Class.h" +#include "../Minecraft.World/CompoundContainer.h" +#include "../Minecraft.World/Connection.h" +#include "../Minecraft.World/DamageSource.h" +#include "../Minecraft.World/Explosion.h" +#include "../Minecraft.World/ItemEntity.h" +#include "../Minecraft.World/ItemInstance.h" +#include "../Minecraft.World/LevelData.h" +#include "../Minecraft.World/LightningBolt.h" +#include "../Minecraft.World/Player.h" +#include "../Minecraft.World/PlayerAbilitiesPacket.h" +#include "../Minecraft.World/SetCarriedItemPacket.h" +#include "../Minecraft.World/SetExperiencePacket.h" +#include "../Minecraft.World/SetHealthPacket.h" +#include "../Minecraft.World/LevelSoundPacket.h" +#include "../Minecraft.World/LevelParticlesPacket.h" +#include "../Minecraft.World/SetEntityLinkPacket.h" +#include "../Minecraft.World/SimpleContainer.h" +#include "../Minecraft.World/Slot.h" +#include "../Minecraft.World/Tile.h" +#include "../Minecraft.World/net.minecraft.world.entity.player.h" +#include "Access/Access.h" +#include "Common/NetworkUtils.h" +#include "ServerLogManager.h" +#include "../Minecraft.World/ItemInstance.cpp" + +namespace +{ + +static shared_ptr FindPlayer(int entityId) +{ + PlayerList *list = MinecraftServer::getPlayerList(); + if (!list) + return nullptr; + for (auto &p : list->players) + { + if (p && p->entityId == entityId) + return p; + } + return nullptr; +} + +static shared_ptr FindEntity(int entityId) +{ + MinecraftServer *srv = MinecraftServer::getInstance(); + if (!srv) + return nullptr; + const int dims[] = {0, -1, 1}; + for (int dim : dims) + { + ServerLevel *level = srv->getLevel(dim); + if (!level) + continue; + shared_ptr entity = level->getEntity(entityId); + if (entity) + return entity; + } + return nullptr; +} + +static ServerLevel *GetLevel(int dimId) +{ + MinecraftServer *srv = MinecraftServer::getInstance(); + if (!srv) + return nullptr; + return srv->getLevel(dimId); +} + +class VirtualContainer : public SimpleContainer +{ + int m_containerType; + + public: + VirtualContainer(int containerType, const std::wstring &name, int size) + : SimpleContainer(0, name, !name.empty(), size), m_containerType(containerType) + { + } + virtual int getContainerType() override + { + return m_containerType; + } +}; + +} + +namespace FourKitBridge +{ +void __cdecl NativeDamagePlayer(int entityId, float amount) +{ + auto player = FindPlayer(entityId); + if (player) + { + player->hurt(DamageSource::genericSource, amount); + return; + } + auto entity = FindEntity(entityId); + if (entity) + { + entity->hurt(DamageSource::genericSource, amount); + } +} + +void __cdecl NativeSetPlayerHealth(int entityId, float health) +{ + auto player = FindPlayer(entityId); + if (player) + { + player->setHealth(health); + } +} + +void __cdecl NativeTeleportPlayer(int entityId, double x, double y, double z) +{ + auto player = FindPlayer(entityId); + if (player && player->connection) + { + double outX, outY, outZ; + bool cancelled = FirePlayerTeleport(entityId, + player->x, player->y, player->z, player->dimension, + x, y, z, player->dimension, + 2 /* PLUGIN */, + &outX, &outY, &outZ); + if (!cancelled) + { + player->connection->teleport(outX, outY, outZ, player->yRot, player->xRot); + } + } +} + +void __cdecl NativeSetPlayerGameMode(int entityId, int gameMode) +{ + auto player = FindPlayer(entityId); + if (player && player->gameMode) + { + GameType *type = GameType::byId(gameMode); + if (type) + { + player->setGameMode(type); + } + } +} + +void __cdecl NativeBroadcastMessage(const char *utf8, int len) +{ + if (!utf8 || len <= 0) + return; + std::wstring wide = ServerRuntime::StringUtils::Utf8ToWide(utf8); + if (wide.empty()) + return; + PlayerList *list = MinecraftServer::getPlayerList(); + if (list) + { + list->broadcastAll(std::make_shared(wide)); + } +} + +void __cdecl NativeSetFallDistance(int entityId, float distance) +{ + auto player = FindPlayer(entityId); + if (player) + { + player->fallDistance = distance; + } +} + +// double[27] = { x, y, z, health, maxHealth, fallDistance, gameMode, walkSpeed, yaw, pitch, dimension, isSleeping, sleepTimer, sneaking, sprinting, onGround, velocityX, velocityY, velocityZ, allowFlight, sleepingIgnored, experienceLevel, experienceProgress, totalExperience, foodLevel, saturation, exhaustion } +void __cdecl NativeGetPlayerSnapshot(int entityId, double *outData) +{ + auto player = FindPlayer(entityId); + if (!player) + { + memset(outData, 0, 27 * sizeof(double)); + outData[3] = 20.0; + outData[4] = 20.0; + outData[7] = 0.1; + outData[24] = 20.0; + outData[25] = 5.0; + return; + } + outData[0] = player->x; + outData[1] = player->y; + outData[2] = player->z; + outData[3] = (double)player->getHealth(); + outData[4] = (double)player->getMaxHealth(); + outData[5] = (double)player->fallDistance; + GameType *gm = player->gameMode ? player->gameMode->getGameModeForPlayer() : GameType::SURVIVAL; + outData[6] = (double)(gm ? gm->getId() : 0); + outData[7] = (double)player->abilities.getWalkingSpeed(); + outData[8] = (double)player->yRot; + outData[9] = (double)player->xRot; + outData[10] = (double)player->dimension; + outData[11] = player->isSleeping() ? 1.0 : 0.0; + outData[12] = (double)player->getSleepTimer(); + outData[13] = player->isSneaking() ? 1.0 : 0.0; + outData[14] = player->isSprinting() ? 1.0 : 0.0; + outData[15] = player->onGround ? 1.0 : 0.0; + outData[16] = player->xd; + outData[17] = player->yd; + outData[18] = player->zd; + outData[19] = player->abilities.mayfly ? 1.0 : 0.0; + outData[20] = player->fk_sleepingIgnored ? 1.0 : 0.0; + outData[21] = (double)player->experienceLevel; + outData[22] = (double)player->experienceProgress; + outData[23] = (double)player->totalExperience; + FoodData *fd = player->getFoodData(); + outData[24] = fd ? (double)fd->getFoodLevel() : 20.0; + outData[25] = fd ? (double)fd->getSaturationLevel() : 5.0; + outData[26] = fd ? (double)fd->getExhaustionLevel() : 0.0; +} + +void __cdecl NativeSendMessage(int entityId, const char *utf8, int len) +{ + if (!utf8 || len <= 0) + return; + auto player = FindPlayer(entityId); + if (player && player->connection) + { + std::wstring wide = ServerRuntime::StringUtils::Utf8ToWide(utf8); + if (!wide.empty()) + { + player->connection->send(std::make_shared(wide)); + } + } +} + +void __cdecl NativeSetWalkSpeed(int entityId, float speed) +{ + auto player = FindPlayer(entityId); + if (player) + { + player->abilities.setWalkingSpeed(speed); + if (player->connection) + { + player->connection->send(std::make_shared(&player->abilities)); + } + } +} + +void __cdecl NativeTeleportEntity(int entityId, int dimId, double x, double y, double z) +{ + auto player = FindPlayer(entityId); + if (player && player->connection) + { + double outX = x, outY = y, outZ = z; + bool cancelled = FirePlayerTeleport(entityId, + player->x, player->y, player->z, player->dimension, + x, y, z, dimId, + 2 /* PLUGIN */, + &outX, &outY, &outZ + ); + + if (!cancelled) + { + if (dimId != player->dimension) + { + MinecraftServer::getInstance()->getPlayers()->toggleDimension(player, dimId); + } + player->connection->teleport(outX, outY, outZ, player->yRot, player->xRot); + } + return; + } + ServerLevel *level = GetLevel(dimId); + if (!level) + return; + shared_ptr entity = level->getEntity(entityId); + if (entity) + { + entity->moveTo(x, y, z, entity->yRot, entity->xRot); + } +} + +int __cdecl NativeGetTileId(int dimId, int x, int y, int z) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + return level->getTile(x, y, z); +} + +int __cdecl NativeGetTileData(int dimId, int x, int y, int z) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + return level->getData(x, y, z); +} + +void __cdecl NativeSetTile(int dimId, int x, int y, int z, int tileId, int data) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return; + level->setTileAndData(x, y, z, tileId, data, Tile::UPDATE_ALL); +} + +void __cdecl NativeSetTileData(int dimId, int x, int y, int z, int data) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return; + level->setData(x, y, z, data, Tile::UPDATE_ALL); +} + +int __cdecl NativeBreakBlock(int dimId, int x, int y, int z) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + if (level->getTile(x, y, z) == 0) + return 0; + return level->destroyTile(x, y, z, true) ? 1 : 0; +} + +int __cdecl NativeGetHighestBlockY(int dimId, int x, int z) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + return level->getHeightmap(x, z); +} + +// double[7] = { spawnX, spawnY, spawnZ, seed, dayTime, isRaining, isThundering } +void __cdecl NativeGetWorldInfo(int dimId, double *outBuf) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + { + memset(outBuf, 0, 7 * sizeof(double)); + return; + } + LevelData *ld = level->getLevelData(); + Pos *spawn = level->getSharedSpawnPos(); + outBuf[0] = spawn ? (double)spawn->x : 0.0; + outBuf[1] = spawn ? (double)spawn->y : 64.0; + outBuf[2] = spawn ? (double)spawn->z : 0.0; + outBuf[3] = (double)level->getSeed(); + outBuf[4] = (double)level->getDayTime(); + outBuf[5] = ld && ld->isRaining() ? 1.0 : 0.0; + outBuf[6] = ld && ld->isThundering() ? 1.0 : 0.0; +} + +void __cdecl NativeSetWorldTime(int dimId, int64_t time) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return; + level->setDayTime(time); +} + +void __cdecl NativeSetWeather(int dimId, int storm, int thundering, int thunderDuration) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return; + LevelData *ld = level->getLevelData(); + if (!ld) + return; + if (storm >= 0) + ld->setRaining(storm != 0); + if (thundering >= 0) + ld->setThundering(thundering != 0); + if (thunderDuration >= 0) + ld->setThunderTime(thunderDuration); +} + +int __cdecl NativeCreateExplosion(int dimId, double x, double y, double z, float power, int setFire, int breakBlocks) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + Explosion explosion(level, nullptr, x, y, z, power); + explosion.fire = (setFire != 0); + explosion.destroyBlocks = (breakBlocks != 0); + explosion.explode(); + explosion.finalizeExplosion(true); + return 1; +} + +int __cdecl NativeStrikeLightning(int dimId, double x, double y, double z, int effectOnly) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + + std::shared_ptr lightning = std::shared_ptr(new LightningBolt(level, x, y, z)); + + if (effectOnly != 0) + { + PlayerList *playerList = MinecraftServer::getPlayerList(); + if (playerList == NULL) + return 0; + playerList->broadcast(x, y, z, 512.0, dimId, std::shared_ptr(new AddGlobalEntityPacket(lightning))); + level->playSound(x, y, z, eSoundType_AMBIENT_WEATHER_THUNDER, 10000, 0.8f + level->random->nextFloat() * 0.2f); + level->playSound(x, y, z, eSoundType_RANDOM_EXPLODE, 2, 0.5f + level->random->nextFloat() * 0.2f); + return 1; + } + + return level->addGlobalEntity(lightning) ? 1 : 0; +} + +int __cdecl NativeSetSpawnLocation(int dimId, int x, int y, int z) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + level->setSpawnPos(x, y, z); + return 1; +} + +void __cdecl NativeDropItem(int dimId, double x, double y, double z, int itemId, int count, int auxValue, int naturally) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return; + if (itemId <= 0 || count <= 0) + return; + + auto itemInstance = std::make_shared(itemId, count, auxValue); + double spawnX = x, spawnY = y, spawnZ = z; + if (naturally) + { + float s = 0.7f; + spawnX += level->random->nextFloat() * s + (1 - s) * 0.5; + spawnY += level->random->nextFloat() * s + (1 - s) * 0.5; + spawnZ += level->random->nextFloat() * s + (1 - s) * 0.5; + } + + auto item = std::make_shared(level, spawnX, spawnY, spawnZ, itemInstance); + item->throwTime = 10; + level->addEntity(item); +} + +void __cdecl NativeKickPlayer(int entityId, int reason) +{ + auto player = FindPlayer(entityId); + if (player && player->connection) + { + DisconnectPacket::eDisconnectReason r = static_cast(reason); + player->connection->disconnect(r); + } +} + +int __cdecl NativeBanPlayer(int entityId, const char *reasonUtf8, int reasonByteLen) +{ + if (!ServerRuntime::Access::IsInitialized()) + return 0; + + auto player = FindPlayer(entityId); + if (!player) + return 0; + + std::vector xuids; + PlayerUID xuid1 = player->getXuid(); + PlayerUID xuid2 = player->getOnlineXuid(); + if (xuid1 != INVALID_XUID) + xuids.push_back(xuid1); + if (xuid2 != INVALID_XUID && xuid2 != xuid1) + xuids.push_back(xuid2); + + if (xuids.empty()) + return 0; + + std::string reason = (reasonUtf8 && reasonByteLen > 0) ? std::string(reasonUtf8, reasonByteLen) : "Banned by plugin."; + std::string playerName = ServerRuntime::StringUtils::WideToUtf8(player->getName()); + + ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Plugin"); + metadata.reason = reason; + + for (auto xuid : xuids) + { + if (!ServerRuntime::Access::IsPlayerBanned(xuid)) + ServerRuntime::Access::AddPlayerBan(xuid, playerName, metadata); + } + + if (player->connection) + player->connection->disconnect(DisconnectPacket::eDisconnect_Banned); + + return 1; +} + +int __cdecl NativeBanPlayerIp(int entityId, const char *reasonUtf8, int reasonByteLen) +{ + if (!ServerRuntime::Access::IsInitialized()) + return 0; + + auto player = FindPlayer(entityId); + if (!player || !player->connection || !player->connection->connection || !player->connection->connection->getSocket()) + return 0; + + unsigned char smallId = player->connection->connection->getSocket()->getSmallId(); + if (smallId == 0) + return 0; + + std::string playerIp; + if (!ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, &playerIp)) + return 0; + + std::string reason = (reasonUtf8 && reasonByteLen > 0) ? std::string(reasonUtf8, reasonByteLen) : "Banned by plugin."; + + ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Plugin"); + metadata.reason = reason; + + std::string normalizedIp = ServerRuntime::NetworkUtils::NormalizeIpToken(playerIp); + if (ServerRuntime::Access::IsIpBanned(normalizedIp)) + return 0; + + if (!ServerRuntime::Access::AddIpBan(normalizedIp, metadata)) + return 0; + + PlayerList *list = MinecraftServer::getPlayerList(); + if (list) + { + std::vector> snapshot = list->players; + for (auto &p : snapshot) + { + if (!p || !p->connection || !p->connection->connection || !p->connection->connection->getSocket()) + continue; + unsigned char sid = p->connection->connection->getSocket()->getSmallId(); + if (sid == 0) + continue; + std::string pIp; + if (!ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(sid, &pIp)) + continue; + if (ServerRuntime::NetworkUtils::NormalizeIpToken(pIp) == normalizedIp) + { + if (p->connection) + p->connection->disconnect(DisconnectPacket::eDisconnect_Banned); + } + } + } + + return 1; +} + +int __cdecl NativeGetPlayerAddress(int entityId, char *outIpBuf, int outIpBufSize, int *outPort) +{ + if (outPort) + *outPort = 0; + if (outIpBuf && outIpBufSize > 0) + outIpBuf[0] = '\0'; + + auto player = FindPlayer(entityId); + if (!player || !player->connection || !player->connection->connection || !player->connection->connection->getSocket()) + return 0; + + unsigned char smallId = player->connection->connection->getSocket()->getSmallId(); + if (smallId == 0) + return 0; + + std::string playerIp; + if (!ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, &playerIp)) + { + SOCKET sock = WinsockNetLayer::GetSocketForSmallId(smallId); + if (sock != INVALID_SOCKET) + { + sockaddr_in addr; + int addrLen = sizeof(addr); + if (getpeername(sock, (sockaddr *)&addr, &addrLen) == 0) + { + char ipBuf[64] = {}; + if (inet_ntop(AF_INET, &addr.sin_addr, ipBuf, sizeof(ipBuf))) + { + playerIp = ipBuf; + if (outPort) + *outPort = (int)ntohs(addr.sin_port); + } + } + } + if (playerIp.empty()) + return 0; + } + else + { + SOCKET sock = WinsockNetLayer::GetSocketForSmallId(smallId); + if (sock != INVALID_SOCKET && outPort) + { + sockaddr_in addr; + int addrLen = sizeof(addr); + if (getpeername(sock, (sockaddr *)&addr, &addrLen) == 0) + *outPort = (int)ntohs(addr.sin_port); + } + } + + if (outIpBuf && outIpBufSize > 0) + { + int copyLen = (int)playerIp.size(); + if (copyLen >= outIpBufSize) + copyLen = outIpBufSize - 1; + memcpy(outIpBuf, playerIp.c_str(), copyLen); + outIpBuf[copyLen] = '\0'; + } + + return 1; +} + +int __cdecl NativeGetPlayerLatency(int entityId) +{ + auto player = FindPlayer(entityId); + if (!player) return -1; + + return player->latency; +} + +int __cdecl NativeSendRaw(int entityId, unsigned char *bufferData, int bufferSize) +{ + auto player = FindPlayer(entityId); + if (!player) return -1; + + if (!player->connection || !player->connection->connection) + return -1; + + player->connection->connection->send(bufferData, bufferSize); +} + +void WriteInventoryItemData(std::shared_ptr item, int index, int* outBuffer) { + if (item) { + //ItemFlags Key: + // 0x1 = hasMetadata (has data that needs to be gotten from "ReadMetaFromNative") + + uint8_t itemFlags = 0; + if (item->getTag() == nullptr) goto doneWithMetadataFlag; + CompoundTag* itemTag = item->getTag(); + + if (itemTag->contains(L"ench")) { + itemFlags |= 0x1; + goto doneWithMetadataFlag; + } + else { //we just want to check one tag for metadata and return for this flag, not all of them + CompoundTag* displayTag = itemTag->getCompound(L"display"); + if (displayTag->contains(L"Name") || displayTag->contains(L"Lore")) { + itemFlags |= 0x1; + goto doneWithMetadataFlag; + } + } + + + doneWithMetadataFlag: + + outBuffer[(index * 3) + 0] = item->id; + outBuffer[(index * 3) + 1] = item->getAuxValue(); + outBuffer[(index * 3) + 2] = (((int)itemFlags << 24) | ((int)item->count << 8)); + } +} + +void __cdecl NativeGetPlayerInventory(int entityId, int *outData) +{ + // 9 slots per row, 3 slots in the inventory and the hotbar, 4 armor slots, 1 hand slot + // (((slotsPerRow * Rows) + ArmorSlots) * AmountOfIntsPerSlot) + hand slot + // (((9 * 4) + 4) * 3) + 1 = 121 + memset(outData, 0, 121 * sizeof(int)); + + auto player = FindPlayer(entityId); + if (!player || !player->inventory) + return; + + unsigned int size = player->inventory->getContainerSize(); + if (size > 40) + size = 40; + + for (unsigned int i = 0; i < size; i++) + { + WriteInventoryItemData(player->inventory->getItem(i), i, outData); + } + + outData[120] = player->inventory->selected; +} + +void __cdecl NativeSetPlayerInventorySlot(int entityId, int slot, int itemId, int count, int aux) +{ + auto player = FindPlayer(entityId); + if (!player || !player->inventory) + return; + + if (itemId <= 0 || count <= 0) + player->inventory->setItem(slot, nullptr); + else + player->inventory->setItem(slot, std::make_shared(itemId, count, aux)); +} + +void __cdecl NativeGetContainerContents(int entityId, int *outData, int maxSlots) +{ + memset(outData, 0, maxSlots * 3 * sizeof(int)); + + auto player = FindPlayer(entityId); + if (!player || !player->containerMenu || player->containerMenu == player->inventoryMenu) + return; + + auto *menu = player->containerMenu; + auto *items = menu->getItems(); + int count = (int)items->size(); + if (count > maxSlots) + count = maxSlots; + + for (int i = 0; i < count; i++) + { + WriteInventoryItemData((*items)[i], i, outData); + } + delete items; +} + +void __cdecl NativeSetContainerSlot(int entityId, int slot, int itemId, int count, int aux) +{ + auto player = FindPlayer(entityId); + if (!player || !player->containerMenu || player->containerMenu == player->inventoryMenu) + return; + + auto *menu = player->containerMenu; + if (slot < 0 || slot >= (int)menu->slots.size()) + return; + + if (itemId <= 0 || count <= 0) + menu->setItem(slot, nullptr); + else + menu->setItem(slot, std::make_shared(itemId, count, aux)); + + menu->broadcastChanges(); +} + +void __cdecl NativeGetContainerViewerEntityIds(int entityId, int *outIds, int maxCount, int *outCount) +{ + *outCount = 0; + + auto player = FindPlayer(entityId); + if (!player || !player->containerMenu || player->containerMenu == player->inventoryMenu) + return; + + auto *menu = player->containerMenu; + if (menu->slots.empty()) + return; + + Container *myContainer = menu->slots[0]->container.get(); + if (!myContainer) + return; + + CompoundContainer *myCompound = dynamic_cast(myContainer); + if (myCompound) + myContainer = myCompound->getFirstContainer().get(); + + PlayerList *list = MinecraftServer::getPlayerList(); + if (!list) + return; + + int count = 0; + for (auto &p : list->players) + { + if (!p || !p->containerMenu || p->containerMenu == p->inventoryMenu) + continue; + if (p->containerMenu->slots.empty()) + continue; + Container *theirContainer = p->containerMenu->slots[0]->container.get(); + CompoundContainer *theirCompound = dynamic_cast(theirContainer); + if (theirCompound) + theirContainer = theirCompound->getFirstContainer().get(); + if (theirContainer == myContainer && count < maxCount) + outIds[count++] = p->entityId; + } + *outCount = count; +} + +void __cdecl NativeCloseContainer(int entityId) +{ + auto player = FindPlayer(entityId); + if (player && player->containerMenu != player->inventoryMenu) + player->closeContainer(); +} + +void __cdecl NativeOpenVirtualContainer(int entityId, int nativeType, const char *titleUtf8, int titleByteLen, int slotCount, int *itemsBuf) +{ + auto player = FindPlayer(entityId); + if (!player) + return; + + if (player->containerMenu != player->inventoryMenu) + player->closeContainer(); + + std::wstring title = ServerRuntime::StringUtils::Utf8ToWide(std::string(titleUtf8, titleByteLen)); + auto container = std::make_shared(nativeType, title, slotCount); + + for (int i = 0; i < slotCount; i++) + { + int id = itemsBuf[i * 3]; + int count = itemsBuf[i * 3 + 1]; + int aux = itemsBuf[i * 3 + 2]; + if (id > 0 && count > 0) + container->setItem(i, std::make_shared(id, count, aux)); + } + + player->openContainer(container); +} +//didnt update this for enchants +// [nameLen:int32][nameUTF8:bytes][loreCount:int32][lore0Len:int32][lore0UTF8:bytes] +int __cdecl NativeGetItemMeta(int entityId, int slot, char *outBuf, int bufSize) +{ + auto player = FindPlayer(entityId); + if (!player || !player->inventory) + return 0; + + unsigned int size = player->inventory->getContainerSize(); + if (slot < 0 || slot >= (int)size) + return 0; + + auto item = player->inventory->getItem(slot); + if (!item || !item->hasTag()) + return 0; + + CompoundTag *tag = item->getTag(); + if (!tag || !tag->contains(L"display")) + return 0; + + CompoundTag *display = tag->getCompound(L"display"); + bool hasName = display->contains(L"Name"); + bool hasLore = display->contains(L"Lore"); + + bool hasEnchantments = item->isEnchanted(); + + if (!hasName && !hasLore) + return 0; + + int offset = 0; + + if (hasName) + { + std::wstring wname = display->getString(L"Name"); + std::string nameUtf8 = ServerRuntime::StringUtils::WideToUtf8(wname); + int nameLen = (int)nameUtf8.size(); + if (offset + 4 + nameLen > bufSize) return 0; + memcpy(outBuf + offset, &nameLen, 4); + offset += 4; + memcpy(outBuf + offset, nameUtf8.data(), nameLen); + offset += nameLen; + } + else + { + int zero = 0; + if (offset + 4 > bufSize) return 0; + memcpy(outBuf + offset, &zero, 4); + offset += 4; + } + + if (hasLore) + { + ListTag *lore = (ListTag *)display->getList(L"Lore"); + int loreCount = lore->size(); + if (offset + 4 > bufSize) return 0; + memcpy(outBuf + offset, &loreCount, 4); + offset += 4; + + for (int i = 0; i < loreCount; i++) + { + std::wstring wline = lore->get(i)->data; + std::string lineUtf8 = ServerRuntime::StringUtils::WideToUtf8(wline); + int lineLen = (int)lineUtf8.size(); + if (offset + 4 + lineLen > bufSize) return 0; + memcpy(outBuf + offset, &lineLen, 4); + offset += 4; + memcpy(outBuf + offset, lineUtf8.data(), lineLen); + offset += lineLen; + } + } + else + { + int zero = 0; + if (offset + 4 > bufSize) return 0; + memcpy(outBuf + offset, &zero, 4); + offset += 4; + } + + if (hasEnchantments) { + ListTag* list = item->getEnchantmentTags(); + if (list != nullptr) { + int listSize = list->size(); + + if ((offset + 4 + (listSize * (4 + 4))) > bufSize) return 0; + + memcpy(outBuf + offset, &listSize, 4); + offset += 4; + for (int i = 0; i < listSize; i++) { + int type = list->get(i)->getShort((wchar_t*)ItemInstance::TAG_ENCH_ID); + int level = list->get(i)->getShort((wchar_t*)ItemInstance::TAG_ENCH_LEVEL); + + memcpy(outBuf + offset, &type, 4); + offset += 4; + + memcpy(outBuf + offset, &level, 4); + offset += 4; + } + } + } + else { + int zero = 0; + if (offset + 4 > bufSize) return 0; + memcpy(outBuf + offset, &zero, 4); + offset += 4; + } + + return offset; +} + +void __cdecl NativeSetItemMeta(int entityId, int slot, const char *inBuf, int bufSize) +{ + auto player = FindPlayer(entityId); + if (!player || !player->inventory) + return; + + unsigned int size = player->inventory->getContainerSize(); + if (slot < 0 || slot >= (int)size) + return; + + auto item = player->inventory->getItem(slot); + if (!item) + return; + + if (inBuf == nullptr || bufSize <= 0) + { + item->resetHoverName(); + if (item->hasTag()) + { + CompoundTag *tag = item->getTag(); + if (tag && tag->contains(L"display")) + { + CompoundTag *display = tag->getCompound(L"display"); + display->remove(L"Lore"); + if (display->isEmpty()) + { + tag->remove(L"display"); + if (tag->isEmpty()) + item->setTag(nullptr); + } + } + + if (tag && tag->contains(L"ench")) + { + tag->remove(L"ench"); + } + } + return; + } + + int offset = 0; + + if (offset + 4 > bufSize) return; + int nameLen = 0; + memcpy(&nameLen, inBuf + offset, 4); + offset += 4; + + if (nameLen > 0) + { + if (offset + nameLen > bufSize) return; + std::string nameUtf8(inBuf + offset, nameLen); + offset += nameLen; + std::wstring wname = ServerRuntime::StringUtils::Utf8ToWide(nameUtf8); + item->setHoverName(wname); + } + else + { + item->resetHoverName(); + } + + if (offset + 4 > bufSize) return; + int loreCount = 0; + memcpy(&loreCount, inBuf + offset, 4); + offset += 4; + + if (loreCount > 0) + { + if (!item->hasTag()) item->setTag(new CompoundTag()); + CompoundTag *tag = item->getTag(); + if (!tag->contains(L"display")) tag->putCompound(L"display", new CompoundTag()); + CompoundTag *display = tag->getCompound(L"display"); + + auto *loreList = new ListTag(L"Lore"); + for (int i = 0; i < loreCount; i++) + { + if (offset + 4 > bufSize) break; + int lineLen = 0; + memcpy(&lineLen, inBuf + offset, 4); + offset += 4; + + std::wstring wline; + if (lineLen > 0 && offset + lineLen <= bufSize) + { + std::string lineUtf8(inBuf + offset, lineLen); + offset += lineLen; + wline = ServerRuntime::StringUtils::Utf8ToWide(lineUtf8); + } + loreList->add(new StringTag(L"", wline)); + } + display->put(L"Lore", loreList); + } + else + { + if (item->hasTag()) + { + CompoundTag *tag = item->getTag(); + if (tag && tag->contains(L"display")) + tag->getCompound(L"display")->remove(L"Lore"); + } + } + + if (offset + 4 > bufSize) return; + int enchantCount = 0; + memcpy(&enchantCount, inBuf + offset, 4); + offset += 4; + + if (enchantCount > 0) + { + if (!item->hasTag()) item->setTag(new CompoundTag()); + CompoundTag* tag = item->getTag(); + if (!tag->contains(L"ench")) tag->put(L"ench", new ListTag(L"ench")); + ListTag* enchantments = static_cast *>(tag->get(L"ench")); + + for (int i = 0; i < enchantCount; i++) { + if (offset + (4 + 4) > bufSize) break; + + int type = 0; + memcpy(&type, inBuf + offset, 4); + offset += 4; + + int level = 0; + memcpy(&level, inBuf + offset, 4); + offset += 4; + + CompoundTag* ench = new CompoundTag(); + ench->putShort((wchar_t*)ItemInstance::TAG_ENCH_ID, static_cast(type)); + ench->putShort((wchar_t*)ItemInstance::TAG_ENCH_LEVEL, static_cast(level)); + enchantments->add(ench); + } + } + else + { + if (item->hasTag()) + { + CompoundTag* tag = item->getTag(); + if (tag && tag->contains(L"ench")) + { + tag->remove(L"ench"); + } + } + } +} + +void __cdecl NativeSetHeldItemSlot(int entityId, int slot) +{ + auto player = FindPlayer(entityId); + if (!player || !player->inventory) return; + if (slot < 0 || slot >= Inventory::getSelectionSize()) return; + player->inventory->selected = slot; + if (player->connection) + player->connection->queueSend(std::make_shared(slot)); +} + +void __cdecl NativeSetSneaking(int entityId, int sneak) +{ + auto player = FindPlayer(entityId); + if (player) + player->setSneaking(sneak != 0); +} + +void __cdecl NativeSetVelocity(int entityId, double x, double y, double z) +{ + auto player = FindPlayer(entityId); + if (player) + { + player->xd = x; + player->yd = y; + player->zd = z; + player->hurtMarked = true; + return; + } + auto entity = FindEntity(entityId); + if (entity) + { + entity->xd = x; + entity->yd = y; + entity->zd = z; + entity->hurtMarked = true; + } +} + +void __cdecl NativeSetAllowFlight(int entityId, int allowFlight) +{ + auto player = FindPlayer(entityId); + if (player) + { + player->abilities.mayfly = (allowFlight != 0); + if (!player->abilities.mayfly) + player->abilities.flying = false; + if (player->connection) + player->connection->send(std::make_shared(&player->abilities)); + } +} + +void __cdecl NativePlaySound(int entityId, int soundId, double x, double y, double z, float volume, float pitch) +{ + auto player = FindPlayer(entityId); + if (player && player->connection) + player->connection->send(std::make_shared(soundId, x, y, z, volume, pitch)); +} + +void __cdecl NativeSetSleepingIgnored(int entityId, int ignored) +{ + auto player = FindPlayer(entityId); + if (player) + player->fk_sleepingIgnored = (ignored != 0); +} + +void __cdecl NativeSetLevel(int entityId, int level) +{ + auto player = FindPlayer(entityId); + if (!player) return; + player->experienceLevel = level; + if (player->connection) + player->connection->send(std::make_shared(player->experienceProgress, player->totalExperience, player->experienceLevel)); +} + +void __cdecl NativeSetExp(int entityId, float exp) +{ + auto player = FindPlayer(entityId); + if (!player) return; + player->experienceProgress = exp; + if (player->connection) + player->connection->send(std::make_shared(player->experienceProgress, player->totalExperience, player->experienceLevel)); +} + +void __cdecl NativeGiveExp(int entityId, int amount) +{ + auto player = FindPlayer(entityId); + if (!player) return; + player->increaseXp(amount); + if (player->connection) + player->connection->send(std::make_shared(player->experienceProgress, player->totalExperience, player->experienceLevel)); +} + +void __cdecl NativeGiveExpLevels(int entityId, int amount) +{ + auto player = FindPlayer(entityId); + if (!player) return; + player->giveExperienceLevels(amount); + if (player->connection) + player->connection->send(std::make_shared(player->experienceProgress, player->totalExperience, player->experienceLevel)); +} + +void __cdecl NativeSetFoodLevel(int entityId, int foodLevel) +{ + auto player = FindPlayer(entityId); + if (!player) return; + FoodData *fd = player->getFoodData(); + if (!fd) return; + fd->setFoodLevel(foodLevel); + if (player->connection) + player->connection->send(std::make_shared(player->getHealth(), fd->getFoodLevel(), fd->getSaturationLevel(), eTelemetryChallenges_Unknown)); +} + +void __cdecl NativeSetSaturation(int entityId, float saturation) +{ + auto player = FindPlayer(entityId); + if (!player) return; + FoodData *fd = player->getFoodData(); + if (!fd) return; + fd->setSaturation(saturation); + if (player->connection) + player->connection->send(std::make_shared(player->getHealth(), fd->getFoodLevel(), fd->getSaturationLevel(), eTelemetryChallenges_Unknown)); +} + +void __cdecl NativeSetExhaustion(int entityId, float exhaustion) +{ + auto player = FindPlayer(entityId); + if (!player) return; + FoodData *fd = player->getFoodData(); + if (!fd) return; + fd->setExhaustion(exhaustion); +} + +void __cdecl NativeSpawnParticle(int entityId, int particleId, float x, float y, float z, float offsetX, float offsetY, float offsetZ, float speed, int count) +{ + auto player = FindPlayer(entityId); + if (!player || !player->connection) return; + wchar_t buf[32]; + swprintf_s(buf, L"%d", particleId); + player->connection->send(std::make_shared(std::wstring(buf), x, y, z, offsetX, offsetY, offsetZ, speed, count)); +} + +int __cdecl NativeSetPassenger(int entityId, int passengerEntityId) +{ + auto entity = FindEntity(entityId); + auto passenger = FindEntity(passengerEntityId); + if (!entity || !passenger) return 0; + passenger->ride(entity); + PlayerList *list = MinecraftServer::getPlayerList(); + if (list) + list->broadcastAll(std::make_shared(SetEntityLinkPacket::RIDING, passenger, entity), entity->dimension); + return 1; +} + +int __cdecl NativeLeaveVehicle(int entityId) +{ + auto entity = FindEntity(entityId); + if (!entity || !entity->riding) return 0; + int dim = entity->riding->dimension; + entity->ride(nullptr); + PlayerList *list = MinecraftServer::getPlayerList(); + if (list) + list->broadcastAll(std::make_shared(SetEntityLinkPacket::RIDING, entity, nullptr), dim); + return 1; +} + +int __cdecl NativeEject(int entityId) +{ + auto entity = FindEntity(entityId); + if (!entity) return 0; + auto riderPtr = entity->rider.lock(); + if (!riderPtr) return 0; + riderPtr->ride(nullptr); + PlayerList *list = MinecraftServer::getPlayerList(); + if (list) + list->broadcastAll(std::make_shared(SetEntityLinkPacket::RIDING, riderPtr, nullptr), entity->dimension); + return 1; +} + +int __cdecl NativeGetVehicleId(int entityId) +{ + auto entity = FindEntity(entityId); + if (!entity || !entity->riding) return -1; + return entity->riding->entityId; +} + +int __cdecl NativeGetPassengerId(int entityId) +{ + auto entity = FindEntity(entityId); + if (!entity) return -1; + auto riderPtr = entity->rider.lock(); + if (!riderPtr) return -1; + return riderPtr->entityId; +} + +void __cdecl NativeGetEntityInfo(int entityId, double *outData) +{ + outData[0] = -1; + outData[1] = 0; + outData[2] = 0; + outData[3] = 0; + outData[4] = 0; + auto entity = FindEntity(entityId); + if (!entity) return; + outData[0] = (double)MapEntityType((int)entity->GetType()); + outData[1] = entity->x; + outData[2] = entity->y; + outData[3] = entity->z; + outData[4] = (double)entity->dimension; +} + +} // namespace FourKitBridge diff --git a/Minecraft.Server/FourKitNatives.h b/Minecraft.Server/FourKitNatives.h new file mode 100644 index 00000000..df96d918 --- /dev/null +++ b/Minecraft.Server/FourKitNatives.h @@ -0,0 +1,81 @@ +#pragma once + + +namespace FourKitBridge +{ + // core + void __cdecl NativeDamagePlayer(int entityId, float amount); + void __cdecl NativeSetPlayerHealth(int entityId, float health); + void __cdecl NativeTeleportPlayer(int entityId, double x, double y, double z); + void __cdecl NativeSetPlayerGameMode(int entityId, int gameMode); + void __cdecl NativeBroadcastMessage(const char *utf8, int len); + void __cdecl NativeSetFallDistance(int entityId, float distance); + void __cdecl NativeGetPlayerSnapshot(int entityId, double *outData); + void __cdecl NativeSendMessage(int entityId, const char *utf8, int len); + void __cdecl NativeSetWalkSpeed(int entityId, float speed); + void __cdecl NativeTeleportEntity(int entityId, int dimId, double x, double y, double z); + + // World + int __cdecl NativeGetTileId(int dimId, int x, int y, int z); + int __cdecl NativeGetTileData(int dimId, int x, int y, int z); + void __cdecl NativeSetTile(int dimId, int x, int y, int z, int tileId, int data); + void __cdecl NativeSetTileData(int dimId, int x, int y, int z, int data); + int __cdecl NativeBreakBlock(int dimId, int x, int y, int z); + int __cdecl NativeGetHighestBlockY(int dimId, int x, int z); + void __cdecl NativeGetWorldInfo(int dimId, double *outBuf); + void __cdecl NativeSetWorldTime(int dimId, int64_t time); + void __cdecl NativeSetWeather(int dimId, int storm, int thundering, int thunderDuration); + int __cdecl NativeCreateExplosion(int dimId, double x, double y, double z, float power, int setFire, int breakBlocks); + int __cdecl NativeStrikeLightning(int dimId, double x, double y, double z, int effectOnly); + int __cdecl NativeSetSpawnLocation(int dimId, int x, int y, int z); + void __cdecl NativeDropItem(int dimId, double x, double y, double z, int itemId, int count, int auxValue, int naturally); + + // plr + void __cdecl NativeKickPlayer(int entityId, int reason); + int __cdecl NativeBanPlayer(int entityId, const char *reasonUtf8, int reasonByteLen); + int __cdecl NativeBanPlayerIp(int entityId, const char *reasonUtf8, int reasonByteLen); + int __cdecl NativeGetPlayerAddress(int entityId, char* outIpBuf, int outIpBufSize, int* outPort); + int __cdecl NativeGetPlayerLatency(int entityId); + + //plr connection + int __cdecl NativeSendRaw(int entityId, unsigned char* dataBuf, int dataBufSize); + + // inv + void __cdecl NativeGetPlayerInventory(int entityId, int *outData); + void __cdecl NativeSetPlayerInventorySlot(int entityId, int slot, int itemId, int count, int aux); + void __cdecl NativeGetContainerContents(int entityId, int *outData, int maxSlots); + void __cdecl NativeSetContainerSlot(int entityId, int slot, int itemId, int count, int aux); + void __cdecl NativeGetContainerViewerEntityIds(int entityId, int *outIds, int maxCount, int *outCount); + void __cdecl NativeCloseContainer(int entityId); + void __cdecl NativeOpenVirtualContainer(int entityId, int nativeType, const char *titleUtf8, int titleByteLen, int slotCount, int *itemsBuf); + int __cdecl NativeGetItemMeta(int entityId, int slot, char *outBuf, int bufSize); + void __cdecl NativeSetItemMeta(int entityId, int slot, const char *inBuf, int bufSize); + void __cdecl NativeSetHeldItemSlot(int entityId, int slot); + + // ent + void __cdecl NativeSetSneaking(int entityId, int sneak); + void __cdecl NativeSetVelocity(int entityId, double x, double y, double z); + void __cdecl NativeSetAllowFlight(int entityId, int allowFlight); + void __cdecl NativePlaySound(int entityId, int soundId, double x, double y, double z, float volume, float pitch); + void __cdecl NativeSetSleepingIgnored(int entityId, int ignored); + + // x[p&food + void __cdecl NativeSetLevel(int entityId, int level); + void __cdecl NativeSetExp(int entityId, float exp); + void __cdecl NativeGiveExp(int entityId, int amount); + void __cdecl NativeGiveExpLevels(int entityId, int amount); + void __cdecl NativeSetFoodLevel(int entityId, int foodLevel); + void __cdecl NativeSetSaturation(int entityId, float saturation); + void __cdecl NativeSetExhaustion(int entityId, float exhaustion); + + // particle + void __cdecl NativeSpawnParticle(int entityId, int particleId, float x, float y, float z, float offsetX, float offsetY, float offsetZ, float speed, int count); + + // vehicle + int __cdecl NativeSetPassenger(int entityId, int passengerEntityId); + int __cdecl NativeLeaveVehicle(int entityId); + int __cdecl NativeEject(int entityId); + int __cdecl NativeGetVehicleId(int entityId); + int __cdecl NativeGetPassengerId(int entityId); + void __cdecl NativeGetEntityInfo(int entityId, double *outData); +} diff --git a/Minecraft.Server/FourKitRuntime.cpp b/Minecraft.Server/FourKitRuntime.cpp new file mode 100644 index 00000000..0872b823 --- /dev/null +++ b/Minecraft.Server/FourKitRuntime.cpp @@ -0,0 +1,278 @@ +#include "FourKitRuntime.h" +#include "ServerLogger.h" +#include "stdafx.h" + +#include +#include +#include + +using ServerRuntime::LogError; + +typedef void *hostfxr_handle; + +typedef int(__cdecl *hostfxr_initialize_for_runtime_config_fn)( + const wchar_t *runtime_config_path, + const void *parameters, + hostfxr_handle *host_context_handle); + +// Self-contained component loading must use the command-line init API. +// hostfxr_initialize_for_runtime_config returns 0x80008093 ("Initialization +// for self-contained components is not supported") for self-contained +// publishes. We never call hostfxr_run_app, so the assembly's Main is +// never invoked; we just use this entry point to start the runtime and +// then ask for the load_assembly_and_get_function_pointer delegate. +typedef int(__cdecl *hostfxr_initialize_for_dotnet_command_line_fn)( + int argc, + const wchar_t **argv, + const void *parameters, + hostfxr_handle *host_context_handle); + +enum hostfxr_delegate_type +{ + hdt_com_activation = 0, + hdt_load_in_memory_assembly = 1, + hdt_winrt_activation = 2, + hdt_com_register = 3, + hdt_com_unregister = 4, + hdt_load_assembly_and_get_function_pointer = 5, + hdt_get_function_pointer = 6, +}; + +typedef int(__cdecl *hostfxr_get_runtime_delegate_fn)( + const hostfxr_handle host_context_handle, + hostfxr_delegate_type type, + void **delegate); + +typedef int(__cdecl *hostfxr_close_fn)(const hostfxr_handle host_context_handle); + +struct hostfxr_initialize_parameters +{ + size_t size; + const wchar_t *host_path; + const wchar_t *dotnet_root; +}; + +namespace +{ +static hostfxr_initialize_for_runtime_config_fn s_initConfigFn = nullptr; +static hostfxr_initialize_for_dotnet_command_line_fn s_initCmdLineFn = nullptr; +static hostfxr_get_runtime_delegate_fn s_getDelegateFn = nullptr; +static hostfxr_close_fn s_closeFn = nullptr; +static std::wstring s_dotnetRoot; + +static std::wstring FindNet10SystemRoot() +{ + std::vector candidates; + wchar_t envRoot[MAX_PATH] = {}; + DWORD len = GetEnvironmentVariableW(L"DOTNET_ROOT", envRoot, MAX_PATH); + if (len > 0 && len < MAX_PATH) + { + candidates.push_back(std::wstring(envRoot)); + } + candidates.push_back(L"C:\\Program Files\\dotnet"); + + for (const auto &root : candidates) + { + std::wstring fxrDir = root + L"\\host\\fxr"; + WIN32_FIND_DATAW fd; + HANDLE h = FindFirstFileW((fxrDir + L"\\*").c_str(), &fd); + if (h == INVALID_HANDLE_VALUE) + { + continue; + } + bool has10 = false; + do + { + if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && fd.cFileName[0] != L'.') + { + if (std::wstring(fd.cFileName).substr(0, 3) == L"10.") + { + has10 = true; + } + } + } while (!has10 && FindNextFileW(h, &fd)); + FindClose(h); + if (has10) + { + return root; + } + } + + return L"C:\\Program Files\\dotnet"; +} + +static bool TryLoadHostfxrFromPath(const std::wstring &path) +{ + HMODULE lib = LoadLibraryW(path.c_str()); + if (!lib) + { + return false; + } + + s_initConfigFn = (hostfxr_initialize_for_runtime_config_fn)GetProcAddress(lib, "hostfxr_initialize_for_runtime_config"); + s_initCmdLineFn = (hostfxr_initialize_for_dotnet_command_line_fn)GetProcAddress(lib, "hostfxr_initialize_for_dotnet_command_line"); + s_getDelegateFn = (hostfxr_get_runtime_delegate_fn)GetProcAddress(lib, "hostfxr_get_runtime_delegate"); + s_closeFn = (hostfxr_close_fn)GetProcAddress(lib, "hostfxr_close"); + + // We require the command-line init function for self-contained loading. + // The runtime-config init function is optional (kept for diagnostics). + if (s_initCmdLineFn && s_getDelegateFn && s_closeFn) + { + return true; + } + + s_initConfigFn = nullptr; + s_initCmdLineFn = nullptr; + s_getDelegateFn = nullptr; + s_closeFn = nullptr; + FreeLibrary(lib); + return false; +} + +static bool LoadHostfxr() +{ + wchar_t exePath[MAX_PATH] = {}; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + std::wstring exeDir(exePath); + size_t lastSlash = exeDir.find_last_of(L"\\/"); + if (lastSlash != std::wstring::npos) + { + exeDir = exeDir.substr(0, lastSlash); + } + + // Preferred layout: self-contained publish output is staged in "\runtime\" + // by the CMake POST_BUILD step so the server root stays clean. hostfxr.dll + // discovers the rest of the runtime relative to its own location. + if (TryLoadHostfxrFromPath(exeDir + L"\\runtime\\hostfxr.dll")) + { + s_dotnetRoot = exeDir + L"\\runtime"; + return true; + } + + // Legacy fallback: hostfxr.dll dropped directly next to the exe. + if (TryLoadHostfxrFromPath(exeDir + L"\\hostfxr.dll")) + { + s_dotnetRoot = FindNet10SystemRoot(); + return true; + } + + wchar_t dotnetRoot[MAX_PATH] = {}; + DWORD len = GetEnvironmentVariableW(L"DOTNET_ROOT", dotnetRoot, MAX_PATH); + if (len == 0 || len >= MAX_PATH) + { + wcscpy_s(dotnetRoot, L"C:\\Program Files\\dotnet"); + } + + std::wstring hostfxrDir = std::wstring(dotnetRoot) + L"\\host\\fxr"; + + WIN32_FIND_DATAW fd; + HANDLE hFind = FindFirstFileW((hostfxrDir + L"\\*").c_str(), &fd); + if (hFind != INVALID_HANDLE_VALUE) + { + std::wstring bestVersion; + do + { + if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && fd.cFileName[0] != L'.') + { + std::wstring ver(fd.cFileName); + if (ver.substr(0, 3) == L"10." && ver > bestVersion) + { + bestVersion = ver; + } + } + } while (FindNextFileW(hFind, &fd)); + FindClose(hFind); + + if (!bestVersion.empty()) + { + if (TryLoadHostfxrFromPath(hostfxrDir + L"\\" + bestVersion + L"\\hostfxr.dll")) + { + s_dotnetRoot = std::wstring(dotnetRoot); + return true; + } + } + } + + LogError("fourkit", "hostfxr.dll not found. Install the .NET 10 x64 runtime (https://aka.ms/dotnet/download) or copy hostfxr.dll from C:\\Program Files\\dotnet\\host\\fxr\\10.x.x\\ next to the server executable."); + return false; +} +} + +namespace FourKitBridge +{ + bool LoadManagedRuntime(const wchar_t *assemblyPath, + const wchar_t *hostPath, + load_assembly_fn *outLoadAssembly) + { + if (!LoadHostfxr()) + { + return false; + } + + hostfxr_initialize_parameters initParams = {}; + initParams.size = sizeof(hostfxr_initialize_parameters); + initParams.host_path = hostPath; + initParams.dotnet_root = s_dotnetRoot.c_str(); + + // Use the dotnet_command_line API for self-contained component loading. + // We pass the FourKit assembly path as argv[0]; hostfxr starts the + // self-contained runtime that ships in the same directory. We never + // call hostfxr_run_app, so the assembly's Main entry point is never + // executed -- we only need the load_assembly_and_get_function_pointer + // delegate to invoke individual [UnmanagedCallersOnly] methods. + const wchar_t *argv[1] = { assemblyPath }; + + hostfxr_handle ctx = nullptr; + int rc = s_initCmdLineFn(1, argv, &initParams, &ctx); + // hostfxr returns Success_HostAlreadyInitialized (0x00000001) when the + // runtime is already up; both 0 and 1 are success for our purposes. + if ((rc != 0 && rc != 1) || ctx == nullptr) + { + char msg[256]; + sprintf_s(msg, "hostfxr_initialize_for_dotnet_command_line failed (0x%08X). Check the FourKit assembly path.", rc); + LogError("fourkit", msg); + if (ctx) + { + s_closeFn(ctx); + } + return false; + } + + load_assembly_fn loadAssembly = nullptr; + rc = s_getDelegateFn(ctx, hdt_load_assembly_and_get_function_pointer, (void **)&loadAssembly); + s_closeFn(ctx); + + if (rc != 0 || loadAssembly == nullptr) + { + LogError("fourkit", "Failed to get load_assembly_and_get_function_pointer delegate."); + return false; + } + + *outLoadAssembly = loadAssembly; + return true; + } + + bool GetManagedEntryPoint(load_assembly_fn loadAssembly, + const wchar_t *assemblyPath, + const wchar_t *typeName, + const wchar_t *methodName, + void **outFnPtr) + { + int rc = loadAssembly( + assemblyPath, + typeName, + methodName, + UNMANAGEDCALLERSONLY_METHOD, + nullptr, + outFnPtr); + + if (rc != 0 || *outFnPtr == nullptr) + { + char methodNarrow[256]; + sprintf_s(methodNarrow, "%S::%S", typeName, methodName); + LogError("fourkit", (std::string("Failed to resolve managed entry point: ") + methodNarrow).c_str()); + return false; + } + return true; + } +} diff --git a/Minecraft.Server/FourKitRuntime.h b/Minecraft.Server/FourKitRuntime.h new file mode 100644 index 00000000..51844de6 --- /dev/null +++ b/Minecraft.Server/FourKitRuntime.h @@ -0,0 +1,28 @@ +#pragma once + +#define UNMANAGEDCALLERSONLY_METHOD ((const wchar_t *)-1) + +namespace FourKitBridge +{ +typedef int(__stdcall *load_assembly_fn)( + const wchar_t *assembly_path, + const wchar_t *type_name, + const wchar_t *method_name, + const wchar_t *delegate_type_name, + void *reserved, + void **delegate); + +// Loads the .NET runtime via hostfxr's command-line init API and returns a +// delegate that can resolve individual [UnmanagedCallersOnly] entry points +// from the FourKit assembly. The command-line API is required because +// self-contained components are not supported by hostfxr_initialize_for_runtime_config. +bool LoadManagedRuntime(const wchar_t *assemblyPath, + const wchar_t *hostPath, + load_assembly_fn *outLoadAssembly); + +bool GetManagedEntryPoint(load_assembly_fn loadAssembly, + const wchar_t *assemblyPath, + const wchar_t *typeName, + const wchar_t *methodName, + void **outFnPtr); +} diff --git a/Minecraft.Server/Security/IdentityTokenManager.cpp b/Minecraft.Server/Security/IdentityTokenManager.cpp index f20a3d85..b47063bf 100644 --- a/Minecraft.Server/Security/IdentityTokenManager.cpp +++ b/Minecraft.Server/Security/IdentityTokenManager.cpp @@ -5,10 +5,10 @@ #include #endif -#include "..\Common\FileUtils.h" -#include "..\Common\StringUtils.h" -#include "..\ServerLogger.h" -#include "..\vendor\nlohmann\json.hpp" +#include "../Common/FileUtils.h" +#include "../Common/StringUtils.h" +#include "../ServerLogger.h" +#include "../vendor/nlohmann/json.hpp" #include diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index 98052de6..04c82eae 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -16,6 +16,7 @@ #include "../Security/SecurityConfig.h" #include "../Security/RateLimiter.h" #include "../Security/IdentityTokenManager.h" +#include "../FourKitBridge.h" #include "Tesselator.h" #include "Windows64/4JLibs/inc/4J_Render.h" #include "Windows64/GameConfig/Minecraft.spa.h" @@ -370,6 +371,19 @@ int main(int argc, char **argv) config.showHelp = false; SetConsoleCtrlHandler(ConsoleCtrlHandlerProc, TRUE); + + // Disable QuickEdit mode so clicking in the console window doesn't freeze + // the server process. Without this, any accidental click pauses all threads + // that write to stdout until a key is pressed. + { + HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode = 0; + GetConsoleMode(hInput, &mode); + mode &= ~ENABLE_QUICK_EDIT_MODE; + mode |= ENABLE_EXTENDED_FLAGS; + SetConsoleMode(hInput, mode); + } + SetExeWorkingDirectory(); // Load base settings from server.properties, then override with CLI values when provided @@ -681,6 +695,8 @@ int main(int argc, char **argv) LogStartupStep("server startup complete"); LogInfof("startup", "Dedicated server listening on %s:%d", g_Win64MultiplayerIP, g_Win64MultiplayerPort); + + FourKitBridge::Initialize(); if (worldBootstrap.status == eWorldBootstrap_CreatedNew && !IsShutdownRequested() && !app.m_bShutdown) { // Windows64 suppresses saveToDisc right after new world creation @@ -737,6 +753,7 @@ int main(int argc, char **argv) { LogWorldIO("requesting autosave"); app.SetXuiServerAction(kServerActionPad, eXuiServerAction_AutoSaveGame); + FourKitBridge::FireWorldSave(); autosaveRequested = true; } nextAutosaveTick = now + autosaveIntervalMs; @@ -747,6 +764,8 @@ int main(int argc, char **argv) serverCli.Stop(); app.m_bShutdown = true; + FourKitBridge::Shutdown(); //close out the translation layer early for plugin shutdown + LogInfof("shutdown", "Dedicated server stopped"); MinecraftServer *server = MinecraftServer::getInstance(); if (server != NULL) diff --git a/Minecraft.Server/cmake/sources/Common.cmake b/Minecraft.Server/cmake/sources/Common.cmake index 407c45a6..d5ac10a7 100644 --- a/Minecraft.Server/cmake/sources/Common.cmake +++ b/Minecraft.Server/cmake/sources/Common.cmake @@ -1,628 +1,671 @@ +# Resolve the Minecraft.Server source dir from this script's own location so +# the source list can be included from any CMakeLists.txt without depending on +# CMAKE_CURRENT_SOURCE_DIR (which would otherwise resolve to the includer dir). +get_filename_component(_MS_SRC "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) + set(_MINECRAFT_SERVER_COMMON_ROOT - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/AbstractTexturePack.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/AchievementPopup.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/AchievementScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/AllowAllCuller.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ArchiveFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ArrowRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ArmorStandModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ArmorStandRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BatModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BatRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BeaconRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BlazeModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BlazeRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BoatModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BoatRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BookModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BossMobGuiInfo.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BreakingItemParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BubbleParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BufferedImage.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Button.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Camera.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CaveSpiderRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ChatScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ChestModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ChestRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ChickenModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ChickenRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Chunk.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ClientConnection.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ClientConstants.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ClockTexture.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Audio/Consoles_SoundEngine.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Audio/SoundEngine.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Audio/SoundNames.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Colours/ColourTable.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/ConsoleGameMode.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Console_Utils.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Consoles_App.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCAudioFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCCapeFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCColourTableFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCGameRulesFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCGameRulesHeader.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCLocalisationFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCPack.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCSkinFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCTextureFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/DLC/DLCUIDataFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/AddEnchantmentRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/AddItemRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/ApplySchematicRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/BiomeOverride.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/CollectItemRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/CompleteAllRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/CompoundGameRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/ConsoleGenerateStructure.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/ConsoleSchematicFile.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/GameRule.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/GameRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/GameRuleManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/LevelGenerationOptions.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/LevelGenerators.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/LevelRules.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/LevelRuleset.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/NamedAreaRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/StartFeature.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/UpdatePlayerRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/UseTileRuleDefinition.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/XboxStructureActionGenerateBox.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/XboxStructureActionPlaceBlock.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/XboxStructureActionPlaceContainer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/GameRules/XboxStructureActionPlaceSpawner.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Leaderboards/LeaderboardInterface.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Leaderboards/LeaderboardManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Network/GameNetworkManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Telemetry/TelemetryManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Trial/TrialMode.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/AreaConstraint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/AreaHint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/AreaTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/ChangeStateConstraint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/ChoiceTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/CompleteUsingItemTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/ControllerTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/CraftTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/DiggerItemHint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/EffectChangedTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/FullTutorial.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/FullTutorialActiveTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/FullTutorialMode.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/HorseChoiceTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/InfoTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/InputConstraint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/LookAtEntityHint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/LookAtTileHint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/PickupTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/ProcedureCompoundTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/ProgressFlagTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/RideEntityTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/StatTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/TakeItemHint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/Tutorial.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/TutorialHint.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/TutorialMessage.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/TutorialMode.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/TutorialTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/UseItemTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/UseTileTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/Tutorial/XuiCraftingTask.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_AnvilMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_BeaconMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_BrewingMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_CommandBlockMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_ContainerMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_CraftingMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_DispenserMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_EnchantingMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_FireworksMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_FurnaceMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_HUD.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_HopperMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_HorseInventoryMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_InventoryMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_StartGame.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_TradingMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/IUIScene_WritingBookMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIBitmapFont.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIUnicodeBitmapFont.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_Chat.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_DebugUIConsole.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_DebugUIMarketingGuide.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_Logo.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_MenuBackground.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_Panorama.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_PressStartToPlay.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIComponent_TutorialPopup.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_Base.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_BeaconEffectButton.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_BitmapIcon.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_Book.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_Button.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_ButtonList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_CheckBox.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_Cursor.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_DLCList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_DynamicLabel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_EnchantmentBook.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_EnchantmentButton.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_HTMLLabel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_Label.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_LeaderboardList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_MinecraftHorse.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_MinecraftPlayer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_PlayerList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_PageFlip.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_PlayerSkinPreview.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_Progress.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_SaveList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_Slider.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_SlotList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_SpaceIndicatorBar.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_TextInput.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIControl_TexturePackList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIController.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIFontData.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIGroup.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UILayer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_AnvilMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_BeaconMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_BrewingStandMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_ContainerMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_ControlsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_CraftingMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_CreativeMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_Credits.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_DLCMainMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_DLCOffersMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_DebugCreateSchematic.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_DebugOptions.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_DebugOverlay.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_DebugSetCamera.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_DispenserMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_EULA.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_EnchantingMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_EndPoem.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_FireworksMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_FullscreenProgress.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_FurnaceMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_HUD.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_HelpAndOptionsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_HopperMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_HorseInventoryMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_HowToPlay.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_HowToPlayMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_InGameHostOptionsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_InGamePlayerOptionsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_Intro.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_InventoryMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_Keyboard.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LanguageSelector.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LeaderboardsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_MainMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_MessageBox.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_NewUpdateMessage.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_PauseMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_QuadrantSignin.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_ReinstallMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SaveMessage.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SettingsControlMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SettingsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SettingsUIMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SignEntryMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_TeleportMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_Timer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_TradingMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_TrialExitUpsell.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_BookAndQuillMenu.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIString.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UITTFFont.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/adler32.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/compress.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/crc32.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/deflate.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/gzclose.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/gzlib.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/gzread.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/gzwrite.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/infback.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/inffast.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/inflate.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/inftrees.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/trees.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/uncompr.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/zlib/zutil.c" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CompassTexture.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ConfirmScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ConsoleInput.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ControlsScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CowModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CowRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CreateWorldScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CreeperModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CreeperRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CritParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/CritParticle2.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Cube.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DLCTexturePack.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DeathScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DefaultRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DefaultTexturePack.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DemoUser.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DerivedServerLevel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DirtyChunkSorter.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DispenserBootstrap.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DistanceChunkSorter.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DragonBreathParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DragonModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/DripParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EchantmentTableParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EditBox.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EnchantTableRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EnderChestRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EnderCrystalModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EnderCrystalRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EnderDragonRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EnderParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EndermanModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EndermanRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EndermiteModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EndermiteRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EntityRenderDispatcher.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EntityRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EntityTileRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/EntityTracker.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ErrorScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ExperienceOrbRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ExplodeParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Extrax64Stubs.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FallingTileRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FileTexturePack.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FireballRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FireworksParticles.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FishingHookRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FlameParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FolderTexturePack.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Font.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FootstepParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Frustum.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FrustumCuller.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/FrustumData.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/GameRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/GhastModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/GhastRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/GiantMobRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Gui.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/GuiComponent.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/GuiMessage.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/GuiParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/GuiParticles.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/HeartParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/HorseRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/HttpTexture.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/HugeExplosionParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/HugeExplosionSeedParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/HumanoidMobRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/HumanoidModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/InBedChatScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Input.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ItemFrameRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ItemInHandRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ItemRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ItemSpriteRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/JoinMultiplayerScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/KeyMapping.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LargeChestModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LavaParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LavaSlimeModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LavaSlimeRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LeashKnotModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LeashKnotRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LevelRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Lighting.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LightningBoltRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LivingEntityRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/LocalPlayer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MemTexture.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MemoryTracker.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MinecartModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MinecartRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MinecartSpawnerRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Minecraft.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MinecraftServer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Minimap.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MobRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MobSkinMemTextureProcessor.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MobSkinTextureProcessor.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MobSpawnerRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Model.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ModelHorse.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ModelPart.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MultiPlayerChunkCache.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MultiPlayerGameMode.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MultiPlayerLevel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MultiPlayerLocalPlayer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/MushroomCowRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/NameEntryScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/NetherPortalParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/NoteParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/OcelotModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/OcelotRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/OffsettedRenderList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Options.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/OptionsScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PS3/PS3Extras/ShutdownManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PaintingRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Particle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ParticleEngine.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PauseScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PendingConnection.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PigModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PigRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PistonPieceRenderer.cpp" - "${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/PlayerList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PlayerRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Polygon.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/PreStitchedTextureMap.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ProgressRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/QuadrupedModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/RabbitModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/RabbitRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Rect2i.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/RedDustParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/RemotePlayer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/RenameWorldScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Screen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ScreenSizeCalculator.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ScrolledSelectionList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SelectWorldScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ServerChunkCache.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ServerCommandDispatcher.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ServerConnection.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ServerLevel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ServerLevelListener.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ServerPlayer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ServerPlayerGameMode.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ServerScoreboard.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Settings.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SheepFurModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SheepModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SheepRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SignModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SignRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SilverfishModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SilverfishRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SimpleIcon.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SkeletonHeadModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SkeletonModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SkeletonRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SkiModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SkullTileRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SlideButton.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SlimeModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SlimeRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SmallButton.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SmokeParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SnowManModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SnowManRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SnowShovelParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SpellParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SpiderModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SpiderRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SplashParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SquidModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SquidRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/StatsCounter.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/StatsScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/StatsSyncher.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/StitchSlot.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/StitchedTexture.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Stitcher.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/StringTable.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SuspendedParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/SuspendedTownParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TakeAnimationParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TeleportCommand.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TerrainParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Tesselator.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TexOffs.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Texture.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TextureAtlas.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TextureHolder.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TextureManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TextureMap.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TexturePack.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TexturePackRepository.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Textures.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TheEndPortalRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TileEntityRenderDispatcher.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TileEntityRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TileRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Timer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TitleScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TntMinecartRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TntRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/TrackedEntity.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/User.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Vertex.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/VideoSettingsScreen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ViewportCuller.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/VillagerGolemModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/VillagerGolemRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/VillagerModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/VillagerRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/VillagerZombieModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WaterDropParticle.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Windows64/Iggy/gdraw/gdraw_d3d11.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Windows64/KeyboardMouseInput.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Windows64/PostProcesser.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Windows64/Windows64_App.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Windows64/Windows64_Minecraft.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Windows64/Windows64_UIController.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WitchModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WitchRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WitherBossModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WitherBossRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WitherSkullRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WolfModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WolfRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/WstringLookup.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Xbox/MinecraftWindows.rc" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Xbox/Network/NetworkPlayerXbox.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ZombieModel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ZombieRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/compat_shims.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/glWrapper.cpp" - "${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/ConsoleSaveFileOriginal.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/ConsoleSaveFileOriginal.h" - "${CMAKE_CURRENT_SOURCE_DIR}/../include/lce_filesystem/lce_filesystem.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliInput.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliInput.h" + "${_MS_SRC}/../Minecraft.Client/AbstractTexturePack.cpp" + "${_MS_SRC}/../Minecraft.Client/AchievementPopup.cpp" + "${_MS_SRC}/../Minecraft.Client/AchievementScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/AllowAllCuller.cpp" + "${_MS_SRC}/../Minecraft.Client/ArchiveFile.cpp" + "${_MS_SRC}/../Minecraft.Client/ArrowRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/BatModel.cpp" + "${_MS_SRC}/../Minecraft.Client/BatRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/BeaconRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/BlazeModel.cpp" + "${_MS_SRC}/../Minecraft.Client/BlazeRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/BoatModel.cpp" + "${_MS_SRC}/../Minecraft.Client/BoatRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/BookModel.cpp" + "${_MS_SRC}/../Minecraft.Client/BossMobGuiInfo.cpp" + "${_MS_SRC}/../Minecraft.Client/BreakingItemParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/BubbleParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/BufferedImage.cpp" + "${_MS_SRC}/../Minecraft.Client/Button.cpp" + "${_MS_SRC}/../Minecraft.Client/Camera.cpp" + "${_MS_SRC}/../Minecraft.Client/CaveSpiderRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/ChatScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/ChestModel.cpp" + "${_MS_SRC}/../Minecraft.Client/ChestRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/ChickenModel.cpp" + "${_MS_SRC}/../Minecraft.Client/ChickenRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Chunk.cpp" + "${_MS_SRC}/../Minecraft.Client/ClientConnection.cpp" + "${_MS_SRC}/../Minecraft.Client/ClientConstants.cpp" + "${_MS_SRC}/../Minecraft.Client/ClockTexture.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Audio/Consoles_SoundEngine.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Audio/SoundEngine.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Audio/SoundNames.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Colours/ColourTable.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/ConsoleGameMode.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Console_Utils.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Consoles_App.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCAudioFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCCapeFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCColourTableFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCGameRulesFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCGameRulesHeader.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCLocalisationFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCManager.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCPack.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCSkinFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCTextureFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/DLC/DLCUIDataFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/AddEnchantmentRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/AddItemRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/ApplySchematicRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/BiomeOverride.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/CollectItemRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/CompleteAllRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/CompoundGameRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/ConsoleGenerateStructure.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/ConsoleSchematicFile.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/GameRule.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/GameRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/GameRuleManager.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/LevelGenerationOptions.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/LevelGenerators.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/LevelRules.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/LevelRuleset.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/NamedAreaRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/StartFeature.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/UpdatePlayerRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/UseTileRuleDefinition.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/XboxStructureActionGenerateBox.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/XboxStructureActionPlaceBlock.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/XboxStructureActionPlaceContainer.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/GameRules/XboxStructureActionPlaceSpawner.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Leaderboards/LeaderboardInterface.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Leaderboards/LeaderboardManager.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Network/GameNetworkManager.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Telemetry/TelemetryManager.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Trial/TrialMode.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/AreaConstraint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/AreaHint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/AreaTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/ChangeStateConstraint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/ChoiceTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/CompleteUsingItemTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/ControllerTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/CraftTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/DiggerItemHint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/EffectChangedTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/FullTutorial.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/FullTutorialActiveTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/FullTutorialMode.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/HorseChoiceTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/InfoTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/InputConstraint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/LookAtEntityHint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/LookAtTileHint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/PickupTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/ProcedureCompoundTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/ProgressFlagTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/RideEntityTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/StatTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/TakeItemHint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/Tutorial.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/TutorialHint.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/TutorialMessage.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/TutorialMode.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/TutorialTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/UseItemTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/UseTileTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/Tutorial/XuiCraftingTask.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_AnvilMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_BeaconMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_BrewingMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_CommandBlockMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_ContainerMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_CraftingMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_DispenserMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_EnchantingMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_FireworksMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_FurnaceMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_HUD.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_HopperMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_HorseInventoryMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_InventoryMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_StartGame.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/IUIScene_TradingMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIBitmapFont.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIUnicodeBitmapFont.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_Chat.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_DebugUIConsole.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_DebugUIMarketingGuide.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_Logo.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_MenuBackground.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_Panorama.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_PressStartToPlay.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIComponent_TutorialPopup.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_Base.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_BeaconEffectButton.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_BitmapIcon.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_Button.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_ButtonList.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_CheckBox.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_Cursor.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_DLCList.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_DynamicLabel.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_EnchantmentBook.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_EnchantmentButton.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_HTMLLabel.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_Label.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_LeaderboardList.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_MinecraftHorse.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_MinecraftPlayer.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_PlayerList.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_PageFlip.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_PlayerSkinPreview.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_Progress.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_SaveList.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_Slider.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_SlotList.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_SpaceIndicatorBar.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_TextInput.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIControl_TexturePackList.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIController.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIFontData.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIGroup.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UILayer.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_AnvilMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_BeaconMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_BrewingStandMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_ContainerMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_ControlsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_CraftingMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_CreativeMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_Credits.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_DLCMainMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_DLCOffersMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_DebugCreateSchematic.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_DebugOptions.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_DebugOverlay.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_DebugSetCamera.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_DispenserMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_EULA.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_EnchantingMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_EndPoem.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_FireworksMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_FullscreenProgress.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_FurnaceMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_HUD.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_HelpAndOptionsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_HopperMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_HorseInventoryMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_HowToPlay.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_HowToPlayMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_InGameHostOptionsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_InGamePlayerOptionsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_Intro.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_InventoryMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_Keyboard.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_LanguageSelector.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_LeaderboardsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_MainMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_MessageBox.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_NewUpdateMessage.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_PauseMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_QuadrantSignin.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_ReinstallMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SaveMessage.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SettingsControlMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SettingsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SettingsUIMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SignEntryMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_SkinSelectMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_TeleportMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_Timer.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_TradingMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_TrialExitUpsell.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIScene_BookAndQuillMenu.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UIString.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/UI/UITTFFont.cpp" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/adler32.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/compress.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/crc32.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/deflate.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/gzclose.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/gzlib.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/gzread.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/gzwrite.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/infback.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/inffast.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/inflate.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/inftrees.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/trees.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/uncompr.c" + "${_MS_SRC}/../Minecraft.Client/Common/zlib/zutil.c" + "${_MS_SRC}/../Minecraft.Client/CompassTexture.cpp" + "${_MS_SRC}/../Minecraft.Client/ConfirmScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/ConsoleInput.cpp" + "${_MS_SRC}/../Minecraft.Client/ControlsScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/CowModel.cpp" + "${_MS_SRC}/../Minecraft.Client/CowRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/CreateWorldScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/CreeperModel.cpp" + "${_MS_SRC}/../Minecraft.Client/CreeperRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/CritParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/CritParticle2.cpp" + "${_MS_SRC}/../Minecraft.Client/Cube.cpp" + "${_MS_SRC}/../Minecraft.Client/DLCTexturePack.cpp" + "${_MS_SRC}/../Minecraft.Client/DeathScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/DefaultRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/DefaultTexturePack.cpp" + "${_MS_SRC}/../Minecraft.Client/DemoUser.cpp" + "${_MS_SRC}/../Minecraft.Client/DerivedServerLevel.cpp" + "${_MS_SRC}/../Minecraft.Client/DirtyChunkSorter.cpp" + "${_MS_SRC}/../Minecraft.Client/DispenserBootstrap.cpp" + "${_MS_SRC}/../Minecraft.Client/DistanceChunkSorter.cpp" + "${_MS_SRC}/../Minecraft.Client/DragonBreathParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/DragonModel.cpp" + "${_MS_SRC}/../Minecraft.Client/DripParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/EchantmentTableParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/EditBox.cpp" + "${_MS_SRC}/../Minecraft.Client/EnchantTableRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/EnderChestRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/EnderCrystalModel.cpp" + "${_MS_SRC}/../Minecraft.Client/EnderCrystalRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/EnderDragonRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/EnderParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/EndermanModel.cpp" + "${_MS_SRC}/../Minecraft.Client/EndermanRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/EndermiteModel.cpp" + "${_MS_SRC}/../Minecraft.Client/EndermiteRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/EntityRenderDispatcher.cpp" + "${_MS_SRC}/../Minecraft.Client/EntityRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/EntityTileRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/EntityTracker.cpp" + "${_MS_SRC}/../Minecraft.Client/ErrorScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/ExperienceOrbRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/ExplodeParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/Extrax64Stubs.cpp" + "${_MS_SRC}/../Minecraft.Client/FallingTileRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/FileTexturePack.cpp" + "${_MS_SRC}/../Minecraft.Client/FireballRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/FireworksParticles.cpp" + "${_MS_SRC}/../Minecraft.Client/FishingHookRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/FlameParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/FolderTexturePack.cpp" + "${_MS_SRC}/../Minecraft.Client/Font.cpp" + "${_MS_SRC}/../Minecraft.Client/FootstepParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/Frustum.cpp" + "${_MS_SRC}/../Minecraft.Client/FrustumCuller.cpp" + "${_MS_SRC}/../Minecraft.Client/FrustumData.cpp" + "${_MS_SRC}/../Minecraft.Client/GameRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/GhastModel.cpp" + "${_MS_SRC}/../Minecraft.Client/GhastRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/GiantMobRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Gui.cpp" + "${_MS_SRC}/../Minecraft.Client/GuiComponent.cpp" + "${_MS_SRC}/../Minecraft.Client/GuiMessage.cpp" + "${_MS_SRC}/../Minecraft.Client/GuiParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/GuiParticles.cpp" + "${_MS_SRC}/../Minecraft.Client/HeartParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/HorseRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/HttpTexture.cpp" + "${_MS_SRC}/../Minecraft.Client/HugeExplosionParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/HugeExplosionSeedParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/HumanoidMobRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/HumanoidModel.cpp" + "${_MS_SRC}/../Minecraft.Client/InBedChatScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/Input.cpp" + "${_MS_SRC}/../Minecraft.Client/ItemFrameRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/ItemInHandRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/ItemRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/ItemSpriteRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/JoinMultiplayerScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/KeyMapping.cpp" + "${_MS_SRC}/../Minecraft.Client/LargeChestModel.cpp" + "${_MS_SRC}/../Minecraft.Client/LavaParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/LavaSlimeModel.cpp" + "${_MS_SRC}/../Minecraft.Client/LavaSlimeRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/LeashKnotModel.cpp" + "${_MS_SRC}/../Minecraft.Client/LeashKnotRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/LevelRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Lighting.cpp" + "${_MS_SRC}/../Minecraft.Client/LightningBoltRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/LivingEntityRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/LocalPlayer.cpp" + "${_MS_SRC}/../Minecraft.Client/MemTexture.cpp" + "${_MS_SRC}/../Minecraft.Client/MemoryTracker.cpp" + "${_MS_SRC}/../Minecraft.Client/MinecartModel.cpp" + "${_MS_SRC}/../Minecraft.Client/MinecartRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/MinecartSpawnerRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Minecraft.cpp" + "${_MS_SRC}/../Minecraft.Client/MinecraftServer.cpp" + "${_MS_SRC}/../Minecraft.Client/Minimap.cpp" + "${_MS_SRC}/../Minecraft.Client/MobRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/MobSkinMemTextureProcessor.cpp" + "${_MS_SRC}/../Minecraft.Client/MobSkinTextureProcessor.cpp" + "${_MS_SRC}/../Minecraft.Client/MobSpawnerRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Model.cpp" + "${_MS_SRC}/../Minecraft.Client/ModelHorse.cpp" + "${_MS_SRC}/../Minecraft.Client/ModelPart.cpp" + "${_MS_SRC}/../Minecraft.Client/MultiPlayerChunkCache.cpp" + "${_MS_SRC}/../Minecraft.Client/MultiPlayerGameMode.cpp" + "${_MS_SRC}/../Minecraft.Client/MultiPlayerLevel.cpp" + "${_MS_SRC}/../Minecraft.Client/MultiPlayerLocalPlayer.cpp" + "${_MS_SRC}/../Minecraft.Client/MushroomCowRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/NameEntryScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/NetherPortalParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/NoteParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/OcelotModel.cpp" + "${_MS_SRC}/../Minecraft.Client/OcelotRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/OffsettedRenderList.cpp" + "${_MS_SRC}/../Minecraft.Client/Options.cpp" + "${_MS_SRC}/../Minecraft.Client/OptionsScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/PS3/PS3Extras/ShutdownManager.cpp" + "${_MS_SRC}/../Minecraft.Client/PaintingRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Particle.cpp" + "${_MS_SRC}/../Minecraft.Client/ParticleEngine.cpp" + "${_MS_SRC}/../Minecraft.Client/PauseScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/PendingConnection.cpp" + "${_MS_SRC}/../Minecraft.Client/PigModel.cpp" + "${_MS_SRC}/../Minecraft.Client/PigRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/PistonPieceRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/PlayerChunkMap.cpp" + "${_MS_SRC}/../Minecraft.Client/PlayerCloudParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/PlayerConnection.cpp" + "${_MS_SRC}/../Minecraft.Client/PlayerConnection.h" + "${_MS_SRC}/../Minecraft.Client/PlayerList.cpp" + "${_MS_SRC}/../Minecraft.Client/PlayerRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Polygon.cpp" + "${_MS_SRC}/../Minecraft.Client/PreStitchedTextureMap.cpp" + "${_MS_SRC}/../Minecraft.Client/ProgressRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/QuadrupedModel.cpp" + "${_MS_SRC}/../Minecraft.Client/RabbitModel.cpp" + "${_MS_SRC}/../Minecraft.Client/RabbitRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Rect2i.cpp" + "${_MS_SRC}/../Minecraft.Client/RedDustParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/RemotePlayer.cpp" + "${_MS_SRC}/../Minecraft.Client/RenameWorldScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/Screen.cpp" + "${_MS_SRC}/../Minecraft.Client/ScreenSizeCalculator.cpp" + "${_MS_SRC}/../Minecraft.Client/ScrolledSelectionList.cpp" + "${_MS_SRC}/../Minecraft.Client/SelectWorldScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerChunkCache.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerCommandDispatcher.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerConnection.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerLevel.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerLevelListener.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerPlayer.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerPlayerGameMode.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerScoreboard.cpp" + "${_MS_SRC}/../Minecraft.Client/Settings.cpp" + "${_MS_SRC}/../Minecraft.Client/SheepFurModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SheepModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SheepRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/SignModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SignRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/SilverfishModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SilverfishRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/SimpleIcon.cpp" + "${_MS_SRC}/../Minecraft.Client/SkeletonHeadModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SkeletonModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SkeletonRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/SkiModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SkullTileRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/SlideButton.cpp" + "${_MS_SRC}/../Minecraft.Client/SlimeModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SlimeRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/SmallButton.cpp" + "${_MS_SRC}/../Minecraft.Client/SmokeParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/SnowManModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SnowManRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/SnowShovelParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/SpellParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/SpiderModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SpiderRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/SplashParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/SquidModel.cpp" + "${_MS_SRC}/../Minecraft.Client/SquidRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/StatsCounter.cpp" + "${_MS_SRC}/../Minecraft.Client/StatsScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/StatsSyncher.cpp" + "${_MS_SRC}/../Minecraft.Client/StitchSlot.cpp" + "${_MS_SRC}/../Minecraft.Client/StitchedTexture.cpp" + "${_MS_SRC}/../Minecraft.Client/Stitcher.cpp" + "${_MS_SRC}/../Minecraft.Client/StringTable.cpp" + "${_MS_SRC}/../Minecraft.Client/SuspendedParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/SuspendedTownParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/TakeAnimationParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/TeleportCommand.cpp" + "${_MS_SRC}/../Minecraft.Client/TerrainParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/Tesselator.cpp" + "${_MS_SRC}/../Minecraft.Client/TexOffs.cpp" + "${_MS_SRC}/../Minecraft.Client/Texture.cpp" + "${_MS_SRC}/../Minecraft.Client/TextureAtlas.cpp" + "${_MS_SRC}/../Minecraft.Client/TextureHolder.cpp" + "${_MS_SRC}/../Minecraft.Client/TextureManager.cpp" + "${_MS_SRC}/../Minecraft.Client/TextureMap.cpp" + "${_MS_SRC}/../Minecraft.Client/TexturePack.cpp" + "${_MS_SRC}/../Minecraft.Client/TexturePackRepository.cpp" + "${_MS_SRC}/../Minecraft.Client/Textures.cpp" + "${_MS_SRC}/../Minecraft.Client/TheEndPortalRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/TileEntityRenderDispatcher.cpp" + "${_MS_SRC}/../Minecraft.Client/TileEntityRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/TileRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/Timer.cpp" + "${_MS_SRC}/../Minecraft.Client/TitleScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/TntMinecartRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/TntRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/TrackedEntity.cpp" + "${_MS_SRC}/../Minecraft.Client/User.cpp" + "${_MS_SRC}/../Minecraft.Client/Vertex.cpp" + "${_MS_SRC}/../Minecraft.Client/VideoSettingsScreen.cpp" + "${_MS_SRC}/../Minecraft.Client/ViewportCuller.cpp" + "${_MS_SRC}/../Minecraft.Client/VillagerGolemModel.cpp" + "${_MS_SRC}/../Minecraft.Client/VillagerGolemRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/VillagerModel.cpp" + "${_MS_SRC}/../Minecraft.Client/VillagerRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/VillagerZombieModel.cpp" + "${_MS_SRC}/../Minecraft.Client/WaterDropParticle.cpp" + "${_MS_SRC}/../Minecraft.Client/Windows64/Iggy/gdraw/gdraw_d3d11.cpp" + "${_MS_SRC}/../Minecraft.Client/Windows64/KeyboardMouseInput.cpp" + "${_MS_SRC}/../Minecraft.Client/Windows64/Leaderboards/WindowsLeaderboardManager.cpp" + "${_MS_SRC}/../Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp" + "${_MS_SRC}/../Minecraft.Client/Windows64/PostProcesser.cpp" + "${_MS_SRC}/../Minecraft.Client/Windows64/Windows64_App.cpp" + "${_MS_SRC}/../Minecraft.Client/Windows64/Windows64_Minecraft.cpp" + "${_MS_SRC}/../Minecraft.Client/Windows64/Windows64_UIController.cpp" + "${_MS_SRC}/../Minecraft.Client/WitchModel.cpp" + "${_MS_SRC}/../Minecraft.Client/WitchRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/WitherBossModel.cpp" + "${_MS_SRC}/../Minecraft.Client/WitherBossRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/WitherSkullRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/WolfModel.cpp" + "${_MS_SRC}/../Minecraft.Client/WolfRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/WstringLookup.cpp" + "${_MS_SRC}/../Minecraft.Client/Xbox/MinecraftWindows.rc" + "${_MS_SRC}/../Minecraft.Client/Xbox/Network/NetworkPlayerXbox.cpp" + "${_MS_SRC}/../Minecraft.Client/ZombieModel.cpp" + "${_MS_SRC}/../Minecraft.Client/ZombieRenderer.cpp" + "${_MS_SRC}/../Minecraft.Client/compat_shims.cpp" + "${_MS_SRC}/../Minecraft.Client/glWrapper.cpp" + "${_MS_SRC}/../Minecraft.Client/iob_shim.asm" + "${_MS_SRC}/../Minecraft.Client/stdafx.cpp" + "${_MS_SRC}/../Minecraft.Client/stubs.cpp" + "${_MS_SRC}/../Minecraft.World/AbstractContainerMenu.cpp" + "${_MS_SRC}/../Minecraft.World/CompoundContainer.h" + "${_MS_SRC}/../Minecraft.World/ItemEntity.cpp" + "${_MS_SRC}/../Minecraft.World/LivingEntity.cpp" + "${_MS_SRC}/../Minecraft.World/LivingEntity.h" + "${_MS_SRC}/../Minecraft.World/Player.cpp" + "${_MS_SRC}/../Minecraft.World/Player.h" + "${_MS_SRC}/../Minecraft.World/ThrownEnderpearl.cpp" + "${_MS_SRC}/../Minecraft.World/Tile.cpp" + "${_MS_SRC}/../Minecraft.World/DyePowderItem.cpp" + "${_MS_SRC}/../Minecraft.World/DyePowderItem.h" + "${_MS_SRC}/../Minecraft.World/Mushroom.cpp" + "${_MS_SRC}/../Minecraft.World/Mushroom.h" + "${_MS_SRC}/../Minecraft.World/Sapling.cpp" + "${_MS_SRC}/../Minecraft.World/Sapling.h" + "${_MS_SRC}/../Minecraft.World/ConsoleSaveFileOriginal.cpp" + "${_MS_SRC}/../Minecraft.World/ConsoleSaveFileOriginal.h" + "${_MS_SRC}/../Minecraft.World/CactusTile.cpp" + "${_MS_SRC}/../Minecraft.World/CocoaTile.cpp" + "${_MS_SRC}/../Minecraft.World/CropTile.cpp" + "${_MS_SRC}/../Minecraft.World/FireTile.cpp" + "${_MS_SRC}/../Minecraft.World/GrassTile.cpp" + "${_MS_SRC}/../Minecraft.World/PistonBaseTile.cpp" + "${_MS_SRC}/../Minecraft.World/ReedTile.cpp" + "${_MS_SRC}/../Minecraft.World/StemTile.cpp" + "${_MS_SRC}/../Minecraft.World/NetherWartTile.cpp" + "${_MS_SRC}/../Minecraft.World/LiquidTileDynamic.cpp" + "${_MS_SRC}/../Minecraft.World/EggTile.cpp" + "${_MS_SRC}/../include/lce_filesystem/lce_filesystem.cpp" + "${_MS_SRC}/Console/ServerCliInput.cpp" + "${_MS_SRC}/Console/ServerCliInput.h" ) source_group("" FILES ${_MINECRAFT_SERVER_COMMON_ROOT}) set(_MINECRAFT_SERVER_COMMON_SERVER - "${CMAKE_CURRENT_SOURCE_DIR}/ServerLogManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/ServerLogManager.h" - "${CMAKE_CURRENT_SOURCE_DIR}/ServerLogger.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/ServerLogger.h" - "${CMAKE_CURRENT_SOURCE_DIR}/ServerProperties.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/ServerProperties.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Windows64/ServerMain.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/WorldManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/WorldManager.h" + "${_MS_SRC}/ServerLogManager.cpp" + "${_MS_SRC}/ServerLogManager.h" + "${_MS_SRC}/ServerLogger.cpp" + "${_MS_SRC}/ServerLogger.h" + "${_MS_SRC}/ServerProperties.cpp" + "${_MS_SRC}/ServerProperties.h" + "${_MS_SRC}/Windows64/ServerMain.cpp" + "${_MS_SRC}/WorldManager.cpp" + "${_MS_SRC}/WorldManager.h" ) source_group("Server" FILES ${_MINECRAFT_SERVER_COMMON_SERVER}) +# FourKit native bridge sources. The FourKitBridge.h header is always part +# of the build (gameplay code includes it unconditionally and gets inline +# no-op stubs in the standalone variant), but the four implementation files +# below are only compiled into the FourKit-enabled exe. +set(_MINECRAFT_SERVER_COMMON_SERVER_FOURKIT + "${_MS_SRC}/FourKitBridge.cpp" + "${_MS_SRC}/FourKitBridge.h" + "${_MS_SRC}/FourKitMappers.cpp" + "${_MS_SRC}/FourKitNatives.cpp" + "${_MS_SRC}/FourKitNatives.h" + "${_MS_SRC}/FourKitRuntime.cpp" + "${_MS_SRC}/FourKitRuntime.h" +) +source_group("Server\\FourKit" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_FOURKIT}) + set(_MINECRAFT_SERVER_COMMON_SERVER_ACCESS - "${CMAKE_CURRENT_SOURCE_DIR}/Access/Access.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Access/Access.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Access/BanManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Access/BanManager.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Access/WhitelistManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Access/WhitelistManager.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Access/OpManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Access/OpManager.h" + "${_MS_SRC}/Access/Access.cpp" + "${_MS_SRC}/Access/Access.h" + "${_MS_SRC}/Access/BanManager.cpp" + "${_MS_SRC}/Access/BanManager.h" + "${_MS_SRC}/Access/WhitelistManager.cpp" + "${_MS_SRC}/Access/WhitelistManager.h" + "${_MS_SRC}/Access/OpManager.cpp" + "${_MS_SRC}/Access/OpManager.h" ) source_group("Server/Access" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_ACCESS}) set(_MINECRAFT_SERVER_COMMON_SERVER_SECURITY - "${CMAKE_CURRENT_SOURCE_DIR}/Security/SecurityConfig.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/SecurityConfig.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/RateLimiter.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/RateLimiter.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/StreamCipher.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/StreamCipher.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/ConnectionCipher.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/ConnectionCipher.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/CipherHandshakeEnforcer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/CipherHandshakeEnforcer.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/IdentityTokenManager.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Security/IdentityTokenManager.h" + "${_MS_SRC}/Security/SecurityConfig.cpp" + "${_MS_SRC}/Security/SecurityConfig.h" + "${_MS_SRC}/Security/RateLimiter.cpp" + "${_MS_SRC}/Security/RateLimiter.h" + "${_MS_SRC}/Security/StreamCipher.cpp" + "${_MS_SRC}/Security/StreamCipher.h" + "${_MS_SRC}/Security/ConnectionCipher.cpp" + "${_MS_SRC}/Security/ConnectionCipher.h" + "${_MS_SRC}/Security/CipherHandshakeEnforcer.cpp" + "${_MS_SRC}/Security/CipherHandshakeEnforcer.h" + "${_MS_SRC}/Security/IdentityTokenManager.cpp" + "${_MS_SRC}/Security/IdentityTokenManager.h" ) source_group("Server/Security" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_SECURITY}) set(_MINECRAFT_SERVER_COMMON_SERVER_COMMON - "${CMAKE_CURRENT_SOURCE_DIR}/Common/AccessStorageUtils.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Common/FileUtils.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Common/FileUtils.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Common/NetworkUtils.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Common/StringUtils.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Common/StringUtils.h" + "${_MS_SRC}/Common/AccessStorageUtils.h" + "${_MS_SRC}/Common/FileUtils.cpp" + "${_MS_SRC}/Common/FileUtils.h" + "${_MS_SRC}/Common/NetworkUtils.h" + "${_MS_SRC}/Common/StringUtils.cpp" + "${_MS_SRC}/Common/StringUtils.h" ) source_group("Server/Common" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_COMMON}) set(_MINECRAFT_SERVER_COMMON_SERVER_CONSOLE - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCli.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCli.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliEngine.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliEngine.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliParser.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliParser.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliRegistry.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/ServerCliRegistry.h" + "${_MS_SRC}/Console/ServerCli.cpp" + "${_MS_SRC}/Console/ServerCli.h" + "${_MS_SRC}/Console/ServerCliEngine.cpp" + "${_MS_SRC}/Console/ServerCliEngine.h" + "${_MS_SRC}/Console/ServerCliParser.cpp" + "${_MS_SRC}/Console/ServerCliParser.h" + "${_MS_SRC}/Console/ServerCliRegistry.cpp" + "${_MS_SRC}/Console/ServerCliRegistry.h" ) source_group("Server/Console" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_CONSOLE}) set(_MINECRAFT_SERVER_COMMON_SERVER_CONSOLE_COMMANDS - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/CommandParsing.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/IServerCliCommand.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/ban-ip/CliCommandBanIp.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/ban-ip/CliCommandBanIp.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/ban-list/CliCommandBanList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/ban-list/CliCommandBanList.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/ban/CliCommandBan.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/ban/CliCommandBan.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/defaultgamemode/CliCommandDefaultGamemode.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/enchant/CliCommandEnchant.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/enchant/CliCommandEnchant.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/experience/CliCommandExperience.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/experience/CliCommandExperience.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/gamemode/CliCommandGamemode.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/gamemode/CliCommandGamemode.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/give/CliCommandGive.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/give/CliCommandGive.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/help/CliCommandHelp.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/help/CliCommandHelp.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/kill/CliCommandKill.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/kill/CliCommandKill.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/list/CliCommandList.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/list/CliCommandList.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/pardon-ip/CliCommandPardonIp.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/pardon-ip/CliCommandPardonIp.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/pardon/CliCommandPardon.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/pardon/CliCommandPardon.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/stop/CliCommandStop.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/stop/CliCommandStop.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/time/CliCommandTime.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/time/CliCommandTime.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/tp/CliCommandTp.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/tp/CliCommandTp.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/weather/CliCommandWeather.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/weather/CliCommandWeather.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/whitelist/CliCommandWhitelist.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/whitelist/CliCommandWhitelist.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/revoketoken/CliCommandRevokeToken.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Console/commands/revoketoken/CliCommandRevokeToken.h" + "${_MS_SRC}/Console/commands/CommandParsing.h" + "${_MS_SRC}/Console/commands/IServerCliCommand.h" + "${_MS_SRC}/Console/commands/ban-ip/CliCommandBanIp.cpp" + "${_MS_SRC}/Console/commands/ban-ip/CliCommandBanIp.h" + "${_MS_SRC}/Console/commands/ban-list/CliCommandBanList.cpp" + "${_MS_SRC}/Console/commands/ban-list/CliCommandBanList.h" + "${_MS_SRC}/Console/commands/ban/CliCommandBan.cpp" + "${_MS_SRC}/Console/commands/ban/CliCommandBan.h" + "${_MS_SRC}/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp" + "${_MS_SRC}/Console/commands/defaultgamemode/CliCommandDefaultGamemode.h" + "${_MS_SRC}/Console/commands/enchant/CliCommandEnchant.cpp" + "${_MS_SRC}/Console/commands/enchant/CliCommandEnchant.h" + "${_MS_SRC}/Console/commands/experience/CliCommandExperience.cpp" + "${_MS_SRC}/Console/commands/experience/CliCommandExperience.h" + "${_MS_SRC}/Console/commands/gamemode/CliCommandGamemode.cpp" + "${_MS_SRC}/Console/commands/gamemode/CliCommandGamemode.h" + "${_MS_SRC}/Console/commands/give/CliCommandGive.cpp" + "${_MS_SRC}/Console/commands/give/CliCommandGive.h" + "${_MS_SRC}/Console/commands/help/CliCommandHelp.cpp" + "${_MS_SRC}/Console/commands/help/CliCommandHelp.h" + "${_MS_SRC}/Console/commands/kill/CliCommandKill.cpp" + "${_MS_SRC}/Console/commands/kill/CliCommandKill.h" + "${_MS_SRC}/Console/commands/list/CliCommandList.cpp" + "${_MS_SRC}/Console/commands/list/CliCommandList.h" + "${_MS_SRC}/Console/commands/pardon-ip/CliCommandPardonIp.cpp" + "${_MS_SRC}/Console/commands/pardon-ip/CliCommandPardonIp.h" + "${_MS_SRC}/Console/commands/pardon/CliCommandPardon.cpp" + "${_MS_SRC}/Console/commands/pardon/CliCommandPardon.h" + "${_MS_SRC}/Console/commands/stop/CliCommandStop.cpp" + "${_MS_SRC}/Console/commands/stop/CliCommandStop.h" + "${_MS_SRC}/Console/commands/time/CliCommandTime.cpp" + "${_MS_SRC}/Console/commands/time/CliCommandTime.h" + "${_MS_SRC}/Console/commands/tp/CliCommandTp.cpp" + "${_MS_SRC}/Console/commands/tp/CliCommandTp.h" + "${_MS_SRC}/Console/commands/weather/CliCommandWeather.cpp" + "${_MS_SRC}/Console/commands/weather/CliCommandWeather.h" + "${_MS_SRC}/Console/commands/whitelist/CliCommandWhitelist.cpp" + "${_MS_SRC}/Console/commands/whitelist/CliCommandWhitelist.h" + "${_MS_SRC}/Console/commands/revoketoken/CliCommandRevokeToken.cpp" + "${_MS_SRC}/Console/commands/revoketoken/CliCommandRevokeToken.h" ) source_group("Server/Console/Commands" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_CONSOLE_COMMANDS}) set(_MINECRAFT_SERVER_COMMON_SERVER_VENDOR - "${CMAKE_CURRENT_SOURCE_DIR}/vendor/linenoise/linenoise.c" - "${CMAKE_CURRENT_SOURCE_DIR}/vendor/linenoise/linenoise.h" + "${_MS_SRC}/vendor/linenoise/linenoise.c" + "${_MS_SRC}/vendor/linenoise/linenoise.h" ) source_group("Server/Vendor" FILES ${_MINECRAFT_SERVER_COMMON_SERVER_VENDOR}) diff --git a/Minecraft.World/AbstractContainerMenu.cpp b/Minecraft.World/AbstractContainerMenu.cpp index 0b2abc23..696a538b 100644 --- a/Minecraft.World/AbstractContainerMenu.cpp +++ b/Minecraft.World/AbstractContainerMenu.cpp @@ -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,70 @@ shared_ptr 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 + { + carried->id = outId; + carried->count = outCount; + carried->setAuxValue(outAux); + player->drop(carried); + 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 + { + shared_ptr dropped = carried->remove(1); + if (carried->count == 0) inventory->setCarried(nullptr); + dropped->id = outId; + dropped->count = outCount; + dropped->setAuxValue(outAux); + player->drop(dropped); + 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 +537,35 @@ shared_ptr AbstractContainerMenu::clicked(int slotIndex, int butto Slot *slot = slots.at(slotIndex); if (slot != nullptr && slot->hasItem() && slot->mayPickup(player)) { - shared_ptr 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 item = slot->remove(dropCount); + slot->onTake(player, item); + item->id = outId; + item->count = outCount; + item->setAuxValue(outAux); + player->drop(item); + dropAllowed = false; + } + } + if (!dropAllowed) + return nullptr; + } +#endif + shared_ptr item = slot->remove(dropCount); slot->onTake(player, item); player->drop(item); } diff --git a/Minecraft.World/BeaconMenu.cpp b/Minecraft.World/BeaconMenu.cpp index 0d7e54b4..f88ded24 100644 --- a/Minecraft.World/BeaconMenu.cpp +++ b/Minecraft.World/BeaconMenu.cpp @@ -39,6 +39,29 @@ void BeaconMenu::addSlotListener(ContainerListener *listener) listener->setContainerData(this, 2, secondaryPower); } +void BeaconMenu::broadcastChanges() +{ + AbstractContainerMenu::broadcastChanges(); + + int currentLevels = beacon->getLevels(); + int currentPrimary = beacon->getPrimaryPower(); + int currentSecondary = beacon->getSecondaryPower(); + + for (auto& listener : containerListeners) + { + if (levels != currentLevels) + listener->setContainerData(this, 0, currentLevels); + if (primaryPower != currentPrimary) + listener->setContainerData(this, 1, currentPrimary); + if (secondaryPower != currentSecondary) + listener->setContainerData(this, 2, currentSecondary); + } + + levels = currentLevels; + primaryPower = currentPrimary; + secondaryPower = currentSecondary; +} + void BeaconMenu::setData(int id, int value) { if (id == 0) beacon->setLevels(value); diff --git a/Minecraft.World/BeaconMenu.h b/Minecraft.World/BeaconMenu.h index 9394d83f..f9f1a02b 100644 --- a/Minecraft.World/BeaconMenu.h +++ b/Minecraft.World/BeaconMenu.h @@ -36,7 +36,8 @@ private: public: BeaconMenu(shared_ptr inventory, shared_ptr beacon); - void addSlotListener(ContainerListener *listener); + void addSlotListener(ContainerListener *listener) override; + void broadcastChanges() override; void setData(int id, int value); shared_ptr getBeacon(); bool stillValid(shared_ptr player); diff --git a/Minecraft.World/CactusTile.cpp b/Minecraft.World/CactusTile.cpp index c306f11a..731e722e 100644 --- a/Minecraft.World/CactusTile.cpp +++ b/Minecraft.World/CactusTile.cpp @@ -7,6 +7,10 @@ #include "net.minecraft.h" #include "net.minecraft.world.h" #include "CactusTile.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif CactusTile::CactusTile(int id) : Tile(id, Material::cactus,isSolidRender()) { @@ -29,6 +33,10 @@ void CactusTile::tick(Level *level, int x, int y, int z, Random *random) int age = level->getData(x, y, z); if (age == 15) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireBlockGrow(level->dimension->id, x, y + 1, z, id, 0)) + return; +#endif level->setTileAndUpdate(x, y + 1, z, id); level->setData(x, y, z, 0, Tile::UPDATE_NONE); neighborChanged(level, x, y + 1, z, id); diff --git a/Minecraft.World/CocoaTile.cpp b/Minecraft.World/CocoaTile.cpp index 3df58eb8..2a2fbd21 100644 --- a/Minecraft.World/CocoaTile.cpp +++ b/Minecraft.World/CocoaTile.cpp @@ -5,6 +5,10 @@ #include "net.minecraft.world.h" #include "net.minecraft.h" #include "CocoaTile.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif const wstring CocoaTile::TEXTURE_AGES[] = { L"cocoa_0", L"cocoa_1", L"cocoa_2"}; @@ -40,6 +44,10 @@ void CocoaTile::tick(Level *level, int x, int y, int z, Random *random) int age = getAge(data); if (age < 2) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireBlockGrow(level->dimension->id, x, y, z, level->getTile(x, y, z), ((age + 1) << 2) | getDirection(data))) + return; +#endif age++; level->setData(x, y, z, (age << 2) | (getDirection(data)), Tile::UPDATE_CLIENTS); } diff --git a/Minecraft.World/CompoundContainer.h b/Minecraft.World/CompoundContainer.h index 906a0618..8eff0555 100644 --- a/Minecraft.World/CompoundContainer.h +++ b/Minecraft.World/CompoundContainer.h @@ -31,4 +31,7 @@ public: virtual void startOpen(); virtual void stopOpen(); virtual bool canPlaceItem(int slot, shared_ptr item); + + shared_ptr getFirstContainer() const { return c1; } + shared_ptr getSecondContainer() const { return c2; } }; \ No newline at end of file diff --git a/Minecraft.World/Connection.cpp b/Minecraft.World/Connection.cpp index 19a033d9..b3e87b43 100644 --- a/Minecraft.World/Connection.cpp +++ b/Minecraft.World/Connection.cpp @@ -1,3 +1,4 @@ +#include "Connection.h" #include "stdafx.h" #include "InputOutputStream.h" #include "Socket.h" @@ -32,6 +33,7 @@ void Connection::_init() disconnectReason = DisconnectPacket::eDisconnect_None; noInputTicks = 0; estimatedRemaining = 0; + estimatedRemainingRaw = 0; fakeLag = 0; slowWriteDelay = 50; @@ -145,6 +147,23 @@ void Connection::setListener(PacketListener *packetListener) this->packetListener = packetListener; } +void Connection::send(unsigned char* buffer, int size) +{ + if (quitting) return; + + MemSect(15); + // 4J Jev, synchronized (&writeLock) + EnterCriticalSection(&writeLock); + + estimatedRemainingRaw += size; + + outgoingRaw.push(std::make_pair(buffer, size)); + + // 4J Jev, end synchronized. + LeaveCriticalSection(&writeLock); + MemSect(0); +} + void Connection::send(shared_ptr packet) { if (quitting) return; @@ -232,6 +251,32 @@ bool Connection::writeTick() didSomething = true; } + if (!outgoingRaw.empty()) + { + std::pair rawPacket; + EnterCriticalSection(&writeLock); + + rawPacket = outgoingRaw.front(); + outgoingRaw.pop(); + estimatedRemainingRaw -= rawPacket.second; + + LeaveCriticalSection(&writeLock); + + for (int i = 0; i < rawPacket.second; i++) { + byteArrayDos->writeByte(rawPacket.first[i]); + } + + // 4J Stu - Changed this so that rather than writing to the network stream through a buffered stream we want to: + // a) Only push whole "game" packets to QNet, rather than amalgamated chunks of data that may include many packets, and partial packets + // b) To be able to change the priority and queue of a packet if required + //sos->writeWithFlags( baos->buf, 0, baos->size(), 0 ); + //baos->reset(); + + int value = rawPacket.first[0]; + writeSizes[value] += rawPacket.second; + didSomething = true; + } + if ((slowWriteDelay-- <= 0) && !outgoing_slow.empty() && (fakeLag == 0 || System::currentTimeMillis() - outgoing_slow.front()->createTime >= fakeLag)) { shared_ptr packet; diff --git a/Minecraft.World/Connection.h b/Minecraft.World/Connection.h index 9db00512..d745932e 100644 --- a/Minecraft.World/Connection.h +++ b/Minecraft.World/Connection.h @@ -54,6 +54,7 @@ private: queue > incoming; // 4J - was using synchronizedList... CRITICAL_SECTION incoming_cs; // ... now has this critical section + queue> outgoingRaw; // 4J - was using synchronizedList - but don't think it is required as usage is wrapped in writeLock critical section queue > outgoing; // 4J - was using synchronizedList - but don't think it is required as usage is wrapped in writeLock critical section queue > outgoing_slow; // 4J - was using synchronizedList - but don't think it is required as usage is wrapped in writeLock critical section @@ -76,6 +77,7 @@ private: int noInputTicks; int estimatedRemaining; + int estimatedRemainingRaw; int tickCount; // 4J Added @@ -99,6 +101,7 @@ public: void setListener(PacketListener *packetListener); void send(shared_ptr packet); + void send(unsigned char *buffer, int size); public: void queueSend(shared_ptr packet); diff --git a/Minecraft.World/CropTile.cpp b/Minecraft.World/CropTile.cpp index 1a161199..9c2acf78 100644 --- a/Minecraft.World/CropTile.cpp +++ b/Minecraft.World/CropTile.cpp @@ -4,6 +4,10 @@ #include "net.minecraft.world.item.h" #include "net.minecraft.world.h" #include "CropTile.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif CropTile::CropTile(int id) : Bush(id) { @@ -42,6 +46,10 @@ void CropTile::tick(Level *level, int x, int y, int z, Random *random) if (random->nextInt(static_cast(25 / growthSpeed) + 1) == 0) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireBlockGrow(level->dimension->id, x, y, z, level->getTile(x, y, z), age + 1)) + return; +#endif age++; level->setData(x, y, z, age, Tile::UPDATE_CLIENTS); } diff --git a/Minecraft.World/DyePowderItem.cpp b/Minecraft.World/DyePowderItem.cpp index c0eaf1ae..c857d782 100644 --- a/Minecraft.World/DyePowderItem.cpp +++ b/Minecraft.World/DyePowderItem.cpp @@ -127,8 +127,11 @@ bool DyePowderItem::useOn(shared_ptr itemInstance, shared_ptrgetAuxValue() == WHITE) { // bone meal is a fertilizer, so instantly grow trees and stuff - +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (growCrop(itemInstance, level, x, y, z, bTestUseOnOnly, player->entityId)) +#else if (growCrop(itemInstance, level, x, y, z, bTestUseOnOnly)) +#endif { if (!level->isClientSide) level->levelEvent(LevelEvent::PARTICLES_PLANT_GROWTH, x, y, z, 0); return true; @@ -167,8 +170,7 @@ bool DyePowderItem::useOn(shared_ptr itemInstance, shared_ptr itemInstance, Level *level, int x, int y, int z, bool bTestUseOnOnly) +bool DyePowderItem::growCrop(shared_ptr itemInstance, Level *level, int x, int y, int z, bool bTestUseOnOnly, int entityId) { int tile = level->getTile(x, y, z); if (tile == Tile::sapling_Id) @@ -177,7 +179,11 @@ bool DyePowderItem::growCrop(shared_ptr itemInstance, Level *level { if (!level->isClientSide) { - if (level->random->nextFloat() < 0.45) static_cast(Tile::sapling)->advanceTree(level, x, y, z, level->random); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (level->random->nextFloat() < 0.45) static_cast(Tile::sapling)->advanceTree(level, x, y, z, level->random, false, entityId); +#else + if (level->random->nextFloat() < 0.45) static_cast(Tile::sapling)->advanceTree(level, x, y, z, level->random); +#endif itemInstance->count--; } } @@ -192,7 +198,11 @@ bool DyePowderItem::growCrop(shared_ptr itemInstance, Level *level { if (!level->isClientSide) { - if (level->random->nextFloat() < 0.4) static_cast(Tile::tiles[tile])->growTree(level, x, y, z, level->random); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (level->random->nextFloat() < 0.4) static_cast(Tile::tiles[tile])->growTree(level, x, y, z, level->random, false, entityId); +#else + if (level->random->nextFloat() < 0.4) static_cast(Tile::tiles[tile])->growTree(level, x, y, z, level->random); +#endif itemInstance->count--; } } diff --git a/Minecraft.World/DyePowderItem.h b/Minecraft.World/DyePowderItem.h index 270b5424..5f41479b 100644 --- a/Minecraft.World/DyePowderItem.h +++ b/Minecraft.World/DyePowderItem.h @@ -43,7 +43,7 @@ public: virtual unsigned int getDescriptionId(shared_ptr itemInstance); virtual unsigned int getUseDescriptionId(shared_ptr itemInstance); virtual bool useOn(shared_ptr itemInstance, shared_ptr player, Level *level, int x, int y, int z, int face, float clickX, float clickY, float clickZ, bool bTestUseOnOnly=false); - static bool growCrop(shared_ptr itemInstance, Level *level, int x, int y, int z, bool bTestUseOnOnly); + static bool growCrop(shared_ptr itemInstance, Level* level, int x, int y, int z, bool bTestUseOnOnly, int entityid = -1); static void addGrowthParticles(Level *level, int x, int y, int z, int count); virtual bool interactEnemy(shared_ptr itemInstance, shared_ptr player, shared_ptr mob); diff --git a/Minecraft.World/EggTile.cpp b/Minecraft.World/EggTile.cpp index 7052fac9..16c9aa0b 100644 --- a/Minecraft.World/EggTile.cpp +++ b/Minecraft.World/EggTile.cpp @@ -1,8 +1,12 @@ -#include "stdafx.h" + #include "stdafx.h" #include "EggTile.h" #include "net.minecraft.world.level.h" #include "net.minecraft.world.level.tile.h" #include "net.minecraft.world.entity.item.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif EggTile::EggTile(int id) : Tile(id, Material::egg, isSolidRender()) { @@ -70,11 +74,15 @@ void EggTile::teleport(Level *level, int x, int y, int z) int zt = z + level->random->nextInt(16) - level->random->nextInt(16); if (level->getTile(xt, yt, zt) == 0) { - // Fix for TU9: Content: Art: Dragon egg teleport particle effect isn't present. - // Don't set tiles on client, and don't create particles on the server (matches later change in Java) - if(!level->isClientSide) - { - level->setTileAndData(xt, yt, zt, id, level->getData(x, y, z), Tile::UPDATE_CLIENTS); + // Fix for TU9: Content: Art: Dragon egg teleport particle effect isn't present. + // Don't set tiles on client, and don't create particles on the server (matches later change in Java) + if(!level->isClientSide) + { + #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireBlockFromTo(level->dimension->id, x, y, z, xt, yt, zt, 6 /*SELF*/)) + continue; + #endif + level->setTileAndData(xt, yt, zt, id, level->getData(x, y, z), Tile::UPDATE_CLIENTS); level->removeTile(x, y, z); // 4J Stu - The PC version is wrong as the particles calculated on the client side will point towards a different diff --git a/Minecraft.World/FireTile.cpp b/Minecraft.World/FireTile.cpp index e63b54e1..f8e2fe4e 100644 --- a/Minecraft.World/FireTile.cpp +++ b/Minecraft.World/FireTile.cpp @@ -7,6 +7,9 @@ #include "SoundTypes.h" #include "../Minecraft.Client/MinecraftServer.h" #include "../Minecraft.Client/PlayerList.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#endif // AP - added for Vita to set Alpha Cut out #include "IntBuffer.h" @@ -199,11 +202,16 @@ void FireTile::tick(Level *level, int x, int y, int z, Random *random) } if (odds > 0 && random->nextInt(rate) <= odds) { - if (!(level->isRaining() && level->isRainingAt(xx, yy, zz) || level->isRainingAt(xx - 1, yy, z) || level->isRainingAt(xx + 1, yy, zz) || level->isRainingAt(xx, yy, zz - 1) || level->isRainingAt(xx, yy, zz + 1))) - { - int tAge = age + random->nextInt(5) / 4; - if (tAge > 15) tAge = 15; - level->setTileAndData(xx, yy, zz, id, tAge, Tile::UPDATE_ALL); + if (!(level->isRaining() && level->isRainingAt(xx, yy, zz) || level->isRainingAt(xx - 1, yy, z) || level->isRainingAt(xx + 1, yy, zz) || level->isRainingAt(xx, yy, zz - 1) || level->isRainingAt(xx, yy, zz + 1))) + { + #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (!FourKitBridge::FireBlockSpread(level->dimension->id, xx, yy, zz, x, y, z, id, min(age + random->nextInt(5) / 4, 15))) + #endif + { + int tAge = age + random->nextInt(5) / 4; + if (tAge > 15) tAge = 15; + level->setTileAndData(xx, yy, zz, id, tAge, Tile::UPDATE_ALL); + } } } } @@ -223,6 +231,10 @@ void FireTile::checkBurnOut(Level *level, int x, int y, int z, int chance, Rando int odds = burnOdds[level->getTile(x, y, z)]; if (random->nextInt(chance) < odds) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireBlockBurn(level->dimension->id, x, y, z)) + return; +#endif bool wasTnt = level->getTile(x, y, z) == Tile::tnt_Id; if (random->nextInt(age + 10) < 5 && !level->isRainingAt(x, y, z) && app.GetGameHostOption(eGameHostOption_FireSpreads)) { diff --git a/Minecraft.World/GrassTile.cpp b/Minecraft.World/GrassTile.cpp index 5b8657eb..4411db9f 100644 --- a/Minecraft.World/GrassTile.cpp +++ b/Minecraft.World/GrassTile.cpp @@ -5,6 +5,10 @@ #include "net.minecraft.world.level.biome.h" #include "net.minecraft.h" #include "net.minecraft.world.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif // AP - included for PSVita Alpha cut out optimisation #include "IntBuffer.h" @@ -108,6 +112,9 @@ void GrassTile::tick(Level *level, int x, int y, int z, Random *random) int above = level->getTile(xt, yt + 1, zt); if (level->getTile(xt, yt, zt) == Tile::dirt_Id && level->getData(xt, yt, zt) == 0 && level->getRawBrightness(xt, yt + 1, zt) >= MIN_BRIGHTNESS && Tile::lightBlock[above] <= 2) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (!FourKitBridge::FireBlockSpread(level->dimension->id, xt, yt, zt, x, y, z, Tile::grass_Id, 0)) +#endif level->setTileAndUpdate(xt, yt, zt, Tile::grass_Id); } } diff --git a/Minecraft.World/ItemEntity.cpp b/Minecraft.World/ItemEntity.cpp index 5dc1db6e..e5f993db 100644 --- a/Minecraft.World/ItemEntity.cpp +++ b/Minecraft.World/ItemEntity.cpp @@ -12,6 +12,9 @@ #include "com.mojang.nbt.h" #include "ItemEntity.h" #include "SoundTypes.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#endif @@ -232,8 +235,27 @@ void ItemEntity::playerTouch(shared_ptr player) } int orgCount = item->count; - if (throwTime == 0 && player->inventory->add(item)) + if (throwTime == 0) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + int outItemId = item->id, outItemCount = orgCount, outItemAux = item->getAuxValue(); + if (FourKitBridge::FirePlayerPickupItem( + player->entityId, entityId, player->dimension, + x, y, z, + item->id, orgCount, item->getAuxValue(), 0, + &outItemId, &outItemCount, &outItemAux)) + return; + if (outItemId != item->id || outItemCount != orgCount || outItemAux != item->getAuxValue()) + { + item = std::make_shared(outItemId, outItemCount, outItemAux); + setItem(item); + orgCount = outItemCount; + } + } +#endif + if (player->inventory->add(item)) + { //if (item.id == Tile.treeTrunk.id) player.awardStat(Achievements.mineWood); //if (item.id == Item.leather.id) player.awardStat(Achievements.killCow); //if (item.id == Item.diamond.id) player.awardStat(Achievements.diamonds); @@ -260,6 +282,7 @@ void ItemEntity::playerTouch(shared_ptr player) player->take(shared_from_this(), orgCount); // System.out.println(item.count + ", " + orgCount); if (item->count <= 0) remove(); + } } } diff --git a/Minecraft.World/LiquidTileDynamic.cpp b/Minecraft.World/LiquidTileDynamic.cpp index 2c7c74f4..43d6366c 100644 --- a/Minecraft.World/LiquidTileDynamic.cpp +++ b/Minecraft.World/LiquidTileDynamic.cpp @@ -2,6 +2,10 @@ #include "net.minecraft.world.level.h" #include "LiquidTileDynamic.h" #include "net.minecraft.world.level.dimension.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif LiquidTileDynamic::LiquidTileDynamic(int id, Material *material) : LiquidTile(id, material) { @@ -156,8 +160,8 @@ void LiquidTileDynamic::mainTick(Level *level, int x, int y, int z, Random *rand } } - if (depth >= 8) trySpreadTo(level, x, y - 1, z, depth); - else trySpreadTo(level, x, y - 1, z, depth + 8); + if (depth >= 8) trySpreadTo(level, x, y - 1, z, x, y, z, depth); + else trySpreadTo(level, x, y - 1, z, x, y, z, depth + 8); } else if (depth >= 0 && (depth == 0 || isWaterBlocking(level, x, y - 1, z))) { @@ -168,17 +172,31 @@ void LiquidTileDynamic::mainTick(Level *level, int x, int y, int z, Random *rand neighbor = 1; } if (neighbor >= 8) return; - if (spreads[0]) trySpreadTo(level, x - 1, y, z, neighbor); - if (spreads[1]) trySpreadTo(level, x + 1, y, z, neighbor); - if (spreads[2]) trySpreadTo(level, x, y, z - 1, neighbor); - if (spreads[3]) trySpreadTo(level, x, y, z + 1, neighbor); + if (spreads[0]) trySpreadTo(level, x - 1, y, z, x, y, z, neighbor); + if (spreads[1]) trySpreadTo(level, x + 1, y, z, x, y, z, neighbor); + if (spreads[2]) trySpreadTo(level, x, y, z - 1, x, y, z, neighbor); + if (spreads[3]) trySpreadTo(level, x, y, z + 1, x, y, z, neighbor); } } -void LiquidTileDynamic::trySpreadTo(Level *level, int x, int y, int z, int neighbor) +void LiquidTileDynamic::trySpreadTo(Level *level, int x, int y, int z, int fromX, int fromY, int fromZ, int neighbor) { if (canSpreadTo(level, x, y, z)) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + int face = 6; // SELF + int dx = x - fromX, dy = y - fromY, dz = z - fromZ; + if (dy < 0) face = 0; // DOWN + else if (dy > 0) face = 1; // UP + else if (dz < 0) face = 2; // NORTH + else if (dz > 0) face = 3; // SOUTH + else if (dx < 0) face = 4; // WEST + else if (dx > 0) face = 5; // EAST + if (FourKitBridge::FireBlockFromTo(level->dimension->id, fromX, fromY, fromZ, x, y, z, face)) + return; + } +#endif { int old = level->getTile(x, y, z); if (old > 0) diff --git a/Minecraft.World/LiquidTileDynamic.h b/Minecraft.World/LiquidTileDynamic.h index bf0c9a6d..3d6d97c2 100644 --- a/Minecraft.World/LiquidTileDynamic.h +++ b/Minecraft.World/LiquidTileDynamic.h @@ -36,7 +36,7 @@ private: public: void tick(Level *level, int x, int y, int z, Random *random); private: - void trySpreadTo(Level *level, int x, int y, int z, int neighbor); + void trySpreadTo(Level *level, int x, int y, int z, int fromX, int fromY, int fromZ, int neighbor); bool *result; int *dist; diff --git a/Minecraft.World/LivingEntity.cpp b/Minecraft.World/LivingEntity.cpp index a7e8dc1d..1113bace 100644 --- a/Minecraft.World/LivingEntity.cpp +++ b/Minecraft.World/LivingEntity.cpp @@ -30,8 +30,12 @@ #include "SoundTypes.h" #include "BasicTypeContainers.h" #include "ParticleTypes.h" +#include "Dimension.h" #include "GenericStats.h" #include "ItemEntity.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#endif const double LivingEntity::MIN_MOVEMENT_DISTANCE = 0.005; @@ -76,6 +80,9 @@ void LivingEntity::_init() deathScore = 0; lastHurt = 0.0f; jumping = false; + + fourKitDeathExp = 0; + fourKitDeathExpSet = false; xxa = 0.0f; yya = 0.0f; @@ -297,7 +304,11 @@ void LivingEntity::tickDeath() { if (!isBaby() && level->getGameRules()->getBoolean(GameRules::RULE_DOMOBLOOT)) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int xpCount = fourKitDeathExpSet ? fourKitDeathExp : this->getExperienceReward(lastHurtByPlayer); +#else int xpCount = this->getExperienceReward(lastHurtByPlayer); +#endif while (xpCount > 0) { int newCount = ExperienceOrb::getExperienceValue(xpCount); @@ -769,6 +780,38 @@ bool LivingEntity::hurt(DamageSource *source, float dmg) noActionTime = 0; if (getHealth() <= 0) return false; +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + // (SYLV)todo: map these properly + + int entityTypeId = FourKitBridge::MapEntityType((int)GetType()); + int dimId = level->dimension ? level->dimension->id : 0; + int causeId = FourKitBridge::MapDamageCause((void *)source); + double outDamage = (double)dmg; + + int damagerEntityId = -1; + int damagerEntityTypeId = 0; + double damagerX = 0, damagerY = 0, damagerZ = 0; + shared_ptr damagerEntity = source->getEntity(); + if (damagerEntity != nullptr) + { + damagerEntityId = damagerEntity->entityId; + damagerEntityTypeId = FourKitBridge::MapEntityType((int)damagerEntity->GetType()); + damagerX = damagerEntity->x; + damagerY = damagerEntity->y; + damagerZ = damagerEntity->z; + } + + bool cancelled = FourKitBridge::FireEntityDamage( + entityId, entityTypeId, dimId, + x, y, z, causeId, (double)dmg, &outDamage, + damagerEntityId, damagerEntityTypeId, damagerX, damagerY, damagerZ); + if (cancelled) + return false; + dmg = (float)outDamage; + } +#endif + if ( source->isFire() && hasEffect(MobEffect::fireResistance) ) { // 4J-JEV, for new achievement Stayin'Frosty, TODO merge with Java version. @@ -901,6 +944,18 @@ void LivingEntity::die(DamageSource *source) dead = true; +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (!level->isClientSide && !instanceof(eTYPE_SERVERPLAYER) && !instanceof(eTYPE_PLAYER)) + { + int entityTypeId = FourKitBridge::MapEntityType((int)GetType()); + int dimId = dimension; + int exp = getExperienceReward(lastHurtByPlayer); + int modifiedExp = FourKitBridge::FireEntityDeath(entityId, entityTypeId, dimId, x, y, z, exp); + fourKitDeathExp = modifiedExp; + fourKitDeathExpSet = true; + } +#endif + if (!level->isClientSide) { int playerBonus = 0; diff --git a/Minecraft.World/LivingEntity.h b/Minecraft.World/LivingEntity.h index ecc819df..4ca84126 100644 --- a/Minecraft.World/LivingEntity.h +++ b/Minecraft.World/LivingEntity.h @@ -92,6 +92,11 @@ protected: float lastHurt; bool jumping; + // DO NOT ADD IFDEF HERE!! + // causes memroy layout disagreement. Not good + int fourKitDeathExp; + bool fourKitDeathExpSet; + public: float xxa; float yya; diff --git a/Minecraft.World/Mushroom.cpp b/Minecraft.World/Mushroom.cpp index ca8a8554..91ba3f7a 100644 --- a/Minecraft.World/Mushroom.cpp +++ b/Minecraft.World/Mushroom.cpp @@ -2,8 +2,13 @@ #include "net.minecraft.world.level.h" #include "net.minecraft.world.level.levelgen.feature.h" #include "net.minecraft.world.h" +#include "Dimension.h" #include "Mushroom.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#endif + Mushroom::Mushroom(int id) : Bush(id) { this->updateDefaultShape(); @@ -48,6 +53,9 @@ void Mushroom::tick(Level *level, int x, int y, int z, Random *random) if (level->isEmptyTile(x2, y2, z2) && canSurvive(level, x2, y2, z2)) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (!FourKitBridge::FireBlockSpread(level->dimension->id, x2, y2, z2, x, y, z, id, 0)) +#endif level->setTileAndData(x2, y2, z2, id, 0, UPDATE_CLIENTS); } } @@ -71,8 +79,7 @@ bool Mushroom::canSurvive(Level *level, int x, int y, int z) return below == Tile::mycel_Id || (level->getDaytimeRawBrightness(x, y, z) < 13 && mayPlaceOn(below)); } - -bool Mushroom::growTree(Level *level, int x, int y, int z, Random *random) +bool Mushroom::growTree(Level *level, int x, int y, int z, Random *random, bool naturalGrowth, int entityId) { int data = level->getData(x, y, z); @@ -88,6 +95,13 @@ bool Mushroom::growTree(Level *level, int x, int y, int z, Random *random) f = new HugeMushroomFeature(1); } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireStructureGrow(level->dimension->id, x, y, z, (id == Tile::mushroom_brown_Id ? 6 : 7), !naturalGrowth, entityId)) { + if (f != nullptr) delete f; + f = nullptr; + } +#endif + if (f == nullptr || !f->place(level, random, x, y, z)) { level->setTileAndData(x, y, z, id, data, Tile::UPDATE_ALL); diff --git a/Minecraft.World/Mushroom.h b/Minecraft.World/Mushroom.h index 65a3ea1d..286cf4d3 100644 --- a/Minecraft.World/Mushroom.h +++ b/Minecraft.World/Mushroom.h @@ -16,5 +16,5 @@ protected: virtual bool mayPlaceOn(int tile); public: virtual bool canSurvive(Level *level, int x, int y, int z); - bool growTree(Level *level, int x, int y, int z, Random *random); + bool growTree(Level* level, int x, int y, int z, Random* random, bool naturalGrowth = true, int entityId = -1); }; diff --git a/Minecraft.World/NetherWartTile.cpp b/Minecraft.World/NetherWartTile.cpp index afe45aec..3c855040 100644 --- a/Minecraft.World/NetherWartTile.cpp +++ b/Minecraft.World/NetherWartTile.cpp @@ -4,6 +4,10 @@ #include "net.minecraft.world.level.biome.h" #include "net.minecraft.world.item.h" #include "net.minecraft.world.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif NetherWartTile::NetherWartTile(int id) : Bush(id) { @@ -36,6 +40,12 @@ void NetherWartTile::tick(Level *level, int x, int y, int z, Random *random) { if (random->nextInt(10) == 0) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireBlockGrow(level->dimension->id, x, y, z, level->getTile(x, y, z), age + 1)) + { + return; + } +#endif age++; level->setData(x, y, z, age, Tile::UPDATE_CLIENTS); } diff --git a/Minecraft.World/PistonBaseTile.cpp b/Minecraft.World/PistonBaseTile.cpp index 36651f7b..b5196471 100644 --- a/Minecraft.World/PistonBaseTile.cpp +++ b/Minecraft.World/PistonBaseTile.cpp @@ -10,6 +10,9 @@ #include "net.minecraft.world.h" #include "LevelChunk.h" #include "Dimension.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#endif const wstring PistonBaseTile::EDGE_TEX = L"piston_side"; const wstring PistonBaseTile::PLATFORM_TEX = L"piston_top"; @@ -230,6 +233,31 @@ bool PistonBaseTile::triggerEvent(Level *level, int x, int y, int z, int param1, if (param1 == TRIGGER_EXTEND) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + int pushLength = 0; + int cx = x + Facing::STEP_X[facing]; + int cy = y + Facing::STEP_Y[facing]; + int cz = z + Facing::STEP_Z[facing]; + for (int i = 0; i < MAX_PUSH_DEPTH + 1; i++) + { + int block = level->getTile(cx, cy, cz); + if (block == 0) break; + if (!isPushable(block, level, cx, cy, cz, true)) break; + if (Tile::tiles[block]->getPistonPushReaction() == Material::PUSH_DESTROY) { pushLength++; break; } + pushLength++; + if (i == MAX_PUSH_DEPTH) break; + cx += Facing::STEP_X[facing]; + cy += Facing::STEP_Y[facing]; + cz += Facing::STEP_Z[facing]; + } + if (FourKitBridge::FirePistonExtend(level->dimension->id, x, y, z, facing, pushLength)) + { + ignoreUpdate(false); + return false; + } + } +#endif PIXBeginNamedEvent(0,"Create push\n"); if (createPush(level, x, y, z, facing)) { @@ -256,6 +284,14 @@ bool PistonBaseTile::triggerEvent(Level *level, int x, int y, int z, int param1, } else if (param1 == TRIGGER_CONTRACT) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FirePistonRetract(level->dimension->id, x, y, z, facing)) + { + level->setData(x, y, z, facing | EXTENDED_BIT, UPDATE_CLIENTS); + ignoreUpdate(false); + return false; + } +#endif PIXBeginNamedEvent(0,"Contract phase A\n"); shared_ptr prevTileEntity = level->getTileEntity(x + Facing::STEP_X[facing], y + Facing::STEP_Y[facing], z + Facing::STEP_Z[facing]); if (prevTileEntity != nullptr && dynamic_pointer_cast(prevTileEntity) != nullptr) diff --git a/Minecraft.World/Player.cpp b/Minecraft.World/Player.cpp index c7099dbc..e4c561cd 100644 --- a/Minecraft.World/Player.cpp +++ b/Minecraft.World/Player.cpp @@ -37,10 +37,10 @@ #include "Player.h" #include "ParticleTypes.h" -#include "..\Minecraft.Client\Textures.h" +#include "../Minecraft.Client/Textures.h" -#include "..\Minecraft.Client\LocalPlayer.h" -#include "..\Minecraft.Client\HumanoidModel.h" +#include "../Minecraft.Client/LocalPlayer.h" +#include "../Minecraft.Client/HumanoidModel.h" #include "SoundTypes.h" @@ -2573,6 +2573,29 @@ void Player::restoreFrom(shared_ptr oldPlayer, bool restoreAll) setScore(oldPlayer->getScore()); portalEntranceDir = oldPlayer->portalEntranceDir; } +#if defined(MINECRAFT_SERVER_BUILD) + else if (oldPlayer->fk_hasDeathState) + { + if (oldPlayer->fk_deathKeepInventory || level->getGameRules()->getBoolean(GameRules::RULE_KEEPINVENTORY)) + { + inventory->replaceWith(oldPlayer->inventory); + } + if (oldPlayer->fk_deathKeepLevel || level->getGameRules()->getBoolean(GameRules::RULE_KEEPINVENTORY)) + { + experienceLevel = oldPlayer->experienceLevel; + totalExperience = oldPlayer->totalExperience; + experienceProgress = oldPlayer->experienceProgress; + } + else + { + experienceLevel = oldPlayer->fk_deathNewLevel; + totalExperience = 0; + int xpNeeded = getXpNeededForNextLevel(); + experienceProgress = (xpNeeded > 0) ? (float)oldPlayer->fk_deathNewExp / (float)xpNeeded : 0.0f; + } + setScore(oldPlayer->getScore()); + } +#endif else if (level->getGameRules()->getBoolean(GameRules::RULE_KEEPINVENTORY)) { inventory->replaceWith(oldPlayer->inventory); diff --git a/Minecraft.World/Player.h b/Minecraft.World/Player.h index d8854fa4..520fc3aa 100644 --- a/Minecraft.World/Player.h +++ b/Minecraft.World/Player.h @@ -121,6 +121,13 @@ public: int experienceLevel, totalExperience; float experienceProgress; + bool fk_hasDeathState = false; + bool fk_deathKeepInventory = false; + bool fk_deathKeepLevel = false; + int fk_deathNewExp = 0; + int fk_deathNewLevel = 0; + bool fk_sleepingIgnored = false; + // 4J Stu - Made protected so that we can access it from MultiPlayerLocalPlayer protected: shared_ptr useItem; diff --git a/Minecraft.World/ReedTile.cpp b/Minecraft.World/ReedTile.cpp index dc60bfba..b67c4874 100644 --- a/Minecraft.World/ReedTile.cpp +++ b/Minecraft.World/ReedTile.cpp @@ -5,6 +5,10 @@ #include "net.minecraft.world.level.material.h" #include "net.minecraft.world.phys.h" #include "ReedTile.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif ReedTile::ReedTile(int id) : Tile( id, Material::plant,isSolidRender() ) { @@ -33,6 +37,10 @@ void ReedTile::tick(Level *level, int x, int y, int z, Random* random) int age = level->getData(x, y, z); if (age == 15) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (FourKitBridge::FireBlockGrow(level->dimension->id, x, y + 1, z, id, 0)) + return; +#endif level->setTileAndUpdate(x, y + 1, z, id); level->setData(x, y, z, 0, Tile::UPDATE_NONE); } diff --git a/Minecraft.World/Sapling.cpp b/Minecraft.World/Sapling.cpp index 526ee2f2..1fb1dcb0 100644 --- a/Minecraft.World/Sapling.cpp +++ b/Minecraft.World/Sapling.cpp @@ -3,37 +3,41 @@ #include "net.minecraft.world.level.tile.h" #include "net.minecraft.world.level.levelgen.feature.h" #include "net.minecraft.world.h" +#include "Dimension.h" #include "Sapling.h" #include "SavannaTreeFeature.h" #include "RoofTreeFeature.h" #include "MegaPineTreeFeature.h" -int Sapling::SAPLING_NAMES[SAPLING_NAMES_SIZE] = { +#if defined(_WINDOWS64) +#include "../Minecraft.Server/FourKitBridge.h" +#endif + +int Sapling::SAPLING_NAMES[SAPLING_NAMES_SIZE] = { IDS_TILE_SAPLING_OAK, IDS_TILE_SAPLING_SPRUCE, IDS_TILE_SAPLING_BIRCH, IDS_TILE_SAPLING_JUNGLE, - IDS_TILE_SAPLING_ACACIA, - IDS_TILE_SAPLING_DARK_OAK + IDS_TILE_SAPLING_ACACIA, + IDS_TILE_SAPLING_DARK_OAK }; const wstring Sapling::TEXTURE_NAMES[] = { - L"sapling", - L"sapling_spruce", - L"sapling_birch", - L"sapling_jungle", - L"sapling_acacia", + L"sapling", + L"sapling_spruce", + L"sapling_birch", + L"sapling_jungle", + L"sapling_acacia", L"sapling_dark_oak" }; -Sapling::Sapling(int id) : Bush( id ) +Sapling::Sapling(int id) : Bush(id) { this->updateDefaultShape(); icons = nullptr; } -// 4J Added override void Sapling::updateDefaultShape() { float ss = 0.4f; @@ -50,7 +54,7 @@ void Sapling::tick(Level *level, int x, int y, int z, Random *random) { if (random->nextInt(7) == 0) { - advanceTree(level, x, y, z, random); + advanceTree(level, x, y, z, random, true, -1); } } } @@ -62,25 +66,25 @@ Icon *Sapling::getTexture(int face, int data) return icons[data]; } -void Sapling::advanceTree(Level *level, int x, int y, int z, Random *random) +void Sapling::advanceTree(Level* level, int x, int y, int z, Random* random, bool naturalGrowth, int entityId) { int data = level->getData(x, y, z); + if ((data & AGE_BIT) == 0) { level->setData(x, y, z, data | AGE_BIT, Tile::UPDATE_NONE); } else { - growTree(level, x, y, z, random); + growTree(level, x, y, z, random, naturalGrowth, entityId); } } -void Sapling::growTree(Level *level, int x, int y, int z, Random *random) +void Sapling::growTree(Level* level, int x, int y, int z, Random* random, bool naturalGrowth, int entityId) { int data = level->getData(x, y, z) & TYPE_MASK; Feature *f = nullptr; - int ox = 0, oz = 0; bool multiblock = false; @@ -90,9 +94,9 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) { for (oz = 0; oz >= -1; oz--) { - if (isSapling(level, x + ox, y, z + oz, TYPE_EVERGREEN) && - isSapling(level, x + ox + 1, y, z + oz, TYPE_EVERGREEN) && - isSapling(level, x + ox, y, z + oz + 1, TYPE_EVERGREEN) && + if (isSapling(level, x + ox, y, z + oz, TYPE_EVERGREEN) && + isSapling(level, x + ox + 1, y, z + oz, TYPE_EVERGREEN) && + isSapling(level, x + ox, y, z + oz + 1, TYPE_EVERGREEN) && isSapling(level, x + ox + 1, y, z + oz + 1, TYPE_EVERGREEN)) { f = new MegaPineTreeFeature(true, random->nextBoolean()); @@ -100,9 +104,10 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) break; } } - if (f != nullptr) break; + if (f) break; } - if (f == nullptr) + + if (!f) { ox = oz = 0; f = new SpruceFeature(true); @@ -118,9 +123,9 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) { for (oz = 0; oz >= -1; oz--) { - if (isSapling(level, x + ox, y, z + oz, TYPE_JUNGLE) && - isSapling(level, x + ox + 1, y, z + oz, TYPE_JUNGLE) && - isSapling(level, x + ox, y, z + oz + 1, TYPE_JUNGLE) && + if (isSapling(level, x + ox, y, z + oz, TYPE_JUNGLE) && + isSapling(level, x + ox + 1, y, z + oz, TYPE_JUNGLE) && + isSapling(level, x + ox, y, z + oz + 1, TYPE_JUNGLE) && isSapling(level, x + ox + 1, y, z + oz + 1, TYPE_JUNGLE)) { f = new MegaTreeFeature(true, 10 + random->nextInt(20), TreeTile::JUNGLE_TRUNK, LeafTile::JUNGLE_LEAF); @@ -128,12 +133,10 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) break; } } - if (f != nullptr) - { - break; - } + if (f) break; } - if (f == nullptr) + + if (!f) { ox = oz = 0; f = new TreeFeature(true, 4 + random->nextInt(7), TreeTile::JUNGLE_TRUNK, LeafTile::JUNGLE_LEAF, false); @@ -141,7 +144,7 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) } else if (data == TYPE_ACACIA) { - f = new SavannaTreeFeature(true); + f = new SavannaTreeFeature(true); } else if (data == TYPE_DARK_OAK) { @@ -149,9 +152,9 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) { for (oz = 0; oz >= -1; oz--) { - if (isSapling(level, x + ox, y, z + oz, TYPE_DARK_OAK) && - isSapling(level, x + ox + 1, y, z + oz, TYPE_DARK_OAK) && - isSapling(level, x + ox, y, z + oz + 1, TYPE_DARK_OAK) && + if (isSapling(level, x + ox, y, z + oz, TYPE_DARK_OAK) && + isSapling(level, x + ox + 1, y, z + oz, TYPE_DARK_OAK) && + isSapling(level, x + ox, y, z + oz + 1, TYPE_DARK_OAK) && isSapling(level, x + ox + 1, y, z + oz + 1, TYPE_DARK_OAK)) { f = new RoofTreeFeature(true); @@ -159,10 +162,10 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) break; } } - if (f != nullptr) break; + if (f) break; } - - if (f == nullptr) return; + + if (!f) return; } else { @@ -174,6 +177,20 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) } } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int TreeType = data; + if (data != TYPE_EVERGREEN && data != TYPE_BIRCH && data != TYPE_JUNGLE) + { + TreeType = (dynamic_cast(f) != nullptr) ? 4 : 5; + } + + if (FourKitBridge::FireStructureGrow(level->dimension->id, x, y, z, TreeType, !naturalGrowth, entityId)) + { + delete f; + f = nullptr; + } +#endif + if (multiblock) { level->setTileAndData(x + ox, y, z + oz, 0, 0, Tile::UPDATE_NONE); @@ -186,7 +203,7 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) level->setTileAndData(x, y, z, 0, 0, Tile::UPDATE_NONE); } - if (!f->place(level, random, x + ox, y, z + oz)) + if (f == nullptr || !f->place(level, random, x + ox, y, z + oz)) { if (multiblock) { @@ -200,39 +217,12 @@ void Sapling::growTree(Level *level, int x, int y, int z, Random *random) level->setTileAndData(x, y, z, id, data, Tile::UPDATE_NONE); } } - - if( f != nullptr ) - delete f; + + if (f) delete f; } -unsigned int Sapling::getDescriptionId(int iData ) +unsigned int Sapling::getDescriptionId(int iData) { - if(iData < 0 || iData >= SAPLING_NAMES_SIZE) iData = 0; + if (iData < 0 || iData >= SAPLING_NAMES_SIZE) iData = 0; return Sapling::SAPLING_NAMES[iData]; -} - -int Sapling::getSpawnResourcesAuxValue(int data) -{ - return data & TYPE_MASK; -} - -bool Sapling::isSapling(Level *level, int x, int y, int z, int type) -{ - return (level->getTile(x, y, z) == id) && ((level->getData(x, y, z) & TYPE_MASK) == type); -} - -bool Sapling::fertilize(Level *level, int x, int y, int z) -{ - this->advanceTree(level, x, y, z, level->random); - return true; -} - -void Sapling::registerIcons(IconRegister *iconRegister) -{ - icons = new Icon*[SAPLING_NAMES_SIZE]; - - for (int i = 0; i < SAPLING_NAMES_SIZE; i++) - { - icons[i] = iconRegister->registerIcon(TEXTURE_NAMES[i]); - } } \ No newline at end of file diff --git a/Minecraft.World/Sapling.h b/Minecraft.World/Sapling.h index 9ebd37e6..7d1e287b 100644 --- a/Minecraft.World/Sapling.h +++ b/Minecraft.World/Sapling.h @@ -9,16 +9,17 @@ class ChunkRebuildData; class Tile; class Sapling : public Bush -{ +{ friend class Tile; friend class ChunkRebuildData; + public: - static const int TYPE_DEFAULT = LeafTile::NORMAL_LEAF; + static const int TYPE_DEFAULT = LeafTile::NORMAL_LEAF; static const int TYPE_EVERGREEN = LeafTile::EVERGREEN_LEAF; - static const int TYPE_BIRCH = LeafTile::BIRCH_LEAF; - static const int TYPE_JUNGLE = LeafTile::JUNGLE_LEAF; - static const int TYPE_ACACIA = LeafTile2::ACACIA_LEAF+4; - static const int TYPE_DARK_OAK = LeafTile2::DARK_OAK_LEAF+4; + static const int TYPE_BIRCH = LeafTile::BIRCH_LEAF; + static const int TYPE_JUNGLE = LeafTile::JUNGLE_LEAF; + static const int TYPE_ACACIA = LeafTile2::ACACIA_LEAF + 4; + static const int TYPE_DARK_OAK = LeafTile2::DARK_OAK_LEAF + 4; static const int SAPLING_NAMES_SIZE = 6; @@ -29,8 +30,8 @@ private: Icon **icons; - static const int TYPE_MASK = 7; - static const int AGE_BIT = 8; + static const int TYPE_MASK = 7; + static const int AGE_BIT = 8; protected: Sapling(int id); @@ -38,15 +39,17 @@ protected: public: virtual void updateDefaultShape(); // 4J Added override virtual void tick(Level *level, int x, int y, int z, Random *random); - + virtual Icon *getTexture(int face, int data); - virtual void advanceTree(Level *level, int x, int y, int z, Random *random); - void growTree(Level *level, int x, int y, int z, Random *random); + virtual void advanceTree(Level *level, int x, int y, int z, Random *random, + bool naturalGrowth = true, int entityId = -1); + void growTree(Level *level, int x, int y, int z, Random *random, + bool naturalGrowth = true, int entityId = -1); virtual unsigned int getDescriptionId(int iData = -1); bool isSapling(Level *level, int x, int y, int z, int type); - virtual bool fertilize(Level *level, int x, int y, int z); + virtual bool fertilize(Level *level, int x, int y, int z); protected: int getSpawnResourcesAuxValue(int data); diff --git a/Minecraft.World/Socket.cpp b/Minecraft.World/Socket.cpp index f6a780c3..f07e3b2b 100644 --- a/Minecraft.World/Socket.cpp +++ b/Minecraft.World/Socket.cpp @@ -506,7 +506,6 @@ void Socket::SocketOutputStreamNetwork::writeWithFlags(byteArray b, unsigned int INetworkPlayer *socketPlayer = m_socket->getPlayer(); if(socketPlayer == nullptr) { - app.DebugPrintf("Trying to write to network, but the socketPlayer is nullptr\n"); return; } @@ -518,30 +517,20 @@ void Socket::SocketOutputStreamNetwork::writeWithFlags(byteArray b, unsigned int bool requireAck = ( ( flags & NON_QNET_SENDDATA_ACK_REQUIRED ) == NON_QNET_SENDDATA_ACK_REQUIRED ); #endif + // Re-validate the socket player immediately before use to minimize + // the TOCTOU window where the network layer could remove the player + // between our initial null-check and the SendData call. if( m_queueIdx == SOCKET_SERVER_END ) { - //printf( "Sent %u bytes of data from \"%ls\" to \"%ls\"\n", - //buffer.dwDataSize, - //hostPlayer->GetGamertag(), - //m_socket->networkPlayer->GetGamertag()); - - hostPlayer->SendData(socketPlayer, buffer.pbyData, buffer.dwDataSize, lowPriority, requireAck); - - // DWORD queueSize = hostPlayer->GetSendQueueSize( nullptr, QNET_GETSENDQUEUESIZE_BYTES ); - // if( queueSize > 24000 ) - // { - // //printf("Queue size is: %d, forcing doWork()\n",queueSize); - // g_NetworkManager.DoWork(); - // } + INetworkPlayer *validatedPlayer = m_socket->getPlayer(); + if(validatedPlayer == nullptr) return; + hostPlayer->SendData(validatedPlayer, buffer.pbyData, buffer.dwDataSize, lowPriority, requireAck); } else { - //printf( "Sent %u bytes of data from \"%ls\" to \"%ls\"\n", - //buffer.dwDataSize, - //m_socket->networkPlayer->GetGamertag(), - //hostPlayer->GetGamertag()); - - socketPlayer->SendData(hostPlayer, buffer.pbyData, buffer.dwDataSize, lowPriority, requireAck); + INetworkPlayer *validatedPlayer = m_socket->getPlayer(); + if(validatedPlayer == nullptr) return; + validatedPlayer->SendData(hostPlayer, buffer.pbyData, buffer.dwDataSize, lowPriority, requireAck); } } } diff --git a/Minecraft.World/StemTile.cpp b/Minecraft.World/StemTile.cpp index a04c70de..66980f58 100644 --- a/Minecraft.World/StemTile.cpp +++ b/Minecraft.World/StemTile.cpp @@ -6,6 +6,10 @@ #include "../Minecraft.Client/Minecraft.h" #include "../Minecraft.Client/Common/Colours/ColourTable.h" #include "StemTile.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#include "Dimension.h" +#endif const wstring StemTile::TEXTURE_ANGLED = L"stem_bent"; @@ -60,6 +64,9 @@ void StemTile::tick(Level *level, int x, int y, int z, Random *random) int below = level->getTile(xx, y - 1, zz); if (level->getTile(xx, y, zz) == 0 && (below == Tile::farmland_Id || below == Tile::dirt_Id || below == Tile::grass_Id)) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (!FourKitBridge::FireBlockGrow(level->dimension->id, xx, y, zz, fruit->id, 0)) +#endif level->setTileAndUpdate(xx, y, zz, fruit->id); } diff --git a/Minecraft.World/ThrownEnderpearl.cpp b/Minecraft.World/ThrownEnderpearl.cpp index 31fa69c2..6fa59db1 100644 --- a/Minecraft.World/ThrownEnderpearl.cpp +++ b/Minecraft.World/ThrownEnderpearl.cpp @@ -7,6 +7,9 @@ #include "../Minecraft.Client/PlayerConnection.h" #include "ThrownEnderpearl.h" #include "Endermite.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/FourKitBridge.h" +#endif @@ -35,13 +38,22 @@ void ThrownEnderpearl::onHit(HitResult *res) { if (res->entity != nullptr) { - DamageSource *damageSource = DamageSource::thrown(shared_from_this(), getOwner() ); + DamageSource *damageSource = DamageSource::thrown(shared_from_this(), getOwner()); res->entity->hurt(damageSource, 0); delete damageSource; } + for (int i = 0; i < 32; i++) { - level->addParticle(eParticleType_ender, x, y + random->nextDouble() * 2, z, random->nextGaussian(), 0, random->nextGaussian()); + level->addParticle( + eParticleType_ender, + x, + y + random->nextDouble() * 2, + z, + random->nextGaussian(), + 0, + random->nextGaussian() + ); } if (!level->isClientSide) @@ -50,34 +62,71 @@ void ThrownEnderpearl::onHit(HitResult *res) // If the owner has been removed, then ignore // 4J-JEV: Cheap type check first. - if ( (getOwner() != nullptr) && getOwner()->instanceof(eTYPE_SERVERPLAYER) ) + if ((getOwner() != nullptr) && getOwner()->instanceof(eTYPE_SERVERPLAYER)) { - shared_ptr serverPlayer = dynamic_pointer_cast(getOwner() ); + shared_ptr serverPlayer = + dynamic_pointer_cast(getOwner()); + if (!serverPlayer->removed) { - if(!serverPlayer->connection->done && serverPlayer->level == this->level) + if (!serverPlayer->connection->done && serverPlayer->level == this->level) { + // Custom: chance to spawn an Endermite + if (random->nextFloat() < 0.05f /* && level->getGameRules()->getBoolean("doMobSpawning") */) + { + Endermite* endermite = new Endermite(level); + endermite->setSpawnedByPlayer(true); - if (random->nextFloat() < 0.05f /* && level->getGameRules()->getBoolean("doMobSpawning") //for a future gamerule*/) - { - Endermite* endermite = new Endermite(level); - endermite->setSpawnedByPlayer(true); - - endermite->moveTo(serverPlayer->x, serverPlayer->y, serverPlayer->z, serverPlayer->yRot, serverPlayer->xRot); - level->addEntity(shared_ptr(endermite)); - } - + endermite->moveTo( + serverPlayer->x, + serverPlayer->y, + serverPlayer->z, + serverPlayer->yRot, + serverPlayer->xRot + ); + level->addEntity(shared_ptr(endermite)); + } + if (getOwner()->isRiding()) { getOwner()->ride(nullptr); } + +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + + { + double outX, outY, outZ; + + bool cancelled = FourKitBridge::FirePlayerTeleport( + serverPlayer->entityId, + serverPlayer->x, serverPlayer->y, serverPlayer->z, + serverPlayer->dimension, + x, y, z, + serverPlayer->dimension, + 0 /* ENDER_PEARL */, + &outX, &outY, &outZ + ); + + if (!cancelled) + { + getOwner()->teleportTo(outX, outY, outZ); + getOwner()->fallDistance = 0; + getOwner()->hurt(DamageSource::fall, 5); + } + } + +#else + getOwner()->teleportTo(x, y, z); getOwner()->fallDistance = 0; getOwner()->hurt(DamageSource::fall, 5); + +#endif } } } + remove(); } } \ No newline at end of file diff --git a/Minecraft.World/Tile.cpp b/Minecraft.World/Tile.cpp index 4d7ec7f3..c1e27960 100644 --- a/Minecraft.World/Tile.cpp +++ b/Minecraft.World/Tile.cpp @@ -23,6 +23,8 @@ +bool g_suppressExpDrops = false; + wstring Tile::TILE_DESCRIPTION_PREFIX = L"Tile."; const float Tile::INDESTRUCTIBLE_DESTROY_TIME = -1.0f; @@ -1064,6 +1066,7 @@ void Tile::popResource(Level *level, int x, int y, int z, shared_ptrisClientSide) { while (amount > 0) diff --git a/build-linux.sh b/build-linux.sh index 38a8de3a..2875cd50 100755 --- a/build-linux.sh +++ b/build-linux.sh @@ -125,9 +125,10 @@ do_build() { do_install() { info "Installing to $INSTALL_DIR..." - mkdir -p "$INSTALL_DIR"/{client,server} + mkdir -p "$INSTALL_DIR"/{client,server,fourkit} cp "$BUILD_DIR/Minecraft.Client/Minecraft.Client.exe" "$INSTALL_DIR/client/" - cp "$BUILD_DIR/Minecraft.Server/Minecraft.Server.exe" "$INSTALL_DIR/server/" + cp "$BUILD_DIR/Minecraft.Server/$BUILD_TYPE/Minecraft.Server.exe" "$INSTALL_DIR/server/" + cp "$BUILD_DIR/Minecraft.Server.FourKit/$BUILD_TYPE/Minecraft.Server.exe" "$INSTALL_DIR/fourkit/" for asset in iggy_w64.dll Common music Windows64 Windows64Media; do [[ -e "$BUILD_DIR/Minecraft.Client/$asset" ]] && \ cp -r "$BUILD_DIR/Minecraft.Client/$asset" "$INSTALL_DIR/client/" || true @@ -136,11 +137,17 @@ do_install() { [[ -e "$BUILD_DIR/Minecraft.Server/$asset" ]] && \ cp -r "$BUILD_DIR/Minecraft.Server/$asset" "$INSTALL_DIR/server/" || true done + for asset in iggy_w64.dll Common Windows64 plugins runtime; do + [[ -e "$BUILD_DIR/Minecraft.Server.FourKit/$BUILD_TYPE/$asset" ]] && \ + cp -r "$BUILD_DIR/Minecraft.Server.FourKit/$BUILD_TYPE/$asset" "$INSTALL_DIR/fourkit/" || true + done write_client_launcher write_server_launcher + write_fourkit_launcher success "Installed to $INSTALL_DIR" info "Run the client: $INSTALL_DIR/minecraft-lce-client" info "Run the server: $INSTALL_DIR/minecraft-lce-server" + info "Run the FourKit server: $INSTALL_DIR/minecraft-lce-fourkit" } write_client_launcher() { @@ -209,6 +216,45 @@ LAUNCHER chmod +x "$INSTALL_DIR/minecraft-lce-server" } + +write_fourkit_launcher() { + cat > "$INSTALL_DIR/minecraft-lce-fourkit" < "\$PERSIST_DIR/\$file" + fi + ln -sf "\$PERSIST_DIR/\$file" "\$WORK_DIR/\$file" +done +cd "\$WORK_DIR" +if [[ -z "\${DISPLAY:-}" ]]; then + export DISPLAY=":99" + Xvfb "\$DISPLAY" -nolisten tcp -screen 0 64x64x16 & + XVFB_PID=\$! + trap 'kill \$XVFB_PID 2>/dev/null || true; rm -rf "\$WORK_DIR"' EXIT + sleep 1 +fi +exec wine "\$WORK_DIR/Minecraft.Server.exe" -port "\$SERVER_PORT" -bind "\$SERVER_BIND_IP" "\$@" +LAUNCHER + chmod +x "$INSTALL_DIR/minecraft-lce-fourkit" +} + BUILD_DIR="$SOURCE_DIR/build/windows64-clang" mkdir -p "$BUILD_DIR" info "LegacyEvolved LCE v$VERSION build script" @@ -220,3 +266,4 @@ patch_winsdk_symlinks do_cmake_configure do_build do_install + diff --git a/build-start-dedicated-server.sh b/build-start-dedicated-server.sh index 7f6bd3a7..57afe4d9 100644 --- a/build-start-dedicated-server.sh +++ b/build-start-dedicated-server.sh @@ -3,7 +3,7 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.dedicated-server.yml" -SERVICE_NAME="minecraft-lce-dedicated-server" +SERVICE_NAME="lce-revelations-dedicated-server" PERSIST_DIR="${SCRIPT_DIR}/server-data" if [[ ! -f "${COMPOSE_FILE}" ]]; then diff --git a/cmake/ServerTarget.cmake b/cmake/ServerTarget.cmake new file mode 100644 index 00000000..efd7c856 --- /dev/null +++ b/cmake/ServerTarget.cmake @@ -0,0 +1,105 @@ +# Shared CMake helper for the dedicated server executable targets. +# +# Both server flavours (vanilla "Minecraft.Server" and FourKit-enabled +# "Minecraft.Server.FourKit") share the same compile/link/asset/debugger +# settings. This helper applies them to a target after add_executable. +# +# The two targets are defined in different source subdirectories so that +# the Visual Studio generator emits their .vcxproj files into matching +# binary subdirectories (build//Minecraft.Server/ and +# build//Minecraft.Server.FourKit/), keeping the variant identity +# consistent across source dir, project file, and runtime output dir. + +include_guard(GLOBAL) + +include("${CMAKE_SOURCE_DIR}/cmake/CopyAssets.cmake") + +# Apply shared compile/link/asset/debugger settings to a server executable target. +function(configure_lce_server_target target) + # compat_shims.cpp redefines internal MSVC CRT symbols which would explode + # if included via the precompiled header. CMake source-file properties are + # directory-scoped, so we have to apply this for the directory of every + # target that consumes the file (one per server flavour), not just once + # at module-include time. + set_source_files_properties( + "${CMAKE_SOURCE_DIR}/Minecraft.Client/compat_shims.cpp" + TARGET_DIRECTORY ${target} + PROPERTIES SKIP_PRECOMPILE_HEADERS ON + ) + + # Asset / redist source paths used by setup_asset_*_copy(). Defined inside + # the function so they are in scope when this helper is called from a + # different CMakeLists.txt. (CMake variables set at module file scope are + # not visible to function bodies invoked from other files.) + set(_asset_folder_pairs + "${CMAKE_SOURCE_DIR}/Minecraft.Client/Common/res" "Common/res" + ) + set(_asset_files_pairs + "${CMAKE_SOURCE_DIR}/Minecraft.Client/Common/Media/MediaWindows64.arc" "Common/Media/" + ) + + target_include_directories(${target} PRIVATE + "${CMAKE_BINARY_DIR}/generated/" # Generated BuildVer.h + "${CMAKE_SOURCE_DIR}/Minecraft.Client/" + "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/Iggy/include" + "${CMAKE_SOURCE_DIR}/Minecraft.Server" + "${CMAKE_SOURCE_DIR}/include/" + ) + target_compile_definitions(${target} PRIVATE + ${MINECRAFT_SHARED_DEFINES} + MINECRAFT_SERVER_BUILD + ) + target_precompile_headers(${target} PRIVATE "$<$:stdafx.h>") + + configure_compiler_target(${target}) + + # Both flavours produce a binary literally named "Minecraft.Server.exe". + # The variant identity lives in the build directory name, not the file name, + # so end users can ship either one as a drop-in replacement. Override the + # per-config RUNTIME_OUTPUT_DIRECTORY explicitly because the default would + # otherwise put both targets in the same Minecraft.Server// dir + # (the source dir of whichever CMakeLists.txt called add_executable) and + # ninja would complain about duplicate output rules. + set_target_properties(${target} PROPERTIES + OUTPUT_NAME "Minecraft.Server" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${target}" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/${target}/Debug" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/${target}/Release" + VS_DEBUGGER_WORKING_DIRECTORY "$" + VS_DEBUGGER_COMMAND_ARGUMENTS "-port 25565 -bind 0.0.0.0 -name DedicatedServer" + ) + + target_link_libraries(${target} PRIVATE + Minecraft.World + d3d11 + dxgi + d3dcompiler + XInput9_1_0 + wsock32 + legacy_stdio_definitions + $<$: + "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Input_d.lib" + "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Storage_d.lib" + "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Render_PC_d.lib" + > + $<$>: + "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Input.lib" + "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Storage.lib" + "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/4JLibs/libs/4J_Render_PC.lib" + > + ) + + foreach(lib IN LISTS IGGY_LIBS) + target_link_libraries(${target} PRIVATE "${CMAKE_SOURCE_DIR}/Minecraft.Client/${PLATFORM_NAME}/Iggy/lib/${lib}") + endforeach() + + # Per-target asset / redist copy steps. These create helper sub-targets + # named after the parent (e.g. AssetFileCopy_${target}) which is why each + # server flavour has its own set in the VS Solution Explorer. + setup_asset_folder_copy(${target} "${_asset_folder_pairs}") + setup_asset_file_copy(${target} "${_asset_files_pairs}") + add_copyredist_target(${target}) + if(PLATFORM_NAME STREQUAL "Windows64") + add_gamehdd_target(${target}) + endif() +endfunction() diff --git a/docker-compose.dedicated-server.ghcr.yml b/docker-compose.dedicated-server.ghcr.yml index cd6e45f0..e03d6bf9 100644 --- a/docker-compose.dedicated-server.ghcr.yml +++ b/docker-compose.dedicated-server.ghcr.yml @@ -1,7 +1,7 @@ services: - minecraft-lce-dedicated-server: - image: ghcr.io/itsrevela/minecraft-lce-dedicated-server:nightly - container_name: minecraft-lce-dedicated-server + lce-revelations-dedicated-server: + image: ghcr.io/lce-hub/lce-revelations-dedicated-server:nightly + container_name: lce-revelations-dedicated-server restart: unless-stopped tty: true stdin_open: true diff --git a/docker-compose.dedicated-server.yml b/docker-compose.dedicated-server.yml index 23caf26f..50debaf1 100644 --- a/docker-compose.dedicated-server.yml +++ b/docker-compose.dedicated-server.yml @@ -1,11 +1,15 @@ services: - minecraft-lce-dedicated-server: + lce-revelations-dedicated-server: build: context: . dockerfile: docker/dedicated-server/Dockerfile args: - MC_RUNTIME_DIR: ${MC_RUNTIME_DIR:-x64/Minecraft.Server/Release} - container_name: minecraft-lce-dedicated-server + # Bakes the vanilla (no plugin support) server flavour built with CMake. + # The FourKit-enabled flavour is intentionally NOT shipped via Docker + # yet because hosting .NET 10 self-contained through Wine has not been + # smoke-tested. Run the FourKit server natively on Windows instead. + MC_RUNTIME_DIR: ${MC_RUNTIME_DIR:-build/windows64/Minecraft.Server/Release} + container_name: lce-revelations-dedicated-server restart: unless-stopped tty: true stdin_open: true diff --git a/docker/dedicated-server/Dockerfile b/docker/dedicated-server/Dockerfile index 14760afd..26ccbce2 100644 --- a/docker/dedicated-server/Dockerfile +++ b/docker/dedicated-server/Dockerfile @@ -1,8 +1,17 @@ FROM debian:bookworm-slim ARG DEBIAN_FRONTEND=noninteractive -# basically, it only works with a Release build.(libs are not included in Debug build) -ARG MC_RUNTIME_DIR=x64/Minecraft.Server/Release +# Build the server with CMake first (see COMPILE.md). The build context is +# the repo root, and the path below points at the **vanilla** server's Release +# output. The vanilla flavour is shipped because it has no .NET dependency +# and runs reliably under Wine. +# +# A FourKit-enabled image is intentionally NOT built here yet: hosting the +# .NET 10 self-contained runtime through Wine has not been smoke-tested. +# Once it's validated, this Dockerfile can be extended (or split into a +# second variant) to bake in the runtime/ and plugins/ folders from +# build/windows64/Minecraft.Server.FourKit/Release. +ARG MC_RUNTIME_DIR=build/windows64/Minecraft.Server/Release RUN dpkg --add-architecture i386 \ && apt-get update \ diff --git a/docs/FOURKIT_PARITY.md b/docs/FOURKIT_PARITY.md new file mode 100644 index 00000000..3f0de9e2 --- /dev/null +++ b/docs/FOURKIT_PARITY.md @@ -0,0 +1,89 @@ +# FourKit Event Parity + +Records which FourKit events ship in this build, mapped to the native hook sites that fire them. This document is the canonical reference for plugin authors: if an event is listed here, you can subscribe to it. Use the [donor reference](https://github.com/) for the upstream FourKit event catalog if you need a richer description of any specific event. + +The Phase 1 reconnaissance showed that every donor hook-bearing source file in this repo was byte-identical to vanilla, so the donor's full set of hooks was applied to LCE-Revelations without policy-driven deferrals. Status is **PORTED** for everything below. + +## Server lifecycle + +| Event | Native hook site | Status | +|---|---|---| +| `FourKitBridge::Initialize` | `Minecraft.Server/Windows64/ServerMain.cpp` post-startup | PORTED | +| `FourKitBridge::Shutdown` | `Minecraft.Server/Windows64/ServerMain.cpp` early-shutdown | PORTED | +| `FireWorldSave` | `ServerMain.cpp` autosave trigger | PORTED | + +## Player lifecycle + +| Event | Native hook site | Status | +|---|---|---| +| `FirePlayerPreLogin` | `Minecraft.Client/PendingConnection.cpp::handlePreLogin` | PORTED | +| `FirePlayerLogin` (offline) | `Minecraft.Client/PendingConnection.cpp::handleLogin` | PORTED | +| `FirePlayerLogin` (online) | `Minecraft.Client/PendingConnection.cpp::handleAcceptedLogin` | PORTED | +| `FirePlayerJoin` | `Minecraft.Client/PlayerConnection.cpp::tick` (first-tick gated by `hasDoneFirstTickFourKit`) | PORTED | +| `FirePlayerKick` | `PlayerConnection.cpp::disconnect` | PORTED | +| `FirePlayerQuit` | `PlayerConnection.cpp::disconnect` and `onDisconnect` paths | PORTED | +| `UpdatePlayerEntityId` | Two `respawn(...)` sites in `PlayerConnection.cpp` | PORTED | +| `FirePlayerMove` | `PlayerConnection.cpp::handleMovePlayer` | PORTED | + +## Player gameplay + +| Event | Native hook site | Status | +|---|---|---| +| `FirePlayerChat` | `PlayerConnection.cpp::handleChat` | PORTED | +| `FireCommandPreprocess` | `PlayerConnection.cpp::handleCommand` | PORTED | +| `HandlePlayerCommand` | `PlayerConnection.cpp::handleCommand` | PORTED | +| `FirePlayerInteract` (USE_ITEM) | `handleUseItem` (face == 255) | PORTED | +| `FirePlayerInteract` (RIGHT_CLICK_BLOCK) | `handleUseItem` (face != 255) | PORTED | +| `FireBlockPlace` | `handleUseItem` post `useItemOn` | PORTED | +| `FirePlayerInteract` (LEFT_CLICK_BLOCK) | `handlePlayerAction::START_DESTROY_BLOCK` | PORTED | +| `FirePlayerInteract` (LEFT_CLICK_AIR) | `handleAnimate::SWING` | PORTED | +| `FirePlayerInteractEntity` | `handleInteract::INTERACT` (uses `MapEntityType`) | PORTED | +| `FirePlayerDropItem` | `handlePlayerAction::DROP_ITEM` and `DROP_ALL_ITEMS` | PORTED | + +## Inventory + +| Event | Native hook site | Status | +|---|---|---| +| `FireInventoryClick` | `PlayerConnection.cpp::handleContainerClick` | PORTED | +| `FireSignChange` | `PlayerConnection.cpp::handleSignUpdate` | PORTED | + +## Block / world + +The Phase 1 recon enumerated 17 hook-bearing files in `Minecraft.World/`. Each file was bulk-copied from the donor (all were vanilla in target). The hooks they contain cover: + +| Source file | Hook category | Status | +|---|---|---| +| `AbstractContainerMenu.cpp` | Inventory open / interact | PORTED | +| `LivingEntity.cpp` | Entity damage, death | PORTED | +| `ItemEntity.cpp` | Item entity events | PORTED | +| `ThrownEnderpearl.cpp` | Teleport / portal | PORTED | +| `PistonBaseTile.cpp` | Block piston extend / retract | PORTED | +| `LiquidTileDynamic.cpp` | Block from-to (water / lava flow) | PORTED | +| `FireTile.cpp` | Block burn | PORTED | +| `GrassTile.cpp`, `Mushroom.cpp`, `Sapling.cpp` | Block spread / form | PORTED | +| `CactusTile.cpp`, `CocoaTile.cpp`, `CropTile.cpp`, `EggTile.cpp`, `NetherWartTile.cpp`, `ReedTile.cpp`, `StemTile.cpp` | Block grow | PORTED | + +The exact `Fire*` calls inside each file match the donor verbatim. + +## Console / commands + +| Event | Native hook site | Status | +|---|---|---| +| `HandleConsoleCommand` | `Minecraft.Server/Console/ServerCliEngine.cpp` (unknown-command branch) | PORTED | +| `GetPluginCommandHelp` | `ServerCliEngine.cpp` (suggest-names branch) | PORTED | + +## Native callback surface (C# → C++) + +`FourKitNatives.cpp` was copied verbatim from the donor. All ~80 native callbacks (player damage / health / teleport / kick / ban, world tile get / set, inventory access, virtual containers, sound, explosion, etc.) are present. Phase 1 spot-checks confirmed signature compatibility against the engine APIs in this repo. + +## Known gaps + +None at the time of writing. If runtime testing in Phase 8 reveals an event that compiles but does not fire (e.g., the donor refactored the calling function in a way the recon missed), it will be added here as a deferred item. + +## Out of scope (per the locked plan) + +- Hot-reload (PluginLoadContext is non-collectible by design) +- Permission system +- Async / thread-safety hardening of the dispatcher +- New events beyond donor parity +- Managed unit tests for the API surface diff --git a/docs/FOURKIT_PORT_RECON.md b/docs/FOURKIT_PORT_RECON.md new file mode 100644 index 00000000..ed84d918 --- /dev/null +++ b/docs/FOURKIT_PORT_RECON.md @@ -0,0 +1,180 @@ +# FourKit Port Reconnaissance + +Phase 1 deliverable for porting the FourKit plugin system into LCE-Revelations. This document is the source of truth for what needs to change, where, and how risky each change is. Phases 2-3 of the port consume this document directly. + +## Source repos referenced + +| Role | Path | +|---|---| +| Vanilla baseline | `C:\Users\revela\Documents\Minecraft\itsRevela` | +| Donor (has FourKit) | `C:\Users\revela\Documents\Minecraft\FourKit\MinecraftConsoles` | +| Target (this repo) | `C:\Users\revela\Documents\Minecraft\LCE-Hub\LCE-Revelations` | + +Vanilla and donor were forked from upstream at similar points; the donor adds FourKit on top. The target is a more recent independent fork that has its own divergence (Security subsystem, OpManager, revoke-token command, additional ServerProperties fields). + +## Executive summary + +- **27 files** in the donor are FourKit-bearing across `Minecraft.Server/`, `Minecraft.World/`, and `Minecraft.Client/`. Of those, **23 are clean patches** (target identical to vanilla in those files), and **only 4 require manual merge work** (all in `Minecraft.Server/`). +- **7 pure-add native files** drop in unmodified. +- **1 entire managed project** (`Minecraft.Server.FourKit/`) drops in unmodified. +- **No blockers identified.** Every Fire* hook site exists in target at vanilla content; the conflicts are concentrated in build files and a small set of `Minecraft.Server/` source files where target diverged. +- **Native callback ABI is intact.** Spot-checks of player/world/inventory subsystem APIs that FourKitNatives.cpp depends on all match between donor and target. + +## A. File diff matrix + +### A.1 Pure adds (drop in verbatim) + +| File | Notes | +|---|---| +| `Minecraft.Server/FourKitBridge.cpp` | Event firing + callback registry | +| `Minecraft.Server/FourKitBridge.h` | Public Fire* and Handle* surface | +| `Minecraft.Server/FourKitNatives.cpp` | C# → C++ callback implementations (~80 functions) | +| `Minecraft.Server/FourKitNatives.h` | Native function declarations | +| `Minecraft.Server/FourKitRuntime.cpp` | hostfxr load + .NET 10 bootstrap | +| `Minecraft.Server/FourKitRuntime.h` | Runtime init API | +| `Minecraft.Server/FourKitMappers.cpp` | Type/enum mapping helpers | +| `Minecraft.Server.FourKit/` (entire project) | Managed plugin host, Bukkit-style API, ~54 events | + +### A.2 Modified files in `Minecraft.Server/` (the merge work) + +| File | Donor delta | Target diverges from vanilla? | Merge class | +|---|---|---|---| +| `CMakeLists.txt` | +24 lines (FourKit dep + post-build copy) | No | CLEAN_PATCH | +| `cmake/sources/Common.cmake` | +7 FourKit sources, donor also strips OpManager/Security (donor lacks those) | No | **SELECTIVE_MERGE**: add donor's FourKit sources only; preserve target's OpManager/Security entries | +| `Windows64/ServerMain.cpp` | FourKit init/shutdown + 1 inline FireWorldSave | No | CLEAN_PATCH | +| `Console/ServerCliEngine.cpp` | HandleConsoleCommand hook + GetPluginCommandHelp integration | No | CLEAN_PATCH | +| `Console/commands/help/CliCommandHelp.cpp` | Plugin command help integration | Unknown: check before merge | LIKELY_CLEAN | +| `Console/commands/whitelist/CliCommandWhitelist.cpp` | Unknown delta | Unknown | LIKELY_CLEAN | +| `Access/Access.cpp` | Donor delta unrelated to FourKit hooks per grep | No | CLEAN_PATCH (verify) | +| `Access/Access.h` | Same | No | CLEAN_PATCH (verify) | +| `ServerLogManager.cpp` | Logger plumbing for FourKit log routing | No | CLEAN_PATCH | +| `ServerLogManager.h` | Same | No | CLEAN_PATCH | +| `ServerLogger.cpp` | Same | No | CLEAN_PATCH | +| `ServerProperties.cpp` | Donor REMOVES fields target relies on | **Yes** | **CONFLICT: KEEP TARGET** | +| `ServerProperties.h` | Same | **Yes** | **CONFLICT: KEEP TARGET** | + +### A.3 Modified files in `Minecraft.World/` and `Minecraft.Client/` + +The donor changed 53 files in `Minecraft.World/` and 95 files in `Minecraft.Client/` overall, but most of those are upstream churn unrelated to FourKit. The FourKit-specific subset is identified by `grep "FourKit"` and yields **23 files**, all of which are identical between vanilla and target: i.e., every single one is CLEAN_PATCH. + +#### `Minecraft.World/` FourKit hook files (17, all CLEAN_PATCH) + +`AbstractContainerMenu.cpp`, `CactusTile.cpp`, `CocoaTile.cpp`, `CropTile.cpp`, `EggTile.cpp`, `FireTile.cpp`, `GrassTile.cpp`, `ItemEntity.cpp`, `LiquidTileDynamic.cpp`, `LivingEntity.cpp`, `Mushroom.cpp`, `NetherWartTile.cpp`, `PistonBaseTile.cpp`, `ReedTile.cpp`, `Sapling.cpp`, `StemTile.cpp`, `ThrownEnderpearl.cpp` + +#### `Minecraft.Client/` FourKit hook files (6, all CLEAN_PATCH) + +`PendingConnection.cpp`, `PlayerConnection.cpp`, `ServerLevel.cpp`, `ServerPlayer.cpp`, `ServerPlayerGameMode.cpp`, `TeleportCommand.cpp` + +> **Note on naming:** Despite the `Minecraft.Client/` folder name, several of these files (`PlayerConnection`, `ServerPlayer`, `ServerLevel`, etc.) are shared engine code that the dedicated server also compiles. They are valid hook sites for a server-side plugin system. + +#### Build files in `Minecraft.World/` and `Minecraft.Client/` + +The donor also modifies `Minecraft.World/cmake/sources/Common.cmake`, `Minecraft.Client/cmake/sources/Common.cmake`, `Minecraft.Client/cmake/sources/Windows.cmake`, and `Minecraft.Client/CMakeLists.txt`, but none of these contain FourKit references: they are upstream churn and should NOT be touched as part of the FourKit port. + +## B. Fire* hook site inventory + +Donor exposes Fire*/Handle* via `FourKitBridge::` (see `Minecraft.Server/FourKitBridge.h`). Hook calls appear in 26 source files across the three subprojects. + +### B.1 Hook sites by subsystem + +| Subsystem | Files (in donor) | +|---|---| +| Server lifecycle | `Minecraft.Server/Windows64/ServerMain.cpp` (init, shutdown, FireWorldSave) | +| Console / commands | `Minecraft.Server/Console/ServerCliEngine.cpp` (HandleConsoleCommand, GetPluginCommandHelp), `Minecraft.Server/Console/commands/help/CliCommandHelp.cpp` | +| Player connection / login | `Minecraft.Client/PlayerConnection.cpp`, `Minecraft.Client/PendingConnection.cpp` | +| Player gameplay | `Minecraft.Client/ServerPlayer.cpp`, `Minecraft.Client/ServerPlayerGameMode.cpp`, `Minecraft.Client/TeleportCommand.cpp` | +| World / level | `Minecraft.Client/ServerLevel.cpp` | +| Living entities & damage | `Minecraft.World/LivingEntity.cpp`, `Minecraft.World/ItemEntity.cpp`, `Minecraft.World/ThrownEnderpearl.cpp` | +| Block / tile (growth, burn, spread, ignite, piston) | `Minecraft.World/CactusTile.cpp`, `CocoaTile.cpp`, `CropTile.cpp`, `EggTile.cpp`, `FireTile.cpp`, `GrassTile.cpp`, `LiquidTileDynamic.cpp`, `Mushroom.cpp`, `NetherWartTile.cpp`, `PistonBaseTile.cpp`, `ReedTile.cpp`, `Sapling.cpp`, `StemTile.cpp` | +| Inventory / containers | `Minecraft.World/AbstractContainerMenu.cpp` | + +### B.2 Hook insertion-site safety + +Per A.3, **every file in B.1 outside of `Minecraft.Server/` is byte-identical between vanilla and target**. That means donor's hook insertions can be applied as a straight patch without adapting around target-side refactors. + +In `Minecraft.Server/`, the hook-bearing files (`ServerMain.cpp`, `ServerCliEngine.cpp`, `CliCommandHelp.cpp`) are also identical or near-identical to vanilla per A.2. Net result: **the locked Phase 3 hook adaptation policy ("adapt to LCE-Revelations refactors") will not need to fire** for any hook site in the current state of the target. If the user pulls upstream changes between now and Phase 3, this assumption must be re-validated. + +### B.3 Three concrete hook sites verified end-to-end (others follow same pattern) + +1. **`Minecraft.Server/Windows64/ServerMain.cpp:681`**: `FourKitBridge::FireWorldSave()` inside the autosave trigger block. Target file is byte-identical to vanilla → clean insertion. +2. **`Minecraft.Server/Console/ServerCliEngine.cpp:165`**: `if (FourKitBridge::HandleConsoleCommand(normalizedLine)) return true;` early return for plugin command preprocessing. Target file byte-identical → clean insertion. +3. **`Minecraft.Server/Console/ServerCliEngine.cpp:215-246`**: `FourKitBridge::GetPluginCommandHelp(...)` integration into the help printer. Same file, clean insertion. + +## C. Native callback surface (FourKitNatives.cpp ↔ engine APIs) + +`FourKitNatives.cpp` implements ~80 functions that wrap engine APIs and expose them to managed code. Spot-check of the highest-risk subsystems: + +| Native function | Engine API called | Target has it? | Signature drift? | +|---|---|---|---| +| `NativeSetPlayerHealth(int, float)` | `ServerPlayer::setHealth(float)` | Yes | None | +| `NativeTeleportPlayer(int, double, double, double)` | `PlayerConnection::teleport(...)` | Yes | None | +| `NativeSetTile(int, int, int, int, int, int)` | `ServerLevel::setBlock(...)` | Yes | None | +| `NativeGetPlayerAddress(int, char*, int, int*)` | `PlayerConnection` accessors | Yes | None | +| `NativeGetPlayerLatency(int)` | `PlayerConnection::latency` | Yes (field) | None | +| `NativeBanPlayer(int, char*, int)` | `Access::AddPlayerBan(...)` | Yes | None | +| `NativeSetItemMeta(int, int, char*, int)` | `ItemInstance` serialization | Yes | None | +| `NativeGetContainerContents(int, int*, int)` | `AbstractContainerMenu` traversal | Yes | None | + +**Verdict:** No drift detected in critical subsystems. Phase 2's native bridge build is expected to compile against target's headers without shimming. Any drift discovered at compile time will be a Phase 2.3 finding, not a known risk. + +## D. Critical hot files + +| File | Vanilla LOC | Donor LOC | Target LOC | Status | +|---|---|---|---|---| +| `Minecraft.Server/Windows64/ServerMain.cpp` | 1,257 | 1,257 | 1,257 | Identical across all three. Donor's FourKit additions sit on the vanilla baseline; target has not touched it. | +| `Minecraft.Server/CMakeLists.txt` | 86 | 110 (+24) | 86 | Target is vanilla. Donor adds FourKit deps and a post-build copy step. Clean apply. | +| `Minecraft.Server/cmake/sources/Common.cmake` | 628 | 640 (+12 net; donor also strips ~16 lines of OpManager/Security) | 628 | Target is vanilla. **Selective merge:** apply donor's `FourKit*.cpp` additions, do NOT apply donor's removals. | +| `Minecraft.Server/ServerProperties.cpp` | 965 | 930 (-35) | 965 | Donor is older / leaner; target carries hardcore + security fields donor never had. **Keep target version unchanged**, do not merge donor's deletions. | +| `Minecraft.Server/ServerProperties.h` | n/a | n/a | n/a | Same logic: keep target. | +| `Minecraft.Server/Console/commands/` | 20 subdirs | 18 subdirs (no `revoketoken`) | 20 subdirs (has `revoketoken`) | Donor predates `revoketoken`; target has it. Keep target's command set. | + +## E. Top-level structure of the target + +| Item | Status | +|---|---| +| `samples/` at repo root | NOT FOUND: Phase 7 will create it | +| `docs/` at repo root | NOT FOUND: created by this document | +| `docker/` | EXISTS: contains `dedicated-server/{Dockerfile,entrypoint.sh}`. Phase 5 must inspect to decide whether the image builds the server from source or consumes the release zip | +| `Minecraft.Server.FourKit/` | NOT FOUND (as expected) | + +## Merge strategy summary (input to Phase 2 and Phase 3) + +1. **Phase 2 source drop:** Copy the 7 PURE_ADD files into `Minecraft.Server/` verbatim. Copy the entire `Minecraft.Server.FourKit/` project to repo root verbatim. +2. **Phase 2 build files:** + - `Minecraft.Server/CMakeLists.txt`: apply donor's diff verbatim (target is vanilla). + - `Minecraft.Server/cmake/sources/Common.cmake`: apply ONLY the donor's additions of `FourKit*.cpp`. Do not delete OpManager or Security entries. +3. **Phase 3 hooks (the entire surface, all CLEAN_PATCH):** + - 3 sites in `Minecraft.Server/` (`ServerMain.cpp`, `ServerCliEngine.cpp`, `CliCommandHelp.cpp`) + - 17 sites in `Minecraft.World/` (block/entity/container) + - 6 sites in `Minecraft.Client/` (player/level/connection) + - 1 lifecycle hook pair in `ServerMain.cpp` for `FourKitRuntime::Initialize` / shutdown + - Logger plumbing in `ServerLogManager.{cpp,h}` and `ServerLogger.cpp` +4. **Phase 3 conflicts to leave alone:** + - `ServerProperties.{cpp,h}`: keep target version, do not merge donor. + - `Console/commands/revoketoken/`: keep target version, donor lacks it. + - `Access/OpManager.{cpp,h}`: keep target version, donor lacks it. + - `Security/`: keep target version, donor lacks it. +5. **Spot-checks needed before merge:** + - `Console/commands/help/CliCommandHelp.cpp`: verify donor's delta is purely the plugin help integration and not entangled with anything target changed. + - `Console/commands/whitelist/CliCommandWhitelist.cpp`: confirm scope of donor's edit. + - `Access/Access.{cpp,h}`: confirm donor's delta is not security-related. + +## Risks updated post-recon + +| Risk | Original severity | Post-recon severity | Reason | +|---|---|---|---| +| LCE-Revelations divergence at hook sites | HIGH | **LOW** | All 26 hook-bearing source files are byte-identical between vanilla and target. | +| CMake structural divergence | HIGH | **LOW** | Target's `CMakeLists.txt` and `Common.cmake` are byte-identical to vanilla. | +| ServerMain.cpp init-order conflicts | MEDIUM | **LOW** | File is identical to vanilla. | +| ServerProperties conflict | NEW | **MEDIUM** | Donor strips fields target relies on. Mitigation: do not merge donor's version. | +| Native ABI drift in subsystem APIs | MEDIUM | **LOW** | Spot-checks all clean. Compile-time will catch any miss. | +| Donor predates target features (revoketoken, OpManager, Security) | NEW | **LOW** | Easy to preserve target's additions; donor doesn't depend on their absence. | +| Self-contained .NET 10 publish size | MEDIUM | **MEDIUM** | Unchanged. ~70-100 MB release zip growth. | +| Hot-reload absent | LOW | **LOW** | Inherited limitation, documented. | + +## Open follow-ups for Phase 2 + +1. Confirm `Console/commands/help/CliCommandHelp.cpp` and `Console/commands/whitelist/CliCommandWhitelist.cpp` deltas are FourKit-related (currently marked LIKELY_CLEAN, not verified). +2. Confirm `Access/Access.{cpp,h}` delta scope. +3. Decide whether the donor's changes to `ServerLogManager` and `ServerLogger` are strictly additive (capture for FourKit log routing) or entangled with other refactoring. +4. Confirm during Phase 2.3 that `FourKitRuntime.cpp`'s hostfxr loader handles the case where `hostfxr.dll` is absent gracefully (server should warn and continue, not crash): this matters because the self-contained publish puts hostfxr next to the server exe, so the failure mode only fires if a user deletes it. diff --git a/global.json b/global.json new file mode 100644 index 00000000..d46d21e5 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/samples/HelloPlugin/HelloPlugin.cs b/samples/HelloPlugin/HelloPlugin.cs new file mode 100644 index 00000000..ea667aad --- /dev/null +++ b/samples/HelloPlugin/HelloPlugin.cs @@ -0,0 +1,39 @@ +using Minecraft.Server.FourKit; +using Minecraft.Server.FourKit.Event; +using Minecraft.Server.FourKit.Event.Player; +using Minecraft.Server.FourKit.Plugin; + +namespace HelloPlugin; + +/// +/// Minimal sample plugin used to verify the FourKit plugin loader and event +/// dispatcher are wired up correctly. Greets each player on join. +/// +public class HelloPlugin : ServerPlugin +{ + public override string name => "HelloPlugin"; + public override string version => "1.0.0"; + public override string author => "LCE-Revelations"; + + public override void onEnable() + { + Console.WriteLine($"[HelloPlugin] {name} v{version} enabled."); + FourKit.addListener(new HelloListener()); + } + + public override void onDisable() + { + Console.WriteLine($"[HelloPlugin] {name} v{version} disabled."); + } +} + +internal sealed class HelloListener : Listener +{ + [EventHandler(Priority = EventPriority.Normal)] + public void onPlayerJoin(PlayerJoinEvent e) + { + var playerName = e.getPlayer().getName(); + Console.WriteLine($"[HelloPlugin] {playerName} joined."); + e.setJoinMessage($"Welcome, {playerName}!"); + } +} diff --git a/samples/HelloPlugin/HelloPlugin.csproj b/samples/HelloPlugin/HelloPlugin.csproj new file mode 100644 index 00000000..ba57aeb4 --- /dev/null +++ b/samples/HelloPlugin/HelloPlugin.csproj @@ -0,0 +1,22 @@ + + + net10.0 + enable + enable + HelloPlugin + HelloPlugin + true + + + + + + false + runtime + + + diff --git a/start-dedicated-server.sh b/start-dedicated-server.sh index 8169601c..4bf91d35 100644 --- a/start-dedicated-server.sh +++ b/start-dedicated-server.sh @@ -3,7 +3,7 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.dedicated-server.ghcr.yml" -SERVICE_NAME="minecraft-lce-dedicated-server" +SERVICE_NAME="lce-revelations-dedicated-server" PERSIST_DIR="${SCRIPT_DIR}/server-data" if [[ ! -f "${COMPOSE_FILE}" ]]; then diff --git a/tools/stress-test/README.md b/tools/stress-test/README.md new file mode 100644 index 00000000..f45f09ab --- /dev/null +++ b/tools/stress-test/README.md @@ -0,0 +1,54 @@ +# LCE Server Stress Test + +Rapid bot connect/disconnect cycles to stress test server thread safety. + +## Requirements + +- Python 3.10+ +- `pycryptodome` (only if server has cipher enabled) + +``` +pip install pycryptodome +``` + +## Usage + +```bash +# Basic: 50 cycles, 8 concurrent bots, 2-10s hold time +python stress_test.py 127.0.0.1 19132 + +# Aggressive: rapid join/leave with short hold times +python stress_test.py 127.0.0.1 19132 --bots 12 --hold 0.5 2 --ramp 0.2 + +# With movement packets (tests "moved wrongly" code path) +python stress_test.py 127.0.0.1 19132 --move --hold 5 15 + +# Burst mode: 4 bots join simultaneously +python stress_test.py 127.0.0.1 19132 --burst 4 --hold 1 3 + +# Run for 5 minutes continuously +python stress_test.py 127.0.0.1 19132 --cycles 0 --duration 300 + +# Quiet mode (only shows stats, no per-bot messages) +python stress_test.py 127.0.0.1 19132 --quiet +``` + +## What it tests + +- **Player list thread safety**: Rapid add/remove from the players vector +- **Socket write races**: Disconnect while server is mid-write +- **Movement validation**: `--move` flag sends position packets that trigger "moved wrongly" checks +- **Concurrent join/leave**: `--burst` flag joins multiple bots at once + +## Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--bots N` | 8 | Max concurrent bot connections | +| `--cycles N` | 50 | Total connect/disconnect cycles (0 = infinite) | +| `--hold MIN MAX` | 2 10 | Random hold time range before disconnect (seconds) | +| `--ramp SECS` | 0.5 | Delay between spawning bots | +| `--move` | off | Send movement packets while connected | +| `--burst N` | 1 | Bots to spawn simultaneously per cycle | +| `--duration SECS` | 0 | Time limit (0 = run until cycles complete) | +| `--quiet` | off | Only show periodic stats, not per-bot logs | diff --git a/tools/stress-test/stress_test.py b/tools/stress-test/stress_test.py new file mode 100644 index 00000000..e746f380 --- /dev/null +++ b/tools/stress-test/stress_test.py @@ -0,0 +1,647 @@ +""" +LCE Server Stress Testing Tool + +Rapid bot connect/disconnect cycles to stress test thread safety +of the server's player list, socket handling, and movement processing. + +Usage: + python stress_test.py [options] + +Options: + --bots N Max concurrent bots (default: 8) + --cycles N Number of connect/disconnect cycles (default: 50, 0=infinite) + --hold MIN MAX Hold time range in seconds before disconnect (default: 2 10) + --ramp SECS Delay between spawning each bot (default: 0.5) + --move Bots send movement packets while connected + --burst N Spawn N bots simultaneously (default: 1) + --duration SECS Run for N seconds then stop (default: 0, unlimited) + --quiet Suppress per-bot log messages +""" + +import argparse +import logging +import os +import random +import secrets +import socket +import struct +import sys +import threading +import time +from dataclasses import dataclass, field + +# Import protocol code from the server-monitor tool. +# Search sibling tools directory first, then the itsRevela tools directory. +_TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_SEARCH_PATHS = [ + os.path.join(_TOOLS, "server-monitor"), + os.path.join(os.path.dirname(_TOOLS), "server-monitor"), + os.path.normpath(os.path.join(_TOOLS, "..", "..", "..", "itsRevela", "tools", "server-monitor")), +] +for _p in _SEARCH_PATHS: + if os.path.isdir(_p) and _p not in sys.path: + sys.path.append(_p) + break + +from protocol import frame_packet, DataInputStream, DataOutputStream +from packets import ( + PRE_LOGIN, LOGIN, KEEP_ALIVE, CUSTOM_PAYLOAD, DISCONNECT, + build_prelogin, build_login, build_keep_alive, + build_custom_payload, parse_prelogin_response, + parse_login_response_stream, DISCONNECT_REASONS, +) + +logger = logging.getLogger("stress") + +SMALLID_REJECT = 0xFF + +CIPHER_KEY_PREFIX = ( + b"\xFA\x00\x07" + b"\x00\x4D\x00\x43\x00\x7C\x00\x43\x00\x4B\x00\x65\x00\x79" + b"\x00\x20" +) +CIPHER_KEY_TOTAL = len(CIPHER_KEY_PREFIX) + 32 + +CIPHER_ON_PATTERN = ( + b"\xFA\x00\x06" + b"\x00\x4D\x00\x43\x00\x7C\x00\x43\x00\x4F\x00\x6E" + b"\x00\x00" +) + +CIPHER_ACK_CHANNEL = "MC|CAck" +IDENTITY_TOKEN_ISSUE = "MC|CTIssue" +IDENTITY_TOKEN_CHALLENGE = "MC|CTChallenge" +IDENTITY_TOKEN_RESPONSE = "MC|CTResponse" + + +def _build_channel_pattern(channel: str) -> bytes: + result = b"\xFA" + result += struct.pack(">h", len(channel)) + for ch in channel: + result += struct.pack(">H", ord(ch)) + return result + + +# --------------------------------------------------------------------------- +# CipherState (AES-CTR, optional dependency) +# --------------------------------------------------------------------------- + +class CipherState: + def __init__(self, key: bytes, iv: bytes): + from Crypto.Cipher import AES + from Crypto.Util import Counter + ctr = Counter.new(128, initial_value=int.from_bytes(iv, "big")) + self._cipher = AES.new(key, AES.MODE_CTR, counter=ctr) + + def process(self, data: bytes) -> bytes: + return self._cipher.encrypt(data) + + +# --------------------------------------------------------------------------- +# Statistics tracker +# --------------------------------------------------------------------------- + +@dataclass +class Stats: + lock: threading.Lock = field(default_factory=threading.Lock) + connects: int = 0 + disconnects: int = 0 + rejections: int = 0 + errors: int = 0 + moves_sent: int = 0 + keepalives_sent: int = 0 + start_time: float = 0.0 + + def summary(self) -> str: + elapsed = time.time() - self.start_time if self.start_time else 0 + with self.lock: + return ( + f"[{elapsed:.0f}s] " + f"connects={self.connects} " + f"disconnects={self.disconnects} " + f"rejections={self.rejections} " + f"errors={self.errors} " + f"moves={self.moves_sent} " + f"keepalives={self.keepalives_sent}" + ) + + +# --------------------------------------------------------------------------- +# Movement packet builder +# --------------------------------------------------------------------------- + +MOVE_PLAYER = 0x0D # MovePlayerPacket ID + +def build_move_player(x: float, y: float, z: float, + yaw: float, pitch: float, on_ground: bool) -> bytes: + dos = DataOutputStream() + dos.write_double(x) + dos.write_double(y + 1.62) # stance + dos.write_double(y) + dos.write_double(z) + dos.write_float(yaw) + dos.write_float(pitch) + dos.write_bool(on_ground) + return dos.getvalue() + + +# --------------------------------------------------------------------------- +# Single bot stress connection +# --------------------------------------------------------------------------- + +class StressBot: + """A bot that connects, optionally moves around, then disconnects.""" + + def __init__(self, name: str, host: str, port: int, xuid: int, + hold_min: float, hold_max: float, send_moves: bool, + stats: Stats, quiet: bool): + self.name = name + self.host = host + self.port = port + self.xuid = xuid + self.hold_min = hold_min + self.hold_max = hold_max + self.send_moves = send_moves + self.stats = stats + self.quiet = quiet + self._sock: socket.socket | None = None + self._recv_buf = bytearray() + self._scan_buf = bytearray() + self._recv_cipher: CipherState | None = None + self._send_cipher: CipherState | None = None + self._cipher_key = b"" + self._cipher_iv = b"" + self._identity_token = b"" + self._entity_id = 0 + self._running = True + + def log(self, msg: str) -> None: + if not self.quiet: + logger.info(msg) + + def run(self) -> bool: + """Run one connect-hold-disconnect cycle. Returns True on success.""" + try: + return self._do_cycle() + except Exception as e: + self.log(f"[{self.name}] error: {e}") + with self.stats.lock: + self.stats.errors += 1 + return False + finally: + if self._sock: + try: + self._sock.close() + except OSError: + pass + self._sock = None + + def _do_cycle(self) -> bool: + # Connect + self._sock = socket.create_connection((self.host, self.port), timeout=10) + self._sock.settimeout(5.0) + + # SmallID + first_byte = self._recv_exact(1) + if first_byte[0] == SMALLID_REJECT: + reject_data = self._recv_exact(5) + reason_id = struct.unpack(">i", reject_data[1:5])[0] + reason = DISCONNECT_REASONS.get(reason_id, f"Unknown({reason_id})") + self.log(f"[{self.name}] rejected: {reason}") + with self.stats.lock: + self.stats.rejections += 1 + return False + + small_id = first_byte[0] + self.log(f"[{self.name}] assigned smallId={small_id}") + + # PreLogin + self._send_packet(PRE_LOGIN, build_prelogin(self.name)) + + # Read PreLogin response + self._sock.settimeout(5.0) + if not self._read_until_packet(PRE_LOGIN, timeout=5.0): + self.log(f"[{self.name}] no PreLogin response") + with self.stats.lock: + self.stats.errors += 1 + return False + + # Login + self._send_packet(LOGIN, build_login(self.name, self.xuid)) + + # Read Login response + if not self._read_until_packet(LOGIN, timeout=5.0): + self.log(f"[{self.name}] no Login response") + with self.stats.lock: + self.stats.errors += 1 + return False + + with self.stats.lock: + self.stats.connects += 1 + + self.log(f"[{self.name}] connected (entityId={self._entity_id})") + + # Cipher scan (3 second window) + self._sock.settimeout(1.0) + self._do_cipher_scan() + + # Hold phase: stay connected, send keepalives and optional movement + hold_time = random.uniform(self.hold_min, self.hold_max) + hold_end = time.time() + hold_time + last_keepalive = time.time() + keepalive_counter = 0 + move_x, move_z = random.uniform(-50, 50), random.uniform(-50, 50) + move_y = 65.0 + + while time.time() < hold_end and self._running: + # Drain incoming data + try: + chunk = self._sock.recv(65536) + if not chunk: + break + self._recv_buf.extend(chunk) + self._drain_frames() + except socket.timeout: + pass + except OSError: + break + + now = time.time() + + # Keepalive every 10s + if now - last_keepalive >= 10.0: + keepalive_counter += 1 + self._send_packet(KEEP_ALIVE, build_keep_alive(keepalive_counter)) + with self.stats.lock: + self.stats.keepalives_sent += 1 + last_keepalive = now + + # Movement packets every 50ms + if self.send_moves: + move_x += random.uniform(-0.5, 0.5) + move_z += random.uniform(-0.5, 0.5) + yaw = random.uniform(0, 360) + self._send_packet(MOVE_PLAYER, + build_move_player(move_x, move_y, move_z, yaw, 0.0, True)) + with self.stats.lock: + self.stats.moves_sent += 1 + time.sleep(0.05) + + # Disconnect (just close the socket) + self.log(f"[{self.name}] disconnecting after {hold_time:.1f}s hold") + with self.stats.lock: + self.stats.disconnects += 1 + return True + + def _do_cipher_scan(self) -> None: + """Scan for cipher handshake for up to 3 seconds.""" + scan_start = time.time() + scan_buf = bytearray() + + while time.time() - scan_start < 0.5 and self._running: + try: + chunk = self._sock.recv(65536) + if not chunk: + return + self._recv_buf.extend(chunk) + except socket.timeout: + pass + except OSError: + return + + # Also drain framed packets + self._drain_frames() + + # Accumulate raw data for cipher pattern scanning + scan_buf.extend(self._recv_buf) + + # Look for MC|CKey + if not self._cipher_key: + idx = scan_buf.find(CIPHER_KEY_PREFIX) + if idx >= 0 and idx + CIPHER_KEY_TOTAL <= len(scan_buf): + key_start = idx + len(CIPHER_KEY_PREFIX) + key_data = bytes(scan_buf[key_start:key_start + 32]) + self._cipher_key = key_data[:16] + self._cipher_iv = key_data[16:32] + self.log(f"[{self.name}] got cipher key") + + self._send_packet(CUSTOM_PAYLOAD, + build_custom_payload(CIPHER_ACK_CHANNEL)) + iv_send = bytearray(self._cipher_iv) + iv_send[0] ^= 0x80 + self._send_cipher = CipherState(self._cipher_key, bytes(iv_send)) + + del scan_buf[:idx + CIPHER_KEY_TOTAL] + + # Look for MC|COn + if self._cipher_key and not self._recv_cipher: + idx = scan_buf.find(CIPHER_ON_PATTERN) + if idx >= 0: + self._recv_cipher = CipherState(self._cipher_key, self._cipher_iv) + self.log(f"[{self.name}] cipher active") + return + + if not self._cipher_key: + self.log(f"[{self.name}] no cipher (server has it disabled)") + + def _read_until_packet(self, expected_id: int, timeout: float) -> bool: + """Read frames until we get the expected packet ID or timeout.""" + deadline = time.time() + timeout + while time.time() < deadline: + try: + chunk = self._sock.recv(65536) + if not chunk: + return False + self._recv_buf.extend(chunk) + except socket.timeout: + continue + except OSError: + return False + + while len(self._recv_buf) >= 4: + payload_len = struct.unpack(">I", self._recv_buf[:4])[0] + total = 4 + payload_len + if len(self._recv_buf) < total: + break + + raw_payload = bytes(self._recv_buf[4:total]) + del self._recv_buf[:total] + + if self._recv_cipher: + raw_payload = self._recv_cipher.process(raw_payload) + + if not raw_payload: + continue + + packet_id = raw_payload[0] + data = raw_payload[1:] + + if packet_id == expected_id: + if packet_id == PRE_LOGIN: + try: + parsed = parse_prelogin_response(data) + self.log(f"[{self.name}] PreLogin: {parsed['player_count']} online") + except Exception: + pass + return True + elif packet_id == LOGIN: + try: + dis = DataInputStream(data) + parsed = parse_login_response_stream(dis) + self._entity_id = parsed["entity_id"] + except Exception: + pass + return True + + elif packet_id == DISCONNECT: + try: + dis = DataInputStream(data) + reason_id = dis.read_int() + reason = DISCONNECT_REASONS.get(reason_id, f"Unknown({reason_id})") + self.log(f"[{self.name}] kicked during login: {reason}") + except Exception: + pass + return False + + return False + + def _drain_frames(self) -> None: + """Drain and discard all complete frames from recv buffer.""" + while len(self._recv_buf) >= 4: + payload_len = struct.unpack(">I", self._recv_buf[:4])[0] + total = 4 + payload_len + if len(self._recv_buf) < total: + break + + raw_payload = bytes(self._recv_buf[4:total]) + del self._recv_buf[:total] + + if self._recv_cipher: + raw_payload = self._recv_cipher.process(raw_payload) + + if not raw_payload: + continue + + packet_id = raw_payload[0] + data = raw_payload[1:] + + # Handle identity tokens + if packet_id == CUSTOM_PAYLOAD: + self._handle_custom_payload(data) + + def _handle_custom_payload(self, data: bytes) -> None: + """Handle identity token packets.""" + try: + dis = DataInputStream(data) + channel = dis.read_utf() + length = dis.read_short() + payload = dis.read_raw(length) if length > 0 else b"" + + if channel == IDENTITY_TOKEN_ISSUE and len(payload) == 32: + self._identity_token = payload + self.log(f"[{self.name}] got identity token") + elif channel == IDENTITY_TOKEN_CHALLENGE: + if self._identity_token and len(self._identity_token) == 32: + self._send_packet(CUSTOM_PAYLOAD, + build_custom_payload(IDENTITY_TOKEN_RESPONSE, self._identity_token)) + else: + self._send_packet(CUSTOM_PAYLOAD, + build_custom_payload(IDENTITY_TOKEN_RESPONSE)) + self.log(f"[{self.name}] answered identity challenge") + except Exception: + pass + + def _send_packet(self, packet_id: int, payload: bytes) -> None: + raw = frame_packet(packet_id, payload) + if self._send_cipher: + header = raw[:4] + encrypted = self._send_cipher.process(raw[4:]) + raw = header + encrypted + try: + if self._sock: + self._sock.sendall(raw) + except OSError: + pass + + def _recv_exact(self, n: int) -> bytes: + data = b"" + while len(data) < n: + chunk = self._sock.recv(n - len(data)) + if not chunk: + raise ConnectionError("Connection closed during recv") + data += chunk + return data + + +# --------------------------------------------------------------------------- +# Orchestrator +# --------------------------------------------------------------------------- + +class StressTestRunner: + def __init__(self, host: str, port: int, max_bots: int, cycles: int, + hold_min: float, hold_max: float, ramp: float, + send_moves: bool, burst: int, duration: float, + quiet: bool): + self.host = host + self.port = port + self.max_bots = max_bots + self.cycles = cycles + self.hold_min = hold_min + self.hold_max = hold_max + self.ramp = ramp + self.send_moves = send_moves + self.burst = burst + self.duration = duration + self.quiet = quiet + self.stats = Stats() + self._active = threading.Semaphore(max_bots) + self._stop = threading.Event() + self._bot_counter = 0 + self._counter_lock = threading.Lock() + + def run(self) -> None: + self.stats.start_time = time.time() + logger.info(f"Stress test: {self.host}:{self.port}") + logger.info(f" max_bots={self.max_bots} cycles={self.cycles or 'infinite'} " + f"hold={self.hold_min}-{self.hold_max}s ramp={self.ramp}s " + f"burst={self.burst} moves={self.send_moves}") + if self.duration: + logger.info(f" duration={self.duration}s") + + # Status printer + status_thread = threading.Thread(target=self._print_status, daemon=True) + status_thread.start() + + # Duration timer + if self.duration > 0: + timer = threading.Timer(self.duration, self._stop.set) + timer.daemon = True + timer.start() + + cycle = 0 + try: + while not self._stop.is_set(): + if self.cycles > 0 and cycle >= self.cycles: + break + + # Spawn a burst of bots + threads = [] + for _ in range(self.burst): + if self._stop.is_set(): + break + if self.cycles > 0 and cycle >= self.cycles: + break + + self._active.acquire() + if self._stop.is_set(): + self._active.release() + break + + with self._counter_lock: + self._bot_counter += 1 + bot_name = f"StressBot{self._bot_counter}" + + xuid = secrets.randbits(62) | (1 << 32) + bot = StressBot( + name=bot_name, + host=self.host, + port=self.port, + xuid=xuid, + hold_min=self.hold_min, + hold_max=self.hold_max, + send_moves=self.send_moves, + stats=self.stats, + quiet=self.quiet, + ) + + t = threading.Thread( + target=self._run_bot, args=(bot,), daemon=True) + t.start() + threads.append(t) + cycle += 1 + + # Ramp delay between bursts + if self.ramp > 0 and not self._stop.is_set(): + self._stop.wait(self.ramp) + + except KeyboardInterrupt: + logger.info("\nInterrupted by user") + self._stop.set() + + # Wait for active bots to finish + logger.info("Waiting for active bots to finish...") + for _ in range(self.max_bots): + self._active.acquire(timeout=30) + + logger.info(f"\nFinal: {self.stats.summary()}") + + def _run_bot(self, bot: StressBot) -> None: + try: + bot.run() + finally: + self._active.release() + + def _print_status(self) -> None: + while not self._stop.is_set(): + self._stop.wait(5.0) + if not self._stop.is_set(): + logger.info(self.stats.summary()) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="LCE Server Stress Testing Tool", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("host", nargs="?", default="127.0.0.1", + help="Server hostname or IP (default: 127.0.0.1)") + parser.add_argument("port", nargs="?", type=int, default=19132, + help="Server port (default: 19132)") + parser.add_argument("--bots", type=int, default=8, + help="Max concurrent bots (default: 8)") + parser.add_argument("--cycles", type=int, default=50, + help="Connect/disconnect cycles (default: 50, 0=infinite)") + parser.add_argument("--hold", type=float, nargs=2, default=[2.0, 10.0], + metavar=("MIN", "MAX"), + help="Hold time range in seconds (default: 2 10)") + parser.add_argument("--ramp", type=float, default=0.5, + help="Delay between spawning bots (default: 0.5)") + parser.add_argument("--move", action="store_true", + help="Bots send movement packets while connected") + parser.add_argument("--burst", type=int, default=1, + help="Bots to spawn simultaneously (default: 1)") + parser.add_argument("--duration", type=float, default=0, + help="Run for N seconds then stop (default: unlimited)") + parser.add_argument("--quiet", action="store_true", + help="Suppress per-bot log messages") + + args = parser.parse_args() + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(message)s", + datefmt="%H:%M:%S", + ) + + runner = StressTestRunner( + host=args.host, + port=args.port, + max_bots=args.bots, + cycles=args.cycles, + hold_min=args.hold[0], + hold_max=args.hold[1], + ramp=args.ramp, + send_moves=args.move, + burst=args.burst, + duration=args.duration, + quiet=args.quiet, + ) + runner.run() + + +if __name__ == "__main__": + main() diff --git a/tools/stress-test/test_aggressive.bat b/tools/stress-test/test_aggressive.bat new file mode 100644 index 00000000..128f0db1 --- /dev/null +++ b/tools/stress-test/test_aggressive.bat @@ -0,0 +1,8 @@ +@echo off +REM Aggressive: short holds, fast ramp - hammers the players.erase() path +set /p HOST="Server IP [127.0.0.1]: " || set HOST=127.0.0.1 +set /p PORT="Server Port [19132]: " || set PORT=19132 +if "%HOST%"=="" set HOST=127.0.0.1 +if "%PORT%"=="" set PORT=19132 +python "%~dp0stress_test.py" %HOST% %PORT% --bots 12 --hold 0.5 2 --ramp 0.2 +pause diff --git a/tools/stress-test/test_basic.bat b/tools/stress-test/test_basic.bat new file mode 100644 index 00000000..1e8ce39e --- /dev/null +++ b/tools/stress-test/test_basic.bat @@ -0,0 +1,8 @@ +@echo off +REM Basic connect/disconnect churn - 50 cycles, 8 concurrent bots +set /p HOST="Server IP [127.0.0.1]: " || set HOST=127.0.0.1 +set /p PORT="Server Port [19132]: " || set PORT=19132 +if "%HOST%"=="" set HOST=127.0.0.1 +if "%PORT%"=="" set PORT=19132 +python "%~dp0stress_test.py" %HOST% %PORT% +pause diff --git a/tools/stress-test/test_burst.bat b/tools/stress-test/test_burst.bat new file mode 100644 index 00000000..984413db --- /dev/null +++ b/tools/stress-test/test_burst.bat @@ -0,0 +1,8 @@ +@echo off +REM Burst joins - 4 bots connect simultaneously +set /p HOST="Server IP [127.0.0.1]: " || set HOST=127.0.0.1 +set /p PORT="Server Port [19132]: " || set PORT=19132 +if "%HOST%"=="" set HOST=127.0.0.1 +if "%PORT%"=="" set PORT=19132 +python "%~dp0stress_test.py" %HOST% %PORT% --burst 4 --hold 1 3 +pause diff --git a/tools/stress-test/test_endurance.bat b/tools/stress-test/test_endurance.bat new file mode 100644 index 00000000..f984b8e2 --- /dev/null +++ b/tools/stress-test/test_endurance.bat @@ -0,0 +1,8 @@ +@echo off +REM Endurance: run all patterns for 5 minutes continuously +set /p HOST="Server IP [127.0.0.1]: " || set HOST=127.0.0.1 +set /p PORT="Server Port [25565]: " || set PORT=25565 +if "%HOST%"=="" set HOST=127.0.0.1 +if "%PORT%"=="" set PORT=19132 +python "%~dp0stress_test.py" %HOST% %PORT% --cycles 0 --duration 300 --bots 512 --burst 32 --move --hold 10 30 --ramp 0.1 --quiet +pause diff --git a/tools/stress-test/test_movement.bat b/tools/stress-test/test_movement.bat new file mode 100644 index 00000000..2af245ee --- /dev/null +++ b/tools/stress-test/test_movement.bat @@ -0,0 +1,8 @@ +@echo off +REM Movement packets - triggers the "moved wrongly" code path +set /p HOST="Server IP [127.0.0.1]: " || set HOST=127.0.0.1 +set /p PORT="Server Port [19132]: " || set PORT=19132 +if "%HOST%"=="" set HOST=127.0.0.1 +if "%PORT%"=="" set PORT=19132 +python "%~dp0stress_test.py" %HOST% %PORT% --move --hold 5 15 +pause