diff --git a/Dockerfile b/Dockerfile index 38f8b7c..483a241 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,20 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo-registry,sharin # Data dir owned by the non-root runtime user. RUN mkdir -p /out/data && chown 65532:65532 /out/data +# ---- 2b. Vantage generator (optional) ---------------------------------------- +# The live map renders with Vantage. Point VANTAGE_URL at a static Linux +# release archive (tar.gz or zip) for the target arch to bake it into the image; +# leave it empty to mount your own binary at /opt/vantage/vantage instead. +FROM --platform=$BUILDPLATFORM alpine:3 AS vantage +ARG VANTAGE_URL="" +RUN apk add --no-cache curl tar unzip && mkdir -p /out/vantage /out/assets \ + && if [ -n "$VANTAGE_URL" ]; then \ + curl -fsSL "$VANTAGE_URL" -o /tmp/vantage.pkg \ + && (tar -xzf /tmp/vantage.pkg -C /out/vantage 2>/dev/null || unzip -q /tmp/vantage.pkg -d /out/vantage) \ + && find /out/vantage -type f -name vantage -exec chmod 755 {} + \ + && (f=$(find /out/vantage -type f -name vantage | head -n1) && [ -n "$f" ] && [ "$f" != /out/vantage/vantage ] && mv "$f" /out/vantage/vantage || true); \ + fi + # ---- 3. Runtime -------------------------------------------------------------- FROM scratch LABEL org.opencontainers.image.title="SCOPENET Panel" \ @@ -44,9 +58,12 @@ LABEL org.opencontainers.image.title="SCOPENET Panel" \ COPY --from=build /scopenet-panel /scopenet-panel COPY --from=web /src/panel/web/dist /web COPY --from=build --chown=65532:65532 /out/data /data +COPY --from=vantage /out/vantage /opt/vantage ENV SCOPENET_BIND=0.0.0.0:8080 \ SCOPENET_DATA_DIR=/data \ SCOPENET_WEB_DIR=/web \ + SCOPENET_VANTAGE_BIN=/opt/vantage/vantage \ + SCOPENET_VANTAGE_ASSETS=/data/livemap/assets \ RUST_LOG=info USER 65532:65532 VOLUME ["/data"] diff --git a/docker-compose.yml b/docker-compose.yml index 4142ea9..208a2ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,11 @@ services: panel: # Tag the image built from this checkout so an older GHCR release is not used. image: scopenet-panel:local - build: . + build: + context: . + args: + # Optional: URL of a static Vantage Linux release to bundle. + VANTAGE_URL: ${VANTAGE_URL:-} container_name: scopenet-panel restart: unless-stopped ports: @@ -25,6 +29,10 @@ services: MAX_UPLOAD_MB: ${MAX_UPLOAD_MB:-2048} PUBLIC_URL: ${PUBLIC_URL:-} SCOPENET_TRUSTED_PROXIES: ${SCOPENET_TRUSTED_PROXIES:-} + # Live Map: Vantage generator + Minecraft client assets (see docs/live-map.md). + SCOPENET_VANTAGE_BIN: ${SCOPENET_VANTAGE_BIN:-/opt/vantage/vantage} + SCOPENET_VANTAGE_ASSETS: ${SCOPENET_VANTAGE_ASSETS:-/data/livemap/assets} + SCOPENET_VANTAGE_ARGS: ${SCOPENET_VANTAGE_ARGS:-} RUST_LOG: ${RUST_LOG:-info} volumes: - panel-data:/data @@ -32,7 +40,8 @@ services: deploy: resources: limits: - memory: 256M + # Raise this if you enable Live Map: Vantage bakes tiles in memory. + memory: ${PANEL_MEMORY:-256M} volumes: panel-data: diff --git a/launcher/src/components/ChunkMap.svelte b/launcher/src/components/ChunkMap.svelte index 23942e6..a47e595 100644 --- a/launcher/src/components/ChunkMap.svelte +++ b/launcher/src/components/ChunkMap.svelte @@ -2,10 +2,10 @@ import { onMount } from 'svelte'; import { Shield, Lock, Plus, Trash2, Compass, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, - MapPin, Globe, LoaderCircle, CheckCircle2, AlertTriangle, RefreshCw + MapPin, Globe, LoaderCircle, RefreshCw, Server } from '@lucide/svelte'; import { invoke } from '../lib/tauri'; - import { toast } from '../lib/store.svelte'; + import { abs, toast } from '../lib/store.svelte'; import type { GuildClaim } from '../lib/types'; import VantageMap from './VantageMap.svelte'; @@ -23,7 +23,7 @@ onunclaim?: (claimId: number) => void; } = $props(); - let servers = $state>([]); + let servers = $state>([]); let serverId = $state(null); let dimension = $state<'minecraft:overworld' | 'minecraft:the_nether' | 'minecraft:the_end'>('minecraft:overworld'); let centerX = $state(0); @@ -37,8 +37,26 @@ let selectedChunk = $state<{ x: number; z: number } | null>(null); let mapDimensions = $state([]); let mapFailed = $state(false); - const mapUrl = $derived(servers.find((s) => s.id === serverId)?.map_url || ''); - $effect(() => { mapUrl; mapFailed = false; mapDimensions = []; }); + // The panel's central live map (if switched on for this server) wins over an external Vantage URL. + type LiveInfo = { ready: boolean; base: string; dimensions: { id: string; available: boolean }[] }; + let liveInfo = $state(null); + const instanceServers = $derived(servers.filter((s) => s.instance_id === instanceId)); + const selectedServer = $derived(servers.find((s) => s.id === serverId)); + $effect(() => { + const id = selectedServer?.live_map ? selectedServer.id : null; + liveInfo = null; + if (!id) return; + let alive = true; + invoke('get_livemap_info', { serverId: id }).then((i) => { if (alive) liveInfo = i; }).catch(() => {}); + return () => { alive = false; }; + }); + const central = $derived(!!liveInfo?.ready); + const dimSlug = $derived(dimension.replace('minecraft:', '')); + const mapUrl = $derived(central && liveInfo ? abs(`${liveInfo.base}/${dimSlug}/v1/worlds/default/manifest.json`) ?? '' : selectedServer?.map_url || ''); + const dimDisabled = (id: string) => central + ? !liveInfo?.dimensions.find((d) => d.id === id)?.available + : !!mapUrl && !mapFailed && mapDimensions.length > 0 && !mapDimensions.includes(id); + $effect(() => { mapUrl; mapFailed = false; if (!central) mapDimensions = []; }); const dimensions = [ { id: 'minecraft:overworld', label: 'Overworld', icon: Globe }, @@ -144,45 +162,53 @@
- - {#if !serverId}

Link a game server to this instance in the panel before claiming territory.

{/if} - -
-
- {#each dimensions as dim} - {@const DimIcon = dim.icon} - - {/each} +
+
+ {#if instanceServers.length > 1} + + {:else if instanceServers[0]} + {instanceServers[0].name} + {/if} +
+ {#each dimensions as dim} + {@const DimIcon = dim.icon} + + {/each} +
+ {#if central} Live map{/if}
-