This commit is contained in:
Jill 2024-03-06 17:06:47 +03:00
commit d8de108153
Signed by: oat
GPG Key ID: 33489AA58A955108
17 changed files with 2791 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/result
/node_modules/

81
cards/error.svg Normal file
View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1012"
height="1404"
viewBox="0 0 1012 1404"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
sodipodi:docname="error.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showborder="false"
shape-rendering="auto"
showgrid="true"
inkscape:zoom="0.96390292"
inkscape:cx="282.70482"
inkscape:cy="283.74227"
inkscape:window-width="1920"
inkscape:window-height="1046"
inkscape:window-x="0"
inkscape:window-y="-2"
inkscape:window-maximized="1"
inkscape:current-layer="svg1"><inkscape:grid
id="grid1"
units="px"
originx="506"
originy="702"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="10"
dotted="true"
gridanglex="30"
gridanglez="30"
visible="true"
snapvisiblegridlinesonly="false"
enabled="false" /></sodipodi:namedview><defs
id="defs1"><rect
x="20.555475"
y="21.720669"
width="947.26782"
height="1339.3308"
id="rect1" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><path
style="display:inline;fill:#000000;fill-opacity:1;stroke:#343434;stroke-width:8;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
id="rect1-2"
width="1003.9467"
height="1395.9467"
x="4.0266695"
y="4.0266695"
sodipodi:type="rect"
ry="58.536545"
mask="none"
inkscape:label="bg back" /><text
xml:space="preserve"
id="text1"
style="font-size:40px;line-height:1;font-family:Alatsi;-inkscape-font-specification:Alatsi;letter-spacing:0px;white-space:pre;shape-inside:url(#rect1);display:inline"><tspan
x="20.554688"
y="57.276259"
id="tspan4"><tspan
style="font-family:Minecraft;-inkscape-font-specification:Minecraft;fill:#ffffff"
id="tspan1">$error$</tspan></tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

491
cards/player-front.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 876 KiB

150
cards/player-help.svg Normal file
View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1012"
height="1404"
viewBox="0 0 1012 1404"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
sodipodi:docname="player-help.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showborder="false"
shape-rendering="auto"
showgrid="true"
inkscape:zoom="0.48195146"
inkscape:cx="401.49271"
inkscape:cy="606.90759"
inkscape:window-width="1920"
inkscape:window-height="1046"
inkscape:window-x="0"
inkscape:window-y="-2"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"><inkscape:grid
id="grid1"
units="px"
originx="506"
originy="702"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="10"
dotted="true"
gridanglex="30"
gridanglez="30"
visible="true"
snapvisiblegridlinesonly="false"
enabled="false" /></sodipodi:namedview><defs
id="defs1"><rect
x="20.555475"
y="21.720669"
width="947.26782"
height="1339.3308"
id="rect1" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><path
style="display:inline;fill:#000000;fill-opacity:1;stroke:#343434;stroke-width:8;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
id="rect1-2"
width="1003.9467"
height="1395.9467"
x="4.0266695"
y="4.0266695"
sodipodi:type="rect"
ry="58.536545"
mask="none"
inkscape:label="bg back" /><text
xml:space="preserve"
id="text1"
style="font-size:40px;line-height:1;font-family:Minecraft;-inkscape-font-specification:Minecraft;fill:#ffffff;letter-spacing:0px;white-space:pre;shape-inside:url(#rect1);display:inline"
transform="translate(18.038217,310.07099)"><tspan
x="20.554688"
y="57.276259"
id="tspan5"><tspan
id="tspan1">Get your player's name, a link to their </tspan></tspan><tspan
x="20.554688"
y="101.58182"
id="tspan7"><tspan
id="tspan6">profile picture (PNG preferred), and </tspan></tspan><tspan
x="20.554688"
y="145.88737"
id="tspan9"><tspan
id="tspan8">the previously calculated damage and </tspan></tspan><tspan
x="20.554688"
y="190.19293"
id="tspan11"><tspan
id="tspan10">health.
</tspan></tspan><tspan
x="20.554688"
y="234.49849"
id="tspan13"><tspan
id="tspan12">
</tspan></tspan><tspan
x="20.554688"
y="278.80404"
id="tspan15"><tspan
id="tspan14">Got all that? Apply it to the end of the </tspan></tspan><tspan
x="20.554688"
y="323.1096"
id="tspan17"><tspan
id="tspan16">URL as so:
</tspan></tspan><tspan
x="20.554688"
y="367.41517"
id="tspan19"><tspan
id="tspan18">
</tspan></tspan><tspan
x="20.554688"
y="407.41516"
id="tspan21"><tspan
style="font-size:26.6667px"
id="tspan20">/front.png?name=player&amp;image=https://example.com/</tspan></tspan><tspan
x="20.554688"
y="447.41516"
id="tspan23"><tspan
style="font-size:26.6667px"
id="tspan22">image.png&amp;dmg=0&amp;hp=0
</tspan></tspan><tspan
x="20.554688"
y="487.41516"
id="tspan25"><tspan
style="font-size:26.6667px"
id="tspan24">
</tspan></tspan><tspan
x="20.554688"
y="531.72072"
id="tspan26">If you're happy with how that looks, </tspan><tspan
x="20.554688"
y="576.02626"
id="tspan27">create a new custom card. Set its </tspan><tspan
x="20.554688"
y="620.3318"
id="tspan28">front texture URL to this, and its back </tspan><tspan
x="20.554688"
y="664.63734"
id="tspan29">texture URL to /back.png.
</tspan><tspan
x="20.554688"
y="708.94288"
id="tspan30">
</tspan><tspan
x="20.554688"
y="753.24843"
id="tspan31">After you're done, continue as normal.</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
cards/skilled-back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

61
flake.lock Normal file
View File

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1709569716,
"narHash": "sha256-iOR44RU4jQ+YPGrn+uQeYAp7Xo7Z/+gT+wXJoGxxLTY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "617579a787259b9a6419492eaac670a5f7663917",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

82
flake.nix Normal file
View File

@ -0,0 +1,82 @@
{
description = "cardgen";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
package = builtins.fromJSON (builtins.readFile ./package.json);
in
rec {
packages = flake-utils.lib.flattenTree rec {
cardgen = pkgs.buildNpmPackage rec {
pname = "cardgen";
inherit (package) version;
npmDepsHash = "sha256-1gQydlNbMBwCWj8++d79aBy5QaIIwiLMpsdurRz8FDA=";
doCheck = false;
nativeBuildInputs = with pkgs; [ makeWrapper ];
dontNpmBuild = true;
installPhase = ''
mkdir -p $out
mv ./* $out/
makeWrapper ${pkgs.nodejs-slim}/bin/node $out/bin/cardgen \
--add-flags $out/index.js \
--chdir $out/
'';
src = ./.;
};
};
defaultPackage = packages.cardgen;
}) // {
nixosModules = {
cardgen = { config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.cardgen;
in {
options.services.cardgen = {
enable = mkEnableOption "Enables the cardgen server";
port = mkOption {
type = types.port;
default = 3500;
};
package = mkOption {
type = types.package;
default = self.defaultPackage.${pkgs.system};
};
};
config = mkIf cfg.enable {
systemd.services."cardgen" = {
wantedBy = [ "multi-user.target" ];
environment = {
PORT = toString cfg.port;
};
serviceConfig = {
Restart = "on-failure";
ExecStart = "${getExe cfg.package}";
DynamicUser = "yes";
StateDirectory = "cardgen";
StateDirectoryMode = "0755";
};
};
};
};
};
};
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
fonts/Minecraft-Bold.otf Normal file

Binary file not shown.

BIN
fonts/Minecraft.otf Normal file

Binary file not shown.

3
fonts/readme.md Normal file
View File

@ -0,0 +1,3 @@
uses the following fonts:
- [Minecraft](https://github.com/IdreesInc/Minecraft-Font/releases/tag/v1.0)
- [Atkinson Hyperlegible](https://brailleinstitute.org/freefont)

174
index.js Normal file
View File

@ -0,0 +1,174 @@
import express from 'express';
import { Resvg } from '@resvg/resvg-js';
import * as fs from 'fs/promises';
import { escape } from 'html-escaper';
import { request } from 'undici';
import { lookup as mimeLookup } from 'mime-types';
const errorSVG = 'cards/error.svg';
const cards = {
player: {
cards: {
front: {
name: 'cards/player-front.svg',
parameters: [
{ name: 'name', type: 'string' },
{ name: 'dmg', type: 'number' },
{ name: 'hp', type: 'number' },
{ name: 'image', type: 'image', optional: true },
],
},
back: {
name: 'cards/skilled-back.png',
ignoreParameters: true,
}
},
special: {
help: 'cards/player-help.svg',
},
}
}
async function svg(filename, params) {
let svg = await fs.readFile(filename, 'utf8');
for (const [key, value] of Object.entries(params)) {
svg = svg.replaceAll('$' + key + '$', escape(value));
}
return svg;
}
async function render(svg) {
const opts = {
font: {
fontFiles: [
'fonts/Atkinson-Hyperlegible-Bold.otf',
'fonts/Atkinson-Hyperlegible-BoldItalic.otf',
'fonts/Atkinson-Hyperlegible-Italic.otf',
'fonts/Atkinson-Hyperlegible-Regular.otf',
'fonts/Minecraft.otf',
'fonts/Minecraft-Bold.otf',
],
loadSystemFonts: false, // It will be faster to disable loading system fonts.
defaultFontFamily: 'Atkinson Hyperlegible',
},
};
const resvg = new Resvg(svg, opts);
const pngData = resvg.render();
const pngBuffer = pngData.asPng();
return pngBuffer;
}
const server = express();
async function returns(req, res, filename, params) {
const ext = req.params.ext;
const s = await svg(filename, params);
if (ext === 'png') {
const r = await render(s);
res.type('png');
res.setHeader('Content-Length', r.byteLength);
res.send(r);
} else if (ext === 'svg') {
res.type('svg');
res.setHeader('Content-Length', s.length);
res.send(s);
} else {
res.status(400).send(`Unknown filetype '${ext}'`);
}
}
async function err(req, res, error) {
returns(req, res, errorSVG, { error });
}
const userAgent = 'cardgen/0.0.0 (https://oat.zone/)';
async function resolveImg(url) {
const { body, statusCode } = await request(url, {
headers: { 'user-agent': userAgent },
});
if (statusCode !== 200) throw `Image returned non-200 error code`;
const blob = await body.blob();
const arrayBuffer = await blob.arrayBuffer();
const b64 = Buffer.from(arrayBuffer).toString('base64');
const mime = mimeLookup(url) || 'image/png';
return 'data:' + mime + ';base64,' + b64;
}
function resolveNumber(str) {
const int = parseInt(str);
if (isNaN(int)) throw 'Invalid number provided';
return int;
}
server.get('/:type/:name.:ext', async (req, res) => {
if (req.headers['user-agent'] === userAgent) return err(req, res, 'Stop that.');
const type = req.params.type;
const name = req.params.name;
const ext = req.params.ext;
const card = cards[type];
if (!card) return err(req, res, `No card named ${type} found!`);
const tex = card.cards[name];
if (!tex) return err(req, res, `Card ${type} does not have ${name}`);
let params = {};
if (card.special.help && Object.entries(req.query).length === 0 && tex.parameters)
return returns(req, res, card.special.help, {});
if (tex.name.endsWith('.png')) {
if (ext !== 'png') {
res.status(400).send(`Cannot offer ${ext}; only a png of this texture xists`);
}
return res.sendFile(tex.name, { root: './' });
}
for (const param of (tex.parameters || [])) {
const name = param.name;
const type = param.type;
const isOptional = param.optional;
const value = req.query[name];
if (!isOptional && (value === undefined || value === null || value === '')) {
return err(req, res, `Required argument ${name} missing`);
}
try {
switch (type) {
case 'string':
params[name] = value || '';
break;
case 'number':
params[name] = value ? resolveNumber(value) : 0;
break;
case 'image':
params[name] = value ? await resolveImg(value) : '';
break;
}
} catch(e) {
return err(req, res, `${name}: ${e}`);
}
}
await returns(req, res, tex.name, params);
});
const port = process.env.PORT || 8080;
server.listen(port);
console.log(`listening on port ${port}`);

1731
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "cardgen",
"version": "1.0.0",
"private": true,
"main": "index.js",
"author": "oatmealine",
"license": "GPL-3.0-only",
"type": "module",
"dependencies": {
"@resvg/resvg-js": "^2.6.0",
"express": "^4.18.3",
"html-escaper": "^3.0.3",
"mime-types": "^2.1.35",
"undici": "^6.7.0"
}
}