Bringing anime girls holding programming books, and literally any other image, onto your ComputerCraft screens!
This is a two-part project: a Bun-powered image proxy that converts any web image (or a random book from the AGHPB API) into a ComputerCraft-native format, and a Lua client that talks to that proxy from inside CC:Tweaked and renders the result on any terminal or monitor, auto-fit to its size.
No re-fetching nor re-converting as every image is content-hashed and perceptually-hashed on the way in, so the exact same image (or a near-duplicate served from a different URL) is served straight from cache instead of being downloaded and reprocessed all over again.
Written in Bun/TypeScript on the server side with no bloated image libraries, decoding/resizing goes through sharp only, everything else (palette matching, hashing, caching, HTTP) is hand-rolled. The client is plain CC:Tweaked Lua with zero dependencies beyond the built-in http and paintutils APIs.
The proxy tries instances in order until one responds, so a single dead instance never breaks /aghpb/random.
| Country | URL | Hosted by | Notes |
|---|---|---|---|
| π«π· | https://aghpb.thenolle.com |
Nolly | β Default instance |
| π¬π§ | https://api.devgoldy.xyz/aghpb/v1 |
DevGoldy | Official instance |
| πΈπͺ | https://aghpb.zeeraa.net |
Zeeraa | Backup |
Edit
AGHPB_INSTANCESinsrc/aghpb.tsto reorder, add, or remove instances -> no config file needed.
- Any image, any URL ->
/convertturns literally any web-hosted image into a ComputerCraft-drawable format - Random AGHPB books ->
/aghpb/random, optionally filtered by category, fully wired into the same conversion pipeline - Two output formats ->
nfp(apaintutils-loadable paint-format image) orblit(a JSON row array oftext/fg/bgfor directterm.blitrendering) - Auto-fit rendering -> the Lua client detects whether it's running on a monitor or the bare terminal, reads its exact size, and requests the image pre-sized to fit so no scaling math on the CC side for faster renders
- Exact-match caching -> every converted image is content-hashed (SHA-256); the same URL, or the same bytes served from a different URL, is served from cache with zero re-fetch and zero re-convert
- Perceptual-hash dedup -> a difference-hash (dHash) catches near-identical images (recompressed, re-hosted, slightly resized) and reuses the existing cached conversion instead of doing the work again
- 16-color palette matching -> every pixel is nearest-neighbor matched (Euclidean RGB distance) against ComputerCraft's default palette
- SQLite-backed cache -> all cache state lives in a single
cache.sqlitefile viabun:sqlite, with per-image hit counters - No bloated image libraries -> decoding/resizing goes through
sharponly; hashing, palette matching, and NFP/blit generation are all hand-written
- Clone the repo and
cdinto itgit clone https://github.com/thenolle/aghpb.cc.git cd aghpb.cc/proxy - Install dependencies
bun install
- Start the server
bun run dev # with --watch, for development # or bun run start # for production
- It listens on
http://localhost:3000by default (override withPORT=xxxx)
- Copy
client.lua,viewer.lua, andaghpb.luaonto your CC:Tweaked computer (via a disk, or whatever gets files onto your machine, I personally usevscode+computer-craftextension) - Edit
client.lua'sBASE_URLto point at your proxy's real reachable address (see Networking caveats below -localhostalmost never works from inside a CC computer) - Allow that address in your server's
http.rulesconfig (see HTTP rules) - Run
viewer <imageUrl>oraghpb [category]
- Bun 1.3.x
- CC:Tweaked 1.120.x+ (any version with
http,paintutils, andtextutils.unserialiseJSON) httpAPI enabled on the CC server, with a rule allowing your proxy's address
| Script | Description |
|---|---|
client.lua |
Shared module that builds requests, talks HTTP to the proxy, decodes NFP/blit responses. Required by the other two |
viewer.lua <imageUrl> |
Renders any image URL, converted through /convert, auto-fit to the current screen |
aghpb.lua [category] |
Fetches a random AGHPB book (optionally by category) through /aghpb/random and renders it |
viewer https://i.imgur.com/mOF9W7D.png
aghpb
aghpb typescript
Both scripts auto-detect an attached monitor (wrapping the first one found) and fall back to the local terminal if none exists, reading its exact getSize() and requesting the image pre-converted to those dimensions.
| Endpoint | Description |
|---|---|
GET /convert?url=<imageUrl>&width=&height=&format=nfp|blit |
Convert any image URL into a CC-drawable format |
GET /aghpb/random?category=&width=&height=&format= |
Convert a random AGHPB book, with Book-* metadata as response headers |
GET /aghpb/get/id/:searchId?width=&height=&format= |
Convert a specific AGHPB book by its search ID |
GET /aghpb/categories |
List all AGHPB categories (proxied straight through) |
GET /aghpb/search?query=&category=&limit= |
Search AGHPB books by name |
GET /aghpb/info |
Stats about the currently-responding AGHPB instance |
GET /health |
Liveness check |
width/height default to 50Γ19 if omitted. Every image response carries x-cache (url-hit / sha256-hit /
phash-hit / miss) and x-image-sha256 headers so you can see exactly why a request was fast or slow.
/convertfirst checks the SQLite cache for this exact URL at this exact width/height/format - if found, nothing is fetched at all- On a miss, the source image is fetched and SHA-256 hashed; if those exact bytes were already converted (e.g. served from a different, mirrored, or redirected URL), the cached conversion is reused and the new URL is linked to it
- On another miss, a difference-hash (dHash, 8Γ8 grayscale gradient, 64-bit) is computed and compared via Hamming distance against every cached image of the same dimensions/format - a close-enough match (recompressed, slightly resized, or re-hosted copy of the same picture) is reused instead of reconverting
- Only if nothing matches does the image actually get resized and every pixel nearest-neighbor matched against ComputerCraft's 16-color palette, producing either an
.nfppaint-format grid or aterm.blit-ready JSON row array - The result, its SHA-256, and its dHash are all written back to
cache.sqlite, and the requesting URL is linked to it for instant future hits
/aghpb/random and /aghpb/get/id/:id share the exact same cache/convert pipeline, just entering it from an already-fetched buffer instead of a URL (since /random has no stable URL to key off of). They skip straight to the SHA-256/dHash checks so the same book resurfacing from a later random pull, or the same book fetched by ID twice, still hits the cache instantly.
The dHash algorithm resizes the source to a 9Γ8 grayscale grid and encodes whether each pixel is brighter or darker than its right-hand neighbor into a 64-bit fingerprint. It's cheap to compute, resistant to resizing/recompression noise, and comparing two images is just a Hamming distance over 64 bits - no perceptual-hash library required.
Why do I bother explaining all of that, do anyone read it even ? If you do, you are a nerd and I love you.
CC:Tweaked blocks requests to private/local IP ranges by default. If your proxy runs on the same LAN as your Minecraft server (or the same machine), add an explicit allow rule in your server config:
[http]
[[http.rules]]
host = "*"
max_upload = 4194304
action = "allow"
use_proxy = false
max_websocket_message = 131072
max_download = 33554432
[[http.rules]] # replaces the old `$private` rule that blocks LAN access
host = "192.168.x.x" # your proxy's actual LAN IP
action = "allow"localhost/127.0.0.1 inside client.lua's BASE_URL refers to the Minecraft server process's own loopback, not the CC computer's. Unless the proxy and the Minecraft server are the exact same process space, point BASE_URL at the proxy machine's real LAN (or public) address, and make sure that address (not 127.0.0.1) is what's allowed in http.rules.
proxy/
src/
index.ts # entry point
server.ts # Bun.serve routes (/convert, /aghpb/*, /health)
convert.ts # fetch + sharp resize + palette mapping -> NFP/blit
cacheFlow.ts # shared sha256 -> phash -> convert -> cache pipeline
phash.ts # dHash computation + Hamming distance
palette.ts # ComputerCraft 16-color palette + nearest-match
database.ts # bun:sqlite cache schema + queries
aghpb.ts # AGHPB API client with instance fallback
client.lua # shared CC HTTP/decode module
viewer.lua # renders any image URL
aghpb.lua # renders a random AGHPB book
Bun runs the TypeScript directly so no more
buildstep [you can tho if you want to, it just isn't necessary], now you can justbun run devand it will work.
- AGHPB API by DevGoldy - the API this proxy wraps
- Anime Girls Holding Programming Books repo by cat-milk - the source material
- CC:Tweaked - the mod this all renders into
WTFPL - Do whatever the f*ck you want