mirror of
https://github.com/lowbit/ps5-doom.git
synced 2026-10-06 09:00:26 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c6f272478 | ||
|
|
9bd75e2494 | ||
|
|
1ac421aa82 | ||
|
|
93814c3e95 | ||
|
|
652cfd6007 | ||
|
|
a62377c80e |
No files matched your search
+35
-14
@@ -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
|
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
|
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
|
## Layout
|
||||||
|
|
||||||
| Path | What |
|
| Path | What |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `src/doom/` | id's original game code with the 64-bit and portability fixes listed below |
|
| `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/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/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/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/` | 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/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 |
|
| `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 |
|
| `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) |
|
| `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
|
## 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
|
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:
|
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
|
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`;
|
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
|
- 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;
|
`/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
|
(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
|
archive goes through libarchive. libarchive seeks with HTTP range requests; a server without them
|
||||||
is read through or re-read from the start instead;
|
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
|
- 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;
|
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`).
|
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`,
|
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
|
`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`,
|
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)
|
## 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
|
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
|
use explicit `/download0/...` paths; output is captured by pointing `stdout`/`stderr` at a pipe
|
||||||
(`fdopen`), and klog wants one line per `sceKernelDebugOutText`.
|
(`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
|
- `sceHttp` with `sceSsl` works in the sandboxed title, plain and HTTPS, including `Range` request
|
||||||
headers added with `sceHttpAddRequestHeader` (answers 206). `sceHttpReadData` returns only when
|
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.
|
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.
|
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
|
## Changes to id's code
|
||||||
|
|
||||||
- 64-bit: pointers stored in `int` (config string defaults, save game pointer fields, the
|
- 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,
|
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
|
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
|
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)
|
## 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`,
|
`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
|
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/`.
|
`.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)
|
- [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
|
[zlib](https://zlib.net/) 1.3.2 and [qrcodegen](https://www.nayuki.io/page/qr-code-generator-library)
|
||||||
`.deps/archive` and compiled into the game for both targets.
|
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).
|
- 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
|
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 \
|
DOOM_WARNINGS := -Wall -Wno-unused-const-variable -Wno-unused-but-set-variable \
|
||||||
-Wno-unused-variable -Wno-logical-not-parentheses -Wno-missing-braces
|
-Wno-unused-variable -Wno-logical-not-parentheses -Wno-missing-braces
|
||||||
OWN_WARNINGS := -Wall -Wextra -Wno-unused-parameter
|
OWN_WARNINGS := -Wall -Wextra -Wno-unused-parameter
|
||||||
@@ -52,11 +53,17 @@ $(HOST_DIR)/%.o: src/%.c
|
|||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
$(HOST_CC) -std=gnu11 $(HOST_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -c $< -o $@
|
$(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 $@)
|
@mkdir -p $(dir $@)
|
||||||
$(HOST_CC) $(THIRD_FLAGS) -g -MMD -MP $(call third_flags,$<) -c $< -o $@
|
$(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)
|
tools: $(NATIVE_TOOL)
|
||||||
|
|
||||||
@@ -95,11 +102,11 @@ $(PS5_DIR)/%.o: src/%.c $(SDK)/target/lib/libkernel.so
|
|||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
$(PS5_CC) -std=gnu11 $(PS5_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -I$(PS5_GEN) -c $< -o $@
|
$(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 $@)
|
@mkdir -p $(dir $@)
|
||||||
$(PS5_CC) $(THIRD_FLAGS) -MMD -MP $(call third_flags,$<) -c $< -o $@
|
$(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
|
$(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 \
|
$(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 $(LA_DIR)/../COPYING $(APP)/licenses/libarchive.txt
|
||||||
cp $(XZ_DIR)/../../COPYING.0BSD $(APP)/licenses/liblzma.txt
|
cp $(XZ_DIR)/../../COPYING.0BSD $(APP)/licenses/liblzma.txt
|
||||||
cp $(ZL_DIR)/LICENSE $(APP)/licenses/zlib.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 $@
|
||||||
$(NATIVE_TOOL) self --inspect --file $(APP)/sce_module/libc.prx
|
$(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
|
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.
|
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
|
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
|
DOOM from your own game files, which you can send from a PC or phone browser, download from a
|
||||||
your PC, including straight out of ZIP, 7Z and RAR archives.
|
link or copy to the console, including straight out of ZIP, 7Z and RAR archives.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -38,18 +38,23 @@ The files from the original releases and from the 2024 *DOOM + DOOM II* re-relea
|
|||||||
|
|
||||||
### Adding your games
|
### 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.
|
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,
|
- **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.
|
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
|
- **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
|
`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.
|
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
|
To download from your own PC instead, open a terminal in the folder that holds the files and run a
|
||||||
folder that holds them and run a small web server, then use the address it prints with your PC's
|
small web server, then use the address it prints with your PC's local IP (for example
|
||||||
local IP (for example `http://192.168.1.20:8000/`):
|
`http://192.168.1.20:8000/`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m http.server 8000
|
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) | |
|
| Triangle / touchpad | Automap (Square: follow mode) | |
|
||||||
| Options | Menu | Close menu |
|
| 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.
|
the GPU, should a firmware ever disagree with the GPU path.
|
||||||
|
|
||||||
## Building and internals
|
## Building and internals
|
||||||
|
|||||||
@@ -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 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 |
|
| 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`) |
|
| [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`) |
|
| [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`) |
|
| `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
|
## Used to build
|
||||||
|
|||||||
@@ -2,22 +2,25 @@
|
|||||||
|
|
||||||
## Add-ons (PWADs)
|
## 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
|
| WAD | Maps | Needs beyond loading the file |
|
||||||
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
|
| `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 |
|
||||||
2024 re-release: classic-format DOOM II maps, so they should run once loading works. Check
|
| `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 |
|
||||||
their level order and secret exits.
|
| `sigil.wad` (*SIGIL*) | E5M1-9 | `UMAPINFO`, episode 5 (menu entry `M_EPI5`, `SKY5`, intermission), `DEHACKED` (par times only) |
|
||||||
- [ ] *SIGIL* and *SIGIL II* (`sigil.wad`, `sigil2.wad`): the re-release versions are episodes 5 and
|
| `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` |
|
||||||
6, which need episode, intermission and sky support beyond episode 4.
|
| `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 |
|
||||||
- [ ] *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.
|
|
||||||
|
|
||||||
## 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
|
- [ ] Pick an add-on in the launcher under its game and start it with `-file`; accept PWADs in the
|
||||||
relative links are resolved against the typed address, not the address the redirect lands
|
importer and the send screen into their own folder; give each add-on its own saves.
|
||||||
on, so the files cannot be opened. Learn the final address (`sceHttpSetRedirectCallback`,
|
- [ ] A `UMAPINFO` reader (level names, music, skies, progression, intermission and end screens,
|
||||||
`CURLINFO_EFFECTIVE_URL` on the PC) and resolve against it. Short links straight to a file
|
episode entries). Covers *No Rest for the Living* and the *Master Levels*, and is the base for
|
||||||
already work.
|
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
@@ -10,7 +10,7 @@
|
|||||||
"conceptId": "99666",
|
"conceptId": "99666",
|
||||||
"contentBadgeType": 1,
|
"contentBadgeType": 1,
|
||||||
"contentId": "UP9000-PPSA99666_00-DOOMNATIVEPS5000",
|
"contentId": "UP9000-PPSA99666_00-DOOMNATIVEPS5000",
|
||||||
"contentVersion": "01.000.001",
|
"contentVersion": "01.001.000",
|
||||||
"downloadDataSize": 256,
|
"downloadDataSize": 256,
|
||||||
"gameIntent": {
|
"gameIntent": {
|
||||||
"permittedIntents": [
|
"permittedIntents": [
|
||||||
|
|||||||
+3
-3
@@ -36,8 +36,8 @@
|
|||||||
//
|
//
|
||||||
// M_Menu.C
|
// M_Menu.C
|
||||||
//
|
//
|
||||||
#define PRESSKEY "press a key."
|
#define PRESSKEY "press "GLYPH_CROSS"."
|
||||||
#define PRESSYN "press y or n."
|
#define PRESSYN GLYPH_CROSS" yes "GLYPH_CIRCLE" no"
|
||||||
#define QUITMSG "are you sure you want to\nquit this great game?"
|
#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 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
|
#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 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 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 DETAILHI "High detail"
|
||||||
#define DETAILLO "Low detail"
|
#define DETAILLO "Low detail"
|
||||||
|
|||||||
+3
-3
@@ -32,8 +32,8 @@
|
|||||||
//
|
//
|
||||||
// M_Menu.C
|
// M_Menu.C
|
||||||
//
|
//
|
||||||
#define PRESSKEY "APPUYEZ SUR UNE TOUCHE."
|
#define PRESSKEY "APPUYEZ SUR "GLYPH_CROSS"."
|
||||||
#define PRESSYN "APPUYEZ SUR Y OU N"
|
#define PRESSYN GLYPH_CROSS" OUI "GLYPH_CIRCLE" NON"
|
||||||
#define QUITMSG "VOUS VOULEZ VRAIMENT\nQUITTER CE SUPER JEU?"
|
#define QUITMSG "VOUS VOULEZ VRAIMENT\nQUITTER CE SUPER JEU?"
|
||||||
#define LOADNET "VOUS NE POUVEZ PAS CHARGER\nUN JEU EN RESEAU!\n\n"PRESSKEY
|
#define LOADNET "VOUS NE POUVEZ PAS CHARGER\nUN JEU EN RESEAU!\n\n"PRESSKEY
|
||||||
#define QLOADNET "CHARGEMENT RAPIDE INTERDIT EN RESEAU!\n\n"PRESSKEY
|
#define QLOADNET "CHARGEMENT RAPIDE INTERDIT EN RESEAU!\n\n"PRESSKEY
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"RESEAU!\n\n"PRESSKEY
|
"RESEAU!\n\n"PRESSKEY
|
||||||
#define ENDGAME "VOUS VOULEZ VRAIMENT METTRE FIN AU JEU?\n\n"PRESSYN
|
#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 DETAILHI "GRAPHISMES MAXIMUM "
|
||||||
#define DETAILLO "GRAPHISMES MINIMUM "
|
#define DETAILLO "GRAPHISMES MINIMUM "
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
// All important printed strings.
|
// All important printed strings.
|
||||||
// Language selection (message strings).
|
// Language selection (message strings).
|
||||||
// Use -DFRENCH etc.
|
// Use -DFRENCH etc.
|
||||||
|
// Prompts name DualSense buttons through the glyph characters.
|
||||||
|
#include "glyphs.h"
|
||||||
|
|
||||||
#ifdef FRENCH
|
#ifdef FRENCH
|
||||||
#include "d_french.h"
|
#include "d_french.h"
|
||||||
|
|||||||
+71
-15
@@ -748,23 +748,58 @@ void M_QuickLoad(void)
|
|||||||
// Read This Menus
|
// Read This Menus
|
||||||
// Had a "quick hack to fix romero bug"
|
// 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)
|
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;
|
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:
|
M_WriteText(24, 30 + i*11, controls[i].button);
|
||||||
V_DrawPatchDirect (0,0,0,W_CacheLumpName("HELP",PU_CACHE));
|
M_WriteText(136, 30 + i*11, controls[i].action);
|
||||||
break;
|
|
||||||
case shareware:
|
|
||||||
case registered:
|
|
||||||
case retail:
|
|
||||||
V_DrawPatchDirect (0,0,0,W_CacheLumpName("HELP1",PU_CACHE));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
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++)
|
for (i = 0;i < strlen(string);i++)
|
||||||
{
|
{
|
||||||
|
if (glyph_is_button(string[i]))
|
||||||
|
{
|
||||||
|
w += GLYPH_ADVANCE;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
c = toupper(string[i]) - HU_FONTSTART;
|
c = toupper(string[i]) - HU_FONTSTART;
|
||||||
if (c < 0 || c >= HU_FONTSIZE)
|
if (c < 0 || c >= HU_FONTSIZE)
|
||||||
w += 4;
|
w += 4;
|
||||||
@@ -1321,6 +1361,17 @@ M_WriteText
|
|||||||
continue;
|
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;
|
c = toupper(c) - HU_FONTSTART;
|
||||||
if (c < 0 || c>= HU_FONTSIZE)
|
if (c < 0 || c>= HU_FONTSIZE)
|
||||||
{
|
{
|
||||||
@@ -1866,9 +1917,14 @@ void M_Init (void)
|
|||||||
// This is used because DOOM 2 had only one HELP
|
// This is used because DOOM 2 had only one HELP
|
||||||
// page. I use CREDIT as second page now, but
|
// page. I use CREDIT as second page now, but
|
||||||
// kept this hack for educational purposes.
|
// kept this hack for educational purposes.
|
||||||
MainMenu[readthis] = MainMenu[quitdoom];
|
// The entry stays when the WAD has its graphic,
|
||||||
MainDef.numitems--;
|
// since the page now shows the controls.
|
||||||
MainDef.y += 8;
|
if (W_CheckNumForName("M_RDTHIS") < 0)
|
||||||
|
{
|
||||||
|
MainMenu[readthis] = MainMenu[quitdoom];
|
||||||
|
MainDef.numitems--;
|
||||||
|
MainDef.y += 8;
|
||||||
|
}
|
||||||
NewDef.prevMenu = &MainDef;
|
NewDef.prevMenu = &MainDef;
|
||||||
ReadDef1.routine = M_DrawReadThis1;
|
ReadDef1.routine = M_DrawReadThis1;
|
||||||
ReadDef1.x = 330;
|
ReadDef1.x = 330;
|
||||||
|
|||||||
+3
-1
@@ -79,7 +79,7 @@ plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *i
|
|||||||
char range[32];
|
char range[32];
|
||||||
long status = 0;
|
long status = 0;
|
||||||
curl_off_t length = -1;
|
curl_off_t length = -1;
|
||||||
const char *type = NULL;
|
const char *type = NULL, *final = NULL;
|
||||||
|
|
||||||
pthread_once(&once, start_curl);
|
pthread_once(&once, start_curl);
|
||||||
if (!http)
|
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_LENGTH_DOWNLOAD_T, &length);
|
||||||
curl_easy_getinfo(http->easy, CURLINFO_CONTENT_TYPE, &type);
|
curl_easy_getinfo(http->easy, CURLINFO_CONTENT_TYPE, &type);
|
||||||
|
curl_easy_getinfo(http->easy, CURLINFO_EFFECTIVE_URL, &final);
|
||||||
info->status = (int)status;
|
info->status = (int)status;
|
||||||
info->length = length;
|
info->length = length;
|
||||||
snprintf(info->type, sizeof(info->type), "%s", type ? type : "");
|
snprintf(info->type, sizeof(info->type), "%s", type ? type : "");
|
||||||
|
snprintf(info->url, sizeof(info->url), "%s", final ? final : url);
|
||||||
return http;
|
return http;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "games.h"
|
#include "games.h"
|
||||||
#include "import.h"
|
#include "import.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
#include "url.h"
|
||||||
|
|
||||||
#define CHUNK (256 * 1024)
|
#define CHUNK (256 * 1024)
|
||||||
#define PEEK (64 * 1024)
|
#define PEEK (64 * 1024)
|
||||||
@@ -25,6 +26,7 @@ typedef struct
|
|||||||
{
|
{
|
||||||
int remote;
|
int remote;
|
||||||
const char *url;
|
const char *url;
|
||||||
|
char final_url[LINK_URL]; // where url led after redirects
|
||||||
plat_http_t *http;
|
plat_http_t *http;
|
||||||
int fd;
|
int fd;
|
||||||
long long position, size;
|
long long position, size;
|
||||||
@@ -96,12 +98,14 @@ static int open_remote(source_t *s, long long offset)
|
|||||||
char error[128];
|
char error[128];
|
||||||
|
|
||||||
plat_http_close(s->http);
|
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)
|
if (!s->http)
|
||||||
{
|
{
|
||||||
fail("%s", error);
|
fail("%s", error);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
snprintf(s->final_url, sizeof(s->final_url), "%s", info.url);
|
||||||
if (info.status == 206)
|
if (info.status == 206)
|
||||||
{
|
{
|
||||||
s->ranges = 1;
|
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)
|
while (used < LISTING_LIMIT && (got = raw_read(s, html + used, LISTING_LIMIT - used)) > 0)
|
||||||
used += got;
|
used += got;
|
||||||
html[used] = 0;
|
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);
|
listing_parse(&listing, base, html, (size_t)used);
|
||||||
free(html);
|
free(html);
|
||||||
pthread_mutex_lock(&lock);
|
pthread_mutex_lock(&lock);
|
||||||
|
|||||||
+257
-34
@@ -5,6 +5,7 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "games.h"
|
#include "games.h"
|
||||||
|
#include "glyphs.h"
|
||||||
#include "import.h"
|
#include "import.h"
|
||||||
#include "launcher.h"
|
#include "launcher.h"
|
||||||
#include "listing.h"
|
#include "listing.h"
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
#include "screen.h"
|
#include "screen.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "test_plan.h"
|
#include "test_plan.h"
|
||||||
|
#include "upload.h"
|
||||||
|
#include "url.h"
|
||||||
|
|
||||||
#define FRAME_US 16667
|
#define FRAME_US 16667
|
||||||
#define REPEAT_DELAY 18
|
#define REPEAT_DELAY 18
|
||||||
@@ -23,6 +26,7 @@
|
|||||||
#define BROWSE_ROWS 12
|
#define BROWSE_ROWS 12
|
||||||
#define FOOTER_Y 188
|
#define FOOTER_Y 188
|
||||||
#define MB (1024.0 * 1024.0)
|
#define MB (1024.0 * 1024.0)
|
||||||
|
#define SEND_LINES 4
|
||||||
|
|
||||||
typedef enum
|
typedef enum
|
||||||
{
|
{
|
||||||
@@ -31,6 +35,7 @@ typedef enum
|
|||||||
VIEW_NOTICE,
|
VIEW_NOTICE,
|
||||||
VIEW_BROWSE,
|
VIEW_BROWSE,
|
||||||
VIEW_IMPORT,
|
VIEW_IMPORT,
|
||||||
|
VIEW_SEND,
|
||||||
} view_t;
|
} view_t;
|
||||||
|
|
||||||
static game_list_t games;
|
static game_list_t games;
|
||||||
@@ -48,6 +53,12 @@ static int frame, typing, understood, auto_import;
|
|||||||
static uint32_t held, pressed;
|
static uint32_t held, pressed;
|
||||||
static int repeat_timer, input_locked = 1;
|
static int repeat_timer, input_locked = 1;
|
||||||
static const game_t *chosen;
|
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)
|
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(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(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)
|
static void add_view(void)
|
||||||
@@ -230,14 +241,16 @@ static void add_view(void)
|
|||||||
{
|
{
|
||||||
url_clean(settings.url, sizeof(settings.url), text);
|
url_clean(settings.url, sizeof(settings.url), text);
|
||||||
settings_save(&settings);
|
settings_save(&settings);
|
||||||
add_row = 1;
|
add_row = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
move(&add_row, 3);
|
move(&add_row, 4);
|
||||||
if ((pressed & PAD_CROSS) && add_row == 0)
|
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",
|
if (plat_text_open("Link to a WAD, ZIP, 7Z or RAR file, or a folder",
|
||||||
settings.url[0] ? settings.url : "http://") == 0)
|
settings.url[0] ? settings.url : "http://") == 0)
|
||||||
@@ -245,7 +258,7 @@ static void add_view(void)
|
|||||||
else
|
else
|
||||||
set_status("The keyboard is not available", INK_RED);
|
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)
|
if (settings.accepted)
|
||||||
{
|
{
|
||||||
@@ -261,30 +274,30 @@ static void add_view(void)
|
|||||||
view = VIEW_NOTICE;
|
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;
|
view = VIEW_GAMES;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen_background();
|
screen_background();
|
||||||
screen_text_center(6, "Add games", INK_GOLD, 2);
|
screen_text_center(6, "Add games", INK_GOLD, 2);
|
||||||
y = screen_wrap(16, 28, SCREEN_WIDTH - 32,
|
y = screen_wrap(16, 28, SCREEN_WIDTH - 32,
|
||||||
"Copy game WADs, or ZIP, 7Z or RAR files with them inside, to this folder, "
|
"DOOM.WAD, DOOM2.WAD, TNT.WAD, PLUTONIA.WAD or Freedoom, on their own or inside "
|
||||||
"for example over FTP:", INK_RED);
|
"ZIP, 7Z or RAR files.", INK_GRAY);
|
||||||
screen_text_fit(16, y + 1, SCREEN_WIDTH - 32, plat_wad_folder(), INK_GOLD, 1);
|
draw_row(30, y + 6, "Send from PC or phone", add_row == 0, 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 + 6 + ROW + 6, SCREEN_WIDTH - 32,
|
||||||
y = screen_wrap(16, y + 3, SCREEN_WIDTH - 32,
|
"Or download a file, or a folder your PC shares over HTTP:", INK_RED);
|
||||||
"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);
|
|
||||||
snprintf(line, sizeof(line), "Link: %s", settings.url[0] ? settings.url : "not set");
|
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_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);
|
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 == 1, settings.url[0] != 0);
|
draw_row(30, y + 6 + ROW, "Download", add_row == 2, settings.url[0] != 0);
|
||||||
draw_row(30, y + 6 + ROW * 2, "Back", add_row == 2, 1);
|
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);
|
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)
|
static void notice_view(void)
|
||||||
@@ -324,7 +337,7 @@ static void notice_view(void)
|
|||||||
screen_box(42, y + 17, 4, 4, 1);
|
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, "Continue", notice_row == 1, understood);
|
||||||
draw_row(40, y + 16 + ROW * 2, "Back", notice_row == 2, 1);
|
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)
|
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);
|
screen_text_fit(30, y, SCREEN_WIDTH - 46, line, i == browse_row ? INK_WHITE : INK_RED, 0);
|
||||||
}
|
}
|
||||||
draw_status(FOOTER_Y - 11, 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)
|
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;
|
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)
|
static void draw_progress(const import_status_t *status)
|
||||||
{
|
{
|
||||||
char line[160];
|
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);
|
snprintf(line, sizeof(line), "File %d of %d", status->source_index + 1, status->source_count);
|
||||||
screen_text(20, 64, line, INK_GRAY, 1);
|
screen_text(20, 64, line, INK_GRAY, 1);
|
||||||
}
|
}
|
||||||
screen_box(20, 80, SCREEN_WIDTH - 40, 10, 0);
|
draw_bar(80, status->done, status->total);
|
||||||
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);
|
|
||||||
if (status->current[0])
|
if (status->current[0])
|
||||||
{
|
{
|
||||||
snprintf(line, sizeof(line), "Writing %s", status->current);
|
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);
|
snprintf(line, sizeof(line), "Found: %s", status->found);
|
||||||
screen_wrap(20, y, SCREEN_WIDTH - 40, line, INK_GREEN);
|
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)
|
static void import_view(void)
|
||||||
@@ -471,6 +491,206 @@ static void import_view(void)
|
|||||||
draw_progress(&status);
|
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)
|
static const game_t *preselected(void)
|
||||||
{
|
{
|
||||||
const game_t *game = games_find(&games, test_plan_game());
|
const game_t *game = games_find(&games, test_plan_game());
|
||||||
@@ -507,7 +727,7 @@ const game_t *launcher_choose(void)
|
|||||||
while (!chosen)
|
while (!chosen)
|
||||||
{
|
{
|
||||||
read_input();
|
read_input();
|
||||||
if (view != VIEW_IMPORT && pressed)
|
if (view != VIEW_IMPORT && view != VIEW_SEND && pressed)
|
||||||
status_line[0] = 0;
|
status_line[0] = 0;
|
||||||
switch (view)
|
switch (view)
|
||||||
{
|
{
|
||||||
@@ -526,6 +746,9 @@ const game_t *launcher_choose(void)
|
|||||||
case VIEW_IMPORT:
|
case VIEW_IMPORT:
|
||||||
import_view();
|
import_view();
|
||||||
break;
|
break;
|
||||||
|
case VIEW_SEND:
|
||||||
|
send_view();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (!chosen)
|
if (!chosen)
|
||||||
present();
|
present();
|
||||||
|
|||||||
+1
-96
@@ -5,102 +5,7 @@
|
|||||||
|
|
||||||
#include "games.h"
|
#include "games.h"
|
||||||
#include "listing.h"
|
#include "listing.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void decode_entities(char *text)
|
static void decode_entities(char *text)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ typedef struct
|
|||||||
int count;
|
int count;
|
||||||
} listing_t;
|
} 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);
|
void listing_parse(listing_t *listing, const char *base, const char *html, size_t length);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+52
-2
@@ -7,7 +7,9 @@
|
|||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "glyphs.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
#include "qrcodegen.h"
|
||||||
#include "screen.h"
|
#include "screen.h"
|
||||||
|
|
||||||
#define FONT_FIRST '!'
|
#define FONT_FIRST '!'
|
||||||
@@ -18,6 +20,8 @@
|
|||||||
#define BACKGROUND_SHADE 29
|
#define BACKGROUND_SHADE 29
|
||||||
#define COLORMAP_SIZE (34 * 256)
|
#define COLORMAP_SIZE (34 * 256)
|
||||||
#define MAX_PATCH_HEIGHT 256
|
#define MAX_PATCH_HEIGHT 256
|
||||||
|
#define QR_VERSION_MAX 5
|
||||||
|
#define QR_QUIET 2
|
||||||
|
|
||||||
static uint8_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT];
|
static uint8_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT];
|
||||||
static uint32_t palette[256];
|
static uint32_t palette[256];
|
||||||
@@ -26,7 +30,7 @@ static uint8_t shade[256];
|
|||||||
static uint8_t inks[INKS][256];
|
static uint8_t inks[INKS][256];
|
||||||
static uint8_t *font[FONT_COUNT];
|
static uint8_t *font[FONT_COUNT];
|
||||||
static uint8_t *title, *logo, *skulls[2];
|
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)
|
static uint32_t le32(const uint8_t *p)
|
||||||
{
|
{
|
||||||
@@ -100,6 +104,8 @@ static void build_inks(void)
|
|||||||
}
|
}
|
||||||
outline_color = nearest(150, 150, 150);
|
outline_color = nearest(150, 150, 150);
|
||||||
fill_color = nearest(190, 30, 30);
|
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)
|
int screen_init(const char *wad_path)
|
||||||
@@ -244,7 +250,10 @@ int screen_text_width(const char *text, int scale)
|
|||||||
int width = 0;
|
int width = 0;
|
||||||
|
|
||||||
for (; *text && *text != '\n'; text++)
|
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;
|
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);
|
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)
|
if (g)
|
||||||
draw_patch(x, y, g, inks[ink], scale, 1);
|
draw_patch(x, y, g, inks[ink], scale, 1);
|
||||||
x += (g ? s16(g) : SPACE_WIDTH) * scale;
|
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;
|
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)
|
void screen_present(void)
|
||||||
{
|
{
|
||||||
plat_video_present(pixels, palette);
|
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_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);
|
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);
|
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);
|
void screen_present(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -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 = ¬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>
|
||||||
@@ -47,6 +47,7 @@ typedef struct
|
|||||||
int status;
|
int status;
|
||||||
int64_t length;
|
int64_t length;
|
||||||
char type[64];
|
char type[64];
|
||||||
|
char url[768]; // where the request ended up after redirects
|
||||||
} plat_http_info_t;
|
} plat_http_info_t;
|
||||||
|
|
||||||
typedef struct plat_http plat_http_t;
|
typedef struct plat_http plat_http_t;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "sce.h"
|
#include "sce.h"
|
||||||
|
#include "url.h"
|
||||||
|
|
||||||
#define NET_POOL (1024 * 1024)
|
#define NET_POOL (1024 * 1024)
|
||||||
#define SSL_POOL (304 * 1024)
|
#define SSL_POOL (304 * 1024)
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
#define METHOD_GET 0
|
#define METHOD_GET 0
|
||||||
#define HEADER_OVERWRITE 0
|
#define HEADER_OVERWRITE 0
|
||||||
#define TIMEOUT_US 15000000
|
#define TIMEOUT_US 15000000
|
||||||
|
#define MAX_REDIRECTS 8
|
||||||
#define VERIFY_FLAGS (0x01 | 0x04 | 0x08 | 0x10 | 0x20 | 0x80)
|
#define VERIFY_FLAGS (0x01 | 0x04 | 0x08 | 0x10 | 0x20 | 0x80)
|
||||||
|
|
||||||
struct plat_http
|
struct plat_http
|
||||||
@@ -39,7 +41,7 @@ static int start_http(void)
|
|||||||
template_id = sceHttpCreateTemplate(http, "DOOM-PS5", HTTP_1_1, 0);
|
template_id = sceHttpCreateTemplate(http, "DOOM-PS5", HTTP_1_1, 0);
|
||||||
if (template_id < 0)
|
if (template_id < 0)
|
||||||
return template_id;
|
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 = sceHttpSetResolveTimeOut(template_id, TIMEOUT_US)) < 0 ||
|
||||||
(result = sceHttpSetConnectTimeOut(template_id, TIMEOUT_US)) < 0 ||
|
(result = sceHttpSetConnectTimeOut(template_id, TIMEOUT_US)) < 0 ||
|
||||||
(result = sceHttpSetSendTimeOut(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;
|
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,
|
plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *info,
|
||||||
char *error, int error_size)
|
char *error, int error_size)
|
||||||
{
|
{
|
||||||
plat_http_t *http;
|
plat_http_t *http;
|
||||||
char range[40];
|
char location[sizeof(info->url)], next[sizeof(info->url)];
|
||||||
uint64_t length = 0;
|
uint64_t length = 0;
|
||||||
int status = 0, has_length = -1, result;
|
int status = 0, has_length = -1, redirects, result;
|
||||||
|
|
||||||
pthread_mutex_lock(&lock);
|
pthread_mutex_lock(&lock);
|
||||||
result = template_id < 0 ? start_http() : 0;
|
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));
|
http = calloc(1, sizeof(*http));
|
||||||
if (!http)
|
if (!http)
|
||||||
return NULL;
|
return NULL;
|
||||||
http->request = -1;
|
http->request = http->connection = -1;
|
||||||
http->connection = result = sceHttpCreateConnectionWithURL(template_id, url, 1);
|
|
||||||
if (result >= 0)
|
// Redirects are followed here rather than by sceHttp, to learn the address the request ends
|
||||||
http->request = result = sceHttpCreateRequestWithURL(http->connection, METHOD_GET, url, 0);
|
// up at: a folder listing's relative links are relative to that one.
|
||||||
if (result >= 0 && offset)
|
snprintf(info->url, sizeof(info->url), "%s", url);
|
||||||
|
for (redirects = 0;; redirects++)
|
||||||
{
|
{
|
||||||
snprintf(range, sizeof(range), "bytes=%llu-", (unsigned long long)offset);
|
result = send_get(http, info->url, offset, &status);
|
||||||
result = sceHttpAddRequestHeader(http->request, "Range", range, HEADER_OVERWRITE);
|
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)
|
if (result < 0)
|
||||||
{
|
{
|
||||||
snprintf(error, error_size, "connection failed (0x%08x)", (unsigned)result);
|
snprintf(error, error_size, "connection failed (0x%08x)", (unsigned)result);
|
||||||
@@ -130,9 +170,6 @@ void plat_http_close(plat_http_t *http)
|
|||||||
{
|
{
|
||||||
if (!http)
|
if (!http)
|
||||||
return;
|
return;
|
||||||
if (http->request >= 0)
|
drop_request(http);
|
||||||
sceHttpDeleteRequest(http->request);
|
|
||||||
if (http->connection >= 0)
|
|
||||||
sceHttpDeleteConnection(http->connection);
|
|
||||||
free(http);
|
free(http);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
text http://192.168.0.10:8666/
|
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
|
capture 25,80,120,180,250,400,1200,2400,3600,4800,6000,7100
|
||||||
frames 7200
|
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
|
THIRD_DEPS := .deps/third-party
|
||||||
ARCHIVE_STAMP := $(ARCHIVE_DEPS)/zlib-1.3.2/.complete
|
THIRD_STAMP := $(THIRD_DEPS)/QR-Code-generator-1.8.0/.complete
|
||||||
LA_DIR := $(ARCHIVE_DEPS)/libarchive-3.8.9/libarchive
|
LA_DIR := $(THIRD_DEPS)/libarchive-3.8.9/libarchive
|
||||||
XZ_DIR := $(ARCHIVE_DEPS)/xz-5.8.4/src/liblzma
|
XZ_DIR := $(THIRD_DEPS)/xz-5.8.4/src/liblzma
|
||||||
ZL_DIR := $(ARCHIVE_DEPS)/zlib-1.3.2
|
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 \
|
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 \
|
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/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)
|
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)
|
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)
|
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 \
|
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)/lz -I$(XZ_DIR)/rangecoder -I$(XZ_DIR)/lzma -I$(XZ_DIR)/delta -I$(XZ_DIR)/simple \
|
||||||
-I$(XZ_DIR)/../common
|
-I$(XZ_DIR)/../common
|
||||||
ZL_FLAGS := -I$(ZL_DIR)
|
ZL_FLAGS := -I$(ZL_DIR)
|
||||||
|
QR_FLAGS := -I$(QR_DIR)
|
||||||
THIRD_FLAGS := -std=gnu11 -O2 -w -fno-strict-aliasing -DNDEBUG -Dmalloc=import_malloc \
|
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
|
-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_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),$(ZL_FLAGS)))
|
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):
|
$(THIRD_STAMP):
|
||||||
bash tools/fetch-archive-libs.sh $(ARCHIVE_DEPS)
|
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
|
#!/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
|
set -euo pipefail
|
||||||
|
|
||||||
out=${1:-.deps/archive}
|
out=${1:-.deps/third-party}
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
|
||||||
fetch() {
|
fetch() {
|
||||||
@@ -37,3 +38,4 @@ fetch xz-5.8.4 0014c7886930454fe8bd4228665b51af55eeae560ea135c9c4cd33f55b2591d9
|
|||||||
fetch zlib-1.3.2 bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 \
|
fetch zlib-1.3.2 bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 \
|
||||||
https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz \
|
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
|
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