From 244d27a3093ec8764f9ccee5115d0bd7ff42ab26 Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Sun, 4 Oct 2026 17:48:39 +0500 Subject: [PATCH] Refresh the homepage and improve export and draft guidance --- .env.example | 2 - README.md | 57 ++ app/block-theme.css | 918 ++++++++++++++++++ app/downloads/page.tsx | 15 +- app/layout.tsx | 1 + app/page.tsx | 282 +++--- artifacts-api-smoke.json | 11 + artifacts-plugin-regression.json | 16 + components/DraftEditor.tsx | 47 +- components/Shell.tsx | 8 +- docs/server-plugin.md | 61 ++ docs/verification.md | 22 + lib/api.ts | 33 +- lib/catalog.ts | 3 +- minecraft/build.gradle | 2 +- .../dev/minegeo/fabric/CaptureScreen.java | 2 +- .../dev/minegeo/fabric/MineGeoClient.java | 4 +- .../dev/minegeo/plugin/MineGeoPlugin.java | 155 ++- .../java/dev/minegeo/plugin/UploadClient.java | 89 +- .../plugin/src/main/resources/plugin.yml | 2 +- next-env.d.ts | 4 +- public/fonts/OFL-VT323.txt | 93 ++ public/fonts/VT323-Regular.ttf | Bin 0 -> 153116 bytes public/tools/mod/minegeo-fabric-1.21.11.jar | Bin 29602 -> 35024 bytes .../tools/plugin/minegeo-plugin-1.21.11.jar | Bin 327923 -> 336868 bytes scripts/java-build.mjs | 3 +- scripts/setup.mjs | 2 +- scripts/test-api.ts | 210 ++++ scripts/test-plugin.ts | 232 +++++ shared/protocol.ts | 17 + tests/java/UploadSmoke.java | 9 + tests/protocol.test.ts | 23 + worker/main.ts | 2 + 33 files changed, 2110 insertions(+), 215 deletions(-) create mode 100644 README.md create mode 100644 app/block-theme.css create mode 100644 artifacts-api-smoke.json create mode 100644 artifacts-plugin-regression.json create mode 100644 docs/server-plugin.md create mode 100644 docs/verification.md create mode 100644 public/fonts/OFL-VT323.txt create mode 100644 public/fonts/VT323-Regular.ttf create mode 100644 scripts/test-api.ts create mode 100644 scripts/test-plugin.ts create mode 100644 tests/java/UploadSmoke.java diff --git a/.env.example b/.env.example index c6a0c91..67599d7 100644 --- a/.env.example +++ b/.env.example @@ -12,7 +12,5 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= SUPABASE_SECRET_KEY= # Optional direct PostgreSQL connection for maintenance. Runtime uses the Supabase API. DATABASE_URL= -# Explicit local guest sandbox when no hosted project is configured. -MINEGEO_LOCAL_MODE=true MINEGEO_FACE_SIZE=1024 MINEGEO_MAX_UPLOAD_BYTES=268435456 diff --git a/README.md b/README.md new file mode 100644 index 0000000..01c26e1 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# MineGeo + +A Minecraft panorama guessing game: Next.js website, Fabric capture mod, and standalone Bukkit/Paper exporter. The interface is English; the game uses fixed 360° views and Minecraft X/Z coordinates. + +## Run the website + +Use Node.js 22 or later. From the repository root on Windows, macOS, or Linux: + +```sh +npm install +npm run setup +``` + +Fill `.env.local` with the dedicated Supabase project's URL, publishable key, and server secret. Apply the SQL files in `supabase/migrations` in filename order to a new project. Never put the server secret in a `NEXT_PUBLIC_` variable. Configure the sign-in callback as described in [LAN and domain setup](docs/local-network.md). + +```sh +npm run dev +# Production: +npm run build +npm start +``` + +`npm start` and `npm run dev` supervise both the website and processing worker. The worker is required to turn an upload into a previewable draft. `npm run worker` also loads `.env.local` when run separately. Setup downloads the pinned vanilla rendering assets and the Chromium runtime used by the existing terrain-rendering worker. Browser UI checks are separate from this worker. + +`GET /api/v1/health` checks database connectivity. An empty catalog means no map has passed moderator review yet. Moderator access is controlled by `profiles.role` in Supabase; the project owner can set their profile's role to `moderator` using the database dashboard, then use `/moderation` on the website. + +## Install the capture tools + +Ready-to-install JARs are committed under `public/tools/plugin` and `public/tools/mod` and served by `/downloads`. After pulling an update, replace the Minecraft server's installed JAR and restart that server; pulling the website alone does not update an already installed plugin. + +- **Server:** Java 21 and Bukkit/Paper 1.21.11. Follow [the plugin guide](docs/server-plugin.md). +- **Player:** Minecraft 1.21.11, Fabric Loader and Fabric API. Put the mod JAR in `mods/`, press G, enter the website address, link your account, capture at least five positions, and upload. + +To rebuild both tools with Java 21 installed: + +```sh +npm run java:build +``` + +The build copies both JARs to their Git-visible download folders. Include those files in your commit before pulling on another machine. + +## Checks + +```sh +npm test +npm run typecheck -- --incremental false +npm run build +node --import tsx scripts/test-plugin.ts +``` + +The plugin regression uses the Paper JAR already downloaded into `.cache/paper-proof/paper.jar`, starts a temporary loopback-only Paper server on port 25580, and stops it automatically. It verifies export, manifest validation, missing-export instructions, interrupted TUS upload/resume, idempotent retries, and cancellation. Its upload endpoint is a local test server; it does not publish a real community map or launch a Minecraft client. + +## Rendering scope + +Plugin exports include generated Overworld surface terrain (up to 24 blocks below each column's top), vanilla block states, and biome identifiers. Buried opaque blocks are omitted. Each export is limited to 5 million retained blocks, 240 MiB of raw terrain JSON, and 50 MiB compressed. Start with a 128 × 128 area, check the preview, then increase coverage. Lighting is simplified; custom packs, entities, and dynamic block-entity visuals are not captured by the server exporter. + +The pixel heading font is VT323 by Peter Hull, distributed under the SIL Open Font License. Its license is in `public/fonts/OFL-VT323.txt`. diff --git a/app/block-theme.css b/app/block-theme.css new file mode 100644 index 0000000..2984311 --- /dev/null +++ b/app/block-theme.css @@ -0,0 +1,918 @@ +@font-face { + font-family: MineGeoPixel; + src: url("/fonts/VT323-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +:root { + --bg: #080c12; + --panel: #121b26; + --panel2: #192430; + --line: #2c3743; + --green: #86ce56; + --muted: #99a5ae; + --text: #edf1f4; + --sand: #dcc197; +} +body { + background-color: var(--bg); + background-image: radial-gradient( + ellipse at 75% 8%, + #18202b66, + transparent 52% + ); + color: var(--text); +} +h1, +h2, +h3, +.brand, +.button, +.header-account, +.site-header nav, +.world-stats strong { + font-family: MineGeoPixel, "Courier New", monospace; + font-weight: 400; + letter-spacing: 0; +} +h1, +h2, +h3 { + text-shadow: 2px 2px 0 #0008; +} +h1 { + font-size: 64px; +} +h2 { + font-size: 40px; +} +h3 { + font-size: 28px; +} +.button { + border: 2px solid #4b5965; + border-bottom: 4px solid #03070b; + box-shadow: + inset 2px 2px #ffffff15, + 3px 3px 0 #0008; + background: #25313c; + font-size: 23px; + padding: 7px 20px; + line-height: 1.2; + border-radius: 0; +} +.button.primary { + color: #10240b; + background: linear-gradient(#94df61, #5da732); + border: 2px solid #afd98b; + border-bottom: 4px solid #294b17; + box-shadow: + inset 2px 2px #d7ffb066, + 0 3px 0 #050904; + font-weight: 400; + text-shadow: 1px 1px #d7ffa555; +} +.button.primary:hover { + background: linear-gradient(#acf77a, #76bd4b); +} +.button:hover { + transform: translateY(-2px); + background-color: #334552; +} +.button:active { + transform: translateY(1px); +} +.site-header { + height: 68px; + background: #090e16ed; + border-bottom: 2px solid #222b37; + box-shadow: 0 3px #03070b; +} +.header-inner, +.section, +.site-footer { + max-width: 1160px; +} +.header-inner { + padding: 0 28px; + gap: 24px; +} +.brand { + font-size: 34px; + white-space: nowrap; +} +.brand-cube { + transform: none; + box-shadow: 3px 3px #000; + border: 1px solid #748952; + border-bottom: 11px solid #785033; + background: repeating-linear-gradient(90deg, #6faf3f 0 7px, #81c64b 7px 14px); + width: 27px; + height: 29px; +} +.brand sup { + font-size: 8px; + margin-top: -10px; +} +.site-header nav { + font-size: 23px; + gap: 25px; +} +.header-account { + font-size: 21px; + padding: 5px 13px; + border: 2px solid #9f87c3; + border-bottom: 4px solid #45325f; + background: linear-gradient(#8560b2, #694890); + box-shadow: inset 2px 2px #ffffff20; + color: #fff; + white-space: nowrap; +} +.home-page { + position: relative; + background-image: + radial-gradient(#9dabb332 1px, transparent 2px), + radial-gradient(#9dabb318 1px, transparent 2px); + background-size: + 103px 89px, + 179px 151px; + background-position: + 18px 8px, + 62px 40px; +} +.hero { + max-width: 1160px; + min-height: 394px; + padding: 44px 28px 46px; + grid-template-columns: 1.3fr 1fr; + gap: 35px; + overflow: visible; +} +.hero h1 { + font-size: clamp(44px, 4.7vw, 66px); + line-height: 0.97; + font-weight: 400; + letter-spacing: 0; + margin-bottom: 18px; +} +.hero h1 span { + color: #87c855; + text-shadow: 3px 3px #23421d; +} +.hero p { + font-size: 14px; + line-height: 1.7; + color: #a6afb8; + margin-bottom: 19px; +} +.status-label { + font-size: 10px; + border: 1px solid #2b3541; + background: #111822; + display: inline-flex; + padding: 3px 8px; + margin-bottom: 15px; + letter-spacing: 0.5px; +} +.hero-actions { + margin-top: 19px; + gap: 16px; +} +.hero-mode-note { + font-size: 11px; + color: #94a0aa; +} +.hero-notes { + margin-top: 20px; + align-items: center; + color: #9ba6af; + font-size: 11px; +} +.mode-dots { + display: flex; + gap: 9px !important; +} +.mode-dots i { + display: block; + width: 13px; + height: 13px; + background: #6cab43; + border: 2px solid #a1c875; + box-shadow: 2px 2px #000; +} +.mode-dots i:nth-child(2) { + background: #cca34c; + border-color: #e7c47b; +} +.mode-dots i:nth-child(3) { + background: #4786b8; + border-color: #88b1d1; +} +.mode-dots i:nth-child(4) { + background: #b64d5b; + border-color: #df7d89; +} +.hero-art { + min-height: 300px; + margin: 0; +} +.map-art-frame { + width: 277px; + height: 277px; + transform: rotate(-5deg); + border: 12px solid var(--sand); + box-shadow: + 0 0 0 4px #81684a, + 8px 8px 0 #0009; + clip-path: polygon( + 0 3%, + 5% 3%, + 5% 0, + 22% 0, + 22% 2%, + 43% 2%, + 43% 0, + 65% 0, + 65% 2%, + 89% 2%, + 89% 0, + 97% 0, + 97% 7%, + 100% 7%, + 100% 27%, + 98% 27%, + 98% 71%, + 100% 71%, + 100% 95%, + 95% 95%, + 95% 100%, + 70% 100%, + 70% 98%, + 30% 98%, + 30% 100%, + 3% 100%, + 3% 93%, + 0 93% + ); +} +.square-pin { + position: absolute; + width: 16px; + height: 16px; + border: 3px solid #16231b; + box-shadow: 2px 2px #0008; +} +.pin-green { + top: 65%; + left: 30%; + background: #60c8a0; +} +.pin-red { + top: 40%; + left: 56%; + background: #cd5367; +} +.pin-gold { + top: 20%; + left: 70%; + background: #e4b851; +} +.compass-art { + right: 12%; + top: 0; + transform: none; + border: 2px solid #535e68; + background: linear-gradient(135deg, #26313e, #080d16); + box-shadow: + inset 3px 3px #ffffff0c, + 4px 4px #0008; + width: 66px; + height: 70px; + padding: 10px; + display: flex; + align-items: center; + justify-content: center; + color: #d5dade; +} +.compass-art small { + position: absolute; + top: 2px; + left: 27px; + font: 10px monospace; + color: #dd6270; +} +.floating-tag { + bottom: 0; + right: auto; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + padding: 6px 10px; + background: #0d151f; + border: 2px solid #273240; + display: flex; + gap: 9px; + font: 11px monospace; + color: #b8c1c9; +} +.floating-tag strong { + color: #eff2f5; +} +.floating-tag svg { + color: #ce5367; +} +.grass-divider { + height: 23px; + background: repeating-linear-gradient( + 90deg, + #5d422c 0 8px, + #513827 8px 16px, + #624731 16px 24px + ); + border-top: 5px solid #578b30; + border-bottom: 3px solid #332419; + box-shadow: 0 -2px #8db85a; +} +.grass-divider span { + display: block; + height: 6px; + background: repeating-linear-gradient( + 90deg, + #427329 0 13px, + transparent 13px 19px, + #355d21 19px 26px + ); +} +.world-stats { + position: relative; + max-width: 1104px; + margin: -3px auto 0; + background: #131d29; + border: 2px solid #303b46; + border-top: 0; + box-shadow: 4px 4px #0008; + padding: 19px 28px; + gap: 20px; +} +.world-stats > div { + gap: 13px; + border-right: 1px solid #2e3743; +} +.world-stats > div:last-child { + border: 0; +} +.world-stats > div > div { + display: flex; + flex-direction: column; + gap: 0; +} +.world-stats strong { + font-size: 32px; + line-height: 0.95; + color: #f0f2f5; +} +.world-stats span:not(.live-dot) { + font-size: 8px; + letter-spacing: 0.3px; + max-width: none; + color: #929eac; +} +.stat-cube { + display: block; + flex-shrink: 0; + width: 25px; + height: 25px; + border: 2px solid #b5d99b; + box-shadow: + inset 3px 3px #ffffff25, + 3px 3px #000a; +} +.green-cube { + background: linear-gradient(#8ccb55, #4b8f31); +} +.gold-cube { + background: linear-gradient(#ebcc65, #b78a32); + border-color: #f0d797; +} +.cyan-cube { + background: linear-gradient(#73c6c3, #378f91); + border-color: #a5dfd8; +} +.red-cube { + background: linear-gradient(#d97988, #a73d52); + border-color: #e8a5af; +} +.section { + padding: 48px 28px; +} +.section-heading { + margin-bottom: 24px; +} +.section-heading h2 { + font-size: 38px; +} +.eyebrow { + font: + 11px/1.4 "Courier New", + monospace; + letter-spacing: 0.5px; + margin-bottom: 5px; + color: #9ec581; +} +.text-link { + font-size: 12px; + color: #b4c0ca; +} +.cards-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; +} +.map-card { + border: 2px solid #303c49; + background: #141e2b; + box-shadow: 3px 3px #0008; +} +.map-card:hover { + border-color: #83ab5f; + transform: translateY(-3px); +} +.card-image { + height: auto; + aspect-ratio: 1; + margin: 24px 11px 12px; + border: 9px solid #d2b98d; + outline: 2px solid #876e4f; + overflow: visible; + box-shadow: 3px 3px #0007; +} +.card-type { + top: -28px; + left: -9px; + border: 0; + background: #080c12; + padding: 1px 4px; + color: #d1d9de; + letter-spacing: 0; + font: 10px monospace; +} +.card-number { + display: none; +} +.card-body { + padding: 12px; + background: #17212f; + border-top: 1px solid #303a45; +} +.card-body .eyebrow { + font-size: 8px; + margin-bottom: 5px; +} +.card-body h3 { + font-size: 26px; + line-height: 1; + margin-bottom: 7px; + gap: 6px; +} +.card-body h3 svg { + width: 14px; + flex-shrink: 0; +} +.card-body p { + font-size: 11px; + line-height: 1.5; + color: #a3adb8; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + min-height: 33px; +} +.card-meta { + font-size: 9px; + padding-top: 9px; + color: #9ba9b7; + border-color: #35414d; + gap: 5px; +} +.first-map-panel { + display: grid; + grid-template-columns: 210px 1fr; + gap: 36px; + align-items: center; + border: 2px solid #2d3b47; + background: #111a26; + padding: 24px; + box-shadow: 5px 5px #0007; +} +.first-map-panel h3 { + font-size: 35px; + margin-bottom: 9px; +} +.first-map-panel p { + color: #a4aeb9; + max-width: 620px; + font-size: 13px; +} +.empty-map-art { + position: relative; + border: 10px solid #d4bc91; + box-shadow: 3px 3px #7b6040; + transform: rotate(-3deg); +} +.empty-map-art img { + display: block; + width: 100%; + height: auto; + image-rendering: pixelated; + filter: saturate(0.65); +} +.empty-map-art span { + position: absolute; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + padding: 4px 7px; + background: #090d17dd; + font: 11px monospace; + color: #f5dfb4; +} +.home-panels { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 20px; + padding-top: 0; +} +.home-panel { + background: linear-gradient(135deg, #17222e, #101923); + border: 2px solid #2e3a48; + padding: 24px; + box-shadow: 4px 4px #0007; +} +.home-panel h2 { + font-size: 36px; + margin-bottom: 22px; +} +.play-steps { + list-style: none; + margin: 0 0 20px; + padding: 0; + display: grid; + gap: 18px; +} +.play-steps li { + display: flex; + gap: 15px; + align-items: flex-start; +} +.play-steps li > span { + font: 24px MineGeoPixel; + color: #9bbb7d; + background: #202d23; + border: 1px solid #43553a; + padding: 0 6px; +} +.play-steps h3 { + font-size: 25px; + margin-bottom: 3px; +} +.play-steps p, +.mode-row p { + font-size: 12px; + color: #a4afb9; + margin: 0; +} +.mode-list { + display: grid; + gap: 10px; +} +.mode-row { + display: flex; + gap: 13px; + align-items: center; + border: 2px solid #384651; + background: #192430; + padding: 12px; +} +.mode-row:hover { + border-color: #84b459; +} +.mode-row > div { + flex: 1; +} +.mode-row h3 { + font-size: 25px; + margin: 0 0 4px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.mode-row h3 span { + font: 10px monospace; + color: #acb9c3; + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; +} +.mode-footnote { + font-size: 11px; + color: #899ca9; + margin: 16px 0 0; +} +.contribute-banner { + max-width: 1104px; + padding: 25px 28px; + margin-bottom: 48px; + background: #131e24; + border: 2px solid #3a5233; + gap: 22px; +} +.contribute-banner > div { + flex: 1; +} +.contribute-banner h2 { + font-size: 33px; + margin-bottom: 6px; +} +.contribute-banner > svg { + color: var(--green); +} +.site-footer { + border-color: #263440; +} +.site-footer .brand { + font-size: 29px; +} +.site-footer p, +.site-footer > div:nth-child(2) { + color: #91a0ad; +} +.tool-card, +.start-panel, +.auth-card, +.draft-row, +.token-box, +.result-summary, +.round-result { + background: var(--panel); + border: 2px solid var(--line); + box-shadow: 4px 4px #0007; +} +.badge { + border-color: #485b3f; + background: #1d2c22; + color: #a9cf8d; +} +.empty-state { + background: #111b25; + border-color: #43525d; +} +.field input, +.field textarea, +.search-field { + background: #0b121b; + border-color: #3c4a56; +} +.difficulty-options button { + background: #17232d; +} +.difficulty-options button.selected { + background: #223522; +} +.draft-row h3 { + font-size: 27px; +} +.game-info { + background: #111b27e8; +} +.game-map { + background: #111b27; + border: 3px solid #576147; +} +.round-result { + background: #111b27f5; +} +.page-content h1 { + font-size: 58px; +} +.header-inner .mobile-nav { + display: none; +} +@media (max-width: 1000px) { + .hero { + grid-template-columns: 1.2fr 1fr; + gap: 15px; + } + .hero h1 { + font-size: 55px; + } + .world-stats { + margin-left: 28px; + margin-right: 28px; + padding: 17px; + } + .cards-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .compass-art { + right: 1%; + } + .site-header nav { + gap: 15px; + } + .home-panels { + grid-template-columns: 1fr 1fr; + } + .mode-row h3 { + flex-wrap: wrap; + gap: 3px; + } + .contribute-banner { + margin-left: 28px; + margin-right: 28px; + } + .brand sup { + display: none; + } +} +@media (max-width: 700px) { + .site-header { + height: auto; + min-height: 66px; + } + .header-inner { + padding: 13px 18px; + flex-wrap: wrap; + gap: 12px; + } + .header-inner nav { + display: flex; + order: 3; + flex-basis: 100%; + justify-content: space-between; + border-top: 1px solid #263340; + padding-top: 9px; + font-size: 21px; + gap: 12px; + } + .brand { + font-size: 30px; + } + .header-account { + font-size: 20px; + padding: 4px 9px; + } + .hero { + padding: 32px 22px 28px; + gap: 20px; + } + .hero h1 { + font-size: 52px; + } + .hero p { + font-size: 13px; + } + .hero-art { + width: 100%; + min-height: 294px; + margin: 15px 0 0; + } + .map-art-frame { + width: 254px; + height: 254px; + } + .compass-art { + right: calc(50% - 144px); + top: 0; + } + .hero-actions { + gap: 15px; + flex-wrap: wrap; + } + .hero-notes { + gap: 10px; + } + .hero-mode-note { + font-size: 10px; + } + .world-stats { + margin: 0 18px; + padding: 18px; + gap: 19px; + grid-template-columns: 1fr 1fr; + } + .world-stats > div { + gap: 9px; + border: 0; + } + .world-stats strong { + font-size: 27px; + } + .world-stats span:not(.live-dot) { + font-size: 7px; + } + .stat-cube { + width: 22px; + height: 22px; + } + .section { + padding: 34px 20px; + } + .section-heading { + align-items: flex-end; + gap: 10px; + } + .section-heading h2 { + font-size: 33px; + } + .section-heading .text-link { + font-size: 10px; + white-space: nowrap; + } + .first-map-panel { + grid-template-columns: 1fr; + padding: 21px; + gap: 24px; + } + .empty-map-art { + width: 150px; + margin: auto; + } + .first-map-panel h3 { + font-size: 31px; + } + .home-panels { + grid-template-columns: 1fr; + padding-top: 0; + gap: 18px; + } + .home-panel { + padding: 21px; + } + .home-panel h2 { + font-size: 33px; + } + .contribute-banner { + margin: 0 20px 32px; + padding: 22px; + gap: 15px; + align-items: flex-start; + } + .contribute-banner > svg { + display: none; + } + .cards-grid { + gap: 12px; + } + .card-image { + margin: 23px 9px 9px; + border-width: 7px; + aspect-ratio: 1; + height: auto; + } + .card-body { + padding: 10px; + } + .card-body h3 { + font-size: 24px; + } + .card-body p { + display: none; + } + .card-body .eyebrow { + font-size: 7px; + } + .card-meta { + font-size: 8px; + } + .card-meta span:last-child { + display: none; + } + .page-content h1 { + font-size: 44px; + } + .tool-card { + padding: 23px; + } + .tool-card .button { + font-size: 21px; + } + .mode-row h3 { + flex-wrap: nowrap; + } + .game-header .brand { + font-size: 25px; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation: none !important; + transition: none !important; + scroll-behavior: auto !important; + } +} diff --git a/app/downloads/page.tsx b/app/downloads/page.tsx index 343ea73..e5a6413 100644 --- a/app/downloads/page.tsx +++ b/app/downloads/page.tsx @@ -77,13 +77,20 @@ export default function Downloads() {

The plugin build is not available yet.

)}
-              /minegeo area world -256 -256 256 256{"\n"}/minegeo export My
-              Overworld{"\n"}/minegeo link <dashboard token>{"\n"}/minegeo
-              upload
+              {
+                "/minegeo website https://your-minegeo-site.example\n/minegeo link \n/minegeo area world -64 -64 64 64\n/minegeo export My Overworld\n/minegeo status\n# Wait for Export complete, then:\n/minegeo upload"
+              }
             
+

+ Start with a small, explored area around your builds. Exports are + saved in plugins/MineGeo/exports/export-…/. The + manifest appears when export finishes. Upload sends the latest + completed export; it does not capture the world. +

Put the JAR in plugins/ and restart. Set website and server-name - in plugins/MineGeo/config.yml. Permission: minegeo.admin. + in plugins/MineGeo/config.yml, then run /minegeo reload. HTTP LAN + addresses are supported for testing. Permission: minegeo.admin.

diff --git a/app/layout.tsx b/app/layout.tsx index 45d02f2..5254f51 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import "./globals.css"; +import "./block-theme.css"; export const metadata: Metadata = { title: { default: "MineGeo — Get lost in a familiar world", diff --git a/app/page.tsx b/app/page.tsx index 8ac0125..6206057 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,190 +2,246 @@ import Link from "next/link"; import Image from "next/image"; import { ArrowRight, - ArrowUpRight, Compass, - Cuboid, - Map, MapPin, - Move, Timer, + Blocks, + Upload, } from "lucide-react"; -import { Header, Footer, EmptyState } from "@/components/Shell"; +import { Header, Footer } from "@/components/Shell"; import { MapCard } from "@/components/MapCard"; import { catalog } from "@/lib/catalog"; export const dynamic = "force-dynamic"; export default async function Home() { const maps = await catalog(); - const serverCount = new Set(maps.map((m) => m.server_id)).size; - const locations = maps.reduce((n, m) => n + m.panorama_count, 0); + const servers = new Set(maps.map((map) => map.server_id)).size; + const locations = maps.reduce((total, map) => total + map.panorama_count, 0); return ( <>
-
+
-
- MINECRAFT WORLDS. REAL EXPLORATION. + MINECRAFT · EXPLORE · GUESS

- Get lost in a
- familiar world. + Guess where you are +
+ in a blocky world.

- Somewhere between the blocks, there’s a place you know. -
Look around. Trust your instincts. Make - your guess. + Drop into a random corner of a Minecraft world. +
Look around, spot the landmarks, and + place your pin +
before the clock runs out.

- Find your next world - - - How it works + Start exploring + 3 difficulty levels
- - 360° panoramas - - - No roads. No moving. - - - Just you and the blocks. + + Five rounds. No roads. Just your instincts.
-
-
- YOUR NEXT ADVENTURE IS OUT THERE -
-
- OVERWORLD - X ? · Z ? -
+
Top-down map of the real Paper renderer test world -
-
- -
-
-
+ + +
- - +
- ONE WORLD. COUNTLESS POSSIBILITIES. + + X: ??? Z: ??? + Overworld
-
+ -
+
- {serverCount} - COMMUNITY SERVERS + +
+ {servers.toLocaleString()} + COMMUNITY SERVERS +
- {maps.length} - WORLDS TO EXPLORE + +
+ {maps.length.toLocaleString()} + PUBLISHED MAPS +
- {locations} - PLACES TO GET LOST + +
+ {locations.toLocaleString()} + PLACES TO DISCOVER +
-
- - Built by explorers. -
- For explorers. +
+ +
+ 5 rounds + EVERY EXPEDITION +
- PICK A WORLD. DROP A PIN. -

Where to today?

+ COMMUNITY MAPS +

Where will you go today?

- All worlds + All {maps.length} maps
{maps.length ? (
- {maps.slice(0, 3).map((m, i) => ( - + {maps.slice(0, 4).map((map, index) => ( + ))}
) : ( - - The catalog is waiting for its first approved community map. - Capture a world with our mod or server plugin. - +
+ +
+ + THE FIRST EXPEDITION STARTS WITH YOU + +

Put your server on the map.

+

+ No community maps have been published yet. Export your world + with the server plugin, or capture five locations with the + Fabric mod. Preview your map and submit it for review. +

+ + Get the capture tools + +
+
)}
-
-
- A NEW WAY TO SEE MINECRAFT -

- Every block -
- tells a story. -

-

- That mountain. That oddly placed tower. That very suspicious dirt - hut. You’ve been here before… right? -

- - Learn how to play +
+
+ HOW TO PLAY +

Know your blocks?

+
    +
  1. + 01 +
    +

    Pick your world

    +

    Choose a community server, map, and difficulty.

    +
    +
  2. +
  3. + 02 +
    +

    Look for the clues

    +

    + Rotate the panorama. Find builds, biomes, and landmarks. +

    +
    +
  4. +
  5. + 03 +
    +

    Make your guess

    +

    Drop a pin on the map. Closer guesses earn more points.

    +
    +
  6. +
+ + Read the field guide -
-
-
- 01 - -

Take a look around

-

Find clues in a full 360° view of a Minecraft world.

-
-
- 02 - -

Trust your coordinates

-

Open the map and pin the spot where you think you are.

-
-
- 03 - -

Beat the clock

-

Five rounds. One world. How close can you get?

-
-
+ +
+ CHOOSE YOUR CHALLENGE +

One world. Three ways.

+
+ {[ + { + name: "Easy", + time: "180 seconds", + detail: "Take your time. Rotate and zoom.", + color: "green", + }, + { + name: "Normal", + time: "90 seconds", + detail: "Trust your eye. Rotate and zoom.", + color: "gold", + }, + { + name: "Hard", + time: "30 seconds", + detail: "Think fast. No zoom allowed.", + color: "red", + }, + ].map((mode) => ( + + +
+

+ {mode.name} + + + {mode.time} + +

+

{mode.detail}

+
+ + + ))} +
+

+ Up to 5,000 points per round. How close can you get? +

+
+
- YOUR SERVER DESERVES TO BE EXPLORED -

Put your world on the map.

-

- Bring your builds, your landscapes, and your community to MineGeo. -

+ BUILT BY THE COMMUNITY +

Your world deserves to be explored.

+

Share your builds and your favorite corners of Minecraft.

- - Create a map + + Create a map
diff --git a/artifacts-api-smoke.json b/artifacts-api-smoke.json new file mode 100644 index 0000000..43d9d4b --- /dev/null +++ b/artifacts-api-smoke.json @@ -0,0 +1,11 @@ +{ + "passed": true, + "checks": [ + "production homepage/catalog/downloads/assets", + "Java uploader to real Supabase Storage", + "private draft before rendered assets exist", + "guests denied private drafts", + "cancel queued processing", + "cancel unfinished upload without a queue row" + ] +} \ No newline at end of file diff --git a/artifacts-plugin-regression.json b/artifacts-plugin-regression.json new file mode 100644 index 0000000..51d71f3 --- /dev/null +++ b/artifacts-plugin-regression.json @@ -0,0 +1,16 @@ +{ + "passed": true, + "checks": [ + "missing export guidance", + "empty area failure", + "real Paper export without a player", + "shared schema validation", + "TUS interruption and byte-perfect resume", + "repeated upload is idempotent", + "cancellation preserves completed export" + ], + "blocks": 10112, + "chunks": 16, + "viewpoints": 5, + "directory": ".cache/plugin-regression-1791117070908" +} \ No newline at end of file diff --git a/components/DraftEditor.tsx b/components/DraftEditor.tsx index e1750fc..5bd8d67 100644 --- a/components/DraftEditor.tsx +++ b/components/DraftEditor.tsx @@ -9,6 +9,7 @@ type Draft = { description: string; state: string; source: string; + rejection_reason: string | null; map: string | null; manifest: DatasetManifest; previews: { id: string; image: string | null }[]; @@ -22,14 +23,27 @@ export function DraftEditor({ id }: { id: string }) { [message, setMessage] = useState(""), [busy, setBusy] = useState(false); useEffect(() => { - api("datasets/" + id) - .then((d) => { - setDraft(d); - setName(d.name); - setDescription(d.description); - setSelected(d.manifest.viewpoints.map((p) => p.id)); - }) - .catch((e) => setError(e.message)); + let disposed = false; + let timer: ReturnType; + const load = () => + api("datasets/" + id) + .then((d) => { + if (disposed) return; + setDraft(d); + setName(d.name); + setDescription(d.description); + setSelected(d.manifest.viewpoints.map((p) => p.id)); + if (["uploading", "processing"].includes(d.state)) + timer = setTimeout(load, 5000); + }) + .catch((e) => { + if (!disposed) setError(e.message); + }); + load(); + return () => { + disposed = true; + clearTimeout(timer); + }; }, [id]); async function save(submit = false) { setBusy(true); @@ -55,6 +69,11 @@ export function DraftEditor({ id }: { id: string }) { setBusy(true); try { await api("datasets/" + id + "/cancel", {}); + setDraft((d) => + d + ? { ...d, state: "failed", rejection_reason: "Processing cancelled" } + : d, + ); setMessage("Processing cancelled."); } catch (e) { setError((e as Error).message); @@ -70,6 +89,18 @@ export function DraftEditor({ id }: { id: string }) { return ( <> {draft.state} + {["uploading", "processing"].includes(draft.state) && ( +

+ {draft.state === "uploading" + ? "Waiting for the capture tool to finish uploading. Retry upload in the tool if the connection was interrupted." + : "Your map and panoramas are being rendered. This page updates automatically."} +

+ )} + {draft.rejection_reason && ( +

+ {draft.rejection_reason} +

+ )}

{draft.source === "plugin" ? "Server-generated views use vanilla terrain and simplified lighting. Entities, shaders, custom packs and dynamic sign text are omitted." diff --git a/components/Shell.tsx b/components/Shell.tsx index 5b6a073..4bdf777 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -17,12 +17,12 @@ export function Header() {

- My expedition + My account
diff --git a/docs/server-plugin.md b/docs/server-plugin.md new file mode 100644 index 0000000..c1f868a --- /dev/null +++ b/docs/server-plugin.md @@ -0,0 +1,61 @@ +# Server plugin 0.1.1 + +## Update and connect + +1. Stop the Minecraft server, replace its old MineGeo JAR with `public/tools/plugin/minegeo-plugin-1.21.11.jar`, and restart. Run `/version MineGeo` to verify 0.1.1. +2. Open your MineGeo website dashboard, sign in, and generate a plugin token. +3. In the Minecraft server console, configure the website and token. Console commands do not need the leading slash: + +```text +minegeo website https://your-minegeo-domain.example +minegeo link +``` + +For LAN testing the website may be `http://192.168.1.50:3000`; use your actual address. `localhost` means the computer running Minecraft, which may be different from the Mac running the website. HTTP is supported for testing; use HTTPS when transmitting contributor tokens over a public connection. + +## Export before uploading + +Choose a small, already generated area around your builds. Coordinates below are examples: + +```text +minegeo area world -64 -64 64 64 +minegeo export My Overworld +minegeo status +``` + +Wait for **Export complete**. The plugin reports the full path, for example: + +```text +plugins/MineGeo/exports/export-1791117138143/ + terrain.json.gz + manifest.json +``` + +Each attempt gets a separate folder. A manifest is created only after the complete export passes the local limits and has five safe viewpoints. If the area is unexplored, underwater, too large, or has too few safe positions, the console and status tell you why. No new terrain is generated by exporting. + +If no area has been selected, export uses a 512 × 512 area centered on the player issuing the command, or on world spawn for console commands. The configured maximum area size still applies. + +Then run: + +```text +minegeo upload +minegeo status +``` + +Upload sends the latest **completed** export. It does not generate an export. The selection survives server restarts through `plugins/MineGeo/latest-export.txt`. Older `plugins/MineGeo/export/manifest.json` exports are still recognized when no newer selection exists. + +## Finish on the website + +The worker renders the map and panoramas. Open the private draft in your dashboard; the page updates while processing. Inspect the result, choose at least five locations, and submit for moderation. Once a moderator approves it, players can select it in the catalog. + +## Retry, cancel, and configure + +- An interrupted transfer resumes with `minegeo upload`. Progress is saved in `.upload-state.json` beside the export. Already received exports are not submitted twice. +- `minegeo cancel` cancels capture or stops upload at the next transfer boundary. Completed exports are kept. HTTP requests already in flight can take up to their timeout to stop. +- `minegeo reload` reloads `config.yml` after you edit `website`, `server-name`, `chunks-per-tick`, or other settings. +- `minegeo status` reports the active stage and the last completed export folder. +- `minegeo help` lists commands. + +If processing fails on the website, read the draft's error. Fix the worker/setup or reduce the export area, then create and upload a new export. Keep the old folders until you no longer need them. + +Expected limits: Minecraft 1.21.11 / Java 21, Overworld only, five or more safe viewpoints, at most 2048 blocks per selected side and the worker's terrain/file-size limits. A large selection may need to be split into smaller maps. diff --git a/docs/verification.md b/docs/verification.md new file mode 100644 index 0000000..b407293 --- /dev/null +++ b/docs/verification.md @@ -0,0 +1,22 @@ +# Verification of the export/upload fixes + +The following checks passed on Windows with Java 21 and Minecraft/Paper 1.21.11: + +- A temporary Paper server with no connected player exported 16 chunks / 10,112 retained blocks and five valid surface viewpoints. +- Upload without an export produced instructions instead of a missing-file exception. +- An ungenerated area failed clearly, and a later export succeeded. +- A simulated mid-transfer interruption resumed from the server's byte offset and reproduced the original SHA-256 checksum. +- Retrying a completed upload did not create or finalize a duplicate dataset. +- Cancellation preserved the pointer to the previous complete export. +- The final Java uploader uploaded a real terrain file to the configured Supabase Storage project through the production Next.js API, then queued processing. +- An unrendered draft could be opened without trying to sign nonexistent preview images. +- Guests could not read that private draft. +- Both an unfinished upload and queued processing could be cancelled. +- Production homepage, catalog, downloads, local pixel font, example map, and both JAR URLs returned HTTP 200. +- Both Gradle projects built; TypeScript, 14 Node tests, and the Next.js production build passed. + +The real Storage test caught two additional issues that a mock endpoint did not: Java's cleartext HTTP/2 upgrade was incompatible with the website, and signed TUS transfers require `/storage/v1/upload/resumable/sign` plus the public API key and scoped signature. Both were corrected and the real upload passed afterward. + +Reports: `artifacts-plugin-regression.json` and `artifacts-api-smoke.json`. Temporary test datasets were deleted and the temporary servers stopped. + +This pass did not launch a Minecraft client, use Playwright, or use the Computer plugin. The browser appearance, an actual Fabric capture session, Spigot loading, and a fresh plugin panorama rendering run on macOS still need runtime verification. The existing renderer has not been replaced; earlier renderer proof is recorded separately in `artifacts-renderer-proof.json`. diff --git a/lib/api.ts b/lib/api.ts index 5ff161f..9904bfa 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -294,7 +294,7 @@ export async function handle(request: NextRequest, path: string[]) { .single(), ); const endpoint = new URL( - "/storage/v1/upload/resumable", + "/storage/v1/upload/resumable/sign", process.env.NEXT_PUBLIC_SUPABASE_URL, ); endpoint.hostname = endpoint.hostname.replace( @@ -315,6 +315,7 @@ export async function handle(request: NextRequest, path: string[]) { path: a.path, objectName, token: signed.token, + apiKey: process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, endpoint: endpoint.toString(), }; }), @@ -368,7 +369,9 @@ export async function handle(request: NextRequest, path: string[]) { previews: await Promise.all( manifest.viewpoints.map(async (p: { id: string }) => ({ id: p.id, - image: await mediaUrl(`${d.id}/${p.id}/preview.webp`), + image: d.map_path + ? await mediaUrl(`${d.id}/${p.id}/preview.webp`) + : null, })), ), }); @@ -441,20 +444,28 @@ export async function handle(request: NextRequest, path: string[]) { return NextResponse.json({ ok: true }); } if (method === "POST" && path[2] === "cancel") { + if (!["uploading", "processing"].includes(d.state)) + throw new Error("This dataset is not uploading or processing"); + const cancelled = await client + .from("upload_jobs") + .update({ state: "cancelled" }) + .eq("dataset_id", d.id) + .in("state", ["queued", "running"]) + .select("id") + .maybeSingle(); + if (cancelled.error) throw new Error(cancelled.error.message); required( await client - .from("upload_jobs") - .update({ state: "cancelled" }) - .eq("dataset_id", d.id) - .in("state", ["queued", "running"]) + .from("datasets") + .update({ + state: "failed", + rejection_reason: "Processing cancelled", + }) + .eq("id", d.id) + .in("state", ["uploading", "processing"]) .select("id") .single(), ); - await client - .from("datasets") - .update({ state: "failed", rejection_reason: "Processing cancelled" }) - .eq("id", d.id) - .in("state", ["uploading", "processing"]); return NextResponse.json({ ok: true }); } } diff --git a/lib/catalog.ts b/lib/catalog.ts index 8ef2d10..f3174f2 100644 --- a/lib/catalog.ts +++ b/lib/catalog.ts @@ -40,7 +40,8 @@ export async function catalog() { return Promise.all( maps.map(async (m) => ({ ...m, - image: (await mediaUrl(m.cover_path)) ?? undefined, + image: (await mediaUrl(m.map_path || m.cover_path)) ?? undefined, + mapImage: (await mediaUrl(m.map_path)) ?? undefined, server: servers.find((s) => s.id === m.server_id), })), ); diff --git a/minecraft/build.gradle b/minecraft/build.gradle index b1c32f7..7891281 100644 --- a/minecraft/build.gradle +++ b/minecraft/build.gradle @@ -1 +1 @@ -allprojects { group = 'dev.minegeo'; version = '0.1.0'; repositories { mavenCentral() } } +allprojects { group = 'dev.minegeo'; version = '0.1.1'; repositories { mavenCentral() } } diff --git a/minecraft/fabric/src/main/java/dev/minegeo/fabric/CaptureScreen.java b/minecraft/fabric/src/main/java/dev/minegeo/fabric/CaptureScreen.java index 3f0428d..a0e01ba 100644 --- a/minecraft/fabric/src/main/java/dev/minegeo/fabric/CaptureScreen.java +++ b/minecraft/fabric/src/main/java/dev/minegeo/fabric/CaptureScreen.java @@ -17,6 +17,6 @@ public final class CaptureScreen extends Screen { addDrawableChild(ButtonWidget.builder(Text.literal("New map at this location"),b->{mod.newMap();init(width,height);}).dimensions(x,182,260,20).build()); addDrawableChild(ButtonWidget.builder(Text.literal("Close"),b->close()).dimensions(x,210,260,20).build()); } - @Override public void render(DrawContext context,int mouseX,int mouseY,float delta){super.render(context,mouseX,mouseY,delta);int x=width/2-130;context.drawCenteredTextWithShadow(textRenderer,title,width/2,20,0xff8cdf6b);context.drawTextWithShadow(textRenderer,"Website (HTTPS outside localhost)",x,38,0xffffffff);context.drawTextWithShadow(textRenderer,"Map name",x,80,0xffffffff);var mod=MineGeoClient.INSTANCE;String counts=mod.dataset==null?"Join an Overworld to begin":mod.dataset.points.size()+" panoramas · "+mod.dataset.coverage.size()+" explored chunks";context.drawCenteredTextWithShadow(textRenderer,Text.literal(counts),width/2,242,0xffffffff);int y=264;for(var line:textRenderer.wrapLines(Text.literal(mod.status),260)){context.drawTextWithShadow(textRenderer,line,x,y,0xffd5d5d5);y+=10;}} + @Override public void render(DrawContext context,int mouseX,int mouseY,float delta){super.render(context,mouseX,mouseY,delta);int x=width/2-130;context.drawCenteredTextWithShadow(textRenderer,title,width/2,20,0xff8cdf6b);context.drawTextWithShadow(textRenderer,"Website URL (HTTP for LAN testing)",x,38,0xffffffff);context.drawTextWithShadow(textRenderer,"Map name",x,80,0xffffffff);var mod=MineGeoClient.INSTANCE;String counts=mod.dataset==null?"Join an Overworld to begin":mod.dataset.points.size()+" panoramas · "+mod.dataset.coverage.size()+" explored chunks";context.drawCenteredTextWithShadow(textRenderer,Text.literal(counts),width/2,242,0xffffffff);int y=264;for(var line:textRenderer.wrapLines(Text.literal(mod.status),260)){context.drawTextWithShadow(textRenderer,line,x,y,0xffd5d5d5);y+=10;}} @Override public boolean shouldPause(){return false;} } diff --git a/minecraft/fabric/src/main/java/dev/minegeo/fabric/MineGeoClient.java b/minecraft/fabric/src/main/java/dev/minegeo/fabric/MineGeoClient.java index e8e4a8b..e62a45f 100644 --- a/minecraft/fabric/src/main/java/dev/minegeo/fabric/MineGeoClient.java +++ b/minecraft/fabric/src/main/java/dev/minegeo/fabric/MineGeoClient.java @@ -17,7 +17,7 @@ public final class MineGeoClient implements ClientModInitializer { public static MineGeoClient INSTANCE; public final ExecutorService background=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"MineGeo upload");t.setDaemon(true);return t;}); public final Gson gson=new GsonBuilder().setPrettyPrinting().create(); - public String website="http://localhost:3000",token="",status="Explore normally to collect surface coverage."; + public volatile String website="http://localhost:3000",token="",status="Explore normally to collect surface coverage."; public volatile boolean uploading=false;public SurfaceDataset dataset;private SurfaceDataset.Point pending; private Path config; @Override public void onInitializeClient(){INSTANCE=this;config=MinecraftClient.getInstance().runDirectory.toPath().resolve("config/minegeo.json");try{if(Files.exists(config)){var j=JsonParser.parseString(Files.readString(config)).getAsJsonObject();website=j.get("website").getAsString();token=j.get("token").getAsString();}}catch(Exception e){message("Could not read MineGeo configuration");} @@ -31,5 +31,5 @@ public final class MineGeoClient implements ClientModInitializer { public void savedCapture(Path path){try{if(pending==null||!path.getFileName().toString().equals(pending.id()))return;dataset.points.add(pending);dataset.save();message("Panorama saved locally ("+dataset.points.size()+" total). Press G for preview or upload.");}catch(Exception e){message("Saving capture failed: "+e.getMessage());}finally{pending=null;}} public void newMap(){try{if(uploading||Capture.active())throw new IllegalStateException("Finish the current operation first");dataset=SurfaceDataset.create(server());message("New 512 × 512 map started. Older maps remain in .minecraft/minegeo.");}catch(Exception e){message(e.getMessage());}} public void pair(){background.execute(()->{try{saveConfig();var uploader=new UploadClient(website,"");var pair=uploader.beginPairing();String code=pair.get("deviceCode").getAsString();String link=pair.get("verificationUri").getAsString();MinecraftClient.getInstance().execute(()->{message("Approve code "+pair.get("userCode").getAsString()+" at "+link);net.minecraft.util.Util.getOperatingSystem().open(link);});for(int i=0;i<120;i++){Thread.sleep(5000);var response=uploader.pollPairing(code);if(response.get("status").getAsString().equals("approved")){token=response.get("token").getAsString();saveConfig();MinecraftClient.getInstance().execute(()->message("Account linked. You can upload maps now."));return;}}throw new IllegalStateException("Pairing expired. Try linking again.");}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Pairing: "+e.getMessage()));}});} - public void upload(String name){if(uploading)return;final SurfaceDataset selected=dataset;if(selected==null||selected.points.size()<5){message("Capture at least five locations before uploading.");return;}uploading=true;background.execute(()->{try{selected.name=name;selected.save();saveConfig();status="Uploading assets. Interrupted transfers resume when you retry.";new UploadClient(website,token).upload(selected.directory);MinecraftClient.getInstance().execute(()->message("Upload received. Preview and submit the private draft in your dashboard."));}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Upload paused: "+e.getMessage()));}finally{uploading=false;}});} + public void upload(String name){if(uploading)return;final SurfaceDataset selected=dataset;if(selected==null||selected.points.size()<5){message("Capture at least five locations before uploading.");return;}uploading=true;background.execute(()->{try{selected.name=name;selected.save();saveConfig();status="Uploading assets. Interrupted transfers resume when you retry.";new UploadClient(website,token).upload(selected.directory, progress -> status=progress, () -> false);MinecraftClient.getInstance().execute(()->message("Upload received. Preview and submit the private draft in your dashboard."));}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Upload paused: "+e.getMessage()));}finally{uploading=false;}});} } diff --git a/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java b/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java index 58edcee..7804004 100644 --- a/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java +++ b/minecraft/plugin/src/main/java/dev/minegeo/plugin/MineGeoPlugin.java @@ -1,73 +1,144 @@ package dev.minegeo.plugin; import com.google.gson.*; +import com.google.gson.stream.JsonWriter; import org.bukkit.*; import org.bukkit.command.*; +import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitRunnable; import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.*; +import java.security.MessageDigest; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import java.util.logging.Level; import java.util.zip.GZIPOutputStream; public final class MineGeoPlugin extends JavaPlugin { private World selectedWorld; private int minX=-256,minZ=-256,maxX=256,maxZ=256; private final AtomicBoolean cancelled=new AtomicBoolean(); - private volatile String status="Idle"; + private volatile String status="Idle — /minegeo export to begin"; private volatile boolean busy; - private final Gson gson=new Gson(); - @Override public void onEnable(){saveDefaultConfig();Objects.requireNonNull(getCommand("minegeo")).setExecutor(this);getLogger().info("MineGeo ready. /minegeo area ");} + private final Gson gson=new GsonBuilder().setPrettyPrinting().create(); + private volatile Path latest; + @Override public void onEnable(){ + saveDefaultConfig();Objects.requireNonNull(getCommand("minegeo")).setExecutor(this); + try { + Files.createDirectories(getDataFolder().toPath().resolve("exports")); + Path root=getDataFolder().toPath().toAbsolutePath().normalize(),pointer=root.resolve("latest-export.txt"); + if(Files.exists(pointer)) {Path candidate=root.resolve(Files.readString(pointer).trim()).normalize();if(candidate.startsWith(root)&&Files.isRegularFile(candidate.resolve("manifest.json")))latest=candidate;} + Path legacy=root.resolve("export");if(latest==null&&Files.isRegularFile(legacy.resolve("manifest.json")))latest=legacy; + }catch(IOException ex){getLogger().log(Level.WARNING,"Cannot prepare export folder",ex);} + getLogger().info("MineGeo ready. /minegeo help — exports: "+getDataFolder().toPath().resolve("exports").toAbsolutePath()); + } @Override public void onDisable(){cancelled.set(true);} @Override public boolean onCommand(CommandSender sender,Command command,String label,String[] args){ if(!sender.hasPermission("minegeo.admin")){sender.sendMessage("MineGeo: administrator permission required.");return true;} try { - if(args.length==0){sender.sendMessage("MineGeo: area, export, status, cancel, link, upload");return true;} - switch(args[0].toLowerCase(Locale.ROOT)){ - case "area" -> { if(busy)throw new IllegalArgumentException("Cancel the current export before changing the area.");if(args.length!=6)throw new IllegalArgumentException("area ");World w=Bukkit.getWorld(args[1]);if(w==null||w.getEnvironment()!=World.Environment.NORMAL)throw new IllegalArgumentException("Select a loaded Overworld.");int ax=Integer.parseInt(args[2]),az=Integer.parseInt(args[3]),bx=Integer.parseInt(args[4]),bz=Integer.parseInt(args[5]);int cap=Math.min(2048,getConfig().getInt("max-area-size",2048));if(bx<=ax||bz<=az||bx-ax>cap||bz-az>cap)throw new IllegalArgumentException("Invalid bounds; maximum "+cap+" blocks per side.");selectedWorld=w;minX=ax;minZ=az;maxX=bx;maxZ=bz;sender.sendMessage("MineGeo area selected: "+w.getName()+" "+ax+","+az+" to "+bx+","+bz);} - case "status" -> sender.sendMessage("MineGeo: "+status); - case "cancel" -> {cancelled.set(true);sender.sendMessage("MineGeo cancellation requested.");} + if(args.length==0||args[0].equalsIgnoreCase("help")) {sender.sendMessage(new String[]{"MineGeo commands:","/minegeo website ","/minegeo link ","/minegeo area ","/minegeo export — saves a local export","/minegeo upload — uploads the latest completed export","/minegeo status | cancel | reload"});return true;} + switch(args[0].toLowerCase(Locale.ROOT)) { + case "area" -> { + requireIdle();if(args.length!=6)throw new IllegalArgumentException("Usage: /minegeo area "); + World world=Bukkit.getWorld(args[1]);if(world==null||world.getEnvironment()!=World.Environment.NORMAL)throw new IllegalArgumentException("Select a loaded Overworld."); + int ax=Integer.parseInt(args[2]),az=Integer.parseInt(args[3]),bx=Integer.parseInt(args[4]),bz=Integer.parseInt(args[5]);int cap=Math.max(16,Math.min(2048,getConfig().getInt("max-area-size",2048))); + if(bx<=ax||bz<=az||(long)bx-ax>cap||(long)bz-az>cap||Math.abs((long)ax)>30000000||Math.abs((long)az)>30000000||Math.abs((long)bx)>30000000||Math.abs((long)bz)>30000000)throw new IllegalArgumentException("Invalid bounds; maximum "+cap+" blocks per side, within the world border."); + selectedWorld=world;minX=ax;minZ=az;maxX=bx;maxZ=bz;sender.sendMessage("MineGeo area selected: "+world.getName()+" "+ax+","+az+" to "+bx+","+bz); + } + case "status" -> {sender.sendMessage("MineGeo: "+status);if(latest!=null)sender.sendMessage("Latest complete export: "+latest.toAbsolutePath());} + case "cancel" -> {cancelled.set(true);sender.sendMessage(busy?"MineGeo cancellation requested. Completed exports are kept.":"MineGeo: no operation is running.");} + case "reload" -> {requireIdle();reloadConfig();sender.sendMessage("MineGeo configuration reloaded.");} + case "website" -> {requireIdle();if(args.length!=2)throw new IllegalArgumentException("Usage: /minegeo website ");new UploadClient(args[1],"");getConfig().set("website",args[1]);saveConfig();sender.sendMessage("MineGeo website saved: "+args[1]);} + case "link" -> {requireIdle();if(args.length!=2||args[1].isBlank())throw new IllegalArgumentException("Usage: /minegeo link ");getConfig().set("token",args[1]);saveConfig();sender.sendMessage("MineGeo contributor token saved.");} case "export" -> export(sender,args.length>1?String.join(" ",Arrays.copyOfRange(args,1,args.length)):"Overworld Explorer"); - case "link" -> { if(args.length!=2)throw new IllegalArgumentException("link ");getConfig().set("token",args[1]);saveConfig();sender.sendMessage("MineGeo contributor token saved. Restrict access to config.yml.");} - case "upload" -> {if(busy)throw new IllegalArgumentException("Wait for the current operation.");busy=true;Bukkit.getScheduler().runTaskAsynchronously(this,()->{try{status="Uploading";new dev.minegeo.plugin.UploadClient(getConfig().getString("website"),getConfig().getString("token")).upload(getDataFolder().toPath().resolve("export"));status="Upload complete; review the private draft on MineGeo.";}catch(Exception ex){status="Upload failed: "+ex.getMessage();getLogger().warning(status);}finally{busy=false;}});sender.sendMessage("MineGeo upload started.");} - default -> throw new IllegalArgumentException("Unknown command."); + case "upload" -> upload(sender); + default -> throw new IllegalArgumentException("Unknown command. Use /minegeo help."); } - }catch(Exception ex){sender.sendMessage("MineGeo: "+ex.getMessage());} - return true; + }catch(Exception ex){sender.sendMessage("MineGeo: "+ex.getMessage());}return true; + } + private void requireIdle(){if(busy)throw new IllegalArgumentException("An operation is running. Use /minegeo status or /minegeo cancel.");} + private void report(CommandSender sender,String message){status=message;getLogger().info(message);if(sender instanceof Player&&isEnabled())Bukkit.getScheduler().runTask(this,()->sender.sendMessage("MineGeo: "+message));} + private void fail(CommandSender sender,String phase,Exception ex){report(sender,cancelled.get()?"Cancelled. Completed exports are kept.":phase+" failed: "+ex.getMessage());if(!cancelled.get())getLogger().log(Level.WARNING,phase+" failure",ex);} + private void upload(CommandSender sender)throws Exception { + requireIdle();if(latest==null||!Files.isRegularFile(latest.resolve("manifest.json")))throw new IOException("No completed export. Run /minegeo export My World, wait for 'Export complete', then /minegeo upload. Files: "+getDataFolder().toPath().resolve("exports").toAbsolutePath()); + String website=getConfig().getString("website"),token=getConfig().getString("token");if(token==null||token.isBlank())throw new IOException("Create a token in the website dashboard, then /minegeo link ."); + UploadClient client=new UploadClient(website,token);Path directory=latest;busy=true;cancelled.set(false);status="Uploading "+directory.getFileName(); + Bukkit.getScheduler().runTaskAsynchronously(this,()->{try{client.upload(directory,message->status=message,cancelled::get);report(sender,"Upload complete. Open "+website+"/dashboard to preview the private draft after processing.");}catch(Exception ex){fail(sender,"Upload",ex);}finally{busy=false;}}); + sender.sendMessage("MineGeo upload started. /minegeo status shows progress; retry /minegeo upload after a connection failure."); } private record Captured(int cx,int cz,ChunkSnapshot snapshot){} - private void export(CommandSender sender,String name)throws IOException{ - if(busy)throw new IllegalArgumentException("An export or upload is already running."); - World world=selectedWorld!=null?selectedWorld:Bukkit.getWorlds().getFirst(); - if(world.getEnvironment()!=World.Environment.NORMAL)throw new IllegalArgumentException("Overworld only."); - busy=true;cancelled.set(false);List coords=new ArrayList<>(); - for(int cz=Math.floorDiv(minZ,16);cz<=Math.floorDiv(maxZ-1,16);cz++)for(int cx=Math.floorDiv(minX,16);cx<=Math.floorDiv(maxX-1,16);cx++)if(world.isChunkGenerated(cx,cz))coords.add(new int[]{cx,cz}); - if(coords.isEmpty()){busy=false;throw new IllegalArgumentException("No generated chunks in the selected area.");} - Path out=getDataFolder().toPath().resolve("export");Files.createDirectories(out); - List captured=new ArrayList<>();int lower=world.getMinHeight(),upper=world.getMaxHeight(); - sender.sendMessage("MineGeo snapshotting "+coords.size()+" generated chunks. No terrain will be generated."); - new BukkitRunnable(){int index; + private record Area(int minX,int minZ,int maxX,int maxZ){} + private void export(CommandSender sender,String name)throws IOException { + requireIdle();if(name.length()<3||name.length()>80)throw new IllegalArgumentException("Map name must contain 3–80 characters."); + String serverName=getConfig().getString("server-name","My Minecraft server");if(serverName.length()<2||serverName.length()>80)throw new IllegalArgumentException("server-name in config.yml must contain 2–80 characters."); + World world=selectedWorld!=null?selectedWorld:sender instanceof Player p?p.getWorld():Bukkit.getWorlds().stream().filter(w->w.getEnvironment()==World.Environment.NORMAL).findFirst().orElseThrow(); + if(world.getEnvironment()!=World.Environment.NORMAL)throw new IllegalArgumentException("Overworld only. Select it with /minegeo area."); + Location center=sender instanceof Player p?p.getLocation():world.getSpawnLocation(); + int half=Math.max(8,Math.min(256,getConfig().getInt("max-area-size",2048)/2)); + Area area=selectedWorld==null?new Area(center.getBlockX()-half,center.getBlockZ()-half,center.getBlockX()+half,center.getBlockZ()+half):new Area(minX,minZ,maxX,maxZ); + Path out=getDataFolder().toPath().resolve("exports").resolve("export-"+System.currentTimeMillis());Files.createDirectories(out); + int lower=world.getMinHeight(),upper=world.getMaxHeight(),desired=Math.min(100,Math.max(5,getConfig().getInt("viewpoints",12))),batch=Math.max(1,Math.min(4,getConfig().getInt("chunks-per-tick",1))); + ArrayBlockingQueue queue=new ArrayBlockingQueue<>(4);AtomicBoolean done=new AtomicBoolean();AtomicReference failure=new AtomicReference<>(); + busy=true;cancelled.set(false);status="Export started: "+out.toAbsolutePath();sender.sendMessage("MineGeo: "+status); + Bukkit.getScheduler().runTaskAsynchronously(this,()->{ + try{encode(queue,done,failure,out,name,serverName,area,lower,upper,desired);Path root=getDataFolder().toPath(),pointer=root.resolve("latest-export.txt.tmp");Files.writeString(pointer,root.relativize(out).toString());Files.move(pointer,root.resolve("latest-export.txt"),StandardCopyOption.REPLACE_EXISTING);latest=out;report(sender,"Export complete: "+out.toAbsolutePath()+". Run /minegeo upload.");} + catch(Exception ex){failure.set(ex);fail(sender,"Export",ex);}finally{done.set(true);busy=false;} + }); + new BukkitRunnable(){int cx=Math.floorDiv(area.minX,16),cz=Math.floorDiv(area.minZ,16),scanned;final int firstX=cx,lastX=Math.floorDiv(area.maxX-1,16),lastZ=Math.floorDiv(area.maxZ-1,16),total=(lastX-firstX+1)*(lastZ-cz+1); @Override public void run(){try{ - if(cancelled.get()){status="Cancelled";busy=false;cancel();return;} - int batch=Math.max(1,Math.min(4,getConfig().getInt("chunks-per-tick",1))); - for(int n=0;n=coords.size()){cancel();status="Encoding terrain";Bukkit.getScheduler().runTaskAsynchronously(MineGeoPlugin.this,()->encode(captured,out,name,lower,upper));} - }catch(Exception ex){cancel();busy=false;status="Export failed: "+ex.getMessage();getLogger().warning(status);}} + if(cancelled.get()||failure.get()!=null||done.get()){done.set(true);cancel();return;} + for(int n=0;n0;n++){ + if(world.isChunkGenerated(cx,cz)){boolean wasLoaded=world.isChunkLoaded(cx,cz);if(world.loadChunk(cx,cz,false)){try{queue.add(new Captured(cx,cz,world.getChunkAt(cx,cz).getChunkSnapshot(true,true,false)));}finally{if(!wasLoaded)world.unloadChunkRequest(cx,cz);}}} + scanned++;if(++cx>lastX){cx=firstX;cz++;} + } + status="Exporting chunks "+scanned+"/"+total+" — "+out.getFileName();if(cz>lastZ){done.set(true);cancel();} + }catch(Exception ex){failure.set(ex);done.set(true);cancel();}} }.runTaskTimer(this,1,1); } - private void encode(List chunks,Path out,String name,int lower,int upper){ - try{JsonObject bounds=new JsonObject();bounds.addProperty("minX",minX);bounds.addProperty("minZ",minZ);bounds.addProperty("maxX",maxX);bounds.addProperty("maxZ",maxZ); - JsonObject terrain=new JsonObject();terrain.add("bounds",bounds);JsonArray blocks=new JsonArray(),coverage=new JsonArray(),points=new JsonArray(); - List candidates=new ArrayList<>(); - for(Captured c:chunks){if(cancelled.get())throw new IOException("Cancelled");JsonArray cv=new JsonArray();cv.add(c.cx);cv.add(c.cz);coverage.add(cv); - for(int z=0;z<16;z++)for(int x=0;x<16;x++){int wx=c.cx*16+x,wz=c.cz*16+z;if(wx=maxX||wz=maxZ)continue;int top=c.snapshot.getHighestBlockYAt(x,z);for(int y=Math.max(lower,top-24);y<=Math.min(upper-1,top+1);y++){var bd=c.snapshot.getBlockData(x,y,z);if(bd.getMaterial().isAir())continue;JsonArray block=new JsonArray();block.add(wx);block.add(y);block.add(wz);block.add(bd.getAsString());block.add(c.snapshot.getBiome(x,y,z).getKey().toString());blocks.add(block);}if(x==8&&z==8){int y=top;while(y>lower&&!c.snapshot.getBlockData(x,y,z).getMaterial().isSolid())y--;Material ground=c.snapshot.getBlockData(x,y,z).getMaterial();if(ground.isSolid()&&!ground.name().contains("LEAVES")&&c.snapshot.getBlockData(x,y+1,z).getMaterial().isAir()&&c.snapshot.getBlockData(x,y+2,z).getMaterial().isAir())candidates.add(new int[]{wx,y+1,wz});}} + private void encode(BlockingQueue queue,AtomicBoolean done,AtomicReference failure,Path out,String name,String serverName,Area area,int lower,int upper,int desired)throws Exception { + JsonObject bounds=new JsonObject();bounds.addProperty("minX",area.minX);bounds.addProperty("minZ",area.minZ);bounds.addProperty("maxX",area.maxX);bounds.addProperty("maxZ",area.maxZ); + JsonArray coverage=new JsonArray(),points=new JsonArray();List candidates=new ArrayList<>();long blocks=0;Path terrain=out.resolve("terrain.json.gz"); + try(JsonWriter writer=new JsonWriter(new BufferedWriter(new OutputStreamWriter(new LimitedStream(new GZIPOutputStream(Files.newOutputStream(terrain))),StandardCharsets.UTF_8)))){ + writer.beginObject().name("bounds");gson.toJson(bounds,writer);writer.name("blocks").beginArray(); + while(!done.get()||!queue.isEmpty()){ + if(cancelled.get())throw new IOException("Cancelled");if(failure.get()!=null)throw failure.get();Captured c=queue.poll(250,TimeUnit.MILLISECONDS);if(c==null)continue; + JsonArray cv=new JsonArray();cv.add(c.cx);cv.add(c.cz);coverage.add(cv); + for(int z=0;z<16;z++)for(int x=0;x<16;x++){ + int wx=c.cx*16+x,wz=c.cz*16+z;if(wx=area.maxX||wz=area.maxZ)continue; + int top=Math.min(upper-1,Math.max(lower,c.snapshot.getHighestBlockYAt(x,z))); + // Skip buried opaque blocks to keep surface exports within the renderer budget. + for(int y=Math.max(lower,top-24);y<=top;y++){ + Material type=c.snapshot.getBlockType(x,y,z);if(type.isAir())continue; + if(type.isOccluding()&&x>0&&x<15&&z>0&&z<15&&y>lower&&y+15000000)throw new IOException("Export exceeds the 5-million-block renderer limit. Select a smaller area."); + writer.beginArray().value(wx).value(y).value(wz).value(c.snapshot.getBlockData(x,y,z).getAsString()).value(c.snapshot.getBiome(x,y,z).getKey().toString()).endArray(); + } + if(x%4==2&&z%4==2){int y=top;while(y>lower&&c.snapshot.getBlockType(x,y,z).isAir())y--;Material ground=c.snapshot.getBlockType(x,y,z);if(y+2 chosen=new ArrayList<>();chosen.add(candidates.remove(candidates.size()/2)); + while(chosen.size()bestDistance){bestDistance=distance;best=i;}}chosen.add(candidates.remove(best)); + } + for(int i=0;i50L*1024*1024)throw new IOException("Compressed export exceeds 50 MiB. Select a smaller area."); + JsonObject manifest=new JsonObject();manifest.addProperty("format","minegeo-dataset/v1");manifest.addProperty("minecraftVersion","1.21.11");manifest.addProperty("dimension","minecraft:overworld");manifest.addProperty("source","plugin");manifest.addProperty("name",name);manifest.addProperty("description","Vanilla surface terrain exported from "+serverName);manifest.addProperty("serverName",serverName);manifest.addProperty("north","-z");manifest.add("bounds",bounds);manifest.add("viewpoints",points); + JsonArray assets=new JsonArray();JsonObject asset=new JsonObject();asset.addProperty("path","terrain.json.gz");asset.addProperty("bytes",Files.size(terrain));MessageDigest digest=MessageDigest.getInstance("SHA-256");try(InputStream input=Files.newInputStream(terrain)){byte[] buffer=new byte[65536];int read;while((read=input.read(buffer))!=-1)digest.update(buffer,0,read);}asset.addProperty("sha256",HexFormat.of().formatHex(digest.digest()));assets.add(asset);manifest.add("assets",assets); + // The manifest is the completion marker. Partial folders never become upload candidates. + if(cancelled.get())throw new IOException("Cancelled");Files.writeString(out.resolve("manifest.json.tmp"),gson.toJson(manifest));Files.move(out.resolve("manifest.json.tmp"),out.resolve("manifest.json"),StandardCopyOption.REPLACE_EXISTING); + } + private static final class LimitedStream extends FilterOutputStream { + private long bytes;LimitedStream(OutputStream out){super(out);} + @Override public void write(int b)throws IOException{limit(1);out.write(b);} + @Override public void write(byte[] b,int off,int len)throws IOException{limit(len);out.write(b,off,len);} + private void limit(int n)throws IOException{bytes+=n;if(bytes>240L*1024*1024)throw new IOException("Terrain exceeds the renderer's uncompressed size limit. Select a smaller area.");} } } diff --git a/minecraft/plugin/src/main/java/dev/minegeo/plugin/UploadClient.java b/minecraft/plugin/src/main/java/dev/minegeo/plugin/UploadClient.java index 2c878fd..e019155 100644 --- a/minecraft/plugin/src/main/java/dev/minegeo/plugin/UploadClient.java +++ b/minecraft/plugin/src/main/java/dev/minegeo/plugin/UploadClient.java @@ -6,30 +6,77 @@ import java.nio.file.*; import java.io.*; import java.util.*; import java.time.Duration; +import java.security.MessageDigest; +import java.util.function.*; +import java.nio.charset.StandardCharsets; -/** TUS resumable upload client shared by plugin and Fabric build. */ +/** Shared TUS client. State is saved beside each completed local dataset. */ public final class UploadClient { private final String website,token; - private final HttpClient client=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); + // Next's HTTP server does not support Java's cleartext HTTP/2 upgrade handshake. + private final HttpClient client=HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).connectTimeout(Duration.ofSeconds(30)).build(); private final Gson gson=new Gson(); - public UploadClient(String website,String token){if(website==null||(!website.startsWith("https://")&&!website.startsWith("http://localhost")&&!website.startsWith("http://127.0.0.1")))throw new IllegalArgumentException("Use HTTPS or local development URL");this.website=website.replaceAll("/$","");this.token=token;} - private JsonObject api(String path,String body)throws Exception{var request=HttpRequest.newBuilder(URI.create(website+"/api/v1"+path)).timeout(Duration.ofMinutes(2)).header("Authorization","Bearer "+token).header("Content-Type","application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build();var response=client.send(request,HttpResponse.BodyHandlers.ofString());if(response.statusCode()>=400)throw new IOException("MineGeo API "+response.statusCode()+": "+response.body());return JsonParser.parseString(response.body()).getAsJsonObject();} - public JsonObject beginPairing()throws Exception{return api("/pairing","{}");} - public JsonObject pollPairing(String code)throws Exception{return api("/pairing/poll","{\"deviceCode\":\""+code+"\"}");} - public void upload(Path directory)throws Exception{ - if(token==null||token.isBlank())throw new IOException("Link a contributor account first."); - JsonObject manifest=JsonParser.parseString(Files.readString(directory.resolve("manifest.json"))).getAsJsonObject(); - Path ledger=directory.resolve(".upload-state.json");JsonObject state=Files.exists(ledger)?JsonParser.parseString(Files.readString(ledger)).getAsJsonObject():new JsonObject(); - String manifestHash=HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256").digest(gson.toJson(manifest).getBytes(java.nio.charset.StandardCharsets.UTF_8))); - if(!state.has("hash")||!state.get("hash").getAsString().equals(manifestHash)){state=new JsonObject();state.addProperty("hash",manifestHash);JsonObject request=new JsonObject();request.add("manifest",manifest);JsonObject created=api("/uploads",gson.toJson(request));state.addProperty("datasetId",created.get("datasetId").getAsString());Files.writeString(ledger,gson.toJson(state));} - String datasetId=state.get("datasetId").getAsString(); - JsonObject request=new JsonObject();request.addProperty("datasetId",datasetId);JsonObject signed=api("/uploads/sign",gson.toJson(request)); - for(JsonElement elem:signed.getAsJsonArray("assets")){JsonObject asset=elem.getAsJsonObject();String name=asset.get("path").getAsString();Path file=directory.resolve(name).normalize();if(!file.startsWith(directory.normalize()))throw new IOException("Unsafe asset path");long length=Files.size(file);String signedToken=asset.get("token").getAsString();String endpoint=asset.get("endpoint").getAsString();String key="url:"+name;String uploadUrl=state.has(key)?state.get(key).getAsString():null;long offset=0; - if(uploadUrl!=null){var head=client.send(HttpRequest.newBuilder(URI.create(uploadUrl)).timeout(Duration.ofSeconds(45)).header("Tus-Resumable","1.0.0").header("x-signature",signedToken).method("HEAD",HttpRequest.BodyPublishers.noBody()).build(),HttpResponse.BodyHandlers.discarding());if(head.statusCode()==200||head.statusCode()==204)offset=Long.parseLong(head.headers().firstValue("Upload-Offset").orElse("0"));else uploadUrl=null;} - if(uploadUrl==null){String metadata="bucketName "+b64("minegeo-raw")+",objectName "+b64(asset.get("objectName").getAsString())+",contentType "+b64("application/octet-stream")+",cacheControl "+b64("3600");var created=client.send(HttpRequest.newBuilder(URI.create(endpoint)).timeout(Duration.ofSeconds(60)).header("Tus-Resumable","1.0.0").header("Upload-Length",Long.toString(length)).header("Upload-Metadata",metadata).header("x-signature",signedToken).POST(HttpRequest.BodyPublishers.noBody()).build(),HttpResponse.BodyHandlers.ofString());if(created.statusCode()!=201)throw new IOException("TUS create failed: "+created.statusCode()+" "+created.body());uploadUrl=URI.create(endpoint).resolve(created.headers().firstValue("Location").orElseThrow()).toString();state.addProperty(key,uploadUrl);Files.writeString(ledger,gson.toJson(state));} - try(RandomAccessFile input=new RandomAccessFile(file.toFile(),"r")){while(offset=300){String detail=response.body();try{var json=JsonParser.parseString(detail).getAsJsonObject();if(json.has("error"))detail=json.get("error").getAsString();}catch(Exception ignored){}if(detail.length()>400)detail=detail.substring(0,400);throw new IOException("MineGeo API "+response.statusCode()+": "+detail+(response.statusCode()/100==3?". Set website to its final HTTPS address.":""));} + try{return JsonParser.parseString(response.body()).getAsJsonObject();}catch(Exception ex){throw new IOException("The website returned HTML or invalid JSON. Check the MineGeo website URL.");} + } + public JsonObject beginPairing()throws Exception{return api("/pairing","{}");} + public JsonObject pollPairing(String code)throws Exception{var body=new JsonObject();body.addProperty("deviceCode",code);return api("/pairing/poll",gson.toJson(body));} + public void upload(Path directory)throws Exception{upload(directory,message->{},()->false);} + private void checkCancelled(BooleanSupplier cancelled)throws IOException{if(cancelled.getAsBoolean()||Thread.currentThread().isInterrupted())throw new IOException("Upload cancelled. Run upload again to resume.");} + private void save(Path ledger,JsonObject state)throws IOException{Path tmp=ledger.resolveSibling(ledger.getFileName()+".tmp");Files.writeString(tmp,gson.toJson(state));Files.move(tmp,ledger,StandardCopyOption.REPLACE_EXISTING);} + private String hash(byte[] bytes)throws Exception{return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));} + public void upload(Path directory,Consumer progress,BooleanSupplier cancelled)throws Exception{ + if(token.isBlank())throw new IOException("Link a contributor account first."); + directory=directory.toAbsolutePath().normalize();Path manifestFile=directory.resolve("manifest.json"); + if(!Files.isRegularFile(manifestFile))throw new IOException("No completed manifest in "+directory+". Export first, wait for completion, then upload."); + JsonObject manifest=JsonParser.parseString(Files.readString(manifestFile)).getAsJsonObject(); + progress.accept("Checking export files"); + for(JsonElement element:manifest.getAsJsonArray("assets")){ + checkCancelled(cancelled);var asset=element.getAsJsonObject();String name=asset.get("path").getAsString();Path file=directory.resolve(name).normalize(); + if(!file.startsWith(directory)||!Files.isRegularFile(file)||!file.toRealPath().startsWith(directory.toRealPath()))throw new IOException("Missing or unsafe export asset: "+name); + if(Files.size(file)!=asset.get("bytes").getAsLong())throw new IOException("Export asset size changed: "+name+". Export again."); + MessageDigest digest=MessageDigest.getInstance("SHA-256");try(InputStream in=Files.newInputStream(file)){byte[] buffer=new byte[65536];int n;while((n=in.read(buffer))!=-1){checkCancelled(cancelled);digest.update(buffer,0,n);}} + if(!HexFormat.of().formatHex(digest.digest()).equals(asset.get("sha256").getAsString()))throw new IOException("Export asset checksum changed: "+name+". Export again."); + } + Path ledger=directory.resolve(".upload-state.json");JsonObject state=Files.exists(ledger)?JsonParser.parseString(Files.readString(ledger)).getAsJsonObject():new JsonObject(); + String fingerprint=hash((website+"\n"+hash(token.getBytes(StandardCharsets.UTF_8))+"\n"+gson.toJson(manifest)).getBytes(StandardCharsets.UTF_8)); + if(!state.has("hash")||!state.get("hash").getAsString().equals(fingerprint)){ + state=new JsonObject();state.addProperty("hash",fingerprint);JsonObject body=new JsonObject();body.add("manifest",manifest);progress.accept("Creating private draft");checkCancelled(cancelled); + JsonObject created=api("/uploads",gson.toJson(body));state.addProperty("datasetId",created.get("datasetId").getAsString());save(ledger,state); + } + String datasetId=state.get("datasetId").getAsString(); + JsonObject existing=api("/datasets/"+datasetId,null);String stage=existing.get("state").getAsString(); + if(!stage.equals("uploading")){ + if(stage.equals("failed"))throw new IOException("The website could not process this export. Check its dashboard error; create a new export after fixing the cause."); + progress.accept("Already uploaded — draft "+datasetId+" ("+stage+")");return; + } + JsonObject body=new JsonObject();body.addProperty("datasetId",datasetId);JsonObject signed=api("/uploads/sign",gson.toJson(body));int index=0,count=signed.getAsJsonArray("assets").size(); + for(JsonElement elem:signed.getAsJsonArray("assets")){ + checkCancelled(cancelled);index++;JsonObject asset=elem.getAsJsonObject();String name=asset.get("path").getAsString();Path file=directory.resolve(name).normalize();if(!file.startsWith(directory))throw new IOException("Unsafe asset path"); + String publishableKey=asset.has("apiKey")?asset.get("apiKey").getAsString():"";long length=Files.size(file);String signedToken=asset.get("token").getAsString(),endpoint=asset.get("endpoint").getAsString(),key="url:"+name; + String uploadUrl=state.has(key)?state.get(key).getAsString():null;long offset=0; + if(uploadUrl!=null){var head=client.send(HttpRequest.newBuilder(URI.create(uploadUrl)).timeout(Duration.ofSeconds(45)).header("Tus-Resumable","1.0.0").header("x-signature",signedToken).header("apikey",publishableKey).method("HEAD",HttpRequest.BodyPublishers.noBody()).build(),HttpResponse.BodyHandlers.discarding());if(head.statusCode()==200||head.statusCode()==204)offset=Long.parseLong(head.headers().firstValue("Upload-Offset").orElseThrow());else if(head.statusCode()==404||head.statusCode()==410)uploadUrl=null;else throw new IOException("Upload resume failed (HTTP "+head.statusCode()+"). Retry upload.");} + if(uploadUrl==null){String metadata="bucketName "+b64("minegeo-raw")+",objectName "+b64(asset.get("objectName").getAsString())+",contentType "+b64("application/octet-stream")+",cacheControl "+b64("3600");var created=client.send(HttpRequest.newBuilder(URI.create(endpoint)).timeout(Duration.ofSeconds(60)).header("Tus-Resumable","1.0.0").header("Upload-Length",Long.toString(length)).header("Upload-Metadata",metadata).header("x-signature",signedToken).header("apikey",publishableKey).POST(HttpRequest.BodyPublishers.noBody()).build(),HttpResponse.BodyHandlers.ofString());if(created.statusCode()!=201)throw new IOException("Storage upload creation failed (HTTP "+created.statusCode()+"). "+created.body());uploadUrl=URI.create(endpoint).resolve(created.headers().firstValue("Location").orElseThrow()).toString();state.addProperty(key,uploadUrl);save(ledger,state);offset=0;} + if(offset<0||offset>length)throw new IOException("Storage returned an invalid upload offset."); + try(RandomAccessFile input=new RandomAccessFile(file.toFile(),"r")){ + while(offset /// -import "./.next/dev/types/routes.d.ts"; -import "./.next/dev/types/root-params.d.ts"; +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/public/fonts/OFL-VT323.txt b/public/fonts/OFL-VT323.txt new file mode 100644 index 0000000..bcb092f --- /dev/null +++ b/public/fonts/OFL-VT323.txt @@ -0,0 +1,93 @@ +Copyright 2011, The VT323 Project Authors (peter.hull@oikoi.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/VT323-Regular.ttf b/public/fonts/VT323-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..afa69098f15bf64849d397f133d9467486904053 GIT binary patch literal 153116 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 literal 0 HcmV?d00001 diff --git a/public/tools/mod/minegeo-fabric-1.21.11.jar b/public/tools/mod/minegeo-fabric-1.21.11.jar index 478b6256190cfd36c0b0c99f570a7cbb30eccca8..d6de6d4994ee4ab693e38a436e09cd8854c3ba33 100644 GIT binary patch delta 19359 zcmV(zK<2-q=K;`>0~WGvCIos)wC#!z*JPxG+OjfwEk*cqWZ3Q8Dm$fs^bujxe^J>1&Cr~K3?=mq#{&rB20u!%iCCRUjU?U{|G!@7 zmc(>F!`Aw^Asm-lwfMLbR&{RMqpi(N-3`a-a+UnDYF6JlRiquwG!)!I2iZFsRZqB7 z&xdUc)vFx6rl|`~^gGCk3E^bd44doYt5gZc(u|4jhEoCDidsKHxQ*eaeJN>?p?4-r7kld$3os?8~_&5-G;g*4jZV zVKhmWW`+$ZH|C?wkw_=QE%gnBFKTNcu2!`9i7QgXRw7c+v7$w5M?1+6u60`g`_Wa7 zPEc4Yn_D^>o9M5(t0fZYf7tKC9eH^7SQNa3a3t=|aNWKp6kbBOq3A)cA1_6eVaMuu zOxMK(*AotRL=GJK7+SIz3ps>?kYe0H6nGd03cHT@wc7Z=Asw8%&J!3`R+n+CnLi$3adTf5cEZ1~42zC92A? z4oAr+wy>s!#juG2SHRIDIOZpw?{{`~6fgSk3UepjpnpkLDkDe`8#MJn=EhCuF#g13`_jycR7a{i515o*x! zI30)OK5@Sj@ABgvcsD~$wlMFL?%78=Wptmp_yi#!$A% zeB>(RR!*f`=wbaB)!nm}<_J$ohCAyEshMLzm>R4`5JJQa0H87GGz@x^6K@dvD(|C2?L%qdmhT$By=mQdqw&-^ExF!=4L*;kyBB@b3G502SWs z2L!RK_yB$=4IRLbNKau|0sO?P=Rfrtu>oP*e|&<-ekPx^t@!!10PF9G#U;Rg86?5d zi?Y27jdKEa(udzrZzxvYj{N4ZyLg#xqO+2eNo}p~fU0Nv*wUz(Bw3YlA|K48#)LJ( z$8;fk9aH7{ac*gHo!#6>t%o`_z|+}vm4~)rZc(8j|57{yF~bm6RNg6Q=}(v$OBJ$K ze-Jr!pbqsVJniw>XeRqg2`TI0mmZ~=Uq;d$E3zYX2<}+zjwR%NYP;$CXIh16WRidP zC2YP(lXdtDeapN}{FT0e?EFUDL}NK>a5GU6GkcMQmC)E5+`fpNGpN};kKN0trlD~e zei~YqQF?Lr0@{}ml*2p@1P?CY&;@LKe+vC4gGVmm<Ea1c< z?tBt_785~j0mdSn8H~(gdIkf*bBnl_B;P-e2WGNT-Xx{a-rE-Np5Xf~+wCrHSidGL9BdJ&&n!UE{uAYCqB^tPYAjBhZ^;>u&V;~Kf&l5%sY=5U3?e-Z5x zzURs*nZxr{2j=j?ES@jD>m;pa@xr5IbBQ~>;(G)N88uBd{rGWg9hk+7dl&GFCH&fD z5B^pfBXuD^_7YI*3Cs(uT8=Ep>fCh+{ziVULB%#~F8>cg3I=vIp{}m7qF=$*8aH0M zC@z44?eO6s&3g$9qZGLXY-H%0(-1^|l&H3|V>Xu1c4&IXlscJ=MLGu6IU}9a#v@5hOGqF>Lqao5s7O+sGCCr0 zHI=#~*xe(clHex50CZcgI z)iR>%GwC@A#}HN>8B{_&q1u8cNV~MjRLsx_jU%OG=?(@42vrGnmO-jF#1b*%R6?b{ zbsM2}GXtNk>5A}3351pYl5m34K7-RXCA_H>P7L!kUq#f^J|zzCb7#2$zmiTj{}g zOiLIN+6m6#(ea6mql0IaVN!s>N}P=KF04Zbp=K(k#hF{6r?-zl!!o)MaG?U-WkQ49 zAt98H*w>**0v-7b-0YZJL@L+PoI7g6s#e%=Odpg5xh+r9Q z!of0OB~D~)#;BX6-@>lktj;MS*qwN#j3!H~aZZz{YN#pA;BrvP!{h1Xlp4{_w1Aj@ z;#O=E8f_YNkjb9q>jh=dD;0Bo3x@G2%#YzPUL@U9)d%AA_elj^MM zgjzN=M9pIJc@QBii>tGfQME0pdnBSVG)xH_?x=NOn)6^$4H_{SawSq( zNkdQWk#L#i@9YNQ(R_a&q09{|4vTVsh!cC}w3l~iu{<(v>*CWvH69=5?BXDZW7drs zBxVcN2s7jMSDpJ zL@Q(o(%9*S3%d#>2ol=;qQ)F8nLW7N4H>T~B*PuL*e&H*J7d}| zWh%zO}$ChJE%%!tha~#nJoWO+FCb%;Jy45;KY#iRkHs;lx{uqzK5k z&XVHooJ{ZV@q(RKyLB$Et5Kyx;UG$j0(E^6Ubl=JEWGzH-g|wh6nHB}Szj(YifSmL z5h`&#q8c$h;l$n|0znx!Sp+^?zyv<%FP#~Jz)<&0Gr z)<l7z{ArbO@<@;@Ujv`68#2&ywH}1mSTwb?ZEnL}p?ug>wG%+!zOzBBw zWaFq}XeomWVyhEhDll0mx5us{Fke(rZKhwweS+y%*d*uZv4o-~lr1~8JC1t%_BK(cGz#@JA&eOIVuZ|Bz&(1-xuf#<=SFOxqm-@YP&gy zl3G-WOs5kwTmYURv@ELNmi~i!g7ai3+pB)SZk|#Py0}fpRyNODI=Yd@pEe^`$f6< z4)8tfS9ud#Pk^r}Czp;e4*lMNEa&{V_ziyR#;@@^b~ekKRN2@<%6!OSd!M2IfIo`R z_!Ezji_azeg;Rc7O^uo<&%tzojKAX98a#vN#1+PW+^+MtE;gZzzvCYQ?w@7uFQH&B z-(UV!dHC_iAI}>)Lp_1c|2?Gzv21qz2QP~3dWq$WGg!I0W1^Iu^Si|rjfU8FW=G+G zq&OeUkE7dw4OTLsHINPx~#S-Q@~P` ztywjwN6k{mx{ao1C$;2+Iw_d?M!2QNx2egP__jBfjT>rYhW%!Ld#t)LrgA;hIAz2< zIGjjm$w6TjjX&MvdO8`=Y(>t{bt7dY)j10<#W&LmZj|76g}CmSt0wc;&121RXa-S# zh`)loPhz*tx9Tpm?8iyB3s?&CyV^W)Q4ham3krU;@^2N|vJ~h!9u_P4H+ly;N808Q zbi}>5I(kam-3Xu0-~G*=fS0N~b-oQIgVXNBM&91&#X}jKC06b8*p$IYn^G#0f?`??E_y7wm(7Tde1Cu}x>fq~n`pytej(Fu-ZMyV-Lk|EtU3 ziVR+70bI=heEt^e+>2{3T@3R!o5;JeKsOabyxWF&{{op3P&e98AI?H$Oy%-HJ}N*C zVqX^Gu_6fJn@Z~C_erQYj4M$op+e$-14#%UW{>4I_#KvTsp-fzcARitEo%6GHZJ3O zEaRzYIiBF1=i#AMoLn2>qX>>Movm07%{rSd#b>ad1E7|+;hlvP)tC?*L z!yRgCZ(i9@wI8=HL_yfmE*#0voz^BhGn2tR=C1GFdWYx4iVW_zKF{UPG4aWf)L-3L zZF~1?`>@9Rn8!m0@GUN^*2iOi2f*e1Abtk)OD2UzN!ZV3{w!0v$4kzz(>!9FU-Bs% zG=twCz@G{Iu141Z{EcuU){52hg!}M9zuO|^u6NWsegDqj@HI~NzQa#0n7#>BFXBQr z__~YPz%N;5mhj9AXwOTVI@6L*4%)y1r8^H_Ou ztUNhZp3MeU&>ano>{9ePsFL7saSGzbeU0kqST&%qCo$f*%OWlA*8ZhJ4 zK^yq@OopCgR#C+Gu~ygSR`1NM-j`c_D7X3~KZjVPC;tmjO9u!)1sY(&3IG807ytlJ zO9KQH00;;O0001!fEzr2Q`fowzT+WTT!WDz!9cHYqO;<=j7*o1S_kJDec-`Z5H{ER--v7JzN|tR2czy5jGrHrP z^PO+}zjdU~K0WavBHE;0#-#U~BXz@}h&gCR>jsRzcqmxcg~#oGX0$yVG9#9Un5pcD zF=Et(jmTi#uD&B?5N`{ZRzyt8T{9f4Gh(4SXGvGIzatW|LPj|BY%}hmA}0S7nzcfX zX_>5-y@PRMz^V&|jYQ&bQ)^pGrF&c7<{>ZBf_(ElrOq6Y&v0nK42}iEX5C1GtA^fC@aR?8j3PbhvIX8XbgvP+djnHJ)yjPut)?|TDx|ecRaIG1 z?MX%=Mqk)mw=(Nx?@-({`q!1wdL}Jw4EOaLl@W7)w6b2K8)&_c6g^{3F>QpAW*E2A zo(&Tl;2ByU^U!Aa;_&1Prq5?Ha0&dZo2mVL3wy_6=D#=SY6sc;(aI8-y@zR0E~nX?42An8N$mB}KEc>rAa!@# zw5+k+u$yIrdkM60iN)COCORPN4+^-!uz28tJc}^Ol7s)f5}G0h*ctk~MR zx^OgTgomODYi(0~eZ80ZG^(LhG6`lOFAZpaRO>tsF|FEq3=SMO^++^647-l$R#Xp0 z!(pUIJ>hsCo=42MF=*C$>4-*k&f>5}_0%ADhzEt%Pzce$AB{}rO(#+@l}`d_>#?92 zvqI5`hvFJtN7u`8D_=5eK&BgU90+Jc^lfQ?4WN`fv!Rz`H0GmGdN#u2K*$VZQR^Ci zWYyV@)e$AOAxdE0bLn{{nD>16c=m{gUI_hLQ3)dml4%G}4KEtqL3jG-cDf6)Ie{o0 zwrY>SJYKp7nwYuJLoY(Si<^mPc*L9*id+}Zv)_y9{#kS%y+or{Y7>>A>{&2%W@jjJ zbeF5zSEO~G85P;!Kd*vt=CM%1f~gvRt8(pis52kgbt?~Q^cs4tk6uj=K)vEr4%d^S zOr^DQrhFPbNXLc3Lre>&LwhGab{p}8_|h9-_L$tKw`j=UyM{lEddZ{LlFzG_`-N-|Kps&$; zgrP_1y%5gcFD=}>s+_udKYc)Uc+^X^OqK1?WVj!0YU!eS-7N=n2Y|`AAx84jBrL$SS(~5ehGPk=;DRRN=Ir?A;mC$+F z-=XKF591+d#H?i8^wLN3l);UEYg*ywABQKpe%@N&+S0OwGA90%r0n1WNdSDt*mIU!nhi z2W{=%a=5*-V{3Qs;ogpRr1KS>`FM321iKRejc?F5OK3iQi)nElCK^NwW%O-0vRjeO z%rlk94K4YFxCCL1zDwT|fB4TiMf81CF$fLm#^k=L%a`JpQQZGc;{`qakbdMRrXRaa z;$;-;p`W6sNSk_EEO3K=n8^Nf`h|~vM*oHK{Rk{@shEc;5>z^KM0W8vRWwl)uv7;rC9h7z-O? zX535voN^H0xT=T#1;K)OSqDZyK4FZQ?QYXig5Zrr1tn{&=uV`6W-dtgg_#s+SfJjQ zw4CI-DgET(B2D~KnaIa`t|%Ypp_->~U=MqdZMj6U4l2`GMt}eAIKq6~ zf~l^*vH-hJV~tBefoCIA<*G+LTxM5IJMGpInfg*`Iaj7*KbQM>F3+1$*6cz`a?2XN z3IdGA(RZZN<}EpYRhC7+eaJvXe_PUwproI!%1P4sw6}mOM0*RRyfu$mlX4$OzjY~Czyn?UuaV1|rrcCq zcym*8TRl^4PWrhhv^2ILX>%Rd&*EBc$P-OqJr6fc2B@~CmL@3$b5{#sYTZHcj}~tA zaWl7R)JRRJ&v^|xzDdS9&`k{*;T{V!picU(NIJ;BKj`G#nEUS1s)8jn{H1w z5Q--(FK>{40wCq%_56%U4Ky}2HMH3W99uNr#G8eI_9?@S4H|EC2DTv;pis1oPz3NP zs@k#z)jaRu4j(P#olNU@C9OoL-_)gz(}#`N^$ESDp>b6oddZ`DpPVrn@v&MRBE&7- z(9wj;EPcp~qbcN0xPMHJW?t#*~c~=R4_Sua-Z0eYY_rSLj$l4>ECuhFo)$oFsIS+h$rUvwynNFXx_kF_Z1KoRlrIidMu+7YHP4jO z2zdv-ZsuEje28y_a;&I>9*v)cq8l#_YH8-`{iQpJZd(CLCOZwg9HKy@%r72y=&jW{{evMxsZOZ~i zC%rI7?bPn{10KEuKuidp;ETEYM{IZ(-|gdnI~fgiv$GJ==!wuVeI#U#CTu{$UlK5C zc%2?YZ=KM$+b0+X)E@>k(Q^BcuDhI;NaGg^a|ym53Y3)I@3I3J#_XP78Z$|)-NrCx zzud|0uaFNzn&;b+MmUkDn1V;WnqMQfdM)6LRIBmp_%#CO2LrC&>t8F`=wUw<36!!9XnKd@(NQ@Q_3%3&FZ{gAh~WllDKz4EDa3pi zzgw*G2-C7{p-3n(gt%eq?pc~H^@Z(!o9X!VUXA`BYFGSzrna0R`|?M{8r{r~itbLr z5xb!Y#3%jZud^=_C2S~}{k8foIZ%^V2{g*X2pBs4Y9|q$?smQWxNY@Nyo?a9OWn>w zUR1Vdd`2v#czjL~qa!YeCsEq0zJhKb+Z>q#W{uDB2mMsc=hFqiWey-cEWNXTX}>fl z9c0?EH5}xl{IOa55k~u5?BD`2MSxw@VJ@_F!_vd1k+5Kn5pyzd>V4*bB$>t_@$*c>SJF-WTU+H*`WOA=;V&V;CrmlfG~tsM zq&Zl~U)7i$6Qg;(VQ*RqI>McQX(6(7M9?KwA}*O2@ziNK{keuBSUM!!JIJXO`L8h_6|m|--c{QWXY z*^Vdmf_-9frO>mXHIR%1#mse5HiG3i)bS(!v7fyBlQMqyjLq9L{yDsV8k_$Dr5n0I z%|4;p9E}1yk?F_z8S3tMPcjw*53{F9%2d|oov$7jr3Gc8k2wz9Ip-}vuBEDrSc$ALP*4(_GrK9RVC0E ziqg+EsJr6e)C$16QJd-T&DhdDO1_ekxVv zMP&agf6ts<_Pk<07DFZhfIMIlehz~9M_N&nx&nWiDvRuau7hl zMfL}HT;6O9xB00|FkH1M7)0}6**prr2M{w+o4HBH$$R&bK3bx-W-V{4 z*VJ~2+SjNZS;cg7Q>&(SN$fDXy0?Xn z*^P=y$oWqwcvMqK&{UQ}*TnFV+3Bxf958#;7}LssoP}EMZ(&xe=SWz2uJnPXwZ$-p z<@l_yN{$rN^VJJv@P(P5I83Do4y>s^(Qtpa00Ql{Th8eOciqY1J~Q4cOJugR82gNP zNS@u7rRbo7N1-A2*_TOP(#tz%L%R+1DJIGxNgf@M2%J=GCa0FZ9^{&!DIF80BUM}I zXN{PDvo1jmq^OVpbn;P&eBX<(J6FC?x32-JmLGA+x6}5u*1gub*LtictZt+xT$MDf z_O+GTaGj5l)wG6K-HoRN72ug@P2dcz3rtWa(KzinO}&A0wC}(f+Ml9BDLQR!CnoJ}ZZi^6nU zhJoLb2doj@5qO-EfpL1yDSAPQ?)Kk%mR_ppG`&1ar*Q)ef|u8 zeQBJ&-uW0k5vZP^pAea3_=T->6RUe9XUkoEPn% zw7A2=;w&CEyF63?sT(kSHwCyZ3seJ05WlPD6zdmgo?NPbC%6VM z&&uRfX4ftUCxS_&Ox?R|~Cx9-q#_r@JBhASXGfDh4xQZL5aBF1Y`%SY4TF3_Utv;0b?v-~P1m(3JE@DP=M1Q82v+Bbq?GUqs4nouwa5TBf za}1yIEZ2iHWM3Uslv2@>A`7zb_3%4AS@JK*$bSfvL}~%TSO!C5{4#_~P9x`Kwkl3! z!qy$GJGH?(q7ZqU-*Y^FZT$Dy#t)?U1LOSIX=|>wZ~(me+2`XncblF<`uu z>iH|k92u{@09KV8iMTxe6g&zJj#Ss2=C4%?q*DBKu=GvDl5_mT0SRLgz|Qb@s>OvS z_@@M<{T7(wKftl^;xBUlS3859pyl22a@7Q1%zj(Cw$Q$-Cf1GaJozaO5f%4?_CM43HXKAbA7jsxU862S8qjXqivWK-~2U z?Gdagc&|V!dnNYyUIu7uT@i}y3~(vdi!-bjt0F;&%Q_x^g!MC-Xnp|7^KETrI9%l~VfJ z;)>#OwbWTsu9hRt9Y^x2D9$Q5WLFzps>&vG@~$F2aWQ4b1Qn2&&?&>!6ROS`x23y> z9t5cF#pi>6nOIOmg%@cf6(eqY_z4d^f~1c>DCVEE?H^Qh5q^~k>NA{s5A4!QO{yOG zw-{u)RD)`S!}#eQ)ufs+^J=;sq2t#eu#84j3v^n50CPmO;;xX6yZ97}CtM3FX@&EE zZ%t0{Exls!NynZ8eCzTOoeDlB$09Ck2i>w_aiF??Ty1bHQ!}A9+Z>LoZ8a&?k(;Iy zj234YU2H4JDQ{Y{-SIS5x5~Ei>lb(1zhkikhz9_(&f}!p$v`Vt-Q((+CocY`=A7y| za7Ohash_y`{hCwirVA8u>YYA$N~v2@sz0U7ay4iJ%Pu@n4B3fN39vq3C)U|cQc+En5S1FV?zjU2%o|LwwS-Pmm z1gX2Sg46>Utvuisv2r(ASd}QeCb}q*Kz{7)YGpgfi1o|QsRajM&}W}f&+Du%OsU(e zADw0ce->EE)iHI4ZDo(TQ{6S`?#JvPd%q2TlVaq{S)^^O0HVs!5>=qzm`_&~m!S4p zNQ>}%4ax$2V+jymiPE4#y+pPI$(Q2#7R=rO(KaBIZ?NyaLqDMJ@Jgh@W%g&p{(O`D z`8Jt>ifn>Y?%fyMyYKQ3_=oo0&-j=8Gk5gQnR!p)KPV`6p4Hs+h`K&=SL@DMZGS!# z!~VSOzX4E72MC7R@PAtu005{nlYs#ovtBZCCV#bi31A!5we~r8Y>h0-PAtVHii53e z;CM|~5^&h;#7P{Sg*XcV3dq*j7Lg^RB@0XGMoV|v=Cve1ffh(+43>*L|f6ZD|Q1dEdD+lI27;=Kb#nGjr$8z2}~L*6-XA&mJ9pf{5CT)0t|+){d6J zNPo=gx8f~B(R6<#*0Omh8V`k+MphqG2mxl8b5PM59b{GnvlN$*66y63IwBW>P7}I}c2A zP8`3{x;kwo^Oi4X3QRc6r0Gm`lTe9=cYkk4_e3Mf0jz0~kEwioAJeH*oNiJjQ`xw# z2_`3p;<2QqXRi*$!co2SOr}{A4>oBQlQ(|`(8XiIx2X)QTp zen+e~uEiP5T1VbnljcGz&Jd;rW7ZRi?TBx;TGm>rfq2*^pPb(nk8e*8nY4hZW`FXb zCY{RUvd3eEomNjWlCqe5U1RgQQ;A5de;HG83iDwUu3l|25Leek=7iBj{YkN)s;=Jn zU`u~I-XFDEq^o5WE_m-cClZUK&Vz>PPF(E7o`qYO{A0ZxyLzpmlr~uSoKd%MigcTC&pIg8p^EQL=pkz^>hr-m4;bVh4qfx6r z6m3iNrw6gmJS&Yboz`uof`5*S1yk{0DA<>dMuQ@o)|QrFJfWLOX@|n$gq2J-yJ1WxAJ73>2dqKMOV@}O1d+g4e?7=G&p?(pxm)>XhzuzXj zA(R^M(g*Fn51}uT>=+zM?be|JY}`yA5i|HG6Suf&4|I=&5{f3h^l^Hty!iyvrsLEl zlC4WFgn963_E32P`C>b2UU(J_yFgV zst6Ug9^_9L9=CvW%Z|nDriT&RPe^Yz(dF@2Zz#1g)SHSYcANAF0^(q3yJdH8g}%^A zrb4k4_H#=63au;Xg6#QI?T`k4?Rl5(_AznPj~r{ z0S+>qUN>HGg?}RLaX3w9xW_$IK~H2;iP9-!Z6uaXS+M*k?ZogEcuOjq5zd}+MmTY~ zr|9WYnnuqc)I!=R7)F!G#Z6yF=nf4HMI*hMT+3BZx0}8JCmgUsI%Rjh8MA)_Hoi&E z3mY%!AkdEa+r9Lkw)=k@aIq~7g(V`_Xja_x9e8KE1%IDS1Yu#{1IC*;7mW9}4E?}I zOfSOtS9KtnEhw1(PUP=L^kd=gCrl?#HbbY(pUKGo28+n_dR?Y-C(l)rBW4i&1^v=P ze)<(WY{U9)@V%}Oaw$A}Y8(6w{Z`oi9aH@T8$1y*YsUWIp#c36j3uoY07YmL#+FT$ z?qm#L(SMid&jJGff-sEmkV+?)199u5(dnhX(aWV&O@BWgTsW@mrHAOBGT;>;<3b^A zLnx6Hg#T~kt|4h>b-1{o4hxa!h+2a-emYFAdFWL-!c;xUK$DI#O+!L#OO6FqR0VaD z23;@GB75i|CBGecN9ecgi3!8(-W%o!b8SIP1LeZe0|L!pa#?>gy5hSfuRy^3Yp)<&>LB?)Qmgv~| zzkg288Lssq0tY=jOAuV=rU5I*PQps16EQ0cR@Qb2E<|PsMI+(hn(p=MnuE&+tlsUx z6eeWZ%1+$8u?q~(gUei{i?7*}r&d?k0$c;kFnhAa(!43SG{z_FE*Re=K8ZM$a9oRN z(Ik#{&WQp#Z<%mEiLWB7^jJV18$uD=9Dm#`Lw3{6ivVPju-CjP2P7~8U7wIL-REw0t!=YU4Et1myY#nP$K{nH6St>Z{e*z zGWg=@^pO*!z1Yi_3K&s*8R(*X^?&MM*dC8Ztx!zhM?vb&`|h;BD80xPe5H?y7=`V{ z1r1_S9;dPdDo*a;USSnhH>1!Ra-<-!X?ZBtYoQVjj~(v35p7s&@7S!lZ#>83tFUSM zNGWgU=(w|hO#>+7!zM#|WxeS{0?uT29iO-DUdV2Uul8`95&p^|$xcAkM1LYZglf&p zDd+}o(o9Wug%iwi*&N)MjssP&93=?>i0x z7pPlu(dk`$qa@yUgBxy64Sx=~`6eF~3nVUwT;kCkR(Ay0QQ~jiB(U{3mfp`F5SBie zJLS&4Uq58U);l@%NaC=t#dwHJ21v13D5+kf(oD>^Rj?B3M5 zZq?YC3Gd_EVP1oAF|TctYv9|wN)CvT zUD{s2PUMJiH!9Ao#5$Q=IZwHar;^|llU^tOXA%Bp43ErEExAnk((Xwx%r1#t1fICCjVHF#p+W23I9~!NHS@qFzvsgu3Wv>mPmvE#9F2dcJ&0X z_Y3}|AlP3q1tuJ3@^4^eh^&ba>?SX#XofqTeh~W||9{@YzvVwfq=2j z+J&m{&yvw!Vie#-G2QVu)YRyB*~|SrAmHO4ZhFuz&l9Pn9OX?2fua6?@xR5^Ud4n| ze4`bLZqSE`i|Zyco5DfvH9jJwUWeDLjUbnca`V0Rv3gswD=x-l)X8g8xls+P5~hZZyfuPCHrP7=g<9d(JPX!9 zq}i=XedN}OH)ieZj18qzP`ecx%=<7ksTZA?=A?)oqqEGb%GESse7ZqQlwWF(jWU(r z?^K z%jGMbxZhABvIDXYh7+MaEU-q-0Bofs-G8b}>fcZ*1Q~RO-D;heuil*+DTAVk@ z76@RYk4ou9H(!q#_H;Q!Py(*|LU0DmzL@Fi`~wJUtycgCOlusj06v!UX z$ca)IK&U4UO$YTc+OnYii4iFmewm9JeTan!`@3XQa;(!EN+(f6s7rtn+oIw;yMLX` zgUt1dx9zca9OPgfFLbNReB_mn0%h6Fw zGc**r(Aw=*mYW(OKPwgSib{p6G}sEgeL?>Z9~ekNSxESoR~bUP986sZiIH=)-F} zOBForMc(>^y2YbDu28`=I)$@c83%2b3f9RfYmgVBindaT-8o>TZWWpZ&vg-5Zue2S z`lOsQ*K{;Gx(4h9w6@@)Sa{kk49)cog|weU9m}|78@N za$Nnq4<`79tRJ2;WIqZ<`sWksOL=SOj=e*%+=;it zPh7hQ%;U*y)qqsD5lJ*Y*w!lnSV{y_eN`W?+U3IZx-sM;J#d4k)iZ!_3e^ZF-0E4T zd3EE56xfse&O$xsQGZXVZ)PPg$YXOhQ3Sb#gHIj^+Lr}xg;HZ-U1tID6jn?|p_Hgl z-_f7BtnGqV)=0wpp8CEl`U88>D+OZZ`QlnR)=@u(;lmRIS*=+kUiVY=GmrX-LJd$N zSz~278r|*SE|C0%`lXQkRdyc+Q|&mv&_Zq)?;gLy{0(iJmVd8yt3ODX=o8H0)D=x* z1^$#`_n*{D61)G*beAT zVhDb1<;-ea3>vr0KSf}*d8dC)0l6%Q9i}afLP98kLxZ6#6wu(|-^jSRbgVxs0k6B& zVc2I+y01?T?0@8Zk~^7{D6q01$cE^3zLsK0AKMl6Y12M=(xVC_Bg;lkGIBCtg^@NM z3Co#{#^}vc{7kbY9trD@H10tsjtF2MNXKBoCg264Bd9SjS#d;ml!LVe{8rA5hio$u zJOMlcLlX#Lg*Vy1I5f6PpM%aeC!!Fetvc{6dqqX}rhm^-a$=m|Oq|J^1+R8MDTN;U ztpre~NY<@!zApRa=(XRH!b0<`w;Yxq(iH2H6o>(^zyr9R`vK?dai#M3Uw|(28bM=@ zhvpe`As0(`Zp!{%$e1tqeU7mJQXaIm4iVTNsmoAR^lo1p8rmdMM4(4aYpqKU_E-t0 z$0rdux_>23_lzTCZq?x|trO&GiRq>$U@Q@uPG} z!!zXeUACXhh79!#)4;m@G_AGhAYHZ17d=4nL%yqh$zj^vl%eZJ=xvJj(wS(#^J!|v z?cLANskpuO84BX|foCXy+lMpsv3+*S_a7O$WgnGka{Gi_J-ToA-Ql||L!W+>?x}Cg zE`Q&M<#*%cK1NH)FiDwoIsS;26IVZkR6+9~!e+>29aYlpR7H=|NrnjcJaS=agFZv|;ktr6 z^jZ3xR>l)_KYd>3g(tL9Shqwi3RH&d8GlA2dP@|^a?(`tffe1*j#>-SZfL2t{t>#T zVU!*tg0P3_&YFBv#Qh zLO*3Z|D0(*`I;Js>DMFldu8{u7GcKbK+!P$DMNp4Ee;fq&_57z>1%h<^y~vS-hV~A z;lHy`EvC)eM+^F3xc2UW~Szu11_gAJsfr2NhN-sjfuX?MtKg=0iJtkY*Bl`2kq&8o-c19v|DHt8aacR zE7%7!DAH!Ii@Zmu&7^4+>rAS6oqrbMYEtzPYJ!H_VGLY(jDlq58Nf7qh?q=~o4$)b zM*A3)H^Hdlu~GOtOzsV!tF31i)JGJc1!%QVHEe4(CFo>ks?nBn8I{ndXeKY4e@fbO<1b^dh-{(n1SDkPZp~MVdeadFZ-t`QH56 zXJ*e@XPx;ud+&xk&ij6JWTjguZ)rA8{rLJBW&JhxNs5TKxsE=}OWzYmsFxln@{VK{ zCFGaAp2NqSgak$5Qyg@*-WA(Z$sX7RXAI3>L9keN>mn#GLn63bxSV`uC{%(GnEZ5E zr$Z%{P7t-fwqq2G)xsg(?wT8ZD9|_&KX%b`TrY@L%CzrGS;kG($K!8NBHYyG3k}_& zL7_MyBp=5r^d*BeGk4>HP5b>+Wx2)$U%Q^re+0$rbxtR1@@aCM#VJdr8cH0VCMg^w z`DdUZbFb3-5z@2#F2XUyMLNABrN9CFfOUsJV+f;Wz`@jxl1OXFJqZ+}{e~!t@|{+f zB{6J&{=LlL7G(9{?ZFZ1ufJvVE!8*^c874s+gN=@^@Lrqnb_zf^epbU9NS0l*zu`k zP9SsH<%Ydr_Xu`Fo&0yC&(Zjrbo_Gdy=^}u+k%}1fJsw}4R5k?{>cFt zCGCsMJAR$43%g~MW*m;t?I&=Xx%=312uTa8t*@N)J%M~v(JwvM24VYXC@XdBp0%hH zWIaIXOc9&SBcyn4h+0RQWF`bySh^u$rMq0xadOf>5;{f}P_Uslhu2ivA2tL?09VVM zire#*3|7KK`RV$dPjPT}4Y7eTpPpJuEm;u_K+(9Gvrg^=-aM=U7?>W?HpvPua%rD1 zwTIZfsl0Bq1mi9!SKvG$-Nm|!#Rqj-Ri=x);=Ak{^?rjAYLjEjTX8KR6XKWjBo**$ z_XxVAH<9-YMVFlmD-Jj5Bvu4k2@N6KT;c#*;k$5#lyf?ogw>_~g_T0l?71-5Wr@)w zGuv{=u0m`oM7Q8VpA3Lcc5kMcTHovPoiT{hEMA>*e>S(&+AnW(t%O8>*sDjFXZiLy zlN8$(OWgJNX1d$iF zC{Z31`9IdLBi0@t2z`A2eVF9BCSgRdh4}fmZ=TroxqVxM<-bDa_aMxaL246g2gP76S`N@^HaTXp;GSLY4+g3q8fqAIq2re;<-H5RlL=eg(`}9L?-}3DLk4*$8*n6vKmo@Au`9GL*cGPp@7|y)Toi3F zB%b)T2r7iD0f8$P8@kjxl9!X-$-7+nsjE7^16uaJ4E3cnY*VYT%>paOk}_fy>~@+2 zya_la0g9qWgoj7*2{Ly7>;^C*SN{r;Kk(+^5$?e6hi!m(yyKekJVP^?r)w?tO&wP+ zD9o;K+Lbm7WD9iq##X1bXrT)6EBP61wzpQ|wEK!Edn`<>*ylzMbiB7*?vbq5XQ%oivvC^EP^~^40lxMV1-3~TBRod#SE+70 z>~tC=tbZWXjIFQc@vTS`Ol!t>S=z-NtzP`puLi6r`OS7wJujXZ8LrCsF=%ZL(I3@D zu7BlMYbyndO%`_9YULV{FE1R5eA{D!-z^^3lQ5#x^SI$wffs=XMdzs40c<%{Gs3YF| zGAhTMZC16BSRXP_qgjbp1A7Nlr@)}!|PaUiDe-}G9Ncy7KhVr!4A zjC+7nFHxZEf>oqmEZTxF4}ca1b_ZyVe+SIFjY8ta>acP@UhRg`H7Un5Ev;Dq^?I&M!@itsY+Rg{4?7tMAY{auXDWEK zJ;)Kwx~aE(puuQ0?%N)d)su!bPDZ<#8pP|D@gw3ZdkOJkEk)8>|^{o=GEGH&L1 zCrx-&d$zBFi}I=j8S9AiUmF7QB9q}Nd4}Qa5>E-DlbS$JuX~ey^fbnT9786DHhVK7 z1tJv##cIZ}x{1l+qsY0+n)xIVC-LjBnhv?OjUzGW59bNXn_N+9o$BSF2}jP*(L^Z81my7EX5ARQ4@$Yvy-AN8dB%iV$~@=d z-*EdAQT6r6rtkhW%PH%d*Kht{fDHqfm#2Pd{89I2c|Jlr#{6O)(d|E-&!d^0gjTG* zPGgHgiWbHyt+0)yCr0F2nQ2OyC$xyp>#EG}{!D{@udp|R*<10tzWmU-_fBP*-KG}q zUtFX3X6-h{5I$R#edpIa2IbGR$v@8geYF(7g#@<{>R`q{u@fMWAo@8{gy!F@DKwMk zWv>5q2nw9t{Ii0upuT}T$LKRN{v=>N${e80)XoR`q7p2A>N#ouME zCllJyQx4p$@^7gE?lwDvoM?J4VK6=L8*1oXKnHoVpnbeJz}2wdTNPevU4)@59Z+3StGVQ-MIE5x+?P0rFCTj{pDw delta 13955 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 diff --git a/public/tools/plugin/minegeo-plugin-1.21.11.jar b/public/tools/plugin/minegeo-plugin-1.21.11.jar index 28ad1b9371cd941f51d215f6bc1966438d33be18..3b5ad60f88515f6464fd39c7e1d165ae8a049482 100644 GIT binary patch delta 29309 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 delta 20472 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 !target.startsWith("-"))) + targets.unshift(":plugin:build", ":fabric:build"); await new Promise((resolve, reject) => { const child = spawn(command, windows ? targets : ["./gradlew", ...targets], { cwd: "minecraft", diff --git a/scripts/setup.mjs b/scripts/setup.mjs index c08e723..bbdc648 100644 --- a/scripts/setup.mjs +++ b/scripts/setup.mjs @@ -29,5 +29,5 @@ const configured = Boolean( console.log( configured ? "Supabase configuration present. Apply the checked-in migration before starting." - : "Supabase is not configured. Local guest sandbox is available; hosted uploads/moderation remain disabled.", + : "Supabase is not configured. Fill the URL, publishable key and server secret in .env.local before starting MineGeo.", ); diff --git a/scripts/test-api.ts b/scripts/test-api.ts new file mode 100644 index 0000000..7300569 --- /dev/null +++ b/scripts/test-api.ts @@ -0,0 +1,210 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises"; +import { createClient } from "@supabase/supabase-js"; +process.loadEnvFile(".env.local"); +const db = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SECRET_KEY!, + { auth: { persistSession: false } }, +); +const account = JSON.parse(await readFile(".data/test-account.json", "utf8")); +const origin = "http://127.0.0.1:3100", + directory = `.data/api-smoke-${Date.now()}`; +await mkdir(directory, { recursive: true }); +const manifest = JSON.parse( + await readFile( + ".cache/paper-proof/plugins/MineGeo/export/manifest.json", + "utf8", + ), +); +manifest.name = "Private API regression"; +manifest.serverName = "MineGeo API regression"; +await writeFile(`${directory}/manifest.json`, JSON.stringify(manifest)); +await copyFile( + ".cache/paper-proof/plugins/MineGeo/export/terrain.json.gz", + `${directory}/terrain.json.gz`, +); +const server = spawn( + process.execPath, + [ + "node_modules/next/dist/bin/next", + "start", + "--hostname", + "127.0.0.1", + "--port", + "3100", + ], + { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }, +); +let serverLog = ""; +server.stdout.on("data", (data) => (serverLog += data)); +server.stderr.on("data", (data) => (serverLog += data)); +const stopped = new Promise((resolve) => + server.on("exit", () => resolve()), +); +const request = (path: string, body?: unknown, authenticated = true) => + fetch(origin + "/api/v1/" + path, { + method: body === undefined ? "GET" : "POST", + headers: { + ...(authenticated ? { authorization: "Bearer " + account.token } : {}), + "content-type": "application/json", + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); +let datasetId: string | undefined, serverId: string | undefined; +try { + let ready = false; + for (let i = 0; i < 60; i++) { + try { + const response = await fetch(origin + "/api/v1/health"); + if (response.ok) { + ready = true; + break; + } + } catch {} + await new Promise((r) => setTimeout(r, 1000)); + } + assert.ok(ready, "Production server and database must become healthy"); + for (const path of [ + "/", + "/servers", + "/downloads", + "/fonts/VT323-Regular.ttf", + "/images/example-map.webp", + "/tools/plugin/minegeo-plugin-1.21.11.jar", + "/tools/mod/minegeo-fabric-1.21.11.jar", + ]) { + const response = await fetch(origin + path); + assert.equal(response.status, 200, path); + const bytes = await response.arrayBuffer(); + assert.ok(bytes.byteLength > 0); + if (path === "/") { + const html = new TextDecoder().decode(bytes); + assert.ok(html.includes("blocky world")); + assert.ok(html.includes("COMMUNITY MAPS")); + } + console.log("HTTP 200 " + path); + } + const child = spawn( + "java", + [ + "--class-path", + "public/tools/plugin/minegeo-plugin-1.21.11.jar", + "tests/java/UploadSmoke.java", + origin, + directory, + ".data/test-account.json", + ], + { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }, + ); + let javaLog = ""; + child.stdout.on("data", (data) => { + javaLog += data; + process.stdout.write(data); + }); + child.stderr.on("data", (data) => (javaLog += data)); + const code = await new Promise((resolve) => + child.on("exit", resolve), + ); + assert.equal(code, 0, javaLog); + const ledger = JSON.parse( + await readFile(`${directory}/.upload-state.json`, "utf8"), + ); + datasetId = ledger.datasetId; + const draftResponse = await request("datasets/" + datasetId); + assert.equal(draftResponse.status, 200); + const draft = await draftResponse.json(); + serverId = draft.server_id; + assert.ok(["processing", "draft"].includes(draft.state)); + if (draft.state === "processing") + assert.ok( + draft.previews.every((p: any) => p.image === null), + "Pending media must not be signed", + ); + const anonymous = await request("datasets/" + datasetId, undefined, false); + assert.ok(anonymous.status >= 400, "Private draft must reject guests"); + if (draft.state === "processing") { + const cancel = await request("datasets/" + datasetId + "/cancel", {}); + assert.equal(cancel.status, 200, await cancel.text()); + } + // A never-finalized upload has no queue row: cancellation must still work. + const created = await request("uploads", { manifest }); + assert.equal(created.status, 200); + const empty = await created.json(); + const cancelled = await request( + "datasets/" + empty.datasetId + "/cancel", + {}, + ); + assert.equal(cancelled.status, 200, await cancelled.text()); + const cleanup = await db + .from("datasets") + .delete() + .eq("id", empty.datasetId) + .eq("owner_id", account.id); + if (cleanup.error) throw cleanup.error; + await writeFile( + "artifacts-api-smoke.json", + JSON.stringify( + { + passed: true, + checks: [ + "production homepage/catalog/downloads/assets", + "Java uploader to real Supabase Storage", + "private draft before rendered assets exist", + "guests denied private drafts", + "cancel queued processing", + "cancel unfinished upload without a queue row", + ], + }, + null, + 2, + ), + ); + console.log("REAL STORAGE / API SMOKE PASSED"); +} finally { + if (!datasetId) { + try { + datasetId = JSON.parse( + await readFile(`${directory}/.upload-state.json`, "utf8"), + ).datasetId; + } catch {} + } + if (datasetId) { + const record = await db + .from("datasets") + .select("server_id") + .eq("id", datasetId) + .eq("owner_id", account.id) + .maybeSingle(); + serverId = record.data?.server_id ?? serverId; + await db.storage + .from("minegeo-raw") + .remove( + manifest.assets.map((a: any) => `${account.id}/${datasetId}/${a.path}`), + ); + const deleted = await db + .from("datasets") + .delete() + .eq("id", datasetId) + .eq("owner_id", account.id); + if (deleted.error) + console.error("Test dataset cleanup: " + deleted.error.message); + } + if (serverId) { + const remaining = await db + .from("datasets") + .select("id") + .eq("server_id", serverId) + .limit(1); + if (remaining.data?.length === 0) + await db + .from("catalog_servers") + .delete() + .eq("id", serverId) + .eq("name", "MineGeo API regression"); + } + server.kill(); + await stopped; + await writeFile(`${directory}/server.log`, serverLog); +} diff --git a/scripts/test-plugin.ts b/scripts/test-plugin.ts new file mode 100644 index 0000000..80f606c --- /dev/null +++ b/scripts/test-plugin.ts @@ -0,0 +1,232 @@ +import { createServer } from "node:http"; +import { spawn } from "node:child_process"; +import { mkdir, copyFile, writeFile, readFile } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import { gunzipSync } from "node:zlib"; +import assert from "node:assert/strict"; +import { datasetSchema } from "../shared/protocol"; + +// Disposable, loopback-only Paper test. No game client is launched. +const directory = `.cache/plugin-regression-${Date.now()}`; +await mkdir(`${directory}/plugins/MineGeo`, { recursive: true }); +await copyFile(".cache/paper-proof/paper.jar", `${directory}/paper.jar`); +await copyFile( + "public/tools/plugin/minegeo-plugin-1.21.11.jar", + `${directory}/plugins/minegeo.jar`, +); +await writeFile(`${directory}/eula.txt`, "eula=true\n"); +await writeFile( + `${directory}/server.properties`, + 'server-ip=127.0.0.1\nserver-port=25580\nonline-mode=true\nlevel-type=minecraft:flat\ngenerator-settings={"layers":[{"block":"minecraft:bedrock","height":1},{"block":"minecraft:dirt","height":2},{"block":"minecraft:grass_block","height":1}],"biome":"minecraft:plains"}\ngenerate-structures=false\nview-distance=2\nsimulation-distance=2\nspawn-protection=0\nmax-players=1\n', +); +let manifest: any, + received = Buffer.alloc(0), + stage = "uploading", + interrupted = false, + creates = 0, + finalizes = 0, + heads = 0; +const api = createServer(async (req, res) => { + const chunks: Buffer[] = []; + for await (const chunk of req) chunks.push(chunk); + const bytes = Buffer.concat(chunks); + const json = (data: unknown, status = 200) => { + res.writeHead(status, { "Content-Type": "application/json" }); + res.end(JSON.stringify(data)); + }; + const origin = `http://127.0.0.1:${(api.address() as any).port}`; + if (req.url === "/api/v1/uploads") { + creates++; + manifest = JSON.parse(bytes.toString()).manifest; + json({ datasetId: "fixture" }); + } else if (req.url === "/api/v1/datasets/fixture") json({ state: stage }); + else if (req.url === "/api/v1/uploads/sign") + json({ + assets: manifest.assets.map((a: any) => ({ + ...a, + endpoint: origin + "/tus", + token: "fixture-signature", + objectName: a.path, + })), + }); + else if (req.url === "/tus") { + res.writeHead(201, { Location: origin + "/tus/file" }); + res.end(); + } else if (req.url === "/tus/file" && req.method === "HEAD") { + heads++; + res.writeHead(200, { "Upload-Offset": String(received.length) }); + res.end(); + } else if (req.url === "/tus/file" && req.method === "PATCH") { + assert.equal(Number(req.headers["upload-offset"]), received.length); + if (!interrupted) { + interrupted = true; + received = Buffer.concat([ + received, + bytes.subarray(0, Math.floor(bytes.length / 2)), + ]); + res.writeHead(503); + res.end("Simulated interruption"); + } else { + received = Buffer.concat([received, bytes]); + res.writeHead(204, { "Upload-Offset": String(received.length) }); + res.end(); + } + } else if (req.url === "/api/v1/uploads/finalize") { + finalizes++; + assert.equal( + createHash("sha256").update(received).digest("hex"), + manifest.assets[0].sha256, + ); + stage = "processing"; + json({ state: stage }); + } else + json({ error: "Unexpected request " + req.method + " " + req.url }, 404); +}); +await new Promise((r) => api.listen(0, "127.0.0.1", r)); +await writeFile( + `${directory}/plugins/MineGeo/config.yml`, + `website: 'http://127.0.0.1:${(api.address() as any).port}'\ntoken: 'fixture-token'\nserver-name: 'Regression world'\nchunks-per-tick: 2\nviewpoints: 5\nmax-area-size: 2048\n`, +); +const child = spawn("java", ["-Xmx1536M", "-jar", "paper.jar", "--nogui"], { + cwd: directory, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, +}); +let output = "", + waiters: { + pattern: RegExp; + resolve: () => void; + reject: (e: Error) => void; + start: number; + timer: ReturnType; + }[] = []; +const log = (bytes: Buffer) => { + const text = bytes.toString(); + output += text; + for (const line of text.split("\n")) + if (/MineGeo|Done \(|ERROR/.test(line)) process.stdout.write(line + "\n"); + for (const waiter of [...waiters]) + if (waiter.pattern.test(output.slice(waiter.start))) { + clearTimeout(waiter.timer); + waiters = waiters.filter((w) => w !== waiter); + waiter.resolve(); + } +}; +child.stdout.on("data", log); +child.stderr.on("data", log); +const exited = new Promise((resolve) => + child.on("exit", () => { + for (const w of waiters) { + clearTimeout(w.timer); + w.reject(new Error("Paper stopped before " + w.pattern)); + } + waiters = []; + resolve(); + }), +); +const wait = (pattern: RegExp, start = output.length, timeout = 90000) => + new Promise((resolve, reject) => { + const w = { + pattern, + start, + resolve, + reject, + timer: setTimeout(() => { + waiters = waiters.filter((v) => v !== w); + reject(new Error("Timed out waiting for " + pattern)); + }, timeout), + }; + waiters.push(w); + if (pattern.test(output.slice(start))) { + clearTimeout(w.timer); + waiters = waiters.filter((v) => v !== w); + resolve(); + } + }); +const command = async (text: string, pattern: RegExp) => { + const pending = wait(pattern); + child.stdin.write(text + "\n"); + await pending; +}; +try { + await wait(/Done \(/, 0, 180000); + await command("minegeo upload", /No completed export/); + await command( + "minegeo area world 100000 100000 100032 100032", + /area selected/, + ); + await command( + "minegeo export Empty Region", + /Export failed: No generated chunks/, + ); + await command("minegeo area world -32 -32 32 32", /area selected/); + child.stdin.write("forceload add -32 -32 31 31\n"); + await new Promise((r) => setTimeout(r, 5000)); + await command("minegeo export Regression World", /Export complete:/); + const relative = ( + await readFile(`${directory}/plugins/MineGeo/latest-export.txt`, "utf8") + ).trim(); + const exportDir = `${directory}/plugins/MineGeo/${relative}`; + const local = datasetSchema.parse( + JSON.parse(await readFile(`${exportDir}/manifest.json`, "utf8")), + ); + assert.equal(local.viewpoints.length, 5); + const terrain = JSON.parse( + gunzipSync(await readFile(`${exportDir}/terrain.json.gz`)).toString(), + ); + assert.ok(terrain.blocks.length > 0); + assert.ok(terrain.coverage.length > 0); + assert.ok( + local.viewpoints.every( + (p) => p.x >= -32 && p.x < 32 && p.z >= -32 && p.z < 32, + ), + ); + await command("minegeo upload", /Upload failed: Upload paused/); + await command("minegeo upload", /Upload complete\. Open/); + assert.equal(creates, 1); + assert.equal(finalizes, 1); + assert.equal(heads, 1); + await command("minegeo upload", /Upload complete\. Open/); + assert.equal(finalizes, 1); + await command("minegeo area world -256 -256 256 256", /area selected/); + const cancelled = wait(/Cancelled\. Completed exports are kept/); + child.stdin.write("minegeo export Cancel Test\nminegeo cancel\n"); + await cancelled; + assert.equal( + ( + await readFile(`${directory}/plugins/MineGeo/latest-export.txt`, "utf8") + ).trim(), + relative, + ); + await writeFile( + "artifacts-plugin-regression.json", + JSON.stringify( + { + passed: true, + checks: [ + "missing export guidance", + "empty area failure", + "real Paper export without a player", + "shared schema validation", + "TUS interruption and byte-perfect resume", + "repeated upload is idempotent", + "cancellation preserves completed export", + ], + blocks: terrain.blocks.length, + chunks: terrain.coverage.length, + viewpoints: local.viewpoints.length, + directory, + }, + null, + 2, + ), + ); + console.log("PLUGIN REGRESSION PASSED"); +} finally { + child.stdin.write("stop\n"); + const kill = setTimeout(() => child.kill(), 15000); + await exited; + clearTimeout(kill); + await new Promise((r) => api.close(() => r())); + await writeFile(`${directory}/regression.log`, output); +} diff --git a/shared/protocol.ts b/shared/protocol.ts index 27ebbec..a3f477d 100644 --- a/shared/protocol.ts +++ b/shared/protocol.ts @@ -56,6 +56,23 @@ export const datasetSchema = z .max(4096), }) .superRefine((d, ctx) => { + const paths = new Set(d.assets.map((asset) => asset.path)); + const requiredPaths = + d.source === "plugin" + ? ["terrain.json.gz"] + : [ + "map.png", + ...d.viewpoints.flatMap((point) => + faces.map((face) => `${point.id}/${face}.png`), + ), + ]; + for (const path of requiredPaths) { + if (!paths.has(path)) + ctx.addIssue({ + code: "custom", + message: `Missing required export asset: ${path}`, + }); + } if (new Set(d.viewpoints.map((p) => p.id)).size !== d.viewpoints.length) ctx.addIssue({ code: "custom", message: "Viewpoint IDs must be unique" }); if (new Set(d.assets.map((a) => a.path)).size !== d.assets.length) diff --git a/tests/java/UploadSmoke.java b/tests/java/UploadSmoke.java new file mode 100644 index 0000000..f4494f2 --- /dev/null +++ b/tests/java/UploadSmoke.java @@ -0,0 +1,9 @@ +import dev.minegeo.plugin.UploadClient; +import com.google.gson.JsonParser; +import java.nio.file.*; +public class UploadSmoke { + public static void main(String[] args) throws Exception { + String token=JsonParser.parseString(Files.readString(Path.of(args[2]))).getAsJsonObject().get("token").getAsString(); + new UploadClient(args[0],token).upload(Path.of(args[1]),message->System.out.println(message),()->false); + } +} diff --git a/tests/protocol.test.ts b/tests/protocol.test.ts index 5a80113..c0eeacd 100644 --- a/tests/protocol.test.ts +++ b/tests/protocol.test.ts @@ -46,6 +46,29 @@ test("dataset requires five unique in-bounds viewpoints and safe paths", () => { assets: [{ path: "terrain.json.gz", sha256: "a".repeat(64), bytes: 100 }], }; assert.equal(datasetSchema.safeParse(base).success, true); + assert.equal(datasetSchema.safeParse({ ...base, assets: [] }).success, false); + assert.equal( + datasetSchema.safeParse({ ...base, source: "fabric" }).success, + false, + ); + const capture = { + ...base, + source: "fabric", + assets: [ + "map.png", + ...base.viewpoints.flatMap((point) => + ["px", "nx", "py", "ny", "pz", "nz"].map( + (face) => `${point.id}/${face}.png`, + ), + ), + ].map((path) => ({ path, sha256: "a".repeat(64), bytes: 100 })), + }; + assert.equal(datasetSchema.safeParse(capture).success, true); + assert.equal( + datasetSchema.safeParse({ ...capture, assets: capture.assets.slice(1) }) + .success, + false, + ); assert.equal( datasetSchema.safeParse({ ...base, viewpoints: base.viewpoints.slice(1) }) .success, diff --git a/worker/main.ts b/worker/main.ts index 354ac8a..1bf0af9 100644 --- a/worker/main.ts +++ b/worker/main.ts @@ -1,4 +1,5 @@ import { createClient } from "@supabase/supabase-js"; +import { existsSync } from "node:fs"; import { randomUUID, createHash } from "node:crypto"; import { mkdir, writeFile, readFile, readdir, rm } from "node:fs/promises"; import { gunzipSync } from "node:zlib"; @@ -6,6 +7,7 @@ import sharp from "sharp"; import { datasetSchema, faces, type TerrainSnapshot } from "../shared/protocol"; import { renderSnapshot } from "../renderer/render.mjs"; import type { Database } from "../shared/database"; +if (existsSync(".env.local")) process.loadEnvFile(".env.local"); const client = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!,