mirror of
https://github.com/lowbit/ps5-doom.git
synced 2026-10-06 05:00:25 +02:00
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.
This commit is contained in:
1 parent
652cfd6007
commit
93814c3e95
16 files changed
+1263
-69
No files matched your search
+22
-10
@@ -6,7 +6,8 @@ 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
|
||||
|
||||
@@ -14,16 +15,16 @@ liblzma, zlib) and the build tools listed at the end.
|
||||
| --- | --- |
|
||||
| `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, 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, and the importer (HTTP or local file, WAD or ZIP/7Z/RAR) |
|
||||
| `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/platform.h` | The platform API: time, log, files and folder listing, video present, pad, rumble, audio output, HTTP, text input |
|
||||
| `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
|
||||
@@ -88,7 +89,14 @@ and maps the chosen game to Doom's game mode, mission and language. The launcher
|
||||
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 +130,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 +145,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.
|
||||
@@ -203,7 +214,8 @@ id's keyboard help (`HELP1`, `HELP`); DOOM II keeps its *Read This!* entry whene
|
||||
`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).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+251
-31
@@ -13,6 +13,7 @@
|
||||
#include "screen.h"
|
||||
#include "settings.h"
|
||||
#include "test_plan.h"
|
||||
#include "upload.h"
|
||||
|
||||
#define FRAME_US 16667
|
||||
#define REPEAT_DELAY 18
|
||||
@@ -24,6 +25,7 @@
|
||||
#define BROWSE_ROWS 12
|
||||
#define FOOTER_Y 188
|
||||
#define MB (1024.0 * 1024.0)
|
||||
#define SEND_LINES 4
|
||||
|
||||
typedef enum
|
||||
{
|
||||
@@ -32,6 +34,7 @@ typedef enum
|
||||
VIEW_NOTICE,
|
||||
VIEW_BROWSE,
|
||||
VIEW_IMPORT,
|
||||
VIEW_SEND,
|
||||
} view_t;
|
||||
|
||||
static game_list_t games;
|
||||
@@ -49,6 +52,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)
|
||||
{
|
||||
@@ -231,14 +240,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)
|
||||
@@ -246,7 +257,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)
|
||||
{
|
||||
@@ -262,31 +273,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 ? GLYPH_CROSS " edit link " GLYPH_CIRCLE " back"
|
||||
: GLYPH_CROSS " select " GLYPH_CIRCLE " 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)
|
||||
@@ -412,6 +422,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];
|
||||
@@ -424,17 +451,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);
|
||||
@@ -473,6 +490,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());
|
||||
@@ -509,7 +726,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)
|
||||
{
|
||||
@@ -528,6 +745,9 @@ const game_t *launcher_choose(void)
|
||||
case VIEW_IMPORT:
|
||||
import_view();
|
||||
break;
|
||||
case VIEW_SEND:
|
||||
send_view();
|
||||
break;
|
||||
}
|
||||
if (!chosen)
|
||||
present();
|
||||
|
||||
+41
-1
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "glyphs.h"
|
||||
#include "platform.h"
|
||||
#include "qrcodegen.h"
|
||||
#include "screen.h"
|
||||
|
||||
#define FONT_FIRST '!'
|
||||
@@ -19,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];
|
||||
@@ -27,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)
|
||||
{
|
||||
@@ -101,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)
|
||||
@@ -334,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);
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,568 @@
|
||||
#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"
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
static int hex(int c)
|
||||
{
|
||||
return c >= '0' && c <= '9' ? c - '0'
|
||||
: c >= 'a' && c <= 'f' ? c - 'a' + 10
|
||||
: c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1;
|
||||
}
|
||||
|
||||
// Turns the request path after /upload/ into a safe file name: decoded, without folders, lower
|
||||
// case, 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 *base, *dot;
|
||||
size_t n = 0;
|
||||
|
||||
while (*path && *path != '?' && n < sizeof(decoded) - 1)
|
||||
{
|
||||
if (*path == '%' && hex(path[1]) >= 0 && hex(path[2]) >= 0)
|
||||
{
|
||||
decoded[n++] = (char)(hex(path[1]) * 16 + hex(path[2]));
|
||||
path += 3;
|
||||
}
|
||||
else
|
||||
decoded[n++] = *path++;
|
||||
}
|
||||
decoded[n] = 0;
|
||||
base = decoded + n;
|
||||
while (base > decoded && base[-1] != '/' && base[-1] != '\\')
|
||||
base--;
|
||||
for (n = 0; base[n] && n < size - 1; n++)
|
||||
{
|
||||
unsigned char c = (unsigned char)base[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 = ¬es[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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,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
|
||||
@@ -0,0 +1,3 @@
|
||||
input 30:UP:2;60:CROSS:2;90:CROSS:2;3300:CIRCLE:2
|
||||
capture 150,3200,3450
|
||||
frames 3600
|
||||
Vendored
+14
-12
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user