From c2356bf29ea3d84c658958b29f03c8f46c4812bd Mon Sep 17 00:00:00 2001 From: Sneexy <60184397+SnenxyTengoku@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:10:47 -0500 Subject: [PATCH 01/10] Minor docker/container fixes and edits (#1454) --- docker-compose.dedicated-server.ghcr.yml | 7 +++++-- docker-compose.dedicated-server.yml | 7 +++++-- docker/dedicated-server/entrypoint.sh | 5 ++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docker-compose.dedicated-server.ghcr.yml b/docker-compose.dedicated-server.ghcr.yml index 8bcaf484..7a9a95b2 100644 --- a/docker-compose.dedicated-server.ghcr.yml +++ b/docker-compose.dedicated-server.ghcr.yml @@ -1,12 +1,12 @@ services: minecraft-lce-dedicated-server: - image: ghcr.io/kuwacom/minecraft-lce-dedicated-server:nightly + image: ghcr.io/smartcmd/minecraft-lce-dedicated-server:nightly container_name: minecraft-lce-dedicated-server restart: unless-stopped tty: true stdin_open: true environment: - TZ: ${TZ:-Asia/Tokyo} + TZ: ${TZ:-Etc/UTC} WINEARCH: win64 WINEPREFIX: /var/opt/wineprefix64 WINEDEBUG: -all @@ -15,6 +15,9 @@ services: # minimum required virtual screen XVFB_DISPLAY: ${XVFB_DISPLAY:-:99} XVFB_SCREEN: ${XVFB_SCREEN:-720x1280x16} + # ip & port the server will run on + SERVER_BIND_IP: ${SERVER_BIND_IP:-0.0.0.0} + SERVER_PORT: ${SERVER_PORT:-25565} volumes: # - wineprefix64:/var/opt/wineprefix64 - ./server-data:/srv/persist diff --git a/docker-compose.dedicated-server.yml b/docker-compose.dedicated-server.yml index 4a0d3313..3dbe8bf6 100644 --- a/docker-compose.dedicated-server.yml +++ b/docker-compose.dedicated-server.yml @@ -10,7 +10,7 @@ services: tty: true stdin_open: true environment: - TZ: ${TZ:-Asia/Tokyo} + TZ: ${TZ:-Etc/UTC} WINEARCH: win64 WINEPREFIX: /var/opt/wineprefix64 WINEDEBUG: -all @@ -18,7 +18,10 @@ services: SERVER_CLI_INPUT_MODE: ${SERVER_CLI_INPUT_MODE:-stream} # minimum required virtual screen XVFB_DISPLAY: ${XVFB_DISPLAY:-:99} - XVFB_SCREEN: ${XVFB_SCREEN:-64x64x16} + XVFB_SCREEN: ${XVFB_SCREEN:-720x1280x16} + # ip & port the server will run on + SERVER_BIND_IP: ${SERVER_BIND_IP:-0.0.0.0} + SERVER_PORT: ${SERVER_PORT:-25565} volumes: # - wineprefix64:/var/opt/wineprefix64 - ./server-data:/srv/persist diff --git a/docker/dedicated-server/entrypoint.sh b/docker/dedicated-server/entrypoint.sh index 26b40da9..0eece274 100644 --- a/docker/dedicated-server/entrypoint.sh +++ b/docker/dedicated-server/entrypoint.sh @@ -3,9 +3,8 @@ set -euo pipefail SERVER_DIR="/srv/mc" SERVER_EXE="Minecraft.Server.exe" -# ip & port are fixed since they run inside the container -SERVER_PORT="25565" -SERVER_BIND_IP="0.0.0.0" +SERVER_PORT="${SERVER_PORT:-25565}" +SERVER_BIND_IP="${SERVER_BIND_IP:-0.0.0.0}" PERSIST_DIR="/srv/persist" WINE_CMD="" From c4c4c08b960276d1ecf10fcd18d6ae77ff90841b Mon Sep 17 00:00:00 2001 From: Sylvessa <225480449+sylvessa@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:01:27 -0500 Subject: [PATCH 02/10] Revert "Update actions workflows and add clang format check for PRs (#1418)" This reverts commit 38d58f2d8bb8af5516671b42940279d4e582d9c7 due to it causing spam and other problems with pull requests. --- .github/workflows/clang-format.yml | 48 ---------------------------- .github/workflows/nightly-server.yml | 11 +++---- .github/workflows/nightly.yml | 11 +++---- .github/workflows/pull-request.yml | 10 +++--- 4 files changed, 14 insertions(+), 66 deletions(-) delete mode 100644 .github/workflows/clang-format.yml diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml deleted file mode 100644 index a01bada4..00000000 --- a/.github/workflows/clang-format.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Check formatting - -on: - pull_request: - paths: - - '**' - - '!.gitignore' - - '!*.md' - - '!.github/**' - - '.github/workflows/clang-format.yml' - -permissions: - contents: read - pull-requests: write - -jobs: - format-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - - name: Fetch base commit - run: git fetch origin ${{ github.event.pull_request.base.sha }} - - - name: Install clang-format-20 - run: | - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" - sudo apt-get install -y -qq clang-format-20 - - - uses: reviewdog/action-setup@v1 - - - name: Check formatting on changed lines - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git clang-format-20 --binary clang-format-20 \ - --diff ${{ github.event.pull_request.base.sha }} -- \ - '*.c' '*.cpp' '*.cc' '*.h' '*.hpp' \ - | reviewdog \ - -name="clang-format" \ - -f=diff \ - -reporter=github-pr-check \ - -fail-level=error \ - -filter-mode=added diff --git a/.github/workflows/nightly-server.yml b/.github/workflows/nightly-server.yml index 0fc20eb1..5450de9a 100644 --- a/.github/workflows/nightly-server.yml +++ b/.github/workflows/nightly-server.yml @@ -5,12 +5,11 @@ on: push: branches: - 'main' - paths: - - '**' - - '!.gitignore' - - '!*.md' - - '!.github/**' - - '.github/workflows/nightly-server.yml' + paths-ignore: + - '.gitignore' + - '*.md' + - '.github/**' + - '!.github/workflows/nightly-server.yml' permissions: contents: write diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a5b53be0..789db3e8 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -5,12 +5,11 @@ on: push: branches: - 'main' - paths: - - '**' - - '!.gitignore' - - '!*.md' - - '!.github/**' - - '.github/workflows/nightly.yml' + paths-ignore: + - '.gitignore' + - '*.md' + - '.github/**' + - '!.github/workflows/nightly.yml' permissions: contents: write diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3b5398a0..9d57f4b4 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,12 +4,10 @@ on: workflow_dispatch: pull_request: types: [opened, reopened, synchronize] - paths: - - '**' - - '!.gitignore' - - '!*.md' - - '!.github/**' - - '.github/workflows/pull-request.yml' + paths-ignore: + - '.gitignore' + - '*.md' + - '.github/*.md' jobs: build: From e4c08b8414f662a340005428dda81440a2ad140f Mon Sep 17 00:00:00 2001 From: Logan Gibson Date: Thu, 2 Apr 2026 00:43:33 -0700 Subject: [PATCH 03/10] Replace instances of hard-coded port in ports variable with SERVER_PORT variable in yml files (#1457) --- docker-compose.dedicated-server.ghcr.yml | 4 ++-- docker-compose.dedicated-server.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.dedicated-server.ghcr.yml b/docker-compose.dedicated-server.ghcr.yml index 7a9a95b2..f311f834 100644 --- a/docker-compose.dedicated-server.ghcr.yml +++ b/docker-compose.dedicated-server.ghcr.yml @@ -22,8 +22,8 @@ services: # - wineprefix64:/var/opt/wineprefix64 - ./server-data:/srv/persist ports: - - "25565:25565/tcp" - - "25565:25565/udp" + - "$SERVER_PORT:$SERVER_PORT/tcp" + - "$SERVER_PORT:$SERVER_PORT/udp" stop_grace_period: 30s # volumes: diff --git a/docker-compose.dedicated-server.yml b/docker-compose.dedicated-server.yml index 3dbe8bf6..23caf26f 100644 --- a/docker-compose.dedicated-server.yml +++ b/docker-compose.dedicated-server.yml @@ -26,8 +26,8 @@ services: # - wineprefix64:/var/opt/wineprefix64 - ./server-data:/srv/persist ports: - - "25565:25565/tcp" - - "25565:25565/udp" + - "$SERVER_PORT:$SERVER_PORT/tcp" + - "$SERVER_PORT:$SERVER_PORT/udp" stop_grace_period: 30s # volumes: From 8bf034354426d5e3c2bf30fd755136e78f976fc0 Mon Sep 17 00:00:00 2001 From: Sylvessa <225480449+sylvessa@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:16:31 -0500 Subject: [PATCH 04/10] fix: boat plr height & sneak height (#1459) --- Minecraft.Client/HumanoidModel.cpp | 2 +- Minecraft.Client/ItemInHandRenderer.cpp | 14 ++++++++++++++ Minecraft.Client/PlayerRenderer.cpp | 17 ++++++++++++++++- Minecraft.World/Boat.cpp | 2 +- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Minecraft.Client/HumanoidModel.cpp b/Minecraft.Client/HumanoidModel.cpp index c6c6b9f4..f44654c3 100644 --- a/Minecraft.Client/HumanoidModel.cpp +++ b/Minecraft.Client/HumanoidModel.cpp @@ -241,7 +241,7 @@ void HumanoidModel::setupAnim(float time, float r, float bob, float yRot, float if (riding) { - if(uiBitmaskOverrideAnim&(1<xRot += -HALF_PI * 0.4f; arm1->xRot += -HALF_PI * 0.4f; diff --git a/Minecraft.Client/ItemInHandRenderer.cpp b/Minecraft.Client/ItemInHandRenderer.cpp index 13d4fc20..749337da 100644 --- a/Minecraft.Client/ItemInHandRenderer.cpp +++ b/Minecraft.Client/ItemInHandRenderer.cpp @@ -286,6 +286,20 @@ void ItemInHandRenderer::renderItem(shared_ptr mob, shared_ptrgetAnimOverrideBitmask() & (1 << HumanoidModel::eAnim_SmallModel)) + { + if (mob->isRiding()) + { + std::shared_ptr ridingEntity = mob->riding; + if (ridingEntity != nullptr) // Safety check; + { + yo += 0.3f; // reverts the change in Boat.cpp for smaller models. + } + } + } glEnable(GL_RESCALE_NORMAL); glTranslatef(-xo, -yo, 0); diff --git a/Minecraft.Client/PlayerRenderer.cpp b/Minecraft.Client/PlayerRenderer.cpp index 23dff77f..bc4c61a7 100644 --- a/Minecraft.Client/PlayerRenderer.cpp +++ b/Minecraft.Client/PlayerRenderer.cpp @@ -199,11 +199,26 @@ void PlayerRenderer::render(shared_ptr _mob, double x, double y, double armorParts1->sneaking = armorParts2->sneaking = humanoidModel->sneaking = mob->isSneaking(); double yp = y - mob->heightOffset; - if (mob->isSneaking() && !mob->instanceof(eTYPE_LOCALPLAYER)) + if (mob->isSneaking()) { yp -= 2 / 16.0f; } + if (mob->getAnimOverrideBitmask() & (1 << HumanoidModel::eAnim_SmallModel)) + { + if (mob->isRiding()) + { + std::shared_ptr ridingEntity = mob->riding; + if (ridingEntity != nullptr) // Safety check; + { + if (ridingEntity->instanceof(eTYPE_BOAT)) + { + yp += 0.25f; // reverts the change in Boat.cpp for smaller models. + } + } + } + } + // Check if an idle animation is needed if(mob->getAnimOverrideBitmask()&(1< Date: Fri, 3 Apr 2026 22:01:48 +0200 Subject: [PATCH 05/10] Quality of life netherportal fix (#1337) * Working New portal checks fixed x axis portal with obsidian on z axis * Removed Debug code * Remove unnecessary code removed PortalTile:: from PortalTile::validPortalFrame * Remove more debug code --- Minecraft.World/PortalTile.cpp | 64 ++++++++++++++++++++++++---------- Minecraft.World/PortalTile.h | 1 + 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/Minecraft.World/PortalTile.cpp b/Minecraft.World/PortalTile.cpp index 5e664e65..1b64212b 100644 --- a/Minecraft.World/PortalTile.cpp +++ b/Minecraft.World/PortalTile.cpp @@ -67,21 +67,8 @@ bool PortalTile::isCubeShaped() return false; } -bool PortalTile::trySpawnPortal(Level *level, int x, int y, int z, bool actuallySpawn) +bool PortalTile::validPortalFrame(Level* level, int x, int y, int z, int xd, int zd, bool actuallySpawn) { - int xd = 0; - int zd = 0; - if (level->getTile(x - 1, y, z) == Tile::obsidian_Id || level->getTile(x + 1, y, z) == Tile::obsidian_Id) xd = 1; - if (level->getTile(x, y, z - 1) == Tile::obsidian_Id || level->getTile(x, y, z + 1) == Tile::obsidian_Id) zd = 1; - - if (xd == zd) return false; - - if (level->getTile(x - xd, y, z - zd) == 0) - { - x -= xd; - z -= zd; - } - for (int xx = -1; xx <= 2; xx++) { for (int yy = -1; yy <= 3; yy++) @@ -101,9 +88,7 @@ bool PortalTile::trySpawnPortal(Level *level, int x, int y, int z, bool actually } } } - - if( !actuallySpawn ) - return true; + if (!actuallySpawn) return true; for (int xx = 0; xx < 2; xx++) { @@ -112,9 +97,52 @@ bool PortalTile::trySpawnPortal(Level *level, int x, int y, int z, bool actually level->setTileAndData(x + xd * xx, y + yy, z + zd * xx, Tile::portalTile_Id, 0, Tile::UPDATE_CLIENTS); } } - + return true; +} +bool PortalTile::trySpawnPortal(Level *level, int x, int y, int z, bool actuallySpawn) +{ + int xd = 0; + int zd = 0; + if (level->getTile(x - 1, y, z) == Tile::obsidian_Id || level->getTile(x + 1, y, z) == Tile::obsidian_Id) xd = 1; + if (level->getTile(x, y, z - 1) == Tile::obsidian_Id || level->getTile(x, y, z + 1) == Tile::obsidian_Id) zd = 1; + + bool twoPosible = false; // two neth portals posible (x and z direction) + if (xd == zd) + { + if (xd == 1) twoPosible = true; + else return false; + } + + bool changedx = false; // changed x so it can be reverted if two portals are posible + if (level->getTile(x - xd, y, z) == 0) + { + changedx = true; + x--; + } + else if (level->getTile(x, y, z - zd) == 0 && !twoPosible) + { + z--; + } + + if (!twoPosible) + { + if (!validPortalFrame(level, x, y, z, xd, zd, actuallySpawn)) return false; + } + else + { + if (!validPortalFrame(level, x, y, z, xd, 0, actuallySpawn)) + { + if (changedx) x++; // revert x (this check wants to check z not x and z) + + if (level->getTile(x, y, z - zd) == 0) z--; + + if (!validPortalFrame(level, x, y, z, 0, zd, actuallySpawn)) + return false; + } + } + return true; } void PortalTile::neighborChanged(Level *level, int x, int y, int z, int type) diff --git a/Minecraft.World/PortalTile.h b/Minecraft.World/PortalTile.h index 7be4320f..7415efbf 100644 --- a/Minecraft.World/PortalTile.h +++ b/Minecraft.World/PortalTile.h @@ -13,6 +13,7 @@ public: virtual void updateShape(LevelSource *level, int x, int y, int z, int forceData = -1, shared_ptr forceEntity = shared_ptr()); // 4J added forceData, forceEntity param virtual bool isSolidRender(bool isServerLevel = false); virtual bool isCubeShaped(); + virtual bool validPortalFrame(Level* level, int x, int y, int z, int xd, int zd, bool actuallySpawn); virtual bool trySpawnPortal(Level *level, int x, int y, int z, bool actuallySpawn); virtual void neighborChanged(Level *level, int x, int y, int z, int type); virtual bool shouldRenderFace(LevelSource *level, int x, int y, int z, int face); From 5e5849102f713f9bd4bd7fe6b5bf3684c0ac6bdc Mon Sep 17 00:00:00 2001 From: Loki Date: Fri, 3 Apr 2026 16:55:20 -0500 Subject: [PATCH 06/10] Add info about backports we will accept. --- BACKPORTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 BACKPORTING.md diff --git a/BACKPORTING.md b/BACKPORTING.md new file mode 100644 index 00000000..484fb3d2 --- /dev/null +++ b/BACKPORTING.md @@ -0,0 +1,15 @@ +# Approach to Backported Features +All backported features incorperated into MinecraftConsoles should be, when merged, functionally identical to their state in the version of the game we're currently targeting. This should be in reference to a known 4J build of LCE. Verification can either be done by doing a decompilation based match of the implementation or, alternatively, all functionality and limitations of the given feature should be compared against the version of LCE we're targeting. + +# Approach to Bugfixes +Anything that does not behave in an "expected" manner, especially if its behavior is not widely accepted as a gameplay mechanic, is valid for fixing in our repository. This includes bugfixes that were made in versions past the version we target, but excludes any visual changes that may not have been included at the build we're targeting. + +If you provide a visual bugfix that fixes a distinctive quirk of the LCE renderer, it should be provided in an "off by default" state that can be toggled on in-game by the user. There is no guarantee that we will merge it. + +If your visual bugfix is a fix added in a future version of LCE than the one we're targeting, it should also be put behind a toggle or equivalent system for keeping it off by default. + +# Targeted Version +We are currently accepting backports for up to and including TU24. Feature backports from TU25 and above will not be accepted. + +# Original Codebase +MinecraftConsoles is based on a WIP build of TU19, built on top of the December 2014 codebase. From 7dfe46aa5de55cc643ff7fc007369fd9a8275842 Mon Sep 17 00:00:00 2001 From: Loki Date: Fri, 3 Apr 2026 16:56:05 -0500 Subject: [PATCH 07/10] Add backporting link to Contributor's Guide --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 855491da..fd5d7f03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,9 @@ # Scope of Project At the moment, this project's scope is generally limited outside of adding new content to the game (blocks, mobs, items). We are currently prioritizing stability, quality of life, and platform support over these things. +## Backporting +If you're backporting a feature, please read [BACKPORTING.md](./BACKPORTING.md) + ## Parity We are attempting to keep our version of LCE as close to visual and experience parity with the original console experience of LCE as possible. This means that we will not be accepting changes that... - Backport things from Java Edition that did not ever exist in LCE From a4e689095ce3d7dacf7dfc636890eee6f5c9caae Mon Sep 17 00:00:00 2001 From: Sylvessa <225480449+sylvessa@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:04:02 -0500 Subject: [PATCH 08/10] fix: wither and ender dragon custom names (#1472) --- Minecraft.Client/EnderDragonRenderer.cpp | 5 +++++ Minecraft.Client/WitherBossRenderer.cpp | 4 ++++ Minecraft.World/EnderDragon.h | 2 +- Minecraft.World/WitherBoss.h | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/EnderDragonRenderer.cpp b/Minecraft.Client/EnderDragonRenderer.cpp index 8b5b248c..424bdff8 100644 --- a/Minecraft.Client/EnderDragonRenderer.cpp +++ b/Minecraft.Client/EnderDragonRenderer.cpp @@ -89,6 +89,11 @@ void EnderDragonRenderer::render(shared_ptr _mob, double x, double y, do // 4J - dynamic cast required because we aren't using templates/generics in our version shared_ptr mob = dynamic_pointer_cast(_mob); BossMobGuiInfo::setBossHealth(mob, false); + if (!mob->getCustomName().empty()) + { + BossMobGuiInfo::name = mob->getCustomName(); + } + MobRenderer::render(mob, x, y, z, rot, a); if (mob->nearestCrystal != nullptr) { diff --git a/Minecraft.Client/WitherBossRenderer.cpp b/Minecraft.Client/WitherBossRenderer.cpp index e358c6da..dc1d8f92 100644 --- a/Minecraft.Client/WitherBossRenderer.cpp +++ b/Minecraft.Client/WitherBossRenderer.cpp @@ -20,6 +20,10 @@ void WitherBossRenderer::render(shared_ptr entity, double x, double y, d shared_ptr mob = dynamic_pointer_cast(entity); BossMobGuiInfo::setBossHealth(mob, true); + if (!mob->getCustomName().empty()) + { + BossMobGuiInfo::name = mob->getCustomName(); + } int modelVersion = dynamic_cast(model)->modelVersion(); if (modelVersion != this->modelVersion) diff --git a/Minecraft.World/EnderDragon.h b/Minecraft.World/EnderDragon.h index 9f39767e..1bb8f5ce 100644 --- a/Minecraft.World/EnderDragon.h +++ b/Minecraft.World/EnderDragon.h @@ -183,7 +183,7 @@ public: double getHeadPartYRotDiff(int partIndex, doubleArray bodyPos, doubleArray partPos); Vec3 *getHeadLookVector(float a); - virtual wstring getAName() { return app.GetString(IDS_ENDERDRAGON); }; + virtual wstring getAName() { if (hasCustomName()) return getCustomName(); return app.GetString(IDS_ENDERDRAGON); }; virtual float getHealth() { return LivingEntity::getHealth(); }; virtual float getMaxHealth() { return LivingEntity::getMaxHealth(); }; }; diff --git a/Minecraft.World/WitherBoss.h b/Minecraft.World/WitherBoss.h index 47e2dff1..e066908e 100644 --- a/Minecraft.World/WitherBoss.h +++ b/Minecraft.World/WitherBoss.h @@ -105,5 +105,5 @@ public: // 4J Stu - These are required for the BossMob interface virtual float getMaxHealth() { return Monster::getMaxHealth(); }; virtual float getHealth() { return Monster::getHealth(); }; - virtual wstring getAName() { return app.GetString(IDS_WITHER); }; + virtual wstring getAName() { if (hasCustomName()) return getCustomName(); return app.GetString(IDS_WITHER); }; }; \ No newline at end of file From c2ea1fa62b422ae682c1adf41677b937cd0df13e Mon Sep 17 00:00:00 2001 From: Sylvessa <225480449+sylvessa@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:51:37 -0500 Subject: [PATCH 09/10] optimization: remove redundant lastTime setter (#1479) --- Minecraft.Client/MinecraftServer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 27ee68b6..1e3ed74e 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -1795,7 +1795,6 @@ void MinecraftServer::run(int64_t seed, void *lpParameter) chunkPacketManagement_PostTick(); } - lastTime = getCurrentTimeMillis(); // int64_t afterall = System::currentTimeMillis(); // PIXReportCounter(L"Server time all",(float)(afterall-beforeall)); // PIXReportCounter(L"Server ticks",(float)tickcount); From 034c313ddf3ed6539eac91c9f320ff588ee3111d Mon Sep 17 00:00:00 2001 From: Sylvessa <225480449+sylvessa@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:57:43 -0500 Subject: [PATCH 10/10] optimization: async dedicated server autosaving (#1473) --- Minecraft.Server/Windows64/ServerMain.cpp | 13 +++ Minecraft.Server/cmake/sources/Common.cmake | 2 + Minecraft.World/ConsoleSaveFileOriginal.cpp | 123 ++++++++++++++++++++ Minecraft.World/ConsoleSaveFileOriginal.h | 5 + 4 files changed, 143 insertions(+) diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index a8d5fc66..c54bd385 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -28,6 +28,7 @@ #include "../../Minecraft.World/TilePos.h" #include "../../Minecraft.World/compression.h" #include "../../Minecraft.World/OldChunkStorage.h" +#include "../../Minecraft.World/ConsoleSaveFileOriginal.h" #include "../../Minecraft.World/net.minecraft.world.level.tile.h" #include "../../Minecraft.World/Random.h" @@ -325,6 +326,7 @@ static void TickCoreSystems() g_NetworkManager.DoWork(); ProfileManager.Tick(); StorageManager.Tick(); + ConsoleSaveFileOriginal::flushPendingBackgroundSave(); } /** @@ -701,9 +703,20 @@ int main(int argc, char **argv) { C4JThread waitThread(&WaitForServerStoppedThreadProc, NULL, "WaitServerStopped"); waitThread.Run(); + while (waitThread.isRunning()) + { + TickCoreSystems(); + Sleep(10); + } waitThread.WaitForCompletion(INFINITE); } + while (ConsoleSaveFileOriginal::hasPendingBackgroundSave()) + { + TickCoreSystems(); + Sleep(10); + } + LogInfof("shutdown", "Cleaning up and exiting."); WinsockNetLayer::Shutdown(); LogDebugf("shutdown", "Network layer shutdown complete."); diff --git a/Minecraft.Server/cmake/sources/Common.cmake b/Minecraft.Server/cmake/sources/Common.cmake index 06aa0bfe..1eaceee1 100644 --- a/Minecraft.Server/cmake/sources/Common.cmake +++ b/Minecraft.Server/cmake/sources/Common.cmake @@ -494,6 +494,8 @@ set(_MINECRAFT_SERVER_COMMON_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/iob_shim.asm" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/stdafx.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/stubs.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.World/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" diff --git a/Minecraft.World/ConsoleSaveFileOriginal.cpp b/Minecraft.World/ConsoleSaveFileOriginal.cpp index 8a7d1f9e..4d03c955 100644 --- a/Minecraft.World/ConsoleSaveFileOriginal.cpp +++ b/Minecraft.World/ConsoleSaveFileOriginal.cpp @@ -12,6 +12,26 @@ #include "..\Minecraft.Client\Common\GameRules\LevelGenerationOptions.h" #include "..\Minecraft.World\net.minecraft.world.level.chunk.storage.h" +#ifdef MINECRAFT_SERVER_BUILD +#include +#include +#include + +static std::atomic s_bgSaveActive{false}; +static std::mutex s_bgSaveMutex; + +struct BackgroundSaveResult +{ + ConsoleSaveFile *owner = nullptr; + PBYTE thumbData = nullptr; + DWORD thumbSize = 0; + BYTE textMeta[88] = {}; + int textMetaBytes = 0; + bool pending = false; +}; +static BackgroundSaveResult s_bgResult; +#endif + #ifdef _XBOX #define RESERVE_ALLOCATION MEM_RESERVE | MEM_LARGE_PAGES @@ -673,6 +693,83 @@ void ConsoleSaveFileOriginal::Flush(bool autosave, bool updateThumbnail ) unsigned int fileSize = header.GetFileSize(); +#ifdef MINECRAFT_SERVER_BUILD + // on the server we dont want to block the tick thread doing compression!!! + // sna[pshot pvSaveMem while we still hold the lock then hand it off to a background thread + byte *snap = new (std::nothrow) byte[fileSize]; + if (snap) + { + // copy the save buffer while we still own the lock so nothing can write to it mid-copy + QueryPerformanceCounter(&qwTime); + memcpy(snap, pvSaveMem, fileSize); + QueryPerformanceCounter(&qwNewTime); + app.DebugPrintf("snapshot %u bytes in %.3f sec\n", fileSize, + (qwNewTime.QuadPart - qwTime.QuadPart) * fSecsPerTick); + + PBYTE thumb = nullptr; + DWORD thumbSz = 0; + app.GetSaveThumbnail(&thumb, &thumbSz); + + BYTE meta[88]; + ZeroMemory(meta, 88); + int64_t seed = 0; + bool hasSeed = false; + if (MinecraftServer *sv = MinecraftServer::getInstance(); sv && sv->levels[0]) + { + seed = sv->levels[0]->getLevelData()->getSeed(); + hasSeed = true; + } + int metaLen = app.CreateImageTextData(meta, seed, hasSeed, + app.GetGameHostOption(eGameHostOption_All), Minecraft::GetInstance()->getCurrentTexturePackId()); + + // telemetry + INT uid = 0; + StorageManager.GetSaveUniqueNumber(&uid); + TelemetryManager->RecordLevelSaveOrCheckpoint(ProfileManager.GetPrimaryPad(), uid, fileSize); + + ReleaseSaveAccess(); + s_bgSaveActive.store(true, std::memory_order_release); + + std::thread([snap, fileSize, thumb, thumbSz, meta, metaLen, this]() { + unsigned int compLen = fileSize + 8; + byte *buf = static_cast(StorageManager.AllocateSaveData(compLen)); + if (!buf) + { + // FAIL!! attempt precalc + compLen = 0; + Compression::getCompression()->Compress(nullptr, &compLen, snap, fileSize); + compLen += 8; + buf = static_cast(StorageManager.AllocateSaveData(compLen)); + } + if (buf) + { + // COM,PRESS + Compression::getCompression()->Compress(buf + 8, &compLen, snap, fileSize); + ZeroMemory(buf, 8); + memcpy(buf + 4, &fileSize, sizeof(fileSize)); + + // store the result so flushPendingBackgroundSave() can pick it up on the main thread next tick + // StorageManager isnt thread safe so we cant call SetSaveImages or SaveSaveData from here. Bwoomp + std::lock_guard lk(s_bgSaveMutex); + s_bgResult.owner = this; + s_bgResult.thumbData = thumb; + s_bgResult.thumbSize = thumbSz; + memcpy(s_bgResult.textMeta, meta, sizeof(meta)); + s_bgResult.textMetaBytes = metaLen; + s_bgResult.pending = true; + } + else + { + app.DebugPrintf("save buf alloc failed\n"); + s_bgSaveActive.store(false, std::memory_order_release); + } + delete[] snap; + }).detach(); + return; + } + app.DebugPrintf("snapshot alloc failed (%u bytes)\n", fileSize); +#endif + // Assume that the compression will make it smaller so initially attempt to allocate the current file size // We add 4 bytes to the start so that we can signal compressed data // And another 4 bytes to store the decompressed data size @@ -838,6 +935,10 @@ int ConsoleSaveFileOriginal::SaveSaveDataCallback(LPVOID lpParam,bool bRes) { ConsoleSaveFile *pClass=static_cast(lpParam); +#ifdef MINECRAFT_SERVER_BUILD + s_bgSaveActive.store(false, std::memory_order_release); +#endif + return 0; } @@ -1085,3 +1186,25 @@ void *ConsoleSaveFileOriginal::getWritePointer(FileEntry *file) { return static_cast(pvSaveMem) + file->currentFilePointer;; } + +#ifdef MINECRAFT_SERVER_BUILD +void ConsoleSaveFileOriginal::flushPendingBackgroundSave() +{ + std::lock_guard lk(s_bgSaveMutex); + if (!s_bgResult.pending) + return; + + StorageManager.SetSaveImages( + s_bgResult.thumbData, s_bgResult.thumbSize, + nullptr, 0, s_bgResult.textMeta, s_bgResult.textMetaBytes); + StorageManager.SaveSaveData(&ConsoleSaveFileOriginal::SaveSaveDataCallback, s_bgResult.owner); + + s_bgResult.pending = false; + // the actual write isnt done until SaveSaveDataCallback fires +} + +bool ConsoleSaveFileOriginal::hasPendingBackgroundSave() +{ + return s_bgSaveActive.load(std::memory_order_acquire); +} +#endif diff --git a/Minecraft.World/ConsoleSaveFileOriginal.h b/Minecraft.World/ConsoleSaveFileOriginal.h index 9c91fafc..453cacd9 100644 --- a/Minecraft.World/ConsoleSaveFileOriginal.h +++ b/Minecraft.World/ConsoleSaveFileOriginal.h @@ -77,6 +77,11 @@ public: virtual void LockSaveAccess(); virtual void ReleaseSaveAccess(); +#ifdef MINECRAFT_SERVER_BUILD + static void flushPendingBackgroundSave(); + static bool hasPendingBackgroundSave(); +#endif + virtual ESavePlatform getSavePlatform(); virtual bool isSaveEndianDifferent(); virtual void setLocalPlatform();