Merge pull request #2 from merval/fix/posix-net-layer

Fix POSIX LAN disconnect handling
This commit is contained in:
NOTPIES
2026-05-12 18:12:32 -04:00
committed by GitHub
15 changed files with 756 additions and 48 deletions

View File

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

View File

@@ -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*/)

View File

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

View File

@@ -30,20 +30,20 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v110</PlatformToolset>
<PlatformToolset>v145</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v110</PlatformToolset>
<PlatformToolset>v145</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug-Linux|x64'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset>v145</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release-Linux|x64'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset>v145</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
@@ -72,7 +72,8 @@
<OutDir>$(SolutionDir)x64_Server_Release\</OutDir>
<IntDir>x64_Server_Release\</IntDir>
<IncludePath>$(ProjectDir)\..\Minecraft.World\x64headers;$(ProjectDir)\..\Minecraft.Client\Xbox\Sentient\Include;$(IncludePath)</IncludePath>
</PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug-Linux|x64'">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug-Linux|x64'">
<OutDir>$(SolutionDir)Linux_Server_Debug\</OutDir>
<IntDir>Linux_Server_Debug\</IntDir>
<NMakeBuildCommandLine>"$(ProjectDir)Linux\build.bat" Debug</NMakeBuildCommandLine>
@@ -93,7 +94,8 @@
<NMakePreprocessorDefinitions>__linux__;_DEDICATED_SERVER;_CONTENT_PACKAGE;_LARGE_WORLDS;_WINDOWS64;WITH_SERVER_CODE;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS</NMakePreprocessorDefinitions>
<NMakeIncludeSearchPath>$(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</NMakeIncludeSearchPath>
<AdditionalOptions>-std=c++11</AdditionalOptions>
</PropertyGroup> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>stdafx.h</PrecompiledHeaderFile>
@@ -703,4 +705,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>

View File

@@ -832,5 +832,104 @@
<ClCompile Include="Linux\PosixNetLayer.cpp">
<Filter>Source Files\Linux</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Storage\4J_Storage.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Storage\STO_DLC.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Storage\STO_Main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Storage\STO_SaveGame.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Profile\4J_Profile.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Profile\PRO_AwardManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Profile\PRO_Data.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Profile\PRO_Main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Profile\PRO_RichPresence.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Profile\PRO_Sys.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\4J_Render.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\RendererCBuff.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\RendererCore.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\RendererMatrix.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\RendererState.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\RendererTexture.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\RendererVertex.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\microprofile\microprofile.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\png.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngerror.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngget.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngmem.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngpread.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngread.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngrio.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngrtran.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngrutil.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngset.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngtrans.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngwio.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngwrite.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngwtran.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Minecraft.Client\Platform_Libs\Dev\Render\libpng\pngwutil.c">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>
</Project>

51
docker/Dockerfile Normal file
View File

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

View File

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

View File

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

View File

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

125
docker/README.md Normal file
View File

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

97
docker/build-image.ps1 Normal file
View File

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

108
docker/build-image.sh Normal file
View File

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

View File

@@ -0,0 +1,4 @@
services:
lcemp-server:
build:
dockerfile: Minecraft.Server/docker/Dockerfile.prebuilt

24
docker/compose.yaml Normal file
View File

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

62
docker/entrypoint.sh Normal file
View File

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