Skip to content

Commit

Permalink
1.1.0 release
Browse files Browse the repository at this point in the history
polished homepage
update dependencies
write proper readme
better defaults for Windows
use post or query params on POST endpoint
add build stage to Docker to streamline development
  • Loading branch information
CorySanin committed May 1, 2023
1 parent 3625f70 commit 25dc83f
Show file tree
Hide file tree
Showing 11 changed files with 1,291 additions and 130 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,7 @@ dist
.tern-port

# Custom
config/config.json5
config/config.json5
assets/css/
assets/js/
assets/webp/
15 changes: 13 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
FROM corysanin/openrct2-cli:develop-alpine AS rct2

FROM node:alpine3.16 as build

WORKDIR /usr/src/screenshotter

COPY ./package*json ./

RUN npm install

FROM node:alpine3.16

RUN apk add --no-cache rsync ca-certificates libpng libzip libcurl duktape freetype fontconfig icu sdl2 speexdsp \
RUN apk add --no-cache rsync ca-certificates libpng libzip libcurl freetype fontconfig icu sdl2 speexdsp \
&& ln -sf /game /rct2
COPY --from=rct2 /usr /usr

WORKDIR /usr/src/screenshotter

COPY --from=build /usr/src/screenshotter /usr/src/screenshotter

COPY ./config /home/node/.config/OpenRCT2/
COPY . .

RUN npm install \
RUN npm run build \
&& npm install --production\
&& mkdir /home/node/.config/OpenRCT2/object \
&& chown -R node:node /home/node/.config/OpenRCT2

Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# rct-screenshotter

Screenshotter is a simple web server that generates screenshots of Rollercoaster Tycoon save files.
[![Docker Pulls](https://img.shields.io/docker/pulls/corysanin/rct-screenshotter)](https://hub.docker.com/r/corysanin/rct-screenshotter)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/CorySanin/rct-screenshotter/docker-image.yml)](https://github.com/CorySanin/rct-screenshotter/actions)
[![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/CorySanin/rct-screenshotter)](https://libraries.io/github/CorySanin/rct-screenshotter)
[![GitHub repo size](https://img.shields.io/github/repo-size/CorySanin/rct-screenshotter)](https://github.com/CorySanin/rct-screenshotter)
[![GitHub](https://img.shields.io/github/license/CorySanin/rct-screenshotter)](https://github.com/CorySanin/rct-screenshotter/blob/master/LICENSE)

`rct-screenshotter` is a simple web server that generates screenshots of Rollercoaster Tycoon save files.

This project is intended to be used as a REST API for projects like [ffa-tycoon](https://github.com/CorySanin/ffa-tycoon). But it also has a form on its homepage to allow for use in a web browser.

It uses OpenRCT2 to generate screenshots, and as such is compatible with RCT1, RCT2, and OpenRCT2 save formats.

## API

Submit a multipart post request to /upload with the following fields:

| Name | Description |
|----------|---------------------------------------------------------|
| park | The save file to generate a screenshot of |
| zoom | The zoom level to use in the screenshot. 0-7 (optional) |
| rotation | The rotation of the map. 0-3 (optional) |
169 changes: 169 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Wrote this out of anger.
*/
const fsp = require('fs').promises;
const path = require('path');
const spawn = require('child_process').spawn;
const sass = require('sass');
const csso = require('csso');
const uglifyjs = require("uglify-js");
const { resolve } = require('path');

const STYLESDIR = 'styles';
const SCRIPTSDIR = 'scripts';
const IMAGESDIR = path.join('assets', 'images');
const STYLEOUTDIR = process.env.STYLEOUTDIR || path.join(__dirname, 'assets', 'css');
const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join(__dirname, 'assets', 'js');
const IMAGESOUTDIR = process.env.IMAGESOUTDIR || path.join(__dirname, 'assets', 'webp');
const STYLEOUTFILE = process.env.STYLEOUTFILE || 'styles.css';
const SQUASH = new RegExp('^[0-9]+-');

async function emptyDir(dir) {
await Promise.all((await fsp.readdir(dir, { withFileTypes: true })).map(f => path.join(dir, f.name)).map(p => fsp.rm(p, {
recursive: true,
force: true
})));
}

async function mkdir(dir) {
if (typeof dir === 'string') {
await fsp.mkdir(dir, { recursive: true });
}
else {
await Promise.all(dir.map(mkdir));
}
}

// Process styles
async function styles() {
await mkdir([STYLEOUTDIR, STYLESDIR]);
await await emptyDir(STYLEOUTDIR);
let styles = [];
let files = await fsp.readdir(STYLESDIR);
await Promise.all(files.map(f => new Promise(async (res, reject) => {
let p = path.join(STYLESDIR, f);
if (f.charAt(0) !== '_') {
console.log(`Processing style ${p}`);
let style = sass.compile(p).css;
if (SQUASH.test(f)) {
styles.push(style);
}
else {
let o = path.join(STYLEOUTDIR, f.substring(0, f.lastIndexOf('.')) + '.css');
await fsp.writeFile(o, csso.minify(style).css);
console.log(`Wrote ${o}`);
}
}
res(0);
})));
let out = csso.minify(styles.join('\n')).css;
let outpath = path.join(STYLEOUTDIR, STYLEOUTFILE);
await fsp.writeFile(outpath, out);
console.log(`Wrote ${outpath}`);
}

// Process scripts
async function scripts() {
await mkdir([SCRIPTSOUTDIR, SCRIPTSDIR]);
await emptyDir(SCRIPTSOUTDIR);
let files = await fsp.readdir(SCRIPTSDIR);
await Promise.all(files.map(f => new Promise(async (res, reject) => {
let p = path.join(SCRIPTSDIR, f);
let o = path.join(SCRIPTSOUTDIR, f);
console.log(`Processing script ${p}`);
try {
await fsp.writeFile(o, uglifyjs.minify((await fsp.readFile(p)).toString()).code);
console.log(`Wrote ${o}`);
}
catch (ex) {
console.log(`error writing ${o}: ${ex}`);
}
res(0);
})));
}

// Process images
async function images(dir = '') {
let p = path.join(IMAGESDIR, dir);
await mkdir(p);
if (dir.length === 0) {
await mkdir(IMAGESOUTDIR)
await emptyDir(IMAGESOUTDIR);
}
let files = await fsp.readdir(p, {
withFileTypes: true
});
if (files.length) {
await Promise.all(files.map(f => new Promise(async (res, reject) => {
if (f.isFile()) {
let outDir = path.join(IMAGESOUTDIR, dir);
let infile = path.join(p, f.name);
let outfile = path.join(outDir, f.name.substring(0, f.name.lastIndexOf('.')) + '.webp');
await mkdir(outDir);
console.log(`Processing image ${infile}`)
let process = spawn('cwebp', ['-mt', '-q', '40', infile, '-o', outfile]);
let timeout = setTimeout(() => {
reject('Timed out');
process.kill();
}, 30000);
process.on('exit', async (code) => {
clearTimeout(timeout);
if (code === 0) {
console.log(`Wrote ${outfile}`);
res();
}
else {
reject(code);
}
});
}
else if (f.isDirectory()) {
images(path.join(dir, f.name)).then(res).catch(reject);
}
})));
}
}

(async function () {
await Promise.all([styles(), scripts()]);
if (process.argv.indexOf('--watch') >= 0) {
console.log('watching for changes...');
(async () => {
try {
const watcher = fsp.watch(STYLESDIR);
for await (const _ of watcher)
await styles();
} catch (err) {
if (err.name === 'AbortError')
return;
throw err;
}
})();

(async () => {
try {
const watcher = fsp.watch(SCRIPTSDIR);
for await (const _ of watcher)
await scripts();
} catch (err) {
if (err.name === 'AbortError')
return;
throw err;
}
})();

(async () => {
try {
const watcher = fsp.watch(IMAGESDIR, {
recursive: true // no Linux ☹️
});
for await (const _ of watcher)
await images();
} catch (err) {
if (err.name === 'AbortError')
return;
throw err;
}
})();
}
})();
39 changes: 17 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ const fsp = fs.promises;
const path = require('path');
const spawn = require('child_process').spawn;
const express = require('express');
const fileUpload = require('express-fileupload');
const multer = require('multer');
const moment = require('moment');
const phin = require('phin');

const PORT = process.env.PORT || 8080;
const HOME = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
const PARKDIR = process.env.PARKDIR || path.join(HOME, '.config', 'OpenRCT2', 'save');
const SCREENSHOTDIR = process.env.SCREENSHOTDIR || path.join(HOME, '.config', 'OpenRCT2', 'screenshot');
const GAMEDIR = process.env.GAMEDIR || path.join(HOME, (process.platform === 'win32') ? 'Documents' : '.config', 'OpenRCT2');
const PARKDIR = process.env.PARKDIR || path.join(GAMEDIR, 'save');
const SCREENSHOTDIR = process.env.SCREENSHOTDIR || path.join(GAMEDIR, 'screenshot');
const FILENUMMAX = 100000;
const TIMEOUT = process.env.TIMEOUT || 20000;

const app = express();
const upload = multer({ dest: PARKDIR });

let filenum = 0;

Expand All @@ -25,7 +27,7 @@ function getFileNum() {
function getScreenshot(file, options = {}) {
return new Promise((resolve, reject) => {
let destination = path.join(SCREENSHOTDIR, `screenshot_${moment().format('HHmmssSS')}_${getFileNum()}.png`);
let proc = spawn('openrct2-cli', ['screenshot', `${file}`, destination, 'giant', Math.max(Math.min(parseInt(options.zoom || 3), 7), 0), parseInt(options.rotation || 0) % 4], {
let proc = spawn('openrct2-cli', ['screenshot', `${file}`, destination, 'giant', Math.min(Math.abs(parseInt(options.zoom || 3)), 7), parseInt(options.rotation || 0) % 4], {
stdio: ['ignore', process.stdout, process.stderr]
});
let timeout = setTimeout(() => {
Expand All @@ -49,29 +51,22 @@ function getScreenshot(file, options = {}) {

app.set('trust proxy', 1);
app.set('view engine', 'ejs');
app.use(fileUpload({
createParentPath: true,
abortOnLimit: true,
limits: {
fileSize: 100 * 1024 * 1024
}
}));

app.post('/upload', async (req, res) => {
app.use('/assets/', express.static('assets'));

app.post('/upload', upload.single('park'), async (req, res) => {
try {
if (!req.files || !req.files.park) {
if (!req.file) {
res.status(400).send({
status: 'bad'
});
}
else {
let park = req.files.park;
let fext = req.files.park.name.split('.');
fext = fext[fext.length - 1];
let filename = path.join(PARKDIR, `upload_${moment().format('YYYYMMDD')}_${getFileNum()}.${fext}`);
await park.mv(filename);

let image = await getScreenshot(filename, req.query);
let options = {
zoom: req.body.zoom || req.query.zoom,
rotation: req.body.rotation || req.query.rotation,
};
let image = await getScreenshot(req.file.path, options);
res.sendFile(image, (err) => {
if (err) {
res.status(500).send({
Expand All @@ -84,7 +79,7 @@ app.post('/upload', async (req, res) => {
}
});
});
fs.unlink(filename, (err) => {
fs.unlink(req.file.path, (err) => {
if (err) {
console.log(err);
}
Expand Down Expand Up @@ -158,7 +153,7 @@ app.get('/', (req, res) => {
});

let server = app.listen(PORT, () => {
console.log(`Web server listening on port ${PORT}.`);
console.log(`RCT Screenshotter listening on port ${PORT}.`);
fs.mkdir(PARKDIR, { recursive: true }, err => {
if (err) {
console.log(err);
Expand Down
Loading

0 comments on commit 25dc83f

Please sign in to comment.