// Remote Explore // // The remote explore plugin allows us to use headless explorers that we run on // servers and RaspberryPi devices. // // We use https://github.com/projectsophon/darkforest-rs/tree/main/mimc-fast as // a webserver that exposes a `/mine` endpoint and connect to it from in-game // with this plugin. // // When trying contact a remote server (not also running on your computer) from // a webpage like this plugin does it may not work or you may see an error about // blocked insecure content. Theres 3 ways make this work: // * The right but rather technical way is to install a SSL Certificate on your // server. // * Another technical solution is routing your local port to your remote server // as described https://developer.zkga.me/mining/connecting-to-a-remote-headless-miner // * Finally a bad but quick solution is to google enabling mixed content in your browser // NOTE however this can be extremely dangerous also allowing any other code to do the same. import { html, render, useState, useEffect, useLayoutEffect } from 'https://unpkg.com/htm/preact/standalone.module.js'; import { locationIdFromDecStr } from 'https://cdn.skypack.dev/@darkforest_eth/serde'; const { MinerManager: Miner, SwissCheesePattern, SpiralPattern, TowardsCenterPattern } = df.getConstructors(); const NEW_CHUNK = 'DiscoveredNewChunk'; const CHUNK_SIZES = [16, 32, 64, 128, 256, 512, 1024]; function getPattern(coords, patternType, chunkSize) { if (patternType === 'swiss') { return new SwissCheesePattern(coords, chunkSize); } else if (patternType === 'spiral') { return new SpiralPattern(coords, chunkSize); } else { return new TowardsCenterPattern(coords, chunkSize); } } function worldRadius() { return df.getWorldRadius() } function workerFactory(url) { class RemoteWorker { url = url; async postMessage(msg) { const msgJson = JSON.parse(msg); const resp = await fetch(this.url, { method: 'POST', body: JSON.stringify({ chunkFootprint: msgJson.chunkFootprint, planetRarity: msgJson.planetRarity, planetHashKey: msgJson.planetHashKey, }), headers: { 'Content-Type': 'application/json', }, }); const exploredChunk = await resp.json(); const chunkCenter = { x: exploredChunk.chunkFootprint.bottomLeft.x + exploredChunk.chunkFootprint.sideLength / 2, y: exploredChunk.chunkFootprint.bottomLeft.y + exploredChunk.chunkFootprint.sideLength / 2, }; exploredChunk.perlin = df.spaceTypePerlin(chunkCenter, false); for (const planetLoc of exploredChunk.planetLocations) { planetLoc.hash = locationIdFromDecStr(planetLoc.hash); planetLoc.perlin = df.spaceTypePerlin( { x: planetLoc.coords.x, y: planetLoc.coords.y }, true ); planetLoc.biomebase = df.biomebasePerlin( { x: planetLoc.coords.x, y: planetLoc.coords.y }, true ); } this.onmessage({ data: JSON.stringify([exploredChunk, msgJson.jobId]) }); } onmessage(_a) { console.warn('Unimplemented: onmessage'); } terminate() { console.warn('Unimplemented: terminate'); } onmessageerror() { console.warn('Unimplemented: onmessageerror'); } addEventListener() { console.warn('Unimplemented: addEventListener'); } removeEventListener() { console.warn('Unimplemented: removeEventListener'); } dispatchEvent(_event) { return false; } onerror() { console.warn('Unimplemented: onerror'); } } return RemoteWorker; } function Target() { const wrapper = { width: '1em', height: '1em', display: 'inline-block', position: 'relative', verticalAlign: 'text-bottom', }; const svg = { width: '100%', height: '100%', }; const path = { fill: 'white', }; return html` `; } function MinerUI({ miner, onRemove, }) { const [hashRate, setHashRate] = useState(0); // No idea why useEffect doesn't run useLayoutEffect(() => { const calcHash = (chunk, miningTimeMillis) => { df.addNewChunk(chunk); const hashRate = chunk.chunkFootprint.sideLength ** 2 / (miningTimeMillis / 1000); setHashRate(Math.floor(hashRate)); const res = miner.getCurrentlyExploringChunk(); if (res) { const { bottomLeft, sideLength } = res; ui?.setExtraMinerLocation?.(miner.id, { x: bottomLeft.x + sideLength / 2, y: bottomLeft.y + sideLength / 2, }); } else { ui?.removeExtraMinerLocation?.(miner.id); } }; miner.on(NEW_CHUNK, calcHash); return () => { miner.off(NEW_CHUNK, calcHash); }; }, [miner]); const wrapper = { paddingBottom: '10px', display: 'flex', justifyContent: 'space-between', whiteSpace: 'nowrap', }; const buttonWrapper = { width: '50px', display: 'flex', justifyContent: 'space-between', }; const remove = () => { onRemove(miner); }; const [targeting, setTargeting] = useState(false); const target = () => setTargeting(true); useEffect(() => { const hover = () => { const coords = ui.getHoveringOverCoords(); if (coords) { ui?.setExtraMinerLocation?.(miner.id, coords); } }; const click = () => { window.removeEventListener('mousemove', hover); window.removeEventListener('click', click); const coords = ui.getHoveringOverCoords(); if (coords) { const pattern = getPattern(coords, miner.patternType, miner.chunkSize); miner.setMiningPattern(pattern); } miner.setRadius(worldRadius()); miner.startExplore(); setTargeting(false); }; if (targeting) { miner.stopExplore(); window.addEventListener('mousemove', hover); window.addEventListener('click', click); } return () => { window.removeEventListener('mousemove', hover); window.removeEventListener('click', click); }; }, [targeting, miner]); return html`