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