diff --git a/Linux/LinuxCompat.h b/Linux/LinuxCompat.h
index b4c36ba..a404f34 100644
--- a/Linux/LinuxCompat.h
+++ b/Linux/LinuxCompat.h
@@ -192,9 +192,18 @@ typedef union _LARGE_INTEGER {
#define VK_UP 0x26
#define VK_RIGHT 0x27
#define VK_DOWN 0x28
+#define VK_F1 0x70
+#define VK_F2 0x71
#define VK_F3 0x72
#define VK_F4 0x73
#define VK_F5 0x74
+#define VK_F6 0x75
+#define VK_F7 0x76
+#define VK_F8 0x77
+#define VK_F9 0x78
+#define VK_F10 0x79
+#define VK_F11 0x7A
+#define VK_F12 0x7B
#define VK_LSHIFT 0xA0
#define VK_LCONTROL 0xA2
#ifndef WHEEL_DELTA
diff --git a/Linux/PosixNetLayer.cpp b/Linux/PosixNetLayer.cpp
index cb9c74b..d7e685c 100644
--- a/Linux/PosixNetLayer.cpp
+++ b/Linux/PosixNetLayer.cpp
@@ -683,6 +683,8 @@ void WinsockNetLayer::HandleDataReceived(uint8_t fromSmallId, uint8_t toSmallId,
pthread_mutex_lock(&s_earlyDataLock);
s_earlyDataBuffers[fromSmallId].insert(
s_earlyDataBuffers[fromSmallId].end(), data, data + dataSize);
+ app.DebugPrintf("POSIX LAN: Buffered %u early bytes for smallId=%d (total=%d)\n",
+ dataSize, fromSmallId, (int)s_earlyDataBuffers[fromSmallId].size());
pthread_mutex_unlock(&s_earlyDataLock);
}
return;
@@ -698,6 +700,8 @@ void WinsockNetLayer::HandleDataReceived(uint8_t fromSmallId, uint8_t toSmallId,
pthread_mutex_lock(&s_earlyDataLock);
s_earlyDataBuffers[fromSmallId].insert(
s_earlyDataBuffers[fromSmallId].end(), data, data + dataSize);
+ app.DebugPrintf("POSIX LAN: Buffered %u bytes waiting for socket smallId=%d (total=%d)\n",
+ dataSize, fromSmallId, (int)s_earlyDataBuffers[fromSmallId].size());
pthread_mutex_unlock(&s_earlyDataLock);
}
}
@@ -722,6 +726,8 @@ void WinsockNetLayer::FlushPendingData()
::Socket *pSocket = pPlayer->GetSocket();
if (pSocket == NULL) continue;
+ app.DebugPrintf("POSIX LAN: Flushing %d early bytes for smallId=%d\n",
+ (int)s_earlyDataBuffers[i].size(), (int)i);
pSocket->pushDataToQueue(s_earlyDataBuffers[i].data(),
(DWORD)s_earlyDataBuffers[i].size(), false);
s_earlyDataBuffers[i].clear();
@@ -796,15 +802,6 @@ void* WinsockNetLayer::AcceptThreadProc(void* /*param*/)
continue;
}
- uint8_t assignBuf[1] = { assignedSmallId };
- ssize_t sent = send(clientSocket, (const char *)assignBuf, 1, MSG_NOSIGNAL);
- if (sent != 1)
- {
- app.DebugPrintf("Failed to send small ID to client\n");
- close(clientSocket);
- continue;
- }
-
int flags = fcntl(clientSocket, F_GETFL, 0);
fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK);
@@ -842,7 +839,17 @@ void* WinsockNetLayer::AcceptThreadProc(void* /*param*/)
pthread_mutex_lock(&s_pendingJoinLock);
s_pendingJoinSmallIds.push_back(assignedSmallId);
+ app.DebugPrintf("POSIX LAN: Queued pending join for smallId=%d\n", assignedSmallId);
pthread_mutex_unlock(&s_pendingJoinLock);
+
+ uint8_t assignBuf[1] = { assignedSmallId };
+ ssize_t sent = send(clientSocket, (const char *)assignBuf, 1, MSG_NOSIGNAL);
+ if (sent != 1)
+ {
+ app.DebugPrintf("POSIX LAN: Failed to send small ID to client smallId=%d\n", assignedSmallId);
+ MarkConnectionDisconnected(assignedSmallId);
+ continue;
+ }
}
return NULL;
}
@@ -863,7 +870,11 @@ bool WinsockNetLayer::ProcessRecvData(Win64RemoteConnection &conn)
((uint32_t)conn.recvBuffer[3]);
if (packetSize <= 0 || (unsigned int)packetSize > WIN64_NET_MAX_PACKET_SIZE)
+ {
+ app.DebugPrintf("POSIX LAN: Invalid packet size %d from smallId=%d\n",
+ packetSize, conn.smallId);
return false;
+ }
conn.currentPacketSize = packetSize;
conn.readingHeader = false;
@@ -889,6 +900,50 @@ bool WinsockNetLayer::ProcessRecvData(Win64RemoteConnection &conn)
return true;
}
+void WinsockNetLayer::MarkConnectionDisconnected(uint8_t smallId)
+{
+ bool shouldNotify = false;
+
+ pthread_mutex_lock(&s_connectionsLock);
+ if ((size_t)smallId < s_connections.size())
+ {
+ Win64RemoteConnection &conn = s_connections[smallId];
+ shouldNotify = conn.active;
+ conn.active = false;
+
+ if (conn.tcpSocket != INVALID_SOCKET)
+ {
+ if (s_epollFd >= 0)
+ epoll_ctl(s_epollFd, EPOLL_CTL_DEL, conn.tcpSocket, NULL);
+ shutdown(conn.tcpSocket, SHUT_RDWR);
+ close(conn.tcpSocket);
+ conn.tcpSocket = INVALID_SOCKET;
+ }
+
+ pthread_mutex_lock(&conn.sendBufLock);
+ conn.sendBufferUsed = 0;
+ pthread_mutex_unlock(&conn.sendBufLock);
+
+ conn.recvBufferUsed = 0;
+ conn.currentPacketSize = -1;
+ conn.readingHeader = true;
+ }
+ pthread_mutex_unlock(&s_connectionsLock);
+
+ pthread_mutex_lock(&s_earlyDataLock);
+ if ((size_t)smallId < s_earlyDataBuffers.size())
+ s_earlyDataBuffers[smallId].clear();
+ pthread_mutex_unlock(&s_earlyDataLock);
+
+ if (shouldNotify)
+ {
+ pthread_mutex_lock(&s_disconnectLock);
+ s_disconnectedSmallIds.push_back(smallId);
+ pthread_mutex_unlock(&s_disconnectLock);
+ app.DebugPrintf("POSIX LAN: Queued disconnect for smallId=%d\n", smallId);
+ }
+}
+
void* WinsockNetLayer::EpollThreadProc(void* /*param*/)
{
struct epoll_event events[WIN64_NET_EPOLL_MAX_EVENTS];
@@ -915,14 +970,7 @@ void* WinsockNetLayer::EpollThreadProc(void* /*param*/)
if (events[i].events & (EPOLLERR | EPOLLHUP))
{
- conn.active = false;
- epoll_ctl(s_epollFd, EPOLL_CTL_DEL, conn.tcpSocket, NULL);
- close(conn.tcpSocket);
- conn.tcpSocket = INVALID_SOCKET;
-
- pthread_mutex_lock(&s_disconnectLock);
- s_disconnectedSmallIds.push_back(smallId);
- pthread_mutex_unlock(&s_disconnectLock);
+ MarkConnectionDisconnected(smallId);
continue;
}
@@ -940,6 +988,7 @@ void* WinsockNetLayer::EpollThreadProc(void* /*param*/)
uint8_t *newBuf = (uint8_t *)realloc(conn.recvBuffer, newSize);
if (!newBuf)
{
+ app.DebugPrintf("POSIX LAN: Failed to grow receive buffer for smallId=%d\n", smallId);
disconnected = true;
break;
}
@@ -974,14 +1023,7 @@ void* WinsockNetLayer::EpollThreadProc(void* /*param*/)
if (disconnected)
{
- conn.active = false;
- epoll_ctl(s_epollFd, EPOLL_CTL_DEL, conn.tcpSocket, NULL);
- close(conn.tcpSocket);
- conn.tcpSocket = INVALID_SOCKET;
-
- pthread_mutex_lock(&s_disconnectLock);
- s_disconnectedSmallIds.push_back(smallId);
- pthread_mutex_unlock(&s_disconnectLock);
+ MarkConnectionDisconnected(smallId);
}
}
}
@@ -998,6 +1040,7 @@ void WinsockNetLayer::FlushSendBuffers()
if (!conn.active || conn.tcpSocket == INVALID_SOCKET)
continue;
+ bool disconnectAfterSendError = false;
pthread_mutex_lock(&conn.sendBufLock);
if (conn.sendBufferUsed > 0)
{
@@ -1007,7 +1050,11 @@ void WinsockNetLayer::FlushSendBuffers()
ssize_t sent = send(conn.tcpSocket, (const char *)conn.sendBuffer + totalSent,
conn.sendBufferUsed - totalSent, MSG_NOSIGNAL);
if (sent <= 0)
+ {
+ app.DebugPrintf("POSIX LAN: Send failed for smallId=%d errno=%d\n", (int)i, errno);
+ disconnectAfterSendError = true;
break;
+ }
totalSent += (int)sent;
}
if (totalSent < conn.sendBufferUsed && totalSent > 0)
@@ -1021,6 +1068,9 @@ void WinsockNetLayer::FlushSendBuffers()
}
}
pthread_mutex_unlock(&conn.sendBufLock);
+
+ if (disconnectAfterSendError)
+ MarkConnectionDisconnected((uint8_t)i);
}
pthread_mutex_unlock(&s_connectionsLock);
}
@@ -1036,6 +1086,8 @@ bool WinsockNetLayer::PopDisconnectedSmallId(uint8_t *outSmallId)
found = true;
}
pthread_mutex_unlock(&s_disconnectLock);
+ if (found)
+ app.DebugPrintf("POSIX LAN: Popped disconnect for smallId=%d\n", *outSmallId);
return found;
}
@@ -1044,6 +1096,7 @@ void WinsockNetLayer::PushFreeSmallId(uint8_t smallId)
pthread_mutex_lock(&s_freeSmallIdLock);
s_freeSmallIds.push_back(smallId);
pthread_mutex_unlock(&s_freeSmallIdLock);
+ app.DebugPrintf("POSIX LAN: Returned smallId=%d to free list\n", smallId);
}
bool WinsockNetLayer::PopPendingJoinSmallId(uint8_t *outSmallId)
@@ -1057,6 +1110,8 @@ bool WinsockNetLayer::PopPendingJoinSmallId(uint8_t *outSmallId)
found = true;
}
pthread_mutex_unlock(&s_pendingJoinLock);
+ if (found)
+ app.DebugPrintf("POSIX LAN: Popped pending join for smallId=%d\n", *outSmallId);
return found;
}
@@ -1068,21 +1123,8 @@ bool WinsockNetLayer::IsSmallIdConnected(uint8_t smallId)
void WinsockNetLayer::CloseConnectionBySmallId(uint8_t smallId)
{
- pthread_mutex_lock(&s_connectionsLock);
- if ((size_t)smallId < s_connections.size() && s_connections[smallId].active && s_connections[smallId].tcpSocket != INVALID_SOCKET)
- {
- epoll_ctl(s_epollFd, EPOLL_CTL_DEL, s_connections[smallId].tcpSocket, NULL);
- shutdown(s_connections[smallId].tcpSocket, SHUT_RDWR);
- close(s_connections[smallId].tcpSocket);
- s_connections[smallId].tcpSocket = INVALID_SOCKET;
- app.DebugPrintf("POSIX LAN: Force-closed TCP connection for smallId=%d\n", smallId);
- }
- pthread_mutex_unlock(&s_connectionsLock);
-
- pthread_mutex_lock(&s_earlyDataLock);
- if ((size_t)smallId < s_earlyDataBuffers.size())
- s_earlyDataBuffers[smallId].clear();
- pthread_mutex_unlock(&s_earlyDataLock);
+ MarkConnectionDisconnected(smallId);
+ app.DebugPrintf("POSIX LAN: Force-closed TCP connection for smallId=%d\n", smallId);
}
void* WinsockNetLayer::ClientRecvThreadProc(void* /*param*/)
diff --git a/Linux/PosixNetLayer.h b/Linux/PosixNetLayer.h
index 0073d42..1dcfbdb 100644
--- a/Linux/PosixNetLayer.h
+++ b/Linux/PosixNetLayer.h
@@ -154,6 +154,7 @@ private:
static void* DiscoveryThreadProc(void* param);
static void* AsyncJoinThreadProc(void* param);
static bool ProcessRecvData(Win64RemoteConnection &conn);
+ static void MarkConnectionDisconnected(uint8_t smallId);
static SOCKET s_listenSocket;
static SOCKET s_hostConnectionSocket;
diff --git a/Minecraft.Server.vcxproj b/Minecraft.Server.vcxproj
index 4d83b9e..266a23c 100644
--- a/Minecraft.Server.vcxproj
+++ b/Minecraft.Server.vcxproj
@@ -30,20 +30,20 @@
Application
MultiByte
- v110
+ v145
Application
MultiByte
- v110
+ v145
Makefile
- v143
+ v145
Makefile
- v143
+ v145
@@ -72,7 +72,8 @@
$(SolutionDir)x64_Server_Release\
x64_Server_Release\
$(ProjectDir)\..\Minecraft.World\x64headers;$(ProjectDir)\..\Minecraft.Client\Xbox\Sentient\Include;$(IncludePath)
-
+
+
$(SolutionDir)Linux_Server_Debug\
Linux_Server_Debug\
"$(ProjectDir)Linux\build.bat" Debug
@@ -93,7 +94,8 @@
__linux__;_DEDICATED_SERVER;_CONTENT_PACKAGE;_LARGE_WORLDS;_WINDOWS64;WITH_SERVER_CODE;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS
$(ProjectDir)Linux;$(ProjectDir)Linux\stubs;$(ProjectDir);$(ProjectDir)\..\Minecraft.Client;$(ProjectDir)\..\Minecraft.World;$(ProjectDir)\..\Minecraft.World\x64headers;$(ProjectDir)\..\Minecraft.Client\Windows64\Iggy\include;$(ProjectDir)\..\Minecraft.Client\Xbox\Sentient\Include
-std=c++11
-
+
+
Use
stdafx.h
@@ -703,4 +705,4 @@
-
+
\ No newline at end of file
diff --git a/Minecraft.Server.vcxproj.filters b/Minecraft.Server.vcxproj.filters
index 581a6bc..3254863 100644
--- a/Minecraft.Server.vcxproj.filters
+++ b/Minecraft.Server.vcxproj.filters
@@ -832,5 +832,104 @@
Source Files\Linux
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
-
+
\ No newline at end of file
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..da3db6e
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,51 @@
+# syntax=docker/dockerfile:1
+
+FROM debian:bookworm-slim AS build
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ build-essential \
+ ca-certificates \
+ cmake \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /src
+ARG BUILD_JOBS=2
+
+# Build with the LCEMP repository root as context:
+# docker build -f Minecraft.Server/docker/Dockerfile .
+COPY cmake ./cmake
+COPY Minecraft.Client ./Minecraft.Client
+COPY Minecraft.World ./Minecraft.World
+COPY Minecraft.Server ./Minecraft.Server
+
+RUN cmake -S Minecraft.Server -B /build -DCMAKE_BUILD_TYPE=Release \
+ && cmake --build /build --target MinecraftDedicatedServer --parallel "${BUILD_JOBS}" \
+ && strip /build/MinecraftDedicatedServer
+
+FROM debian:bookworm-slim AS runtime
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ ca-certificates \
+ libstdc++6 \
+ && rm -rf /var/lib/apt/lists/* \
+ && useradd --create-home --home-dir /home/lcemp --shell /usr/sbin/nologin lcemp \
+ && mkdir -p /data \
+ && chown -R lcemp:lcemp /data
+
+COPY --from=build /build/MinecraftDedicatedServer /usr/local/bin/MinecraftDedicatedServer
+COPY Minecraft.Server/docker/entrypoint.sh /usr/local/bin/lcemp-entrypoint
+
+RUN chmod +x /usr/local/bin/lcemp-entrypoint
+
+WORKDIR /data
+VOLUME ["/data"]
+
+EXPOSE 25565/tcp
+EXPOSE 25565/udp
+EXPOSE 25566/udp
+
+USER lcemp
+ENTRYPOINT ["lcemp-entrypoint"]
+CMD ["MinecraftDedicatedServer"]
diff --git a/docker/Dockerfile.dockerignore b/docker/Dockerfile.dockerignore
new file mode 100644
index 0000000..36a5d0f
--- /dev/null
+++ b/docker/Dockerfile.dockerignore
@@ -0,0 +1,33 @@
+.git
+.github
+.vs
+build
+build_linux
+build_dedicated
+ipch
+x64
+x64_Server_Debug
+x64_Server_Release
+
+**/.git
+**/.vs
+**/build
+**/build_linux
+**/build_dedicated
+**/ipch
+**/x64
+**/x64_Server_Debug
+**/x64_Server_Release
+**/*.idb
+**/*.ilk
+**/*.lastbuildstate
+**/*.log
+**/*.obj
+**/*.opensdf
+**/*.pch
+**/*.pdb
+**/*.sdf
+**/*.sln
+**/*.suo
+**/*.tlog
+**/*.user
diff --git a/docker/Dockerfile.prebuilt b/docker/Dockerfile.prebuilt
new file mode 100644
index 0000000..df20f97
--- /dev/null
+++ b/docker/Dockerfile.prebuilt
@@ -0,0 +1,28 @@
+# syntax=docker/dockerfile:1
+
+FROM debian:bookworm-slim AS runtime
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ ca-certificates \
+ libstdc++6 \
+ && rm -rf /var/lib/apt/lists/* \
+ && useradd --create-home --home-dir /home/lcemp --shell /usr/sbin/nologin lcemp \
+ && mkdir -p /data \
+ && chown -R lcemp:lcemp /data
+
+COPY build/MinecraftDedicatedServer /usr/local/bin/MinecraftDedicatedServer
+COPY Minecraft.Server/docker/entrypoint.sh /usr/local/bin/lcemp-entrypoint
+
+RUN chmod +x /usr/local/bin/MinecraftDedicatedServer /usr/local/bin/lcemp-entrypoint
+
+WORKDIR /data
+VOLUME ["/data"]
+
+EXPOSE 25565/tcp
+EXPOSE 25565/udp
+EXPOSE 25566/udp
+
+USER lcemp
+ENTRYPOINT ["lcemp-entrypoint"]
+CMD ["MinecraftDedicatedServer"]
diff --git a/docker/Dockerfile.prebuilt.dockerignore b/docker/Dockerfile.prebuilt.dockerignore
new file mode 100644
index 0000000..6924c2b
--- /dev/null
+++ b/docker/Dockerfile.prebuilt.dockerignore
@@ -0,0 +1,23 @@
+.git
+.github
+.vs
+build/*
+!build/MinecraftDedicatedServer
+Minecraft.Client
+Minecraft.World
+
+**/.git
+**/.vs
+**/*.idb
+**/*.ilk
+**/*.lastbuildstate
+**/*.log
+**/*.obj
+**/*.opensdf
+**/*.pch
+**/*.pdb
+**/*.sdf
+**/*.sln
+**/*.suo
+**/*.tlog
+**/*.user
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000..5e1f7fb
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,125 @@
+# Docker
+
+This directory builds and runs the Linux dedicated server in a container.
+
+Build from the LCEMP repository root, not from `Minecraft.Server`, because the server CMake project depends on sibling `Minecraft.Client`, `Minecraft.World`, and `cmake` directories.
+
+On Windows, use the build wrapper from `Minecraft.Server/docker`. It uses this order:
+
+1. Copy an existing Linux binary from `build/MinecraftDedicatedServer`.
+2. Download a GitHub release asset when `GITHUB_REPOSITORY` is configured.
+3. Fall back to the full Docker build.
+
+```powershell
+.\build-image.ps1
+```
+
+On Linux/macOS:
+
+```bash
+./build-image.sh
+```
+
+You can also call Docker directly for a full build:
+
+```bash
+docker build -f Minecraft.Server/docker/Dockerfile -t lcemp-server .
+```
+
+The Dockerfile defaults to `BUILD_JOBS=2` to avoid overloading Docker Desktop/WSL during the large C++ compile. Override it on stronger Linux hosts:
+
+```bash
+docker build -f Minecraft.Server/docker/Dockerfile -t lcemp-server --build-arg BUILD_JOBS=8 .
+```
+
+To force the wrapper to do a full build:
+
+```powershell
+.\build-image.ps1 -ForceFullBuild
+```
+
+To download a prebuilt binary from a public GitHub release before falling back to the full build:
+
+```powershell
+.\build-image.ps1 `
+ -GitHubRepo "owner/repo" `
+ -GitHubReleaseTag "latest" `
+ -GitHubAssetName "MinecraftDedicatedServer"
+```
+
+The shell wrapper uses equivalent environment variables:
+
+```bash
+GITHUB_REPOSITORY=owner/repo \
+GITHUB_RELEASE_TAG=latest \
+GITHUB_ASSET_NAME=MinecraftDedicatedServer \
+./build-image.sh
+```
+
+To build a runtime image from an already-built Linux binary directly:
+
+```bash
+docker build -f Minecraft.Server/docker/Dockerfile.prebuilt -t lcemp-server .
+```
+
+Run with a persistent data directory:
+
+```bash
+docker run --rm -it \
+ -p 25565:25565/tcp \
+ -p 25565:25565/udp \
+ -p 25566:25566/udp \
+ -v lcemp-data:/data \
+ -e MOTD="A Minecraft LCE Server" \
+ -e LEVEL_NAME="world" \
+ lcemp-server
+```
+
+Or use Compose from this directory:
+
+```bash
+docker compose up --build
+```
+
+To use Compose with an existing `build/MinecraftDedicatedServer` binary:
+
+```bash
+docker compose -f compose.yaml -f compose.prebuilt.yaml up --build
+```
+
+The entrypoint writes `/data/server.properties` from environment variables on every start, then launches `MinecraftDedicatedServer` from `/data` so generated worlds, lists, and server data persist in the mounted volume.
+
+## Environment Variables
+
+| Environment variable | server.properties key | Default |
+|---|---|---|
+| `SERVER_PORT` | `server-port` | `25565` |
+| `SERVER_IP` | `server-ip` | empty |
+| `LEVEL_NAME` | `level-name` | `world` |
+| `LEVEL_SEED` | `level-seed` | empty |
+| `LEVEL_SIZE` | `level-size` | `large` |
+| `GAMEMODE` | `gamemode` | `0` |
+| `DIFFICULTY` | `difficulty` | `2` |
+| `MAX_PLAYERS` | `max-players` | `8` |
+| `PVP` | `pvp` | `true` |
+| `TRUST_PLAYERS` | `trust-players` | `true` |
+| `FIRE_SPREADS` | `fire-spreads` | `true` |
+| `TNT_EXPLODES` | `tnt-explodes` | `true` |
+| `STRUCTURES` | `structures` | `true` |
+| `SPAWN_ANIMALS` | `spawn-animals` | `true` |
+| `SPAWN_NPCS` | `spawn-npcs` | `true` |
+| `ONLINE_MODE` | `online-mode` | `false` |
+| `SHOW_GAMERTAGS` | `show-gamertags` | `true` |
+| `MOTD` | `motd` | `A Minecraft LCE Server` |
+| `WHITE_LIST` | `white-list` | `false` |
+| `VOICE_CHAT` | `voice-chat` | `false` |
+| `ENABLE_CHAT` | `enable-chat` | `false` |
+| `ADVERTISE_LAN` | `advertise-lan` | `true` |
+
+`SERVER_PROPERTIES_FILE` can override the generated properties path. The default is `/data/server.properties`.
+
+LAN discovery uses UDP broadcast. If clients cannot discover the server through Docker port publishing, run the container with host networking on Linux:
+
+```bash
+docker run --rm -it --network host -v lcemp-data:/data lcemp-server
+```
diff --git a/docker/build-image.ps1 b/docker/build-image.ps1
new file mode 100644
index 0000000..e4d170d
--- /dev/null
+++ b/docker/build-image.ps1
@@ -0,0 +1,97 @@
+param(
+ [string]$Image = "lcemp-server:local",
+ [int]$BuildJobs = 2,
+ [string]$GitHubRepo = $env:GITHUB_REPOSITORY,
+ [string]$GitHubReleaseTag = $env:GITHUB_RELEASE_TAG,
+ [string]$GitHubAssetName = $(if ($env:GITHUB_ASSET_NAME) { $env:GITHUB_ASSET_NAME } else { "MinecraftDedicatedServer" }),
+ [switch]$ForceFullBuild
+)
+
+$ErrorActionPreference = "Stop"
+
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$RepoRoot = Resolve-Path (Join-Path $ScriptDir "../..")
+$PrebuiltBinary = Join-Path $RepoRoot "build/MinecraftDedicatedServer"
+
+function Invoke-DockerPrebuiltBuild {
+ Write-Host "Using prebuilt Linux server binary: $PrebuiltBinary"
+ Push-Location $RepoRoot
+ try {
+ docker build `
+ -f Minecraft.Server/docker/Dockerfile.prebuilt `
+ -t $Image `
+ .
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ } finally {
+ Pop-Location
+ }
+}
+
+function Invoke-FullDockerBuild {
+ Push-Location $RepoRoot
+ try {
+ docker build `
+ -f Minecraft.Server/docker/Dockerfile `
+ --build-arg BUILD_JOBS=$BuildJobs `
+ -t $Image `
+ .
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ } finally {
+ Pop-Location
+ }
+}
+
+function Try-DownloadGitHubBinary {
+ if ([string]::IsNullOrWhiteSpace($GitHubRepo)) {
+ return $false
+ }
+
+ $releasePath = if ([string]::IsNullOrWhiteSpace($GitHubReleaseTag) -or $GitHubReleaseTag -eq "latest") {
+ "latest"
+ } else {
+ "tags/$GitHubReleaseTag"
+ }
+
+ $releaseUrl = "https://api.github.com/repos/$GitHubRepo/releases/$releasePath"
+ Write-Host "Looking for GitHub release asset '$GitHubAssetName' at $releaseUrl"
+
+ try {
+ $headers = @{
+ Accept = "application/vnd.github+json"
+ "X-GitHub-Api-Version" = "2022-11-28"
+ "User-Agent" = "lcemp-docker-build"
+ }
+ $release = Invoke-RestMethod -Uri $releaseUrl -Headers $headers
+ $asset = $release.assets | Where-Object { $_.name -eq $GitHubAssetName } | Select-Object -First 1
+ if (-not $asset) {
+ Write-Host "GitHub release found, but asset '$GitHubAssetName' was not present."
+ return $false
+ }
+
+ $buildDir = Split-Path -Parent $PrebuiltBinary
+ New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
+ Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $PrebuiltBinary -Headers @{ "User-Agent" = "lcemp-docker-build" }
+ Write-Host "Downloaded Linux server binary from GitHub: $($asset.browser_download_url)"
+ return $true
+ } catch {
+ Write-Host "GitHub binary download failed: $($_.Exception.Message)"
+ return $false
+ }
+}
+
+if (-not $ForceFullBuild -and (Test-Path $PrebuiltBinary)) {
+ Invoke-DockerPrebuiltBuild
+} else {
+ if ($ForceFullBuild) {
+ Write-Host "ForceFullBuild set; running full Docker build."
+ Invoke-FullDockerBuild
+ } else {
+ Write-Host "No prebuilt Linux server binary found at: $PrebuiltBinary"
+ if (Try-DownloadGitHubBinary -and (Test-Path $PrebuiltBinary)) {
+ Invoke-DockerPrebuiltBuild
+ } else {
+ Write-Host "Running full Docker build."
+ Invoke-FullDockerBuild
+ }
+ }
+}
diff --git a/docker/build-image.sh b/docker/build-image.sh
new file mode 100644
index 0000000..1686b44
--- /dev/null
+++ b/docker/build-image.sh
@@ -0,0 +1,108 @@
+#!/bin/sh
+set -eu
+
+image="${IMAGE:-lcemp-server:local}"
+build_jobs="${BUILD_JOBS:-2}"
+force_full_build="${FORCE_FULL_BUILD:-false}"
+github_repo="${GITHUB_REPOSITORY:-}"
+github_release_tag="${GITHUB_RELEASE_TAG:-latest}"
+github_asset_name="${GITHUB_ASSET_NAME:-MinecraftDedicatedServer}"
+
+script_dir="$(cd "$(dirname "$0")" && pwd)"
+repo_root="$(cd "$script_dir/../.." && pwd)"
+prebuilt_binary="$repo_root/build/MinecraftDedicatedServer"
+
+build_prebuilt_image() {
+ echo "Using prebuilt Linux server binary: $prebuilt_binary"
+ cd "$repo_root"
+ docker build \
+ -f Minecraft.Server/docker/Dockerfile.prebuilt \
+ -t "$image" \
+ .
+}
+
+build_full_image() {
+ cd "$repo_root"
+ docker build \
+ -f Minecraft.Server/docker/Dockerfile \
+ --build-arg BUILD_JOBS="$build_jobs" \
+ -t "$image" \
+ .
+}
+
+try_download_github_binary() {
+ if [ -z "$github_repo" ]; then
+ return 1
+ fi
+
+ if [ "$github_release_tag" = "latest" ] || [ -z "$github_release_tag" ]; then
+ release_url="https://api.github.com/repos/$github_repo/releases/latest"
+ else
+ release_url="https://api.github.com/repos/$github_repo/releases/tags/$github_release_tag"
+ fi
+
+ if ! command -v python3 >/dev/null 2>&1; then
+ echo "python3 is required to download GitHub release assets from build-image.sh."
+ return 1
+ fi
+
+ echo "Looking for GitHub release asset '$github_asset_name' at $release_url"
+ mkdir -p "$(dirname "$prebuilt_binary")"
+
+ GITHUB_RELEASE_URL="$release_url" \
+ GITHUB_ASSET_NAME="$github_asset_name" \
+ PREBUILT_BINARY="$prebuilt_binary" \
+ python3 - <<'PY'
+import json
+import os
+import sys
+import urllib.request
+
+release_url = os.environ["GITHUB_RELEASE_URL"]
+asset_name = os.environ["GITHUB_ASSET_NAME"]
+prebuilt_binary = os.environ["PREBUILT_BINARY"]
+
+headers = {
+ "Accept": "application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28",
+ "User-Agent": "lcemp-docker-build",
+}
+
+try:
+ req = urllib.request.Request(release_url, headers=headers)
+ with urllib.request.urlopen(req) as response:
+ release = json.load(response)
+
+ asset = next((item for item in release.get("assets", []) if item.get("name") == asset_name), None)
+ if asset is None:
+ print(f"GitHub release found, but asset '{asset_name}' was not present.")
+ sys.exit(1)
+
+ download_url = asset["browser_download_url"]
+ req = urllib.request.Request(download_url, headers={"User-Agent": "lcemp-docker-build"})
+ with urllib.request.urlopen(req) as response, open(prebuilt_binary, "wb") as output:
+ output.write(response.read())
+
+ print(f"Downloaded Linux server binary from GitHub: {download_url}")
+except Exception as exc:
+ print(f"GitHub binary download failed: {exc}")
+ sys.exit(1)
+PY
+}
+
+if [ "$force_full_build" != "true" ] && [ -f "$prebuilt_binary" ]; then
+ build_prebuilt_image
+else
+ if [ "$force_full_build" = "true" ]; then
+ echo "FORCE_FULL_BUILD=true; running full Docker build."
+ build_full_image
+ else
+ echo "No prebuilt Linux server binary found at: $prebuilt_binary"
+ if try_download_github_binary && [ -f "$prebuilt_binary" ]; then
+ build_prebuilt_image
+ else
+ echo "Running full Docker build."
+ build_full_image
+ fi
+ fi
+fi
diff --git a/docker/compose.prebuilt.yaml b/docker/compose.prebuilt.yaml
new file mode 100644
index 0000000..501aaad
--- /dev/null
+++ b/docker/compose.prebuilt.yaml
@@ -0,0 +1,4 @@
+services:
+ lcemp-server:
+ build:
+ dockerfile: Minecraft.Server/docker/Dockerfile.prebuilt
diff --git a/docker/compose.yaml b/docker/compose.yaml
new file mode 100644
index 0000000..a8f313a
--- /dev/null
+++ b/docker/compose.yaml
@@ -0,0 +1,24 @@
+services:
+ lcemp-server:
+ build:
+ context: ../..
+ dockerfile: Minecraft.Server/docker/Dockerfile
+ image: lcemp-server:local
+ container_name: lcemp-server
+ restart: unless-stopped
+ network_mode: "host"
+ environment:
+ SERVER_PORT: "25565"
+ LEVEL_NAME: "world"
+ LEVEL_SIZE: "large"
+ GAMEMODE: "0"
+ DIFFICULTY: "2"
+ MAX_PLAYERS: "8"
+ PVP: "true"
+ MOTD: "A Minecraft LCE Server"
+ WHITE_LIST: "false"
+ VOICE_CHAT: "false"
+ ADVERTISE_LAN: "true"
+
+ volumes:
+ - ./config:/data
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100644
index 0000000..23e0cae
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+set -eu
+
+properties_file="${SERVER_PROPERTIES_FILE:-/data/server.properties}"
+
+SERVER_PORT="${SERVER_PORT:-25565}"
+SERVER_IP="${SERVER_IP:-}"
+LEVEL_NAME="${LEVEL_NAME:-world}"
+LEVEL_SEED="${LEVEL_SEED:-}"
+LEVEL_SIZE="${LEVEL_SIZE:-large}"
+GAMEMODE="${GAMEMODE:-0}"
+DIFFICULTY="${DIFFICULTY:-2}"
+MAX_PLAYERS="${MAX_PLAYERS:-8}"
+PVP="${PVP:-true}"
+TRUST_PLAYERS="${TRUST_PLAYERS:-true}"
+FIRE_SPREADS="${FIRE_SPREADS:-true}"
+TNT_EXPLODES="${TNT_EXPLODES:-true}"
+STRUCTURES="${STRUCTURES:-true}"
+SPAWN_ANIMALS="${SPAWN_ANIMALS:-true}"
+SPAWN_NPCS="${SPAWN_NPCS:-true}"
+ONLINE_MODE="${ONLINE_MODE:-false}"
+SHOW_GAMERTAGS="${SHOW_GAMERTAGS:-true}"
+MOTD="${MOTD:-A Minecraft LCE Server}"
+WHITE_LIST="${WHITE_LIST:-false}"
+VOICE_CHAT="${VOICE_CHAT:-false}"
+ENABLE_CHAT="${ENABLE_CHAT:-true}"
+ADVERTISE_LAN="${ADVERTISE_LAN:-true}"
+
+write_property() {
+ printf '%s=%s\n' "$1" "$2"
+}
+
+mkdir -p "$(dirname "$properties_file")"
+
+{
+ printf '#Minecraft server properties\n'
+ printf '#Generated from container environment on %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ write_property server-port "$SERVER_PORT"
+ write_property level-name "$LEVEL_NAME"
+ write_property level-seed "$LEVEL_SEED"
+ write_property gamemode "$GAMEMODE"
+ write_property difficulty "$DIFFICULTY"
+ write_property max-players "$MAX_PLAYERS"
+ write_property pvp "$PVP"
+ write_property trust-players "$TRUST_PLAYERS"
+ write_property fire-spreads "$FIRE_SPREADS"
+ write_property tnt-explodes "$TNT_EXPLODES"
+ write_property structures "$STRUCTURES"
+ write_property spawn-animals "$SPAWN_ANIMALS"
+ write_property spawn-npcs "$SPAWN_NPCS"
+ write_property online-mode "$ONLINE_MODE"
+ write_property show-gamertags "$SHOW_GAMERTAGS"
+ write_property motd "$MOTD"
+ write_property white-list "$WHITE_LIST"
+ write_property voice-chat "$VOICE_CHAT"
+ write_property enable-chat "$ENABLE_CHAT"
+ write_property level-size "$LEVEL_SIZE"
+ write_property advertise-lan "$ADVERTISE_LAN"
+ write_property server-ip "$SERVER_IP"
+} > "$properties_file"
+
+exec "$@"