118 lines
6.5 KiB
Python
118 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Screenshot the game in a headless browser, after setting up a scene with JavaScript.
|
|
|
|
python3 tools/shot.py OUT.png [options]
|
|
|
|
Starts a new world in dist/index.html (run python3 build.py first), runs your setup code in the
|
|
page (every game global is available: setCell, placeBlock, placeMachineAt, bot, SET, GAME ...),
|
|
runs the simulation for --ticks steps (it is paused otherwise, so the same options give the same
|
|
picture), then saves a PNG. Open the PNG to look at it.
|
|
|
|
The camera follows the bot. To frame something else, set CAM = [x, y] (in grains) in your setup
|
|
code and the bot is moved there before the shot. SX, SY are the bot's starting position.
|
|
|
|
Options:
|
|
--look pixel|smooth which look to draw (default: pixel; use smooth only to check graphics)
|
|
--seed N world seed (default 424242)
|
|
--size WxH world size in grains (default 2048x1024)
|
|
--setup FILE.js code to run once the world is ready (see tools/scenes/ for examples)
|
|
--js CODE code to run after --setup (both may be given)
|
|
--ticks N simulation ticks to run after the setup (default 0)
|
|
--zoom Z screen pixels per grain (default 6)
|
|
--hud keep the HUD and build menu (hidden by default)
|
|
--previews keep the cursor previews (drill aim, build ghost); hidden by default
|
|
--viewport WxH browser window size (default 1280x760)
|
|
--clip X,Y,W,H save only this part of the window
|
|
--no-light switch off lighting (shows true colours, as in daylight)
|
|
--freeze draw the last frame at a fixed time and frame count, so animation (belts,
|
|
saws, the bot's bob) is caught at the same moment every time: two shots
|
|
of the same scene then match pixel for pixel, and a before / after pair
|
|
shows only what really changed
|
|
--page PATH another built page to load (default dist/index.html)
|
|
|
|
Exit status 1 if the page threw an error (they are printed).
|
|
|
|
Example: a belt carrying a heap of sand, in the smooth look:
|
|
python3 tools/shot.py /tmp/belt.png --look smooth --setup tools/scenes/clear-sky.js \\
|
|
--js "placeBlocks(BP_BELT, 1, blockLine(SX/4+2, SY/4+6, SX/4+12, SY/4+6), undefined, false)" --ticks 30
|
|
"""
|
|
import argparse, pathlib, sys
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
|
from where import translate
|
|
from use_venv import ensure_playwright; ensure_playwright()
|
|
from browser import launch, gpu_option
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
FREEZE = """(() => { const real = performance.now.bind(performance); window.__frozenNow = null;
|
|
performance.now = () => window.__frozenNow === null ? real() : window.__frozenNow; })();"""
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('out')
|
|
ap.add_argument('--look', choices=['pixel', 'smooth'], default='pixel')
|
|
ap.add_argument('--seed', type=int, default=424242)
|
|
ap.add_argument('--size', default='2048x1024')
|
|
ap.add_argument('--setup')
|
|
ap.add_argument('--js')
|
|
ap.add_argument('--ticks', type=int, default=0)
|
|
ap.add_argument('--zoom', type=float, default=6)
|
|
ap.add_argument('--hud', action='store_true')
|
|
ap.add_argument('--previews', action='store_true')
|
|
ap.add_argument('--viewport', default='1280x760')
|
|
ap.add_argument('--clip')
|
|
ap.add_argument('--freeze', action='store_true')
|
|
ap.add_argument('--no-light', action='store_true')
|
|
ap.add_argument('--page', default=str(ROOT / 'dist' / 'index.html'))
|
|
gpu_option(ap)
|
|
a = ap.parse_args()
|
|
ww, wh = (int(v) for v in a.size.split('x'))
|
|
vw, vh = (int(v) for v in a.viewport.split('x'))
|
|
errors = []
|
|
with launch(gpu=a.gpu, viewport={'width': vw, 'height': vh}, page_path=a.page) as (b, pg, url):
|
|
pg.on('pageerror', lambda e: errors.append(translate(e.stack or str(e))))
|
|
pg.on('console', lambda m: errors.append('console: ' + m.text) if m.type == 'error' else None)
|
|
pg.add_init_script(FREEZE if a.freeze else '')
|
|
pg.goto(url)
|
|
pg.evaluate("SET.fog = false")
|
|
pg.wait_for_timeout(300)
|
|
# pause the moment the world exists, so the shot depends only on the seed, the setup
|
|
# code and --ticks (simTick is deterministic), not on how fast the browser ran
|
|
# (wrapped in a function: Playwright calls a string that evaluates to a function)
|
|
pg.evaluate("() => { const enter = enterGame; enterGame = function () { enter(); GAME.paused = true; }; }")
|
|
pg.evaluate(f"SET.look = '{a.look}'; {"SET.lighting = 'off';" if a.no_light else ''} startNewWorld({a.seed}, {ww}, {wh})")
|
|
pg.wait_for_function('GAME.running === true', timeout=60000)
|
|
pg.evaluate("document.querySelectorAll('.card, .toast').forEach(e => e.remove())")
|
|
# SX, SY: the bot's position when the world starts, handy for placing things near it
|
|
pg.evaluate(f"window.SX = Math.floor(bot.x); window.SY = Math.floor(bot.y); SET.zoom = {a.zoom}; resizeRender();")
|
|
for code in [pathlib.Path(a.setup).read_text() if a.setup else None, a.js]:
|
|
if code:
|
|
pg.evaluate(f'() => {{ {code}\n}}')
|
|
if a.ticks:
|
|
pg.evaluate(f'for (let k = 0; k < {a.ticks}; k++) simTick();')
|
|
pg.evaluate(f"""GAME.paused = true; $('hud').hidden = {'false' if a.hud else 'true'}; GAME.hidePreviews = {'false' if a.previews else 'true'};
|
|
if (window.CAM) {{ bot.x = CAM[0] - bot.w / 2; bot.y = CAM[1] - bot.h / 2; }}
|
|
snapCamera(); R.snapNext = true;""")
|
|
pg.wait_for_timeout(600)
|
|
if a.freeze:
|
|
# stop the frame loop, then draw one last frame at a fixed time and frame count
|
|
pg.evaluate("() => { window.__frozenNow = 5000; window.requestAnimationFrame = () => 0; }")
|
|
pg.wait_for_timeout(100)
|
|
pg.evaluate("() => { R.frame = 999; if (typeof SM !== 'undefined') SM.t0 = 0; frame(lastRender + 1000); }")
|
|
kw = {}
|
|
if a.clip:
|
|
x, y, w, h = (float(v) for v in a.clip.split(','))
|
|
kw['clip'] = {'x': x, 'y': y, 'width': w, 'height': h}
|
|
pathlib.Path(a.out).parent.mkdir(parents=True, exist_ok=True)
|
|
pg.screenshot(path=a.out, **kw)
|
|
note = pg.evaluate("SET.look === 'smooth' ? (SM.ok ? 'smooth look' : 'smooth look FAILED: ' + SM.why) : 'pixel look'")
|
|
b.close()
|
|
print(f'saved {a.out} ({note})')
|
|
for e in errors:
|
|
print('ERROR', e)
|
|
sys.exit(1 if errors else 0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|