6 Commits
Author SHA1 Message Date
rijads 4c6f272478 TODO: what the re-release add-ons need, from their lumps 2026-10-04 15:45:26 +02:00
rijads 9bd75e2494 contentVersion 01.001.000 for v1.1.0 2026-10-04 15:33:05 +02:00
rijads 1ac421aa82 Follow redirects to folder links and resolve the listing where they land
A short link to a folder could not be imported: the listing's relative links were resolved against
the typed address instead of the one the redirect ended at. plat_http_get now reports the final
address. The PS5 backend follows Location headers itself, since sceHttp's automatic redirects do
not say where they land; the PC backend asks libcurl. The importer resolves listings against that
address and sends later range requests straight to it.

The URL helpers move from the launcher's listing code to src/platform/url.c so the PS5 backend can
resolve Location headers with them, and the upload server reuses url_file_name for file names.
2026-10-04 15:30:51 +02:00
rijads 93814c3e95 Send games from a PC or phone browser, with a QR code on screen
Add games gets a first row, Send from PC or phone. Its screen runs a small HTTP server for as long
as it is open (port 9666, or the next one the sandbox allows) and shows the address as text and as
a QR code. The page it serves takes dropped files and whole folders, uploads them one at a time with
progress, and lists what the console did with each. Game WADs are moved into place, archives go
through the importer and are deleted afterwards, add-ons and other files are refused with a reason.

qrcodegen 1.8.0 joins the pinned third-party fetch, renamed to fetch-third-party.sh; the page is
embedded at build time by tools/embed-file.py. tools/send.py does what the page does, for console
tests with test/console-send.cfg.
2026-10-04 15:25:39 +02:00
rijads 652cfd6007 Name DualSense buttons in prompts and show the controls on Read This!
Prompts used to ask for keys ("press y or n", "press y to quit", "press a key"). Text now carries
glyph characters for the face buttons, drawn in their PlayStation colours by both Doom's menu font
and the launcher, so prompts read "Cross yes, Circle no" and the launcher footers show the symbols
instead of X and O.

The first Read This! page shows the controller controls instead of id's keyboard help, and DOOM II
keeps its Read This! entry when the WAD has the graphic, so every game can show it.
2026-10-04 14:55:19 +02:00
rijads a62377c80e TODO: controller wording in prompts and a DualSense help screen 2026-10-04 09:31:00 +02:00
32 changed files with 1655 additions and 237 deletions

No files matched your search

+35 -14
View File
@@ -6,24 +6,25 @@ DualSense through ScePad and playing sound through AudioOut. Title ID `PPSA99666
All PS5, platform, audio, launcher and test code here is written for this project. The external
code is id's game source, the archive libraries the importer is built from (libarchive, xz's
liblzma, zlib) and the build tools listed at the end.
liblzma, zlib), the QR code library of the send screen (qrcodegen) and the build tools listed at
the end.
## Layout
| Path | What |
| --- | --- |
| `src/doom/` | id's original game code with the 64-bit and portability fixes listed below |
| `src/port/` | Doom's `i_*` layer (main, system, video, sound, network, controller mapping) on top of the platform API |
| `src/launcher/` | The game list shown before the engine starts: IWAD discovery, settings, Doom-style drawing from the bundled WAD, HTML folder listings, and the importer (HTTP or local file, WAD or ZIP/7Z/RAR) |
| `src/platform/platform.h` | The platform API: time, log, files and folder listing, video present, pad, rumble, audio output, HTTP, text input |
| `src/port/` | Doom's `i_*` layer (main, system, video, sound, network, controller mapping) on top of the platform API, and the button glyphs shared with the launcher |
| `src/launcher/` | The game list shown before the engine starts: IWAD discovery, settings, Doom-style drawing from the bundled WAD, HTML folder listings, the importer (HTTP or local file, WAD or ZIP/7Z/RAR), and the send screen's HTTP server and web page (`upload.c`, `upload.html`) |
| `src/platform/` | The platform API (`platform.h`: time, log, files and folder listing, video present, pad, rumble, audio output, HTTP, text input), the test plan reader, and the URL helpers (`url.c`) shared by the launcher and the PS5 HTTP backend |
| `src/audio/` | Sound engine: SFX mixer, MUS sequencer with DMX-style voice allocation, OPL FM synth |
| `src/ps5/` | PS5 backend: startup (`crt0.c`), system, VideoOut, AGC compute presenter, pad, AudioOut, `sceHttp`, the IME keyboard |
| `src/ps5/present.cl` | GPU kernel: palette lookup, sharp-bilinear scaling to 1080p, tiled scanout writes (gfx1010, wave64) |
| `src/host/` | Headless Linux backend for testing on the PC: PNG frames, scripted pad, WAV audio, libcurl HTTP |
| `third_party/` | Build configuration for libarchive, liblzma and zlib (`config.h` files and `third_party.mk`); the sources are fetched, not copied |
| `third_party/` | Build configuration for libarchive, liblzma, zlib and qrcodegen (`config.h` files and `third_party.mk`); the sources are fetched, not copied |
| `test/` | `render_music` (MUS lump to WAV), `present_preview` (runs the GPU kernel's math on the CPU) and the console test plans |
| `sce_sys/` | `param.json` (raise `contentVersion` in every release) and the icon (the shareware WAD's `M_DOOM` logo, scaled 3x on black) |
| `tools/` | Tool, library and shareware WAD fetch, kernel embedding, deploy, console test runner |
| `tools/` | Tool, library and shareware WAD fetch, kernel and page embedding, deploy, console test runner, `send.py` (sends files to the send screen like its page) |
## Build
@@ -35,7 +36,7 @@ docker build -t ps5-doom-build docker/
MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/src" -w /src ps5-doom-build make -j16 ps5 host
```
`make ps5` fetches the pinned tools, the archive libraries (`tools/fetch-archive-libs.sh`) and the
`make ps5` fetches the pinned tools, the libraries (`tools/fetch-third-party.sh`) and the
shareware WAD on first use, then:
1. compiles the game, the launcher and the PS5 layer for `x86_64-sie-ps5` against the PS5 payload
@@ -82,13 +83,22 @@ and maps the chosen game to Doom's game mode, mission and language. The launcher
file's header and directory, and tells The Ultimate DOOM from DOOM by the presence of `E4M1`;
- imports any ZIP, 7Z or RAR found there that it has not imported before (recorded in
`/download0/launcher.cfg` by size and path), extracting only known IWAD names;
- downloads from a typed link with `plat_http_get`: an HTML answer is parsed as a folder listing
- downloads from a typed link with `plat_http_get`, which follows redirects and reports where it
ended up (the PS5 backend follows `Location` itself, as `sceHttp`'s automatic redirects do not
say where they land), so a short link to a folder works: an HTML answer is parsed as a folder listing
(links under the folder that end in `.wad`, `.zip`, `.7z`, `.rar` or `/`), a WAD is copied, an
archive goes through libarchive. libarchive seeks with HTTP range requests; a server without them
is read through or re-read from the start instead;
- writes into the first writable WAD folder (`/app0/wads` on the console), through a `.part` file
that is checked as an IWAD before it is renamed;
- shows a one-time notice with a checkbox before the first download.
- shows a one-time notice with a checkbox before the first download;
- receives files from a browser on the send screen (*Add games*, *Send from PC or phone*):
`upload.c` listens only while that screen is open, on port 9666 or the next free one, serves
`upload.html` (embedded at build time by `tools/embed-file.py`) and stores each `PUT /upload/<name>`
as `<name>.upload` in the WAD folder. The launcher moves game WADs into place, runs archives
through the importer and deletes them afterwards, and reports each result on the screen and to the
page, which polls `GET /status`. The address shown is the one the route to the internet leaves
from (a connected UDP socket's local address); the QR code is drawn with qrcodegen.
Saves are per game: `<save dir>/<iwad name>sav<slot>.dsg` (for example `doom2sav0.dsg`).
@@ -122,7 +132,8 @@ without opening it), `reset` (delete the app's saved data in `/download0` first,
state; `console-reset.cfg` does only that). Plans: `console-play.cfg` (menus, play, save, load), `console-cpu.cfg`,
`console-soak.cfg` (6 minutes), `console-import.cfg` (launcher, notice, folder listing and a 7Z
import from `http://192.168.0.10:8666/`), `console-autoimport.cfg`, `console-tnt.cfg`,
`console-ultimate.cfg`.
`console-ultimate.cfg`, `console-send.cfg` (opens the send screen for a minute; run
`tools/send.py <address> <files>` meanwhile).
## PS5 facts learned on hardware (FW 13.00)
@@ -136,6 +147,8 @@ import from `http://192.168.0.10:8666/`), `console-autoimport.cfg`, `console-tnt
plus `sceKernelGetdents` (8-byte records: 32-bit inode, 16-bit length, type, name length). Saves
use explicit `/download0/...` paths; output is captured by pointing `stdout`/`stderr` at a pipe
(`fdopen`), and klog wants one line per `sceKernelDebugOutText`.
- A title can listen on TCP ports, but the sandbox refuses some with `EACCES` (8666 and 50000 of
those tried; 8000, 9000, 9090, 9666, 18666 and 30000 work). BSD sockets come from `libkernel`.
- `sceHttp` with `sceSsl` works in the sandboxed title, plain and HTTPS, including `Range` request
headers added with `sceHttpAddRequestHeader` (answers 206). `sceHttpReadData` returns only when
the buffer is full or the body ends. About 12 MB/s from a PC on the same wired network.
@@ -169,6 +182,12 @@ import from `http://192.168.0.10:8666/`), `console-autoimport.cfg`, `console-tnt
Saving needs no keyboard: an empty slot is pre-filled with the level name, Cross saves.
Text names the face buttons with glyph characters (`GLYPH_CROSS` and the others in
`src/port/glyphs.h`, control characters 1 to 4) that Doom's menu font and the launcher draw as the
button symbols in their PlayStation colours. The first *Read This!* page shows this table instead of
id's keyboard help (`HELP1`, `HELP`); DOOM II keeps its *Read This!* entry whenever the WAD has the
`M_RDTHIS` graphic, which all of them do.
## Changes to id's code
- 64-bit: pointers stored in `int` (config string defaults, save game pointer fields, the
@@ -188,7 +207,8 @@ Saving needs no keyboard: an empty slot is pre-filled with the level name, Cross
static limits raised (visplanes, drawsegs, sprites, openings, intercepts, plats, ceilings,
buttons, scrollers) so limit-removing maps such as Freedoom's run; full-screen pictures wider
than 320 pixels (the 2024 re-release's widescreen title and intermission screens) are drawn
centred and clipped instead of being rejected.
centred and clipped instead of being rejected; prompts name DualSense buttons instead of keys
(`d_englsh.h`, `d_french.h`) and the first help page lists the controller controls.
## External code and tools (pinned)
@@ -196,7 +216,8 @@ Saving needs no keyboard: an empty slot is pre-filled with the level name, Cross
`b1315a9`: its `ps5-native-tool` (PIE to PS5 module, FSELF signing), its clean-room `libc.prx`,
its intermediate PIE linker script, and the PS5 payload SDK v0.42 it fetches. Built in
`.deps/native-app` by `tools/fetch-native-tools.sh`; nothing from it is copied into `src/`.
- [libarchive](https://www.libarchive.org/) 3.8.9, [xz](https://tukaani.org/xz/) 5.8.4 (liblzma)
and [zlib](https://zlib.net/) 1.3.2, fetched and checked by `tools/fetch-archive-libs.sh` into
`.deps/archive` and compiled into the game for both targets.
- [libarchive](https://www.libarchive.org/) 3.8.9, [xz](https://tukaani.org/xz/) 5.8.4 (liblzma),
[zlib](https://zlib.net/) 1.3.2 and [qrcodegen](https://www.nayuki.io/page/qr-code-generator-library)
1.8.0, fetched and checked by `tools/fetch-third-party.sh` into `.deps/third-party` and compiled
into the game for both targets.
- clang/lld/llvm 18 (x86_64-sie-ps5 and amdgcn targets).
+13 -5
View File
@@ -6,7 +6,8 @@ PS5_SRC := $(wildcard src/ps5/*.c)
include third_party/third_party.mk
INCLUDES := -Isrc/doom -Isrc/port -Isrc/platform -Isrc/audio -Isrc/launcher $(ARCHIVE_INCLUDES)
GEN := build/gen
INCLUDES := -Isrc/doom -Isrc/port -Isrc/platform -Isrc/audio -Isrc/launcher -I$(GEN) $(THIRD_INCLUDES)
DOOM_WARNINGS := -Wall -Wno-unused-const-variable -Wno-unused-but-set-variable \
-Wno-unused-variable -Wno-logical-not-parentheses -Wno-missing-braces
OWN_WARNINGS := -Wall -Wextra -Wno-unused-parameter
@@ -52,11 +53,17 @@ $(HOST_DIR)/%.o: src/%.c
@mkdir -p $(dir $@)
$(HOST_CC) -std=gnu11 $(HOST_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -c $< -o $@
$(HOST_DIR)/third_party/%.o: $(ARCHIVE_DEPS)/%.c
$(HOST_DIR)/third_party/%.o: $(THIRD_DEPS)/%.c
@mkdir -p $(dir $@)
$(HOST_CC) $(THIRD_FLAGS) -g -MMD -MP $(call third_flags,$<) -c $< -o $@
$(HOST_OWN_OBJ): | $(ARCHIVE_STAMP)
$(HOST_OWN_OBJ): | $(THIRD_STAMP)
$(GEN)/upload_page.h: src/launcher/upload.html tools/embed-file.py
@mkdir -p $(GEN)
python3 tools/embed-file.py $< upload_page $@
$(HOST_DIR)/launcher/upload.o $(PS5_DIR)/launcher/upload.o: $(GEN)/upload_page.h
tools: $(NATIVE_TOOL)
@@ -95,11 +102,11 @@ $(PS5_DIR)/%.o: src/%.c $(SDK)/target/lib/libkernel.so
@mkdir -p $(dir $@)
$(PS5_CC) -std=gnu11 $(PS5_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -I$(PS5_GEN) -c $< -o $@
$(PS5_DIR)/third_party/%.o: $(ARCHIVE_DEPS)/%.c $(SDK)/target/lib/libkernel.so
$(PS5_DIR)/third_party/%.o: $(THIRD_DEPS)/%.c $(SDK)/target/lib/libkernel.so
@mkdir -p $(dir $@)
$(PS5_CC) $(THIRD_FLAGS) -MMD -MP $(call third_flags,$<) -c $< -o $@
$(PS5_OWN_OBJ): | $(ARCHIVE_STAMP)
$(PS5_OWN_OBJ): | $(THIRD_STAMP)
$(PS5_DIR)/llvm-pie.elf: $(PS5_OBJ) $(AGC_STUBS) src/ps5/symbols.map
$(SDK)/bin/prospero-lld -T $(NATIVE)/tooling/native/ps5-pie.ld --eh-frame-hdr --gc-sections \
@@ -124,6 +131,7 @@ $(APP)/eboot.bin: $(PS5_DIR)/eboot.elf $(RUNTIME) sce_sys/param.json sce_sys/ico
cp $(LA_DIR)/../COPYING $(APP)/licenses/libarchive.txt
cp $(XZ_DIR)/../../COPYING.0BSD $(APP)/licenses/liblzma.txt
cp $(ZL_DIR)/LICENSE $(APP)/licenses/zlib.txt
head -n 22 $(QR_DIR)/qrcodegen.c > $(APP)/licenses/qrcodegen.txt
$(NATIVE_TOOL) self --inspect --file $@
$(NATIVE_TOOL) self --inspect --file $(APP)/sce_module/libc.prx
+13 -7
View File
@@ -6,8 +6,8 @@ id Software's original DOOM source (`linuxdoom-1.10`) running as a native PS5 ti
home-screen icon, drawn on the GPU through Sony's AGC driver and VideoOut, DualSense input with
analog sticks and rumble, and the DOS-era FM music synthesised from the game's own instrument bank.
It ships with the free shareware episode of DOOM and plays The Ultimate DOOM, DOOM II and Final
DOOM from your own game files, which it can import from a folder on the console or download from
your PC, including straight out of ZIP, 7Z and RAR archives.
DOOM from your own game files, which you can send from a PC or phone browser, download from a
link or copy to the console, including straight out of ZIP, 7Z and RAR archives.
## Requirements
@@ -38,18 +38,23 @@ The files from the original releases and from the 2024 *DOOM + DOOM II* re-relea
### Adding your games
Either way, the game files can also be inside ZIP, 7Z or RAR archives: DOOM extracts the game
Whichever way, the game files can also be inside ZIP, 7Z or RAR archives: DOOM extracts the game
WADs it recognises and skips everything else.
- **Send them from a PC or phone.** In the game list choose *Add games...*, then *Send from PC or
phone*. Open the address the screen shows (for example `http://192.168.1.50:9666/`) in a browser
on the same network, or scan its QR code, and drop the files or whole folders on the page. Both
the page and the console show what was added; archives are deleted once their games are out. The
console only accepts files while that screen is open.
- **Copy them to the console.** Put the files or archives in `/data/homebrew/PPSA99666/wads/` (FTP,
ps5upload or USB). DOOM picks them up the next time it starts.
- **Download them from a link.** In the game list choose *Add games...*, then *Link*, type an
`http://` or `https://` address on the console keyboard and choose *Download*. The link can point
at a WAD, an archive, or a folder; for a folder DOOM lists the files and you pick one or all.
The simplest way to serve your own files from a PC on the same network is to open a terminal in the
folder that holds them and run a small web server, then use the address it prints with your PC's
local IP (for example `http://192.168.1.20:8000/`):
To download from your own PC instead, open a terminal in the folder that holds the files and run a
small web server, then use the address it prints with your PC's local IP (for example
`http://192.168.1.20:8000/`):
```bash
python -m http.server 8000
@@ -75,7 +80,8 @@ traffic. Allow the port through the PC's firewall if the console cannot connect.
| Triangle / touchpad | Automap (Square: follow mode) | |
| Options | Menu | Close menu |
Saves and settings stay on the console, and each game keeps its own saves. Holding **L1+R1** at launch uses the CPU scaler instead of
Prompts show the buttons to press, and *Read This!* in the main menu opens this list on the
console. Saves and settings stay on the console, and each game keeps its own saves. Holding **L1+R1** at launch uses the CPU scaler instead of
the GPU, should a firmware ever disagree with the GPU path.
## Building and internals
+2 -1
View File
@@ -9,9 +9,10 @@ release package carries these notices and the license texts in `PPSA99666/licens
| --- | --- | --- |
| [DOOM source code](https://github.com/id-Software/DOOM) (`linuxdoom-1.10`), id Software | The game engine in `src/doom/`, with the changes listed in `DEVELOPMENT.md` | GNU GPL v2 (`LICENSE`) |
| DOOM shareware IWAD v1.9 (`DOOM1.WAD`, episode 1), id Software | Bundled game data, `wads/doom1.wad`, unmodified; fetched by `tools/fetch-shareware.sh`. Its `M_DOOM` logo, scaled up, is the home-screen icon (`sce_sys/icon0.png`) | id Software's shareware terms: free to share unmodified and free of charge |
| [libarchive](https://www.libarchive.org/) 3.8.9 | Reading ZIP, 7Z and RAR archives in the importer (`tools/fetch-archive-libs.sh`, configured by `third_party/`) | BSD 2-Clause, with a few files public domain or CC0 (`licenses/libarchive.txt`) |
| [libarchive](https://www.libarchive.org/) 3.8.9 | Reading ZIP, 7Z and RAR archives in the importer (`tools/fetch-third-party.sh`, configured by `third_party/`) | BSD 2-Clause, with a few files public domain or CC0 (`licenses/libarchive.txt`) |
| [xz](https://tukaani.org/xz/) 5.8.4, liblzma | LZMA and LZMA2 decoding for 7Z archives | BSD Zero Clause (`licenses/liblzma.txt`) |
| [zlib](https://zlib.net/) 1.3.2 | Deflate decoding for ZIP archives | zlib license (`licenses/zlib.txt`) |
| [QR Code generator](https://www.nayuki.io/page/qr-code-generator-library) 1.8.0, Project Nayuki | The QR code of the send screen's address (`qrcodegen.c`) | MIT (`licenses/qrcodegen.txt`) |
| `libc.prx` from [ps5-native-app-boilerplate](https://github.com/blackbearreloaded/ps5-native-app-boilerplate), BlackBearReloaded | Runtime module the PS5 loader requires next to a native title (`sce_module/libc.prx`), built from its source at commit `b1315a9` | GNU GPL v3 or later (`licenses/libc-prx-GPL-3.0.txt`) |
## Used to build
+19 -16
View File
@@ -2,22 +2,25 @@
## Add-ons (PWADs)
The launcher only starts the main games; add-on WADs found in archives are skipped. Order of work:
The launcher only starts the main games; add-on WADs are refused by the importer and the send
screen. What the 2024 re-release add-ons contain (checked 2026-10-04):
- [ ] Pick an add-on in the launcher and load it on top of its game (`-file`), import PWADs from
archives and links, and give each add-on its own saves.
- [ ] *No Rest for the Living* (`nerve.wad`) and the *Master Levels* (`masterlevels.wad`) from the
2024 re-release: classic-format DOOM II maps, so they should run once loading works. Check
their level order and secret exits.
- [ ] *SIGIL* and *SIGIL II* (`sigil.wad`, `sigil2.wad`): the re-release versions are episodes 5 and
6, which need episode, intermission and sky support beyond episode 4.
- [ ] *Legacy of Rust* (`id1.wad` with `id1-*.wad` and `id24res.wad`): needs the id24 extensions
(new weapons, monsters and the rest of the spec). Largest of these.
| WAD | Maps | Needs beyond loading the file |
| --- | --- | --- |
| `nerve.wad` (*No Rest for the Living*) | MAP01-09, DOOM II format | `UMAPINFO`: level names, music, intermission pictures, MAP08 ends the game, secret exit to MAP09 |
| `masterlevels.wad` (*Master Levels*) | MAP01-21 | `UMAPINFO` as above plus its own skies (`SKYM1`...); its new textures (`PNAMES`, `TEXTURE1`, patches) replace the IWAD's the vanilla way |
| `sigil.wad` (*SIGIL*) | E5M1-9 | `UMAPINFO`, episode 5 (menu entry `M_EPI5`, `SKY5`, intermission), `DEHACKED` (par times only) |
| `sigil2.wad` (*SIGIL II*) | E6M1-9 | As SIGIL for episode 6, plus its own flats (a second `F_START` block, which id's engine cannot merge: it only sees the last block) and Boom's `ANIMATED`/`SWITCHES` |
| `id1.wad` (*Legacy of Rust*) | MAP01-16 | The id24 spec: a 63 KB `DEHACKED` with new things, `DECOHACK`, `SBARDEF`, `SKYDEFS`, new sprites and flats, `id24res.wad`; in effect a modern engine |
## Importer
Loading itself exists: id's `-file` still works. Order of work:
- [ ] Folder links behind a redirect (for example a shorturl.at link to a folder): the listing's
relative links are resolved against the typed address, not the address the redirect lands
on, so the files cannot be opened. Learn the final address (`sceHttpSetRedirectCallback`,
`CURLINFO_EFFECTIVE_URL` on the PC) and resolve against it. Short links straight to a file
already work.
- [ ] Pick an add-on in the launcher under its game and start it with `-file`; accept PWADs in the
importer and the send screen into their own folder; give each add-on its own saves.
- [ ] A `UMAPINFO` reader (level names, music, skies, progression, intermission and end screens,
episode entries). Covers *No Rest for the Living* and the *Master Levels*, and is the base for
SIGIL and for community WADs that ship `UMAPINFO`.
- [ ] Episodes 5 and 6 for *SIGIL* and *SIGIL II*; flat merging and `ANIMATED`/`SWITCHES` for *SIGIL II*.
- [ ] *Legacy of Rust* and most community WADs (Boom, MBF21, `DEHACKED`) need far more than id's
engine: decide between extending it and moving the game core to a modern port (for example
DSDA-Doom or Woof!) behind our PS5 layer.
+1 -1
View File
@@ -10,7 +10,7 @@
"conceptId": "99666",
"contentBadgeType": 1,
"contentId": "UP9000-PPSA99666_00-DOOMNATIVEPS5000",
"contentVersion": "01.000.001",
"contentVersion": "01.001.000",
"downloadDataSize": 256,
"gameIntent": {
"permittedIntents": [
+3 -3
View File
@@ -36,8 +36,8 @@
//
// M_Menu.C
//
#define PRESSKEY "press a key."
#define PRESSYN "press y or n."
#define PRESSKEY "press "GLYPH_CROSS"."
#define PRESSYN GLYPH_CROSS" yes "GLYPH_CIRCLE" no"
#define QUITMSG "are you sure you want to\nquit this great game?"
#define LOADNET "you can't do load while in a net game!\n\n"PRESSKEY
#define QLOADNET "you can't quickload during a netgame!\n\n"PRESSKEY
@@ -63,7 +63,7 @@
#define NETEND "you can't end a netgame!\n\n"PRESSKEY
#define ENDGAME "are you sure you want to end the game?\n\n"PRESSYN
#define DOSY "(press y to quit)"
#define DOSY "(press "GLYPH_CROSS" to quit)"
#define DETAILHI "High detail"
#define DETAILLO "Low detail"
+3 -3
View File
@@ -32,8 +32,8 @@
//
// M_Menu.C
//
#define PRESSKEY "APPUYEZ SUR UNE TOUCHE."
#define PRESSYN "APPUYEZ SUR Y OU N"
#define PRESSKEY "APPUYEZ SUR "GLYPH_CROSS"."
#define PRESSYN GLYPH_CROSS" OUI "GLYPH_CIRCLE" NON"
#define QUITMSG "VOUS VOULEZ VRAIMENT\nQUITTER CE SUPER JEU?"
#define LOADNET "VOUS NE POUVEZ PAS CHARGER\nUN JEU EN RESEAU!\n\n"PRESSKEY
#define QLOADNET "CHARGEMENT RAPIDE INTERDIT EN RESEAU!\n\n"PRESSKEY
@@ -55,7 +55,7 @@
"RESEAU!\n\n"PRESSKEY
#define ENDGAME "VOUS VOULEZ VRAIMENT METTRE FIN AU JEU?\n\n"PRESSYN
#define DOSY "(APPUYEZ SUR Y POUR REVENIR AU OS.)"
#define DOSY "(APPUYEZ SUR "GLYPH_CROSS" POUR REVENIR AU OS.)"
#define DETAILHI "GRAPHISMES MAXIMUM "
#define DETAILLO "GRAPHISMES MINIMUM "
+2
View File
@@ -30,6 +30,8 @@
// All important printed strings.
// Language selection (message strings).
// Use -DFRENCH etc.
// Prompts name DualSense buttons through the glyph characters.
#include "glyphs.h"
#ifdef FRENCH
#include "d_french.h"
+71 -15
View File
@@ -748,23 +748,58 @@ void M_QuickLoad(void)
// Read This Menus
// Had a "quick hack to fix romero bug"
//
// The first page shows the DualSense controls instead of id's keyboard
// help (HELP1, or HELP in DOOM 2).
//
#define CONTROLS_SHADE 20
static struct
{
char* button;
char* action;
} controls[] =
{
{"left stick", "move and strafe"},
{"right stick", "turn"},
{"d-pad", "move and turn"},
{"r2", "fire"},
{GLYPH_CROSS" / "GLYPH_SQUARE, "use"},
{GLYPH_CIRCLE" + d-pad", "strafe"},
{"l2", "run"},
{"l1 / r1", "change weapon"},
{GLYPH_TRIANGLE" / touchpad", "automap"},
{"options", "menu"}
};
void M_DrawReadThis1(void)
{
char* title = "controls";
char* automap = "automap: l1 / r1 zoom, "GLYPH_SQUARE" follow";
char* menus = "menus: "GLYPH_CROSS" select, "GLYPH_CIRCLE" back";
byte* flat = NULL;
byte* dest = screens[0];
int x;
int y;
int i;
inhelpscreens = true;
switch ( gamemode )
// A darkened floor behind the text, like the story screens.
if (W_CheckNumForName("FLOOR4_8") >= 0)
flat = W_CacheLumpName("FLOOR4_8",PU_CACHE);
for (y = 0; y < SCREENHEIGHT; y++)
for (x = 0; x < SCREENWIDTH; x++)
*dest++ = flat ? colormaps[CONTROLS_SHADE*256 + flat[(y&63)*64 + (x&63)]] : 0;
V_MarkRect (0, 0, SCREENWIDTH, SCREENHEIGHT);
M_WriteText(160 - M_StringWidth(title)/2, 10, title);
for (i = 0; i < sizeof(controls)/sizeof(controls[0]); i++)
{
case commercial:
V_DrawPatchDirect (0,0,0,W_CacheLumpName("HELP",PU_CACHE));
break;
case shareware:
case registered:
case retail:
V_DrawPatchDirect (0,0,0,W_CacheLumpName("HELP1",PU_CACHE));
break;
default:
break;
M_WriteText(24, 30 + i*11, controls[i].button);
M_WriteText(136, 30 + i*11, controls[i].action);
}
return;
M_WriteText(160 - M_StringWidth(automap)/2, 146, automap);
M_WriteText(160 - M_StringWidth(menus)/2, 158, menus);
}
@@ -1259,6 +1294,11 @@ int M_StringWidth(char* string)
for (i = 0;i < strlen(string);i++)
{
if (glyph_is_button(string[i]))
{
w += GLYPH_ADVANCE;
continue;
}
c = toupper(string[i]) - HU_FONTSTART;
if (c < 0 || c >= HU_FONTSIZE)
w += 4;
@@ -1320,6 +1360,17 @@ M_WriteText
cy += 12;
continue;
}
if (glyph_is_button(c))
{
if (cx+GLYPH_ADVANCE > SCREENWIDTH)
break;
glyph_draw(screens[0], SCREENWIDTH, SCREENHEIGHT, cx, cy, c, 1,
W_CacheLumpName("PLAYPAL",PU_CACHE));
V_MarkRect (cx, cy, GLYPH_ADVANCE, 8);
cx += GLYPH_ADVANCE;
continue;
}
c = toupper(c) - HU_FONTSTART;
if (c < 0 || c>= HU_FONTSIZE)
@@ -1866,9 +1917,14 @@ void M_Init (void)
// This is used because DOOM 2 had only one HELP
// page. I use CREDIT as second page now, but
// kept this hack for educational purposes.
MainMenu[readthis] = MainMenu[quitdoom];
MainDef.numitems--;
MainDef.y += 8;
// The entry stays when the WAD has its graphic,
// since the page now shows the controls.
if (W_CheckNumForName("M_RDTHIS") < 0)
{
MainMenu[readthis] = MainMenu[quitdoom];
MainDef.numitems--;
MainDef.y += 8;
}
NewDef.prevMenu = &MainDef;
ReadDef1.routine = M_DrawReadThis1;
ReadDef1.x = 330;
+3 -1
View File
@@ -79,7 +79,7 @@ plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *i
char range[32];
long status = 0;
curl_off_t length = -1;
const char *type = NULL;
const char *type = NULL, *final = NULL;
pthread_once(&once, start_curl);
if (!http)
@@ -119,9 +119,11 @@ plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *i
}
curl_easy_getinfo(http->easy, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &length);
curl_easy_getinfo(http->easy, CURLINFO_CONTENT_TYPE, &type);
curl_easy_getinfo(http->easy, CURLINFO_EFFECTIVE_URL, &final);
info->status = (int)status;
info->length = length;
snprintf(info->type, sizeof(info->type), "%s", type ? type : "");
snprintf(info->url, sizeof(info->url), "%s", final ? final : url);
return http;
}
+6 -2
View File
@@ -15,6 +15,7 @@
#include "games.h"
#include "import.h"
#include "platform.h"
#include "url.h"
#define CHUNK (256 * 1024)
#define PEEK (64 * 1024)
@@ -25,6 +26,7 @@ typedef struct
{
int remote;
const char *url;
char final_url[LINK_URL]; // where url led after redirects
plat_http_t *http;
int fd;
long long position, size;
@@ -96,12 +98,14 @@ static int open_remote(source_t *s, long long offset)
char error[128];
plat_http_close(s->http);
s->http = plat_http_get(s->url, (uint64_t)offset, &info, error, sizeof(error));
s->http = plat_http_get(s->final_url[0] ? s->final_url : s->url, (uint64_t)offset, &info, error,
sizeof(error));
if (!s->http)
{
fail("%s", error);
return -1;
}
snprintf(s->final_url, sizeof(s->final_url), "%s", info.url);
if (info.status == 206)
{
s->ranges = 1;
@@ -335,7 +339,7 @@ static int read_listing(source_t *s)
while (used < LISTING_LIMIT && (got = raw_read(s, html + used, LISTING_LIMIT - used)) > 0)
used += got;
html[used] = 0;
url_directory(base, sizeof(base), s->url);
url_directory(base, sizeof(base), s->final_url);
listing_parse(&listing, base, html, (size_t)used);
free(html);
pthread_mutex_lock(&lock);
+257 -34
View File
@@ -5,6 +5,7 @@
#include <unistd.h>
#include "games.h"
#include "glyphs.h"
#include "import.h"
#include "launcher.h"
#include "listing.h"
@@ -12,6 +13,8 @@
#include "screen.h"
#include "settings.h"
#include "test_plan.h"
#include "upload.h"
#include "url.h"
#define FRAME_US 16667
#define REPEAT_DELAY 18
@@ -23,6 +26,7 @@
#define BROWSE_ROWS 12
#define FOOTER_Y 188
#define MB (1024.0 * 1024.0)
#define SEND_LINES 4
typedef enum
{
@@ -31,6 +35,7 @@ typedef enum
VIEW_NOTICE,
VIEW_BROWSE,
VIEW_IMPORT,
VIEW_SEND,
} view_t;
static game_list_t games;
@@ -48,6 +53,12 @@ static int frame, typing, understood, auto_import;
static uint32_t held, pressed;
static int repeat_timer, input_locked = 1;
static const game_t *chosen;
static int send_ready, send_added;
static char send_lines[SEND_LINES][192];
static int send_line_ok[SEND_LINES], send_line_count;
static char send_archive[GAME_PATH + UPLOAD_NAME];
static void start_send(void);
static void read_input(void)
{
@@ -211,7 +222,7 @@ static void games_view(void)
screen_text_center(160, "Own Doom, Doom II, TNT or Plutonia?", INK_GRAY, 1);
screen_text_center(170, "Add them under Add games.", INK_GRAY, 1);
}
screen_text_center(FOOTER_Y, "X start O quit", INK_GRAY, 1);
screen_text_center(FOOTER_Y, GLYPH_CROSS " start " GLYPH_CIRCLE " quit", INK_GRAY, 1);
}
static void add_view(void)
@@ -230,14 +241,16 @@ static void add_view(void)
{
url_clean(settings.url, sizeof(settings.url), text);
settings_save(&settings);
add_row = 1;
add_row = 2;
}
}
}
else
{
move(&add_row, 3);
move(&add_row, 4);
if ((pressed & PAD_CROSS) && add_row == 0)
start_send();
else if ((pressed & PAD_CROSS) && add_row == 1)
{
if (plat_text_open("Link to a WAD, ZIP, 7Z or RAR file, or a folder",
settings.url[0] ? settings.url : "http://") == 0)
@@ -245,7 +258,7 @@ static void add_view(void)
else
set_status("The keyboard is not available", INK_RED);
}
else if ((pressed & PAD_CROSS) && add_row == 1 && settings.url[0])
else if ((pressed & PAD_CROSS) && add_row == 2 && settings.url[0])
{
if (settings.accepted)
{
@@ -261,30 +274,30 @@ static void add_view(void)
view = VIEW_NOTICE;
}
}
else if (((pressed & PAD_CROSS) && add_row == 2) || (pressed & PAD_CIRCLE))
else if (((pressed & PAD_CROSS) && add_row == 3) || (pressed & PAD_CIRCLE))
view = VIEW_GAMES;
}
screen_background();
screen_text_center(6, "Add games", INK_GOLD, 2);
y = screen_wrap(16, 28, SCREEN_WIDTH - 32,
"Copy game WADs, or ZIP, 7Z or RAR files with them inside, to this folder, "
"for example over FTP:", INK_RED);
screen_text_fit(16, y + 1, SCREEN_WIDTH - 32, plat_wad_folder(), INK_GOLD, 1);
y = screen_wrap(16, y + 12, SCREEN_WIDTH - 32, "Doom adds them the next time it starts.", INK_RED);
y = screen_wrap(16, y + 3, SCREEN_WIDTH - 32,
"DOOM.WAD DOOM2.WAD TNT.WAD PLUTONIA.WAD DOOMU.WAD FREEDOOM1.WAD FREEDOOM2.WAD",
INK_GRAY);
y = screen_wrap(16, y + 5, SCREEN_WIDTH - 32,
"Or download a WAD, ZIP, 7Z or RAR, or a folder your PC shares over HTTP:", INK_RED);
"DOOM.WAD, DOOM2.WAD, TNT.WAD, PLUTONIA.WAD or Freedoom, on their own or inside "
"ZIP, 7Z or RAR files.", INK_GRAY);
draw_row(30, y + 6, "Send from PC or phone", add_row == 0, 1);
y = screen_wrap(16, y + 6 + ROW + 6, SCREEN_WIDTH - 32,
"Or download a file, or a folder your PC shares over HTTP:", INK_RED);
snprintf(line, sizeof(line), "Link: %s", settings.url[0] ? settings.url : "not set");
if (add_row == 0)
if (add_row == 1)
screen_skull(16, y + 4, frame / SKULL_FRAMES);
screen_text_fit(30, y + 6, SCREEN_WIDTH - 46, line, add_row == 0 ? INK_WHITE : INK_RED, 1);
draw_row(30, y + 6 + ROW, "Download", add_row == 1, settings.url[0] != 0);
draw_row(30, y + 6 + ROW * 2, "Back", add_row == 2, 1);
screen_text_fit(30, y + 6, SCREEN_WIDTH - 46, line, add_row == 1 ? INK_WHITE : INK_RED, 1);
draw_row(30, y + 6 + ROW, "Download", add_row == 2, settings.url[0] != 0);
draw_row(30, y + 6 + ROW * 2, "Back", add_row == 3, 1);
y = screen_wrap(16, y + 6 + ROW * 3 + 4, SCREEN_WIDTH - 32,
"Or copy them over FTP or USB to this folder; Doom adds them when it starts:", INK_RED);
screen_text_fit(16, y + 1, SCREEN_WIDTH - 32, plat_wad_folder(), INK_GOLD, 1);
draw_status(FOOTER_Y - 11, 0);
screen_text_center(FOOTER_Y, add_row == 0 ? "X edit link O back" : "X select O back", INK_GRAY, 1);
screen_text_center(FOOTER_Y, add_row == 1 ? GLYPH_CROSS " edit link " GLYPH_CIRCLE " back"
: GLYPH_CROSS " select " GLYPH_CIRCLE " back", INK_GRAY, 1);
}
static void notice_view(void)
@@ -324,7 +337,7 @@ static void notice_view(void)
screen_box(42, y + 17, 4, 4, 1);
draw_row(40, y + 16 + ROW, "Continue", notice_row == 1, understood);
draw_row(40, y + 16 + ROW * 2, "Back", notice_row == 2, 1);
screen_text_center(FOOTER_Y, "X select O back", INK_GRAY, 1);
screen_text_center(FOOTER_Y, GLYPH_CROSS " select " GLYPH_CIRCLE " back", INK_GRAY, 1);
}
static int browse_files(void)
@@ -382,7 +395,7 @@ static void browse_view(void)
screen_text_fit(30, y, SCREEN_WIDTH - 46, line, i == browse_row ? INK_WHITE : INK_RED, 0);
}
draw_status(FOOTER_Y - 11, 0);
screen_text_center(FOOTER_Y, "X open O back", INK_GRAY, 1);
screen_text_center(FOOTER_Y, GLYPH_CROSS " open " GLYPH_CIRCLE " back", INK_GRAY, 1);
}
static void finish_import(const import_status_t *status)
@@ -410,6 +423,23 @@ static void finish_import(const import_status_t *status)
auto_import = 0;
}
static void draw_bar(int y, long long done, long long total)
{
char line[64];
screen_box(20, y, SCREEN_WIDTH - 40, 10, 0);
if (total > 0)
{
if (done > total)
done = total;
screen_box(22, y + 2, (int)((SCREEN_WIDTH - 44) * done / total), 6, 1);
snprintf(line, sizeof(line), "%.1f / %.1f MB", done / MB, total / MB);
}
else
snprintf(line, sizeof(line), "%.1f MB", done / MB);
screen_text(20, y + 16, line, INK_RED, 1);
}
static void draw_progress(const import_status_t *status)
{
char line[160];
@@ -422,17 +452,7 @@ static void draw_progress(const import_status_t *status)
snprintf(line, sizeof(line), "File %d of %d", status->source_index + 1, status->source_count);
screen_text(20, 64, line, INK_GRAY, 1);
}
screen_box(20, 80, SCREEN_WIDTH - 40, 10, 0);
if (status->total > 0)
{
long long done = status->done < status->total ? status->done : status->total;
screen_box(22, 82, (int)((SCREEN_WIDTH - 44) * done / status->total), 6, 1);
snprintf(line, sizeof(line), "%.1f / %.1f MB", status->done / MB, status->total / MB);
}
else
snprintf(line, sizeof(line), "%.1f MB", status->done / MB);
screen_text(20, 96, line, INK_RED, 1);
draw_bar(80, status->done, status->total);
if (status->current[0])
{
snprintf(line, sizeof(line), "Writing %s", status->current);
@@ -444,7 +464,7 @@ static void draw_progress(const import_status_t *status)
snprintf(line, sizeof(line), "Found: %s", status->found);
screen_wrap(20, y, SCREEN_WIDTH - 40, line, INK_GREEN);
}
screen_text_center(FOOTER_Y, "O cancel", INK_GRAY, 1);
screen_text_center(FOOTER_Y, GLYPH_CIRCLE " cancel", INK_GRAY, 1);
}
static void import_view(void)
@@ -471,6 +491,206 @@ static void import_view(void)
draw_progress(&status);
}
// Reports what happened to a sent file, on the screen and on the page.
static void note(const char *text, int ok)
{
plat_log("launcher: %s\n", text);
upload_note(text, ok);
if (send_line_count == SEND_LINES)
{
memmove(send_lines, send_lines + 1, sizeof(send_lines[0]) * (SEND_LINES - 1));
memmove(send_line_ok, send_line_ok + 1, sizeof(send_line_ok[0]) * (SEND_LINES - 1));
send_line_count--;
}
snprintf(send_lines[send_line_count], sizeof(send_lines[0]), "%s", text);
send_line_ok[send_line_count++] = ok;
}
static void start_send(void)
{
send_ready = upload_start(import_dir) == 0;
send_added = 0;
send_line_count = 0;
send_archive[0] = 0;
status_line[0] = 0;
view = VIEW_SEND;
}
static void leave_send(void)
{
if (send_archive[0])
{
import_cancel();
import_finish();
unlink(send_archive);
send_archive[0] = 0;
}
upload_stop();
if (send_added)
{
char text[64];
snprintf(text, sizeof(text), "Added %d game%s", send_added, send_added == 1 ? "" : "s");
set_status(text, INK_GREEN);
game_row = 0;
}
view = send_added ? VIEW_GAMES : VIEW_ADD;
}
static void add_uploaded_wad(const char *path, const char *name)
{
char target[GAME_PATH + 16], text[192];
const char *file = games_known_file(name);
const game_t *game;
if (!file || games_check_wad(path, NULL))
{
snprintf(text, sizeof(text), file ? "%s is not a valid game WAD" : "%s is not a game Doom knows",
name);
note(text, 0);
unlink(path);
return;
}
snprintf(target, sizeof(target), "%s/%s", import_dir, file);
unlink(target);
if (rename(path, target))
{
snprintf(text, sizeof(text), "Cannot store %s on the console", name);
note(text, 0);
unlink(path);
return;
}
games_scan(&games);
game = games_find(&games, file);
snprintf(text, sizeof(text), "Added %s", game ? game->title : file);
note(text, 1);
send_added++;
}
// Takes the next file the upload server finished. Game WADs are moved into place; archives go
// through the importer under their own name and are deleted once read.
static void take_upload(void)
{
char path[UPLOAD_PATH], name[UPLOAD_NAME], text[192], source[1][LINK_URL];
uint8_t magic[4] = {0};
int fd;
if (send_archive[0] || !upload_take(path, sizeof(path), name, sizeof(name)))
return;
if ((fd = open(path, O_RDONLY)) >= 0)
{
if (read(fd, magic, sizeof(magic)) != (ssize_t)sizeof(magic))
memset(magic, 0, sizeof(magic));
close(fd);
}
if (!memcmp(magic, "IWAD", 4))
add_uploaded_wad(path, name);
else if (!memcmp(magic, "PWAD", 4))
{
snprintf(text, sizeof(text), "%s is an add-on (pwad), not a game", name);
note(text, 0);
unlink(path);
}
else
{
snprintf(send_archive, sizeof(send_archive), "%s/%s", import_dir, name);
if (rename(path, send_archive))
{
snprintf(text, sizeof(text), "Cannot store %s on the console", name);
note(text, 0);
unlink(path);
send_archive[0] = 0;
return;
}
snprintf(source[0], LINK_URL, "%s", send_archive);
import_start(source, 1, import_dir);
}
}
static void check_archive_import(void)
{
import_status_t status;
char text[256];
const char *name;
if (!send_archive[0])
return;
import_status(&status);
if (status.state == IMPORT_RUNNING)
return;
import_finish();
name = strrchr(send_archive, '/') + 1;
if (status.found_count)
snprintf(text, sizeof(text), "Added %s from %s", status.found, name);
else if (status.message[0])
snprintf(text, sizeof(text), "%s", status.message);
else
snprintf(text, sizeof(text), "No game WADs found in %s", name);
note(text, status.found_count > 0);
send_added += status.found_count;
unlink(send_archive);
send_archive[0] = 0;
games_scan(&games);
}
static void send_view(void)
{
const char *address = upload_address();
upload_progress_t progress;
import_status_t status;
char line[UPLOAD_NAME + 32];
int y, x, size, i;
take_upload();
check_archive_import();
if (pressed & PAD_CIRCLE)
{
leave_send();
return;
}
upload_progress(&progress);
screen_background();
screen_text_center(6, "Send games", INK_GOLD, 2);
if (!send_ready || !address[0])
y = screen_wrap(16, 34, SCREEN_WIDTH - 32,
!send_ready ? "The console could not start receiving files. Go back and try again."
: "The console has no network address. Connect it to your network, "
"then open this screen again.",
INK_RED) + 8;
else
{
size = screen_qr(16, 30, address, 3);
x = 16 + size + 12;
y = screen_wrap(x, 32, SCREEN_WIDTH - x - 12,
"On a PC or phone on the same network, open this address or scan the code:", INK_RED);
screen_text_fit(x, y + 3, SCREEN_WIDTH - x - 12, address, INK_GOLD, 0);
y = screen_wrap(x, y + 18, SCREEN_WIDTH - x - 12,
"Then drop your WAD, ZIP, 7Z or RAR files on the page.", INK_RED);
y = y > 30 + size ? y + 8 : 30 + size + 8;
}
if (progress.receiving)
{
snprintf(line, sizeof(line), "Receiving %s", progress.name);
screen_text_fit(20, y, SCREEN_WIDTH - 40, line, INK_WHITE, 0);
draw_bar(y + 12, progress.done, progress.total);
}
else if (send_archive[0])
{
import_status(&status);
snprintf(line, sizeof(line), "Adding games from %s", strrchr(send_archive, '/') + 1);
screen_text_fit(20, y, SCREEN_WIDTH - 40, line, INK_WHITE, 0);
draw_bar(y + 12, status.done, status.total);
}
else
for (i = 0; i < send_line_count; i++)
screen_text_fit(20, y + i * LINE_HEIGHT, SCREEN_WIDTH - 40, send_lines[i],
send_line_ok[i] ? INK_GREEN : INK_RED, 0);
screen_text_center(FOOTER_Y, progress.receiving || send_archive[0] ? GLYPH_CIRCLE " stop and go back"
: GLYPH_CIRCLE " back", INK_GRAY, 1);
}
static const game_t *preselected(void)
{
const game_t *game = games_find(&games, test_plan_game());
@@ -507,7 +727,7 @@ const game_t *launcher_choose(void)
while (!chosen)
{
read_input();
if (view != VIEW_IMPORT && pressed)
if (view != VIEW_IMPORT && view != VIEW_SEND && pressed)
status_line[0] = 0;
switch (view)
{
@@ -526,6 +746,9 @@ const game_t *launcher_choose(void)
case VIEW_IMPORT:
import_view();
break;
case VIEW_SEND:
send_view();
break;
}
if (!chosen)
present();
+1 -96
View File
@@ -5,102 +5,7 @@
#include "games.h"
#include "listing.h"
static const char *path_start(const char *url)
{
const char *scheme = strstr(url, "://");
const char *slash;
if (!scheme)
return url;
slash = strchr(scheme + 3, '/');
return slash ? slash : url + strlen(url);
}
void url_clean(char *out, size_t size, const char *typed)
{
size_t used = 0;
const char *end;
while (*typed == ' ')
typed++;
end = typed + strlen(typed);
while (end > typed && end[-1] == ' ')
end--;
if (!strstr(typed, "://"))
used = (size_t)snprintf(out, size, "http://");
for (; typed < end && used + 4 < size; typed++)
{
if (*typed == ' ')
used += (size_t)snprintf(out + used, size - used, "%%20");
else
out[used++] = *typed;
}
out[used < size ? used : size - 1] = 0;
}
void url_directory(char *out, size_t size, const char *url)
{
const char *path = path_start(url);
const char *last = strrchr(path, '/');
snprintf(out, size, "%s", url);
if (!*path || (last && last[1] && !strchr(last + 1, '.')))
strncat(out, "/", size - strlen(out) - 1);
}
void url_resolve(char *out, size_t size, const char *base, const char *href)
{
const char *path = path_start(base);
const char *last;
if (strstr(href, "://"))
snprintf(out, size, "%s", href);
else if (href[0] == '/' && href[1] == '/')
snprintf(out, size, "%.*s%s", (int)(strstr(base, "//") - base), base, href);
else if (href[0] == '/')
snprintf(out, size, "%.*s%s", (int)(path - base), base, href);
else
{
last = strrchr(path, '/');
if (last)
snprintf(out, size, "%.*s%s", (int)(last + 1 - base), base, href);
else
snprintf(out, size, "%s/%s", base, href);
}
}
static int hex(int c)
{
if (c >= '0' && c <= '9')
return c - '0';
c = tolower(c);
return c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
}
void url_file_name(char *out, size_t size, const char *url)
{
const char *path = path_start(url);
const char *end = path + strcspn(path, "?#");
const char *start;
size_t used = 0;
if (end > path && end[-1] == '/')
end--;
for (start = end; start > path && start[-1] != '/'; start--)
;
while (start < end && used + 1 < size)
{
if (*start == '%' && end - start >= 3 && hex(start[1]) >= 0 && hex(start[2]) >= 0)
{
out[used++] = (char)(hex(start[1]) * 16 + hex(start[2]));
start += 3;
}
else
out[used++] = *start++;
}
out[used] = 0;
}
#include "url.h"
static void decode_entities(char *text)
{
-4
View File
@@ -21,10 +21,6 @@ typedef struct
int count;
} listing_t;
void url_clean(char *out, size_t size, const char *typed);
void url_directory(char *out, size_t size, const char *url);
void url_resolve(char *out, size_t size, const char *base, const char *href);
void url_file_name(char *out, size_t size, const char *url);
void listing_parse(listing_t *listing, const char *base, const char *html, size_t length);
#endif
+52 -2
View File
@@ -7,7 +7,9 @@
#include <strings.h>
#include <unistd.h>
#include "glyphs.h"
#include "platform.h"
#include "qrcodegen.h"
#include "screen.h"
#define FONT_FIRST '!'
@@ -18,6 +20,8 @@
#define BACKGROUND_SHADE 29
#define COLORMAP_SIZE (34 * 256)
#define MAX_PATCH_HEIGHT 256
#define QR_VERSION_MAX 5
#define QR_QUIET 2
static uint8_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT];
static uint32_t palette[256];
@@ -26,7 +30,7 @@ static uint8_t shade[256];
static uint8_t inks[INKS][256];
static uint8_t *font[FONT_COUNT];
static uint8_t *title, *logo, *skulls[2];
static uint8_t outline_color, fill_color;
static uint8_t outline_color, fill_color, paper_color, ink_color;
static uint32_t le32(const uint8_t *p)
{
@@ -100,6 +104,8 @@ static void build_inks(void)
}
outline_color = nearest(150, 150, 150);
fill_color = nearest(190, 30, 30);
paper_color = nearest(255, 255, 255);
ink_color = nearest(0, 0, 0);
}
int screen_init(const char *wad_path)
@@ -244,7 +250,10 @@ int screen_text_width(const char *text, int scale)
int width = 0;
for (; *text && *text != '\n'; text++)
width += (glyph(*text) ? s16(glyph(*text)) : SPACE_WIDTH) * scale;
if (glyph_is_button(*text))
width += GLYPH_ADVANCE * scale;
else
width += (glyph(*text) ? s16(glyph(*text)) : SPACE_WIDTH) * scale;
return width;
}
@@ -254,6 +263,12 @@ void screen_text(int x, int y, const char *text, ink_t ink, int scale)
{
const uint8_t *g = glyph(*text);
if (glyph_is_button(*text))
{
glyph_draw(pixels, SCREEN_WIDTH, SCREEN_HEIGHT, x, y, *text, scale, rgb);
x += GLYPH_ADVANCE * scale;
continue;
}
if (g)
draw_patch(x, y, g, inks[ink], scale, 1);
x += (g ? s16(g) : SPACE_WIDTH) * scale;
@@ -324,6 +339,41 @@ int screen_wrap(int x, int y, int width, const char *text, ink_t ink)
return y;
}
static void fill(int x, int y, int width, int height, uint8_t color)
{
int i, j;
for (j = y; j < y + height; j++)
for (i = x; i < x + width; i++)
if (i >= 0 && i < SCREEN_WIDTH && j >= 0 && j < SCREEN_HEIGHT)
pixels[j * SCREEN_WIDTH + i] = color;
}
int screen_qr(int x, int y, const char *text, int module)
{
static char encoded[128];
static uint8_t code[qrcodegen_BUFFER_LEN_FOR_VERSION(QR_VERSION_MAX)];
static int ready;
uint8_t scratch[qrcodegen_BUFFER_LEN_FOR_VERSION(QR_VERSION_MAX)];
int size, i, j;
if (strcmp(encoded, text))
{
snprintf(encoded, sizeof(encoded), "%s", text);
ready = qrcodegen_encodeText(text, scratch, code, qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN,
QR_VERSION_MAX, qrcodegen_Mask_AUTO, true);
}
if (!ready)
return 0;
size = qrcodegen_getSize(code);
fill(x, y, (size + 2 * QR_QUIET) * module, (size + 2 * QR_QUIET) * module, paper_color);
for (j = 0; j < size; j++)
for (i = 0; i < size; i++)
if (qrcodegen_getModule(code, i, j))
fill(x + (QR_QUIET + i) * module, y + (QR_QUIET + j) * module, module, module, ink_color);
return (size + 2 * QR_QUIET) * module;
}
void screen_present(void)
{
plat_video_present(pixels, palette);
+3
View File
@@ -25,6 +25,9 @@ void screen_text(int x, int y, const char *text, ink_t ink, int scale);
void screen_text_center(int y, const char *text, ink_t ink, int scale);
void screen_text_fit(int x, int y, int width, const char *text, ink_t ink, int keep_end);
int screen_wrap(int x, int y, int width, const char *text, ink_t ink);
// Draws text as a QR code with a white border, each module a square of the given size; returns its
// width in pixels, or 0 when the text does not fit.
int screen_qr(int x, int y, const char *text, int module);
void screen_present(void);
#endif
+549
View File
@@ -0,0 +1,549 @@
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include "platform.h"
#include "upload.h"
#include "upload_page.h"
#include "url.h"
#define HEAD_LIMIT 8192
#define CHUNK (256 * 1024)
#define MAX_FINISHED 16
#define MAX_NOTES 32
#define NOTE_TEXT 160
#define MAX_LEFTOVERS 32
#define RECEIVE_TIMEOUT_S 1
#define SEND_TIMEOUT_S 5
#define IDLE_LIMIT_S 30
#ifdef MSG_NOSIGNAL
#define SEND_FLAGS MSG_NOSIGNAL
#else
#define SEND_FLAGS 0
#endif
typedef struct
{
char path[UPLOAD_PATH];
char name[UPLOAD_NAME];
} finished_t;
typedef struct
{
int id, ok;
char text[NOTE_TEXT];
} note_t;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_t server;
static int listener = -1, running, port;
static volatile int stopping;
static char folder[UPLOAD_PATH];
static char address[64];
static upload_progress_t progress;
static finished_t finished[MAX_FINISHED];
static int finished_count;
static note_t notes[MAX_NOTES];
static int note_count, next_note = 1;
// Only the server thread touches these.
static char head[HEAD_LIMIT + 1];
static uint8_t chunk[CHUNK];
static int send_all(int fd, const void *data, size_t size)
{
const uint8_t *p = data;
while (size)
{
ssize_t sent = send(fd, p, size, SEND_FLAGS);
if (sent <= 0)
return -1;
p += sent;
size -= (size_t)sent;
}
return 0;
}
static void respond(int fd, const char *status, const char *type, const void *body, size_t length)
{
char top[256];
int n = snprintf(top, sizeof(top),
"HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %lu\r\n"
"Cache-Control: no-store\r\nConnection: close\r\n\r\n",
status, type, (unsigned long)length);
if (send_all(fd, top, (size_t)n) == 0 && length)
send_all(fd, body, length);
}
static void respond_text(int fd, const char *status, const char *text)
{
respond(fd, status, "text/plain; charset=utf-8", text, strlen(text));
}
// recv that gives up when the server stops or the browser stays quiet for too long.
static ssize_t receive(int fd, void *buffer, size_t size)
{
int idle = 0;
for (;;)
{
ssize_t got = recv(fd, buffer, size, 0);
if (got >= 0)
return got;
if ((errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) || stopping ||
++idle >= IDLE_LIMIT_S / RECEIVE_TIMEOUT_S)
return -1;
}
}
// Reads the request head into head[]; returns its length including the blank line, with the
// number of bytes read so far (head plus the start of the body) in *used.
static int read_head(int fd, int *used)
{
int length = 0;
while (length < HEAD_LIMIT)
{
ssize_t got = receive(fd, head + length, (size_t)(HEAD_LIMIT - length));
char *end;
if (got <= 0)
return -1;
length += (int)got;
head[length] = 0;
if ((end = strstr(head, "\r\n\r\n")))
{
*used = length;
return (int)(end - head) + 4;
}
}
return -1;
}
static const char *header_value(const char *name)
{
size_t length = strlen(name);
const char *line = strstr(head, "\r\n");
while (line && line[2] != '\r')
{
line += 2;
if (!strncasecmp(line, name, length) && line[length] == ':')
{
const char *value = line + length + 1;
while (*value == ' ' || *value == '\t')
value++;
return value;
}
line = strstr(line, "\r\n");
}
return NULL;
}
// Turns the request path after /upload/ into a safe file name: decoded, lower case, with anything
// that could name a folder replaced, and one of the kinds the importer reads.
static int clean_name(char *out, size_t size, const char *path)
{
char decoded[UPLOAD_NAME * 3];
const char *dot;
size_t n;
url_file_name(decoded, sizeof(decoded), path);
for (n = 0; decoded[n] && n < size - 1; n++)
{
unsigned char c = (unsigned char)decoded[n];
out[n] = isalnum(c) || strchr(" .-_()[]+", c) ? (char)tolower(c) : '_';
}
out[n] = 0;
dot = strrchr(out, '.');
return out[0] != '.' && dot &&
(!strcmp(dot, ".wad") || !strcmp(dot, ".zip") || !strcmp(dot, ".7z") ||
!strcmp(dot, ".rar"))
? 0
: -1;
}
static int write_all(int fd, const uint8_t *data, size_t size)
{
while (size)
{
ssize_t written = write(fd, data, size);
if (written <= 0)
return -1;
data += written;
size -= (size_t)written;
}
return 0;
}
static void set_progress(int receiving, const char *name, long long done, long long total)
{
pthread_mutex_lock(&lock);
progress.receiving = receiving;
snprintf(progress.name, sizeof(progress.name), "%s", name);
progress.done = done;
progress.total = total;
pthread_mutex_unlock(&lock);
}
static void put_file(int fd, const char *target, int head_length, int used)
{
char name[UPLOAD_NAME], part[UPLOAD_PATH];
const char *value = header_value("Content-Length");
long long length = value ? strtoll(value, NULL, 10) : -1, done = 0;
int out, full, lost = 0, error = 0;
if (clean_name(name, sizeof(name), target))
{
respond_text(fd, "415 Unsupported Media Type", "Send WAD, ZIP, 7Z or RAR files.");
return;
}
if (length < 0)
{
respond_text(fd, "411 Length Required", "The upload has no length.");
return;
}
pthread_mutex_lock(&lock);
full = finished_count == MAX_FINISHED;
pthread_mutex_unlock(&lock);
if (full)
{
respond_text(fd, "503 Service Unavailable", "The console is still busy with earlier files.");
return;
}
snprintf(part, sizeof(part), "%s/%s.upload", folder, name);
if ((out = open(part, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0)
{
plat_log("upload: cannot write %s (error %d)\n", part, errno);
respond_text(fd, "500 Internal Server Error", "The console cannot write the file.");
return;
}
set_progress(1, name, 0, length);
plat_log("upload: receiving %s, %lld bytes\n", name, length);
if (used > head_length)
{
long long extra = used - head_length < length ? used - head_length : length;
if (write_all(out, (const uint8_t *)head + head_length, (size_t)extra))
error = errno ? errno : EIO;
done = extra;
}
while (!error && !lost && done < length)
{
ssize_t got = receive(fd, chunk, length - done < CHUNK ? (size_t)(length - done) : CHUNK);
if (got <= 0)
lost = 1;
else if (write_all(out, chunk, (size_t)got))
error = errno ? errno : EIO;
else
{
done += got;
set_progress(1, name, done, length);
}
}
close(out);
set_progress(0, "", 0, 0);
if (lost || error)
{
unlink(part);
plat_log("upload: %s stopped at %lld of %lld bytes (error %d)\n", name, done, length, error);
if (error == ENOSPC)
respond_text(fd, "507 Insufficient Storage", "Not enough space on the console.");
else if (error)
respond_text(fd, "500 Internal Server Error", "Writing on the console failed.");
return;
}
pthread_mutex_lock(&lock);
snprintf(finished[finished_count].path, UPLOAD_PATH, "%s", part);
snprintf(finished[finished_count].name, UPLOAD_NAME, "%s", name);
finished_count++;
pthread_mutex_unlock(&lock);
plat_log("upload: received %s\n", name);
respond_text(fd, "200 OK", "Received.");
}
static void send_status(int fd, const char *query)
{
static char json[MAX_NOTES * (NOTE_TEXT + 40) + 16];
const char *after_text = strstr(query, "after=");
int after = after_text ? atoi(after_text + 6) : 0, n, i, first = 1;
n = snprintf(json, sizeof(json), "{\"notes\":[");
pthread_mutex_lock(&lock);
for (i = 0; i < note_count; i++)
if (notes[i].id > after)
{
n += snprintf(json + n, sizeof(json) - (size_t)n, "%s{\"id\":%d,\"ok\":%s,\"text\":\"%s\"}",
first ? "" : ",", notes[i].id, notes[i].ok ? "true" : "false", notes[i].text);
first = 0;
}
pthread_mutex_unlock(&lock);
n += snprintf(json + n, sizeof(json) - (size_t)n, "]}");
respond(fd, "200 OK", "application/json", json, (size_t)n);
}
static void handle(int fd)
{
char method[8], target[UPLOAD_NAME * 3 + 16];
int used, head_length = read_head(fd, &used);
size_t i = 0, j = 0;
if (head_length < 0)
return;
for (; head[i] && head[i] != ' ' && i < sizeof(method) - 1; i++)
method[i] = head[i];
method[i] = 0;
while (head[i] == ' ')
i++;
while (head[i] && head[i] != ' ' && head[i] != '\r' && j < sizeof(target) - 1)
target[j++] = head[i++];
target[j] = 0;
if (!strcmp(method, "GET") && (!strcmp(target, "/") || !strcmp(target, "/index.html")))
respond(fd, "200 OK", "text/html; charset=utf-8", upload_page, sizeof(upload_page));
else if (!strcmp(method, "GET") && !strncmp(target, "/status", 7))
send_status(fd, target + 7);
else if (!strcmp(method, "PUT") && !strncmp(target, "/upload/", 8))
put_file(fd, target + 8, head_length, used);
else
respond_text(fd, "404 Not Found", "Not found.");
}
static void *serve(void *arg)
{
struct timeval receive_timeout = {RECEIVE_TIMEOUT_S, 0}, send_timeout = {SEND_TIMEOUT_S, 0};
(void)arg;
while (!stopping)
{
int client = accept(listener, NULL, NULL);
if (client < 0)
{
if (!stopping)
plat_sleep_us(100000);
continue;
}
if (!stopping)
{
#ifdef SO_NOSIGPIPE
int yes = 1;
setsockopt(client, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes));
#endif
setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &receive_timeout, sizeof(receive_timeout));
setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout));
handle(client);
}
close(client);
}
return NULL;
}
// Connecting a UDP socket sends nothing; it only picks the route, and with it the local address.
static void find_address(void)
{
struct sockaddr_in remote, local;
socklen_t size = sizeof(local);
int fd = socket(AF_INET, SOCK_DGRAM, 0);
address[0] = 0;
if (fd < 0)
return;
memset(&remote, 0, sizeof(remote));
remote.sin_family = AF_INET;
remote.sin_port = htons(53);
remote.sin_addr.s_addr = htonl(0x08080808);
if (!connect(fd, (struct sockaddr *)&remote, sizeof(remote)) &&
!getsockname(fd, (struct sockaddr *)&local, &size))
{
uint32_t ip = ntohl(local.sin_addr.s_addr);
if (ip)
snprintf(address, sizeof(address), "http://%u.%u.%u.%u:%d/", ip >> 24, (ip >> 16) & 255,
(ip >> 8) & 255, ip & 255, port);
}
close(fd);
}
typedef struct
{
char names[MAX_LEFTOVERS][UPLOAD_NAME + 8];
int count;
} leftovers_t;
static void collect_leftover(const char *name, void *user)
{
leftovers_t *leftovers = user;
size_t length = strlen(name);
if (length > 7 && !strcmp(name + length - 7, ".upload") && leftovers->count < MAX_LEFTOVERS &&
length < sizeof(leftovers->names[0]))
strcpy(leftovers->names[leftovers->count++], name);
}
// Removes partial files a crash or a power loss left behind.
static void remove_leftovers(void)
{
leftovers_t leftovers;
char path[UPLOAD_PATH + UPLOAD_NAME + 8];
int i;
leftovers.count = 0;
plat_list_dir(folder, collect_leftover, &leftovers);
for (i = 0; i < leftovers.count; i++)
{
snprintf(path, sizeof(path), "%s/%s", folder, leftovers.names[i]);
unlink(path);
}
}
static int listen_on(int candidate)
{
struct sockaddr_in local;
int fd = socket(AF_INET, SOCK_STREAM, 0), yes = 1;
if (fd < 0)
return -1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons((uint16_t)candidate);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) || listen(fd, 4))
{
plat_log("upload: port %d refused (error %d)\n", candidate, errno);
close(fd);
return -1;
}
return fd;
}
int upload_start(const char *dir)
{
if (running)
return 0;
snprintf(folder, sizeof(folder), "%s", dir);
remove_leftovers();
note_count = 0;
finished_count = 0;
memset(&progress, 0, sizeof(progress));
for (port = UPLOAD_PORT; port < UPLOAD_PORT + UPLOAD_PORT_TRIES; port++)
if ((listener = listen_on(port)) >= 0)
break;
if (listener < 0)
return -1;
find_address();
stopping = 0;
if (pthread_create(&server, NULL, serve, NULL))
{
plat_log("upload: no server thread\n");
close(listener);
listener = -1;
return -1;
}
running = 1;
plat_log("upload: serving %s into %s\n", address[0] ? address : "without an address", folder);
return 0;
}
void upload_stop(void)
{
struct sockaddr_in self;
int fd, i;
if (!running)
return;
stopping = 1;
// accept() does not return on its own; a connection to ourselves wakes the thread up.
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) >= 0)
{
memset(&self, 0, sizeof(self));
self.sin_family = AF_INET;
self.sin_port = htons((uint16_t)port);
self.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
connect(fd, (struct sockaddr *)&self, sizeof(self));
close(fd);
}
pthread_join(server, NULL);
close(listener);
listener = -1;
running = 0;
// Files that arrived but were never taken are dropped.
for (i = 0; i < finished_count; i++)
unlink(finished[i].path);
finished_count = 0;
plat_log("upload: stopped\n");
}
const char *upload_address(void)
{
return address;
}
void upload_progress(upload_progress_t *out)
{
pthread_mutex_lock(&lock);
*out = progress;
pthread_mutex_unlock(&lock);
}
int upload_take(char *path, int path_size, char *name, int name_size)
{
int taken = 0;
pthread_mutex_lock(&lock);
if (finished_count)
{
snprintf(path, (size_t)path_size, "%s", finished[0].path);
snprintf(name, (size_t)name_size, "%s", finished[0].name);
memmove(finished, finished + 1, (size_t)--finished_count * sizeof(*finished));
taken = 1;
}
pthread_mutex_unlock(&lock);
return taken;
}
void upload_note(const char *text, int ok)
{
note_t *note;
int i;
pthread_mutex_lock(&lock);
if (note_count == MAX_NOTES)
memmove(notes, notes + 1, (size_t)--note_count * sizeof(*notes));
note = &notes[note_count++];
note->id = next_note++;
note->ok = ok;
// Kept safe to place inside a JSON string as it is.
for (i = 0; text[i] && i < NOTE_TEXT - 1; i++)
note->text[i] = text[i] == '"' || text[i] == '\\' ? '\''
: (unsigned char)text[i] < ' ' ? ' ' : text[i];
note->text[i] = 0;
pthread_mutex_unlock(&lock);
}
+35
View File
@@ -0,0 +1,35 @@
#ifndef UPLOAD_H
#define UPLOAD_H
// A small HTTP server for sending game files from a browser on the same network: it serves an
// upload page, stores each file it receives in a folder as <name>.upload and hands the finished
// ones to the caller. It runs only between upload_start and upload_stop.
// The first port tried. A title's sandbox refuses some ports (8666 and 50000 on FW 13.00) and
// others may be taken, so the next few are tried after it.
#define UPLOAD_PORT 9666
#define UPLOAD_PORT_TRIES 8
#define UPLOAD_NAME 64
#define UPLOAD_PATH 512
typedef struct
{
int receiving;
char name[UPLOAD_NAME];
long long done, total;
} upload_progress_t;
// Starts listening; 0 on success. The address is "" when the console has no network address.
int upload_start(const char *dir);
void upload_stop(void);
const char *upload_address(void);
void upload_progress(upload_progress_t *progress);
// Hands over the oldest finished upload: its path and the name it was sent with. 1 when there was one.
int upload_take(char *path, int path_size, char *name, int name_size);
// Adds a line to the log the page shows, for what the console did with a file.
void upload_note(const char *text, int ok);
#endif
+221
View File
@@ -0,0 +1,221 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>Send games to DOOM</title>
<style>
:root {
--bg: #121010;
--panel: #1d1817;
--line: #3a2d2a;
--text: #eee6e2;
--muted: #a99a95;
--red: #d6362b;
--green: #58c26e;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
main { max-width: 720px; margin: 0 auto; padding: 32px 16px 48px; }
h1 { margin: 0 0 8px; font-size: 28px; letter-spacing: 0.02em; }
h1 span { color: var(--red); }
h2 { margin: 32px 0 8px; font-size: 18px; }
p { margin: 0 0 16px; color: var(--muted); }
#drop {
display: block;
padding: 40px 16px;
border: 2px dashed var(--line);
border-radius: 12px;
background: var(--panel);
text-align: center;
cursor: pointer;
}
#drop.over { border-color: var(--red); }
#drop strong { display: block; font-size: 20px; }
#drop span { color: var(--muted); }
#drop input { display: none; }
ul { list-style: none; margin: 0; padding: 0; }
#files li {
display: grid;
grid-template-columns: 1fr auto;
gap: 4px 16px;
padding: 12px 0;
border-bottom: 1px solid var(--line);
}
#files .name { overflow-wrap: anywhere; }
#files .state { color: var(--muted); text-align: right; }
#files .ok .state { color: var(--green); }
#files .bad .state { color: var(--red); }
progress { grid-column: 1 / -1; width: 100%; height: 6px; accent-color: var(--red); }
#log li { padding: 4px 0; }
#log .ok::before { content: "\2713 "; color: var(--green); }
#log .bad::before { content: "\2717 "; color: var(--red); }
#offline {
display: none;
margin: 16px 0;
padding: 12px 16px;
border-radius: 8px;
background: #3a1714;
}
body.offline #offline { display: block; }
</style>
</head>
<body>
<main>
<h1>Send games to <span>DOOM</span></h1>
<p>Drop the game files you own: DOOM.WAD, DOOM2.WAD, TNT.WAD, PLUTONIA.WAD or Freedoom, or ZIP,
7Z and RAR files with them inside. Whole folders work too; other files are left out.</p>
<div id="offline">The console is not listening. On the PS5, open DOOM, then Add games, then Send
from PC or phone.</div>
<label id="drop">
<input type="file" multiple accept=".wad,.zip,.7z,.rar">
<strong>Drop files or folders here</strong>
<span>or click to choose files</span>
</label>
<ul id="files"></ul>
<h2>On the console</h2>
<ul id="log"><li id="empty">Nothing yet.</li></ul>
</main>
<script>
const accepted = /\.(wad|zip|7z|rar)$/i;
const drop = document.getElementById("drop");
const picker = drop.querySelector("input");
const files = document.getElementById("files");
const log = document.getElementById("log");
const queue = [];
let busy = false;
let lastNote = 0;
function addLog(text, ok) {
document.getElementById("empty")?.remove();
const item = document.createElement("li");
item.className = ok ? "ok" : "bad";
item.textContent = text;
log.appendChild(item);
}
function row(name, state, className) {
const item = document.createElement("li");
item.innerHTML = '<span class="name"></span><span class="state"></span>';
item.querySelector(".name").textContent = name;
item.querySelector(".state").textContent = state;
item.className = className;
files.appendChild(item);
return item;
}
function add(list) {
const left = [...list].filter((file) => !accepted.test(file.name));
for (const file of list) {
if (!accepted.test(file.name)) continue;
const item = row(file.name, "Waiting", "");
item.appendChild(Object.assign(document.createElement("progress"), { max: 1, value: 0 }));
queue.push({ file, item });
}
// A dropped folder can hold many other files; past a few they are summed up in one line.
if (left.length > 3) {
row(`${left.length} other files`, "Left out: not WAD, ZIP, 7Z or RAR", "bad");
} else {
for (const file of left) row(file.name, "Left out: not WAD, ZIP, 7Z or RAR", "bad");
}
next();
}
function next() {
if (busy || !queue.length) return;
busy = true;
const { file, item } = queue.shift();
const state = item.querySelector(".state");
const bar = item.querySelector("progress");
const started = performance.now();
const request = new XMLHttpRequest();
const finish = (ok, text) => {
item.className = ok ? "ok" : "bad";
state.textContent = text;
if (ok) bar.value = 1;
busy = false;
poll();
next();
};
request.open("PUT", "/upload/" + encodeURIComponent(file.name));
request.upload.onprogress = (event) => {
if (!event.lengthComputable) return;
const seconds = (performance.now() - started) / 1000;
const speed = seconds > 0 ? event.loaded / seconds / 1048576 : 0;
bar.value = event.loaded / event.total;
state.textContent = `${Math.floor(100 * event.loaded / event.total)}%, ${speed.toFixed(1)} MB/s`;
};
request.onload = () => finish(request.status === 200, request.status === 200 ? "Sent" : request.responseText || "Failed");
request.onerror = () => finish(false, "Lost the connection to the console");
state.textContent = "Sending";
request.send(file);
}
// Folders arrive as entries that have to be walked; the entries must be taken before the
// first await, while the drop event is still being handled.
async function dropped(items) {
const entries = [...items].map((item) => item.webkitGetAsEntry && item.webkitGetAsEntry()).filter(Boolean);
const found = [];
const walk = async (entry) => {
if (entry.isFile) {
found.push(await new Promise((resolve, reject) => entry.file(resolve, reject)));
} else if (entry.isDirectory) {
const reader = entry.createReader();
for (;;) {
const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
if (!batch.length) break;
for (const child of batch) await walk(child);
}
}
};
for (const entry of entries) await walk(entry);
return found;
}
async function poll() {
// The console answers one request at a time, so it is asked only between uploads.
if (busy) return;
try {
const response = await fetch("/status?after=" + lastNote, { cache: "no-store" });
const status = await response.json();
for (const note of status.notes) {
lastNote = note.id;
addLog(note.text, note.ok);
}
document.body.classList.remove("offline");
} catch {
document.body.classList.add("offline");
}
}
// The whole page takes drops, so a file let go next to the box is not opened by the browser.
window.addEventListener("dragover", (event) => {
event.preventDefault();
drop.classList.add("over");
});
window.addEventListener("dragleave", (event) => {
if (!event.relatedTarget) drop.classList.remove("over");
});
window.addEventListener("drop", async (event) => {
event.preventDefault();
drop.classList.remove("over");
const items = event.dataTransfer.items;
add(items && items.length && items[0].webkitGetAsEntry ? await dropped(items) : event.dataTransfer.files);
});
picker.addEventListener("change", () => {
add(picker.files);
picker.value = "";
});
setInterval(poll, 1500);
poll();
</script>
</body>
</html>
+1
View File
@@ -47,6 +47,7 @@ typedef struct
int status;
int64_t length;
char type[64];
char url[768]; // where the request ended up after redirects
} plat_http_info_t;
typedef struct plat_http plat_http_t;
+101
View File
@@ -0,0 +1,101 @@
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include "url.h"
static const char *path_start(const char *url)
{
const char *scheme = strstr(url, "://");
const char *slash;
if (!scheme)
return url;
slash = strchr(scheme + 3, '/');
return slash ? slash : url + strlen(url);
}
void url_clean(char *out, size_t size, const char *typed)
{
size_t used = 0;
const char *end;
while (*typed == ' ')
typed++;
end = typed + strlen(typed);
while (end > typed && end[-1] == ' ')
end--;
if (!strstr(typed, "://"))
used = (size_t)snprintf(out, size, "http://");
for (; typed < end && used + 4 < size; typed++)
{
if (*typed == ' ')
used += (size_t)snprintf(out + used, size - used, "%%20");
else
out[used++] = *typed;
}
out[used < size ? used : size - 1] = 0;
}
void url_directory(char *out, size_t size, const char *url)
{
const char *path = path_start(url);
const char *last = strrchr(path, '/');
snprintf(out, size, "%s", url);
if (!*path || (last && last[1] && !strchr(last + 1, '.')))
strncat(out, "/", size - strlen(out) - 1);
}
void url_resolve(char *out, size_t size, const char *base, const char *href)
{
const char *path = path_start(base);
const char *last;
if (strstr(href, "://"))
snprintf(out, size, "%s", href);
else if (href[0] == '/' && href[1] == '/')
snprintf(out, size, "%.*s%s", (int)(strstr(base, "//") - base), base, href);
else if (href[0] == '/')
snprintf(out, size, "%.*s%s", (int)(path - base), base, href);
else
{
last = strrchr(path, '/');
if (last)
snprintf(out, size, "%.*s%s", (int)(last + 1 - base), base, href);
else
snprintf(out, size, "%s/%s", base, href);
}
}
static int hex(int c)
{
if (c >= '0' && c <= '9')
return c - '0';
c = tolower(c);
return c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
}
void url_file_name(char *out, size_t size, const char *url)
{
const char *path = path_start(url);
const char *end = path + strcspn(path, "?#");
const char *start;
size_t used = 0;
if (end > path && end[-1] == '/')
end--;
for (start = end; start > path && start[-1] != '/'; start--)
;
while (start < end && used + 1 < size)
{
if (*start == '%' && end - start >= 3 && hex(start[1]) >= 0 && hex(start[2]) >= 0)
{
out[used++] = (char)(hex(start[1]) * 16 + hex(start[2]));
start += 3;
}
else
out[used++] = *start++;
}
out[used] = 0;
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef URL_H
#define URL_H
#include <stddef.h>
// Adds http:// when no scheme was typed and encodes spaces.
void url_clean(char *out, size_t size, const char *typed);
// The folder a link points at, ending in a slash.
void url_directory(char *out, size_t size, const char *url);
// A link from a page or a Location header, made absolute against the address it came from.
void url_resolve(char *out, size_t size, const char *base, const char *href);
// The last part of the path, percent-decoded.
void url_file_name(char *out, size_t size, const char *url);
#endif
+76
View File
@@ -0,0 +1,76 @@
#include "glyphs.h"
#define SIZE 7
typedef struct
{
uint8_t rows[SIZE];
uint8_t r, g, b;
} glyph_t;
// One bit per pixel, most significant of the low 7 bits on the left.
static const glyph_t glyphs[] = {
// Cross
{{0x63, 0x77, 0x3e, 0x1c, 0x3e, 0x77, 0x63}, 124, 178, 232},
// Circle
{{0x1c, 0x3e, 0x63, 0x63, 0x63, 0x3e, 0x1c}, 255, 102, 102},
// Square
{{0x7f, 0x7f, 0x63, 0x63, 0x63, 0x7f, 0x7f}, 228, 153, 214},
// Triangle
{{0x08, 0x1c, 0x1c, 0x36, 0x36, 0x7f, 0x7f}, 64, 224, 176},
};
int glyph_is_button(int c)
{
return c >= 1 && c <= (int)(sizeof(glyphs) / sizeof(glyphs[0]));
}
static uint8_t nearest(const uint8_t *rgb, int r, int g, int b)
{
int best = 0, best_distance = 1 << 30, i;
for (i = 0; i < 256; i++)
{
int dr = rgb[i * 3] - r, dg = rgb[i * 3 + 1] - g, db = rgb[i * 3 + 2] - b;
int distance = dr * dr + dg * dg + db * db;
if (distance < best_distance)
{
best = i;
best_distance = distance;
}
}
return (uint8_t)best;
}
static void plot(uint8_t *screen, int width, int height, int x, int y, int scale, uint8_t color)
{
int i, j;
for (j = 0; j < scale; j++)
for (i = 0; i < scale; i++)
if (x + i >= 0 && x + i < width && y + j >= 0 && y + j < height)
screen[(y + j) * width + x + i] = color;
}
void glyph_draw(uint8_t *screen, int width, int height, int x, int y, int c, int scale,
const uint8_t *rgb)
{
const glyph_t *glyph;
uint8_t color, shadow;
int pass, row, column;
if (!glyph_is_button(c))
return;
glyph = &glyphs[c - 1];
color = nearest(rgb, glyph->r, glyph->g, glyph->b);
shadow = nearest(rgb, 0, 0, 0);
// A shadow one pixel down and right keeps the symbol readable over any background.
for (pass = 0; pass < 2; pass++)
for (row = 0; row < SIZE; row++)
for (column = 0; column < SIZE; column++)
if (glyph->rows[row] & (0x40 >> column))
plot(screen, width, height, x + (column + !pass) * scale,
y + (row + !pass) * scale, scale, pass ? color : shadow);
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef GLYPHS_H
#define GLYPHS_H
#include <stdint.h>
// DualSense face buttons inside text: each is one control character that the text drawers
// (Doom's menu font and the launcher) draw as the button symbol in its PlayStation colour.
#define GLYPH_CROSS "\001"
#define GLYPH_CIRCLE "\002"
#define GLYPH_SQUARE "\003"
#define GLYPH_TRIANGLE "\004"
// Horizontal advance of a glyph at scale 1, matching the 7-pixel-high menu font.
#define GLYPH_ADVANCE 10
int glyph_is_button(int c);
// Draws button glyph c at (x, y) into an 8-bit screen, picking the colours from rgb (768 bytes).
void glyph_draw(uint8_t *screen, int width, int height, int x, int y, int c, int scale,
const uint8_t *rgb);
#endif
+55 -18
View File
@@ -5,6 +5,7 @@
#include "platform.h"
#include "sce.h"
#include "url.h"
#define NET_POOL (1024 * 1024)
#define SSL_POOL (304 * 1024)
@@ -13,6 +14,7 @@
#define METHOD_GET 0
#define HEADER_OVERWRITE 0
#define TIMEOUT_US 15000000
#define MAX_REDIRECTS 8
#define VERIFY_FLAGS (0x01 | 0x04 | 0x08 | 0x10 | 0x20 | 0x80)
struct plat_http
@@ -39,7 +41,7 @@ static int start_http(void)
template_id = sceHttpCreateTemplate(http, "DOOM-PS5", HTTP_1_1, 0);
if (template_id < 0)
return template_id;
if ((result = sceHttpSetAutoRedirect(template_id, 1)) < 0 ||
if ((result = sceHttpSetAutoRedirect(template_id, 0)) < 0 ||
(result = sceHttpSetResolveTimeOut(template_id, TIMEOUT_US)) < 0 ||
(result = sceHttpSetConnectTimeOut(template_id, TIMEOUT_US)) < 0 ||
(result = sceHttpSetSendTimeOut(template_id, TIMEOUT_US)) < 0 ||
@@ -65,13 +67,47 @@ static void header_value(int request, const char *name, char *out, size_t size)
out[value_size] = 0;
}
static int send_get(plat_http_t *http, const char *url, uint64_t offset, int *status)
{
char range[40];
int result;
http->connection = result = sceHttpCreateConnectionWithURL(template_id, url, 1);
if (result >= 0)
http->request = result = sceHttpCreateRequestWithURL(http->connection, METHOD_GET, url, 0);
if (result >= 0 && offset)
{
snprintf(range, sizeof(range), "bytes=%llu-", (unsigned long long)offset);
result = sceHttpAddRequestHeader(http->request, "Range", range, HEADER_OVERWRITE);
}
if (result >= 0)
result = sceHttpSendRequest(http->request, NULL, 0);
if (result >= 0)
result = sceHttpGetStatusCode(http->request, status);
return result;
}
static void drop_request(plat_http_t *http)
{
if (http->request >= 0)
sceHttpDeleteRequest(http->request);
if (http->connection >= 0)
sceHttpDeleteConnection(http->connection);
http->request = http->connection = -1;
}
static int is_redirect(int status)
{
return status == 301 || status == 302 || status == 303 || status == 307 || status == 308;
}
plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *info,
char *error, int error_size)
{
plat_http_t *http;
char range[40];
char location[sizeof(info->url)], next[sizeof(info->url)];
uint64_t length = 0;
int status = 0, has_length = -1, result;
int status = 0, has_length = -1, redirects, result;
pthread_mutex_lock(&lock);
result = template_id < 0 ? start_http() : 0;
@@ -85,19 +121,23 @@ plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *i
http = calloc(1, sizeof(*http));
if (!http)
return NULL;
http->request = -1;
http->connection = result = sceHttpCreateConnectionWithURL(template_id, url, 1);
if (result >= 0)
http->request = result = sceHttpCreateRequestWithURL(http->connection, METHOD_GET, url, 0);
if (result >= 0 && offset)
http->request = http->connection = -1;
// Redirects are followed here rather than by sceHttp, to learn the address the request ends
// up at: a folder listing's relative links are relative to that one.
snprintf(info->url, sizeof(info->url), "%s", url);
for (redirects = 0;; redirects++)
{
snprintf(range, sizeof(range), "bytes=%llu-", (unsigned long long)offset);
result = sceHttpAddRequestHeader(http->request, "Range", range, HEADER_OVERWRITE);
result = send_get(http, info->url, offset, &status);
if (result < 0 || !is_redirect(status) || redirects == MAX_REDIRECTS)
break;
header_value(http->request, "Location", location, sizeof(location));
if (!location[0])
break;
url_resolve(next, sizeof(next), info->url, location);
snprintf(info->url, sizeof(info->url), "%s", next);
drop_request(http);
}
if (result >= 0)
result = sceHttpSendRequest(http->request, NULL, 0);
if (result >= 0)
result = sceHttpGetStatusCode(http->request, &status);
if (result < 0)
{
snprintf(error, error_size, "connection failed (0x%08x)", (unsigned)result);
@@ -130,9 +170,6 @@ void plat_http_close(plat_http_t *http)
{
if (!http)
return;
if (http->request >= 0)
sceHttpDeleteRequest(http->request);
if (http->connection >= 0)
sceHttpDeleteConnection(http->connection);
drop_request(http);
free(http);
}
+1 -1
View File
@@ -1,4 +1,4 @@
text http://192.168.0.10:8666/
input 30:DOWN:2;60:CROSS:2;90:CROSS:2;130:CROSS:2;160:CROSS:2;190:DOWN:2;220:CROSS:2;300:CROSS:2
input 30:UP:2;60:CROSS:2;75:DOWN:2;90:CROSS:2;130:CROSS:2;160:CROSS:2;190:DOWN:2;220:CROSS:2;300:CROSS:2
capture 25,80,120,180,250,400,1200,2400,3600,4800,6000,7100
frames 7200
+3
View File
@@ -0,0 +1,3 @@
input 30:UP:2;60:CROSS:2;90:CROSS:2;3300:CIRCLE:2
capture 150,3200,3450
frames 3600
+14 -12
View File
@@ -1,8 +1,9 @@
ARCHIVE_DEPS := .deps/archive
ARCHIVE_STAMP := $(ARCHIVE_DEPS)/zlib-1.3.2/.complete
LA_DIR := $(ARCHIVE_DEPS)/libarchive-3.8.9/libarchive
XZ_DIR := $(ARCHIVE_DEPS)/xz-5.8.4/src/liblzma
ZL_DIR := $(ARCHIVE_DEPS)/zlib-1.3.2
THIRD_DEPS := .deps/third-party
THIRD_STAMP := $(THIRD_DEPS)/QR-Code-generator-1.8.0/.complete
LA_DIR := $(THIRD_DEPS)/libarchive-3.8.9/libarchive
XZ_DIR := $(THIRD_DEPS)/xz-5.8.4/src/liblzma
ZL_DIR := $(THIRD_DEPS)/zlib-1.3.2
QR_DIR := $(THIRD_DEPS)/QR-Code-generator-1.8.0/c
LA_SRC := $(addprefix $(LA_DIR)/, archive_read.c archive_read_set_options.c archive_options.c \
archive_util.c archive_string.c archive_string_sprintf.c archive_entry.c archive_check_magic.c \
@@ -21,21 +22,22 @@ XZ_SRC := $(addprefix $(XZ_DIR)/, common/common.c common/alone_decoder.c common/
simple/simple_decoder.c simple/x86.c simple/arm.c simple/armthumb.c simple/arm64.c \
simple/ia64.c simple/powerpc.c simple/sparc.c simple/riscv.c)
ZL_SRC := $(addprefix $(ZL_DIR)/, adler32.c crc32.c inffast.c inflate.c inftrees.c zutil.c)
THIRD_SRC := $(LA_SRC) $(XZ_SRC) $(ZL_SRC)
THIRD_SRC := $(LA_SRC) $(XZ_SRC) $(ZL_SRC) $(QR_DIR)/qrcodegen.c
ARCHIVE_INCLUDES := -I$(LA_DIR)
THIRD_INCLUDES := -I$(LA_DIR) -I$(QR_DIR)
LA_FLAGS := -DHAVE_CONFIG_H -Ithird_party/libarchive -I$(LA_DIR) -I$(XZ_DIR)/api -I$(ZL_DIR)
XZ_FLAGS := -DHAVE_CONFIG_H -Ithird_party/xz -I$(XZ_DIR)/api -I$(XZ_DIR)/common -I$(XZ_DIR)/check \
-I$(XZ_DIR)/lz -I$(XZ_DIR)/rangecoder -I$(XZ_DIR)/lzma -I$(XZ_DIR)/delta -I$(XZ_DIR)/simple \
-I$(XZ_DIR)/../common
ZL_FLAGS := -I$(ZL_DIR)
QR_FLAGS := -I$(QR_DIR)
THIRD_FLAGS := -std=gnu11 -O2 -w -fno-strict-aliasing -DNDEBUG -Dmalloc=import_malloc \
-Dcalloc=import_calloc -Drealloc=import_realloc -Dfree=import_free -Dstrdup=import_strdup
third_obj = $(patsubst $(ARCHIVE_DEPS)/%.c,$(1)/third_party/%.o,$(THIRD_SRC))
third_flags = $(if $(findstring /libarchive-,$(1)),$(LA_FLAGS),$(if $(findstring /xz-,$(1)),$(XZ_FLAGS),$(ZL_FLAGS)))
third_obj = $(patsubst $(THIRD_DEPS)/%.c,$(1)/third_party/%.o,$(THIRD_SRC))
third_flags = $(if $(findstring /libarchive-,$(1)),$(LA_FLAGS),$(if $(findstring /xz-,$(1)),$(XZ_FLAGS),$(if $(findstring /QR-Code-,$(1)),$(QR_FLAGS),$(ZL_FLAGS))))
$(THIRD_SRC): | $(ARCHIVE_STAMP)
$(THIRD_SRC): | $(THIRD_STAMP)
$(ARCHIVE_STAMP):
bash tools/fetch-archive-libs.sh $(ARCHIVE_DEPS)
$(THIRD_STAMP):
bash tools/fetch-third-party.sh $(THIRD_DEPS)
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env python3
"""Writes a C header that holds a file's bytes as a static array: embed-file.py <file> <name> <header>."""
import sys
source, name, header = sys.argv[1:]
data = open(source, "rb").read()
with open(header, "w", newline="\n") as out:
out.write(f"/* Generated from {source} by tools/embed-file.py. */\n")
out.write(f"static const unsigned char {name}[] = {{\n")
for i in range(0, len(data), 20):
out.write(" " + ", ".join(str(b) for b in data[i:i + 20]) + ",\n")
out.write("};\n")
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# Downloads the archive libraries the importer is built from (libarchive, xz's liblzma, zlib) and checks them.
# Downloads the libraries built into the launcher and checks them: libarchive, xz's liblzma and zlib
# for the importer, qrcodegen for the address shown on the upload screen.
set -euo pipefail
out=${1:-.deps/archive}
out=${1:-.deps/third-party}
mkdir -p "$out"
fetch() {
@@ -37,3 +38,4 @@ fetch xz-5.8.4 0014c7886930454fe8bd4228665b51af55eeae560ea135c9c4cd33f55b2591d9
fetch zlib-1.3.2 bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 \
https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz \
https://zlib.net/zlib-1.3.2.tar.gz
fetch QR-Code-generator-1.8.0 2ec0a4d33d6f521c942eeaf473d42d5fe139abcfa57d2beffe10c5cf7d34ae60 https://github.com/nayuki/QR-Code-generator/archive/refs/tags/v1.8.0.tar.gz https://codeload.github.com/nayuki/QR-Code-generator/tar.gz/refs/tags/v1.8.0
+62
View File
@@ -0,0 +1,62 @@
"""Sends files to DOOM's Send games screen, the way its web page does, and prints what the console did.
uv run --no-project python tools/send.py http://192.168.0.90:9666/ FILE...
The screen must be open on the console (Add games, then Send from PC or phone); it shows the address.
Used with test/console-send.cfg for hands-free tests.
"""
import argparse
import http.client
import json
import time
import urllib.parse
from pathlib import Path
def request(host, port, method, path, body=None):
conn = http.client.HTTPConnection(host, port, timeout=120)
conn.request(method, path, body=body)
response = conn.getresponse()
reply = response.read()
conn.close()
return response.status, reply.decode(errors="replace")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("address", help="the address the screen shows")
ap.add_argument("files", nargs="+", type=Path)
ap.add_argument("--wait", type=int, default=60, help="seconds to wait for the screen to answer")
args = ap.parse_args()
url = urllib.parse.urlsplit(args.address)
deadline = time.time() + args.wait
while True:
try:
request(url.hostname, url.port, "GET", "/")
break
except OSError:
if time.time() > deadline:
raise SystemExit(f"nothing answers at {args.address}")
time.sleep(1)
for path in args.files:
data = path.read_bytes()
started = time.time()
status, reply = request(url.hostname, url.port, "PUT", "/upload/" + urllib.parse.quote(path.name), data)
seconds = max(time.time() - started, 0.001)
print(f"{path.name}: {status} {reply} ({len(data) / 1048576:.1f} MB, {len(data) / 1048576 / seconds:.1f} MB/s)")
# The console reports what it did with each file once it has read it.
last, quiet = 0, 0
while quiet < 5:
notes = json.loads(request(url.hostname, url.port, "GET", f"/status?after={last}")[1])["notes"]
for note in notes:
print(("done: " if note["ok"] else "failed: ") + note["text"])
last = note["id"]
quiet = 0 if notes else quiet + 1
time.sleep(1)
if __name__ == "__main__":
main()