diff --git a/.gitignore b/.gitignore index df384e1..6fd5374 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ .deps/ out/ wads/ +__pycache__/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4d437d4..a3e21ad 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -4,8 +4,9 @@ id Software's original `linuxdoom-1.10` source running as a native PS5 title: it icon, launched like a game, drawing through Sony's VideoOut and AGC (GPU) drivers, reading the DualSense through ScePad and playing sound through AudioOut. Title ID `PPSA99666`. -All PS5, platform, audio and test code here is written for this project. The only external code -is id's game source and the build tools listed at the end. +All PS5, platform, audio, launcher and test code here is written for this project. The external +code is id's game source, the archive libraries the importer is built from (libarchive, xz's +liblzma, zlib) and the build tools listed at the end. ## Layout @@ -13,59 +14,94 @@ is id's game source and the build tools listed at the end. | --- | --- | | `src/doom/` | id's original game code with the 64-bit and portability fixes listed below | | `src/port/` | Doom's `i_*` layer (main, system, video, sound, network, controller mapping) on top of the platform API | -| `src/platform/platform.h` | The platform API: time, log, files, video present, pad, rumble, audio output | +| `src/launcher/` | The game list shown before the engine starts: IWAD discovery, settings, Doom-style drawing from the bundled WAD, HTML folder listings, and the importer (HTTP or local file, WAD or ZIP/7Z/RAR) | +| `src/platform/platform.h` | The platform API: time, log, files and folder listing, video present, pad, rumble, audio output, HTTP, text input | | `src/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 | +| `src/ps5/` | PS5 backend: startup (`crt0.c`), system, VideoOut, AGC compute presenter, pad, AudioOut, `sceHttp`, the IME keyboard | | `src/ps5/present.cl` | GPU kernel: palette lookup, sharp-bilinear scaling to 1080p, tiled scanout writes (gfx1010, wave64) | -| `src/host/` | Headless Linux backend for testing on the PC: PNG frames, scripted pad, WAV audio | -| `test/` | `render_music` (MUS lump to WAV) and `present_preview` (runs the GPU kernel's math on the CPU) | +| `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 | +| `test/` | `render_music` (MUS lump to WAV), `present_preview` (runs the GPU kernel's math on the CPU) and the console test plans | | `title/` | `param.json` and the icon | -| `tools/` | Tool fetch, shareware WAD fetch, kernel embedding, deploy | +| `tools/` | Tool, library and shareware WAD fetch, kernel embedding, deploy, console test runner | ## Build -Everything builds in the `ps5-doom-build` image (`docker/Dockerfile`: clang/lld/llvm 18, ninja, gdb). +Everything builds in the `ps5-doom-build` image (`docker/Dockerfile`: clang/lld/llvm 18, ninja, gdb, +libcurl for the PC build). ```bash docker build -t ps5-doom-build docker/ MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/src" -w /src ps5-doom-build make -j16 ps5 host ``` -`make ps5` fetches the pinned tools and the shareware WAD on first use, then: +`make ps5` fetches the pinned tools, the archive libraries (`tools/fetch-archive-libs.sh`) and the +shareware WAD on first use, then: -1. compiles the game and the PS5 layer for `x86_64-sie-ps5` against the PS5 payload SDK headers; +1. compiles the game, the launcher and the PS5 layer for `x86_64-sie-ps5` against the PS5 payload + SDK headers, and the reading half of libarchive (zip, 7z, rar, rar5), liblzma's decoders and + zlib's inflate with the same compiler; 2. compiles `present.cl` for `gfx1010` (wave64), links it and embeds it with `tools/embed-kernel.py`, which reads the register values from the compiler's kernel descriptor and fails the build if the kernel needs anything the presenter does not set up; -3. generates `libSceAgc` / `libSceAgcDriver` import stubs from `src/ps5/stubs/*.txt` (the SDK has none); -4. links a PIE, converts it to a PS5 module, signs the FSELF and assembles `dist/PPSA99666/`. +3. generates import stubs the SDK lacks (`libSceAgc`, `libSceAgcDriver`, `libSceCommonDialog`) from + `src/ps5/stubs/*.txt`; +4. links a PIE, converts it to a PS5 module, signs the FSELF and assembles `dist/PPSA99666/` with + the shareware `doom1.wad` in `wads/`. + +The libraries' `malloc`, `calloc`, `realloc`, `free` and `strdup` are renamed at compile time to +`src/launcher/import_memory.c`, which takes blocks of 256 KB and more from `mmap`: a native app's +libc heap cannot hold a 64 MB LZMA dictionary. The PC build uses the same wrapper. ## Testing on the PC `build/host/bin/doom` is the same game on the headless backend. Environment variables: -`DOOM_WADDIR`, `DOOM_SAVEDIR`, `DOOM_OUT` (frame output dir), `DOOM_CAPTURE` (frame numbers to -save as PNG plus raw indices and palette), `DOOM_FRAMES` (exit after N frames), `DOOM_AUDIO` -(absolute WAV path), `DOOM_INPUT` (pad script: `frame:BUTTON+BUTTON+LX=-32000:frames;...`). +`DOOM_WADDIR` (the user's WAD folder), `DOOM_SAVEDIR`, `DOOM_OUT` (frame output dir), +`DOOM_CAPTURE` (frame numbers to save as PNG plus raw indices and palette), `DOOM_FRAMES` (exit +after N frames), `DOOM_AUDIO` (absolute WAV path), `DOOM_INPUT` (pad script: +`frame:BUTTON+BUTTON+LX=-32000:frames;...`), `DOOM_IWAD` (skip the launcher and start this game +file, e.g. `doom1.wad`) and `DOOM_TEXT` (the answer to the keyboard prompt). ```bash -DOOM_OUT=out DOOM_CAPTURE=120,900 build/host/bin/doom -timedemo demo1 +DOOM_IWAD=doom1.wad DOOM_OUT=out DOOM_CAPTURE=120,900 build/host/bin/doom -timedemo demo1 ``` +Without `DOOM_IWAD` the launcher runs and the pad script drives it. For the importer, serve files +from inside the container with `python3 -m http.server` (no range support) or reach a server on +the Windows host as `host.docker.internal` (`docker run --add-host=host.docker.internal:host-gateway`). + For memory errors, build with AddressSanitizer: `make host HOST_DIR=build/asan HOST_FLAGS="-O1 -g -fsanitize=address" HOST_CC="clang -fsanitize=address"`. +## The launcher + +`I_ChooseIwad` (in `src/port/i_system.c`) runs the launcher before the engine identifies its IWAD +and maps the chosen game to Doom's game mode, mission and language. The launcher: + +- scans the WAD folders (`/app0/wads`, then `/download0`) for the known IWAD names, checks each + file's header and directory, and tells The Ultimate DOOM from DOOM by the presence of `E4M1`; +- imports any ZIP, 7Z or RAR found there that it has not imported before (recorded in + `/download0/launcher.cfg` by size and path), extracting only known IWAD names; +- downloads from a typed link with `plat_http_get`: an HTML answer is parsed as a folder listing + (links under the folder that end in `.wad`, `.zip`, `.7z`, `.rar` or `/`), a WAD is copied, an + archive goes through libarchive. libarchive seeks with HTTP range requests; a server without them + is read through or re-read from the start instead; +- writes into the first writable WAD folder (`/app0/wads` on the console), through a `.part` file + that is checked as an IWAD before it is renamed; +- shows a one-time notice with a checkbox before the first download. + +Saves are per game: `/sav.dsg` (for example `doom2sav0.dsg`). + ## On the console `uv run --no-project python tools/deploy.py` uploads `dist/PPSA99666` to `/data/homebrew/PPSA99666` -(PS5Upload helper must be running), where ShadowMountPlus registers it. The package bundles Freedoom -(`tools/fetch-freedoom.sh`). id's IWADs (`DOOM.WAD`, `DOOM2.WAD`, `PLUTONIA.WAD`, `TNT.WAD`) dropped -into `/data/homebrew/PPSA99666/wads/` take priority; order: DOOM II-style id WADs, DOOM-style id -WADs, Freedoom 2, Freedoom 1; holding L2 at launch prefers the DOOM-style game. `make release` writes -`dist/PPSA99666.zip` and its `.sha256`. +(PS5Upload helper must be running), where ShadowMountPlus registers it. Uploading adds and replaces +files but never deletes, so files dropped from the package stay on the console until removed. +`make release` writes `dist/PPSA99666.zip` and its `.sha256`. -The app is sandboxed: it reads `/app0` (its folder) and writes `/download0` (config, saves and -`doom.log`). Log lines also go to the kernel log (`sceKernelDebugOutText`), readable with klogsrv. -Fatal errors show as a system notification. +The app is sandboxed: it reads and writes `/app0` (its folder; imports land in `/app0/wads`) and +writes `/download0` (config, saves, `launcher.cfg` and `doom.log`). Log lines also go to the kernel +log (`sceKernelDebugOutText`). Fatal errors show as a system notification. Presentation uses the AGC compute path by default. Hold **L1+R1** while the game starts to force the CPU scaler. If a GPU frame does not complete within 500 ms, the game switches to the CPU scaler by @@ -77,18 +113,32 @@ itself and logs why. into the title folder, launches through the `doomlaunch` payload (`tools/launcher`, put into `/data/pldmgr/payloads/doomlaunch/`), pulls captured frames over TCP from the running game (the app listens on 9119; the PC firewall blocks the other direction), de-tiles them to PNG in -`build/test/run/`, prints the log from klogsrv (port 3232, captured to `build/test/klog.txt`), then +`build/test/run/`, receives `doom.log` over the same connection when the game exits, prints it and deletes `test.cfg`. Plan lines: `input `, `capture `, `frames ` (clean exit), -`present cpu`. Plans: `test/console-play.cfg` (menus, play, save, load), `console-cpu.cfg`, -`console-soak.cfg` (6 minutes). +`present cpu`, `game ` (skip the launcher), `text ` (answer the keyboard prompt +without opening it). Plans: `console-play.cfg` (menus, play, save, load), `console-cpu.cfg`, +`console-soak.cfg` (6 minutes), `console-import.cfg` (launcher, notice, folder listing and a 7Z +import from `http://192.168.0.10:8666/`), `console-autoimport.cfg`, `console-tnt.cfg`, +`console-ultimate.cfg`. ## PS5 facts learned on hardware (FW 13.00) -- `downloadDataSize` 64 is rejected (`0x80a40087`, launch fails); 256 works. -- The libc heap of a native app is small: large blocks must come from `mmap` (the zone does). -- The sandbox refuses `access`, `chdir`, `opendir`, `dup`, `dup2` (EPERM). `open` works. Saves use - explicit `/download0/...` paths; output is captured by pointing `stdout`/`stderr` at a pipe +- `downloadDataSize` 64 is rejected (`0x80a40087`, launch fails); 256 works. `/download0` is a + fixed-size image of that size (`/user/download/PPSA99666/download0.dat`), not a folder the PC can + read; large data belongs in `/app0`. +- The libc heap of a native app is small: large blocks must come from `mmap` (the zone and the + archive libraries do). +- The sandbox refuses `access`, `chdir`, `opendir`, `dup`, `dup2` (EPERM). `open`, `rename` and + `unlink` work, `/app0` is writable, and folders list through `sceKernelOpen` with `O_DIRECTORY` + plus `sceKernelGetdents` (8-byte records: 32-bit inode, 16-bit length, type, name length). Saves + use explicit `/download0/...` paths; output is captured by pointing `stdout`/`stderr` at a pipe (`fdopen`), and klog wants one line per `sceKernelDebugOutText`. +- `sceHttp` with `sceSsl` works in the sandboxed title, plain and HTTPS, including `Range` request + headers added with `sceHttpAddRequestHeader` (answers 206). `sceHttpReadData` returns only when + the buffer is full or the body ends. About 12 MB/s from a PC on the same wired network. +- The SDK's FreeBSD headers do not match the console's C library everywhere: `MB_CUR_MAX` expands + to `___mb_cur_max` (the library has `_Getmbcurmax`), and there is no `localtime_r`, `gmtime_r` or + `timegm`. `assert` needs `__assert`, which is missing too. - Scanout must be tiled (`sceVideoOutSetBufferAttribute2` tiling 0; 1 is `0x80290007`), 16 MB per 1080p buffer. Pixel layout: 512x128 tiles with the bit interleave in `src/ps5/tiling.h`. - GPU memory: direct memory type 12, protection 0x33, CPU writes flushed with `clflush` before GPU @@ -123,18 +173,27 @@ Saving needs no keyboard: an empty slot is pre-filled with the level name, Cross (`r_data.c`, `p_setup.c`), table alignment through `int` casts, 16-byte zone alignment. - Undefined behaviour and latent bugs: the unterminated `sprnames` list, unsequenced event queue updates, button array resets that cleared 8 bytes of pointers, save games written into the - screen buffers (now a dedicated 4 MB buffer). -- Portability: no `values.h`/`alloca.h`/sound server; IWAD search through `I_GetWadDirs`; config in - the working directory (the port changes into the save directory at start). -- Behaviour: 1.9 demos play (the WADs' attract demos); empty save slots get a default name; - default SFX channels 8 (the DOS default) instead of 3; Freedoom IWADs recognised; static limits - raised (visplanes, drawsegs, sprites, openings, intercepts, plats, ceilings, buttons, scrollers) - so limit-removing maps such as Freedoom's run. + screen buffers (now a dedicated 4 MB buffer), the level-load sky check that compared the game + mode with mission values (`pack_plut` equals `retail`, so The Ultimate DOOM got DOOM II skies). +- Portability: no `values.h`/`alloca.h`/sound server; config in the working directory (the port + changes into the save directory at start). +- Game selection: `IdentifyVersion` takes the launcher's choice (`I_ChooseIwad`) instead of probing + file names, sets `gamemission`, and the TNT and Plutonia level names and finale texts that id left + behind `FIXME` comments are used. +- Behaviour: 1.9 demos play (the WADs' attract demos); empty save slots get a default name; saves + are per game; default SFX channels 8 (the DOS default) instead of 3; Freedoom IWADs recognised; + static limits raised (visplanes, drawsegs, sprites, openings, intercepts, plats, ceilings, + buttons, scrollers) so limit-removing maps such as Freedoom's run; full-screen pictures wider + than 320 pixels (the 2024 re-release's widescreen title and intermission screens) are drawn + centred and clipped instead of being rejected. -## External tools (pinned) +## External code and tools (pinned) - [ps5-native-app-boilerplate](https://github.com/blackbearreloaded/ps5-native-app-boilerplate) `b1315a9`: its `ps5-native-tool` (PIE to PS5 module, FSELF signing), its clean-room `libc.prx`, its intermediate PIE linker script, and the PS5 payload SDK v0.42 it fetches. Built in `.deps/native-app` by `tools/fetch-native-tools.sh`; nothing from it is copied into `src/`. +- [libarchive](https://www.libarchive.org/) 3.8.9, [xz](https://tukaani.org/xz/) 5.8.4 (liblzma) + and [zlib](https://zlib.net/) 1.3.2, fetched and checked by `tools/fetch-archive-libs.sh` into + `.deps/archive` and compiled into the game for both targets. - clang/lld/llvm 18 (x86_64-sie-ps5 and amdgcn targets). diff --git a/Makefile b/Makefile index 0c52dc1..5624322 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ DOOM_SRC := $(wildcard src/doom/*.c) -PORT_SRC := $(wildcard src/port/*.c) $(wildcard src/audio/*.c) $(wildcard src/platform/*.c) +PORT_SRC := $(wildcard src/port/*.c) $(wildcard src/audio/*.c) $(wildcard src/platform/*.c) \ + $(wildcard src/launcher/*.c) HOST_SRC := $(wildcard src/host/*.c) PS5_SRC := $(wildcard src/ps5/*.c) -INCLUDES := -Isrc/doom -Isrc/port -Isrc/platform -Isrc/audio +include third_party/third_party.mk + +INCLUDES := -Isrc/doom -Isrc/port -Isrc/platform -Isrc/audio -Isrc/launcher $(ARCHIVE_INCLUDES) DOOM_WARNINGS := -Wall -Wno-unused-const-variable -Wno-unused-but-set-variable \ -Wno-unused-variable -Wno-logical-not-parentheses -Wno-missing-braces OWN_WARNINGS := -Wall -Wextra -Wno-unused-parameter @@ -12,7 +15,8 @@ HOST_CC := clang HOST_FLAGS := -O2 -g -fno-strict-aliasing -MMD -MP HOST_DIR := build/host -HOST_OBJ := $(patsubst src/%.c,$(HOST_DIR)/%.o,$(DOOM_SRC) $(PORT_SRC) $(HOST_SRC)) +HOST_OWN_OBJ := $(patsubst src/%.c,$(HOST_DIR)/%.o,$(DOOM_SRC) $(PORT_SRC) $(HOST_SRC)) +HOST_OBJ := $(HOST_OWN_OBJ) $(call third_obj,$(HOST_DIR)) TITLE := PPSA99666 NATIVE := .deps/native-app @@ -23,7 +27,8 @@ RUNTIME := $(NATIVE)/runtime/libc.prx PS5_DIR := build/ps5 PS5_GEN := $(PS5_DIR)/gen PS5_STUBS := $(PS5_DIR)/stubs -PS5_OBJ := $(patsubst src/%.c,$(PS5_DIR)/%.o,$(DOOM_SRC) $(PORT_SRC) $(PS5_SRC)) +PS5_OWN_OBJ := $(patsubst src/%.c,$(PS5_DIR)/%.o,$(DOOM_SRC) $(PORT_SRC) $(PS5_SRC)) +PS5_OBJ := $(PS5_OWN_OBJ) $(call third_obj,$(PS5_DIR)) AGC_STUBS := $(patsubst src/ps5/stubs/%.txt,$(PS5_STUBS)/%.so,$(wildcard src/ps5/stubs/*.txt)) PS5_CC := clang -target x86_64-sie-ps5 -fvisibility-nodllstorageclass=default \ -isysroot $(SDK) -isystem $(SDK)/target/include \ @@ -37,7 +42,7 @@ host: $(HOST_DIR)/bin/doom $(HOST_DIR)/bin/doom: $(HOST_OBJ) @mkdir -p $(dir $@) - $(HOST_CC) -o $@ $^ -lm -lpthread + $(HOST_CC) -o $@ $^ -lm -lpthread -lcurl $(HOST_DIR)/doom/%.o: src/doom/%.c @mkdir -p $(dir $@) @@ -47,6 +52,12 @@ $(HOST_DIR)/%.o: src/%.c @mkdir -p $(dir $@) $(HOST_CC) -std=gnu11 $(HOST_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -c $< -o $@ +$(HOST_DIR)/third_party/%.o: $(ARCHIVE_DEPS)/%.c + @mkdir -p $(dir $@) + $(HOST_CC) $(THIRD_FLAGS) -g -MMD -MP $(call third_flags,$<) -c $< -o $@ + +$(HOST_OWN_OBJ): | $(ARCHIVE_STAMP) + tools: $(NATIVE_TOOL) $(NATIVE_TOOL) $(RUNTIME) $(SDK)/target/lib/libkernel.so: @@ -55,9 +66,6 @@ $(NATIVE_TOOL) $(RUNTIME) $(SDK)/target/lib/libkernel.so: wads/DOOM1.WAD: bash tools/fetch-shareware.sh $@ -wads/freedoom2.wad: - bash tools/fetch-freedoom.sh wads - ps5: $(APP)/eboot.bin $(PS5_STUBS)/.sdk: $(SDK)/target/lib/libkernel.so @@ -87,6 +95,12 @@ $(PS5_DIR)/%.o: src/%.c $(SDK)/target/lib/libkernel.so @mkdir -p $(dir $@) $(PS5_CC) -std=gnu11 $(PS5_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -I$(PS5_GEN) -c $< -o $@ +$(PS5_DIR)/third_party/%.o: $(ARCHIVE_DEPS)/%.c $(SDK)/target/lib/libkernel.so + @mkdir -p $(dir $@) + $(PS5_CC) $(THIRD_FLAGS) -MMD -MP $(call third_flags,$<) -c $< -o $@ + +$(PS5_OWN_OBJ): | $(ARCHIVE_STAMP) + $(PS5_DIR)/llvm-pie.elf: $(PS5_OBJ) $(AGC_STUBS) src/ps5/symbols.map $(SDK)/bin/prospero-lld -T $(NATIVE)/tooling/native/ps5-pie.ld --eh-frame-hdr --gc-sections \ --version-script src/ps5/symbols.map -e _start -o $@ $(PS5_OBJ) \ @@ -96,13 +110,20 @@ $(PS5_DIR)/eboot.elf: $(PS5_DIR)/llvm-pie.elf $(NATIVE_TOOL) $(NATIVE_TOOL) link --in $< --out $@ --stub-dir $(PS5_STUBS) \ --module-sdk 0x02000009 --companion-sdk 0x08050001 --file-name eboot.elf -$(APP)/eboot.bin: $(PS5_DIR)/eboot.elf $(RUNTIME) title/param.json title/icon0.png wads/freedoom2.wad +$(APP)/eboot.bin: $(PS5_DIR)/eboot.elf $(RUNTIME) title/param.json title/icon0.png wads/DOOM1.WAD rm -rf $(APP) mkdir -p $(APP)/sce_sys $(APP)/sce_module $(APP)/wads $(NATIVE_TOOL) self --sign --in $< --out $@ --magic 0x1D3D154F cp $(RUNTIME) $(APP)/sce_module/libc.prx cp title/param.json title/icon0.png $(APP)/sce_sys/ - cp wads/freedoom1.wad wads/freedoom2.wad wads/FREEDOOM-COPYING.txt wads/FREEDOOM-CREDITS.txt $(APP)/wads/ + cp wads/DOOM1.WAD $(APP)/wads/doom1.wad + mkdir -p $(APP)/licenses + cp LICENSE $(APP)/licenses/DOOM-GPL-2.0.txt + cp THIRD_PARTY_NOTICES.md $(APP)/licenses/ + cp $(NATIVE)/LICENSE $(APP)/licenses/libc-prx-GPL-3.0.txt + cp $(LA_DIR)/../COPYING $(APP)/licenses/libarchive.txt + cp $(XZ_DIR)/../../COPYING.0BSD $(APP)/licenses/liblzma.txt + cp $(ZL_DIR)/LICENSE $(APP)/licenses/zlib.txt $(NATIVE_TOOL) self --inspect --file $@ $(NATIVE_TOOL) self --inspect --file $(APP)/sce_module/libc.prx diff --git a/README.md b/README.md index 9be852a..dd9f788 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ id Software's original DOOM source (`linuxdoom-1.10`) running as a native PS5 title: its own home-screen icon, drawn on the GPU through Sony's AGC driver and VideoOut, DualSense input with analog sticks and rumble, and the DOS-era FM music synthesised from the game's own instrument bank. -It ships with [Freedoom](https://freedoom.github.io/), a free full-length game, and plays id's -DOOM and DOOM II if you add your own WADs. +It ships with the free shareware episode of DOOM and plays The Ultimate DOOM, DOOM II and Final +DOOM from your own game files, which it can import from a folder on the console or download from +your PC, including straight out of ZIP, 7Z and RAR archives. ## Requirements @@ -22,10 +23,42 @@ A jailbroken PS5 with kstuff (fake-signed executables) and ## Games -- Launch normally: Freedoom Phase 2 (DOOM II style, 32 maps). -- Hold **L2** while it starts: Freedoom Phase 1 (DOOM style, 4 episodes). -- Your own `DOOM.WAD`, `DOOM2.WAD`, `PLUTONIA.WAD` or `TNT.WAD` in `/data/homebrew/PPSA99666/wads/` - take priority over Freedoom (DOOM II first; hold L2 for DOOM). +DOOM opens on a game list. Out of the box it holds the shareware episode, *Knee-Deep in the Dead*; +every game you add appears there too, and the last one you played stays selected. + +| File | Game | +| --- | --- | +| `DOOM.WAD`, `DOOMU.WAD` | DOOM, or The Ultimate DOOM when it has episode 4 | +| `DOOM2.WAD` | DOOM II: Hell on Earth | +| `TNT.WAD` | Final DOOM: TNT Evilution | +| `PLUTONIA.WAD` | Final DOOM: The Plutonia Experiment | +| `FREEDOOM1.WAD`, `FREEDOOM2.WAD` | [Freedoom](https://freedoom.github.io/) Phase 1 and 2 | + +The files from the original releases and from the 2024 *DOOM + DOOM II* re-release both work. + +### Adding your games + +Either way, the game files can also be inside ZIP, 7Z or RAR archives: DOOM extracts the game +WADs it recognises and skips everything else. + +- **Copy them to the console.** Put the files or archives in `/data/homebrew/PPSA99666/wads/` (FTP, + ps5upload or USB). DOOM picks them up the next time it starts. +- **Download them from a link.** In the game list choose *Add games...*, then *Link*, type an + `http://` or `https://` address on the console keyboard and choose *Download*. The link can point + at a WAD, an archive, or a folder; for a folder DOOM lists the files and you pick one or all. + +The simplest way to serve your own files from a PC on the same network is to open a terminal in the +folder that holds them and run a small web server, then use the address it prints with your PC's +local IP (for example `http://192.168.1.20:8000/`): + +```bash +python -m http.server 8000 +``` + +Any web server that lists a folder works (nginx, Apache, Caddy, IIS, `npx http-server`). One that +supports range requests (all of those except Python's) lets DOOM read a 7Z or ZIP without +downloading the parts it does not need; with Python's server it still works, just with more +traffic. Allow the port through the PC's firewall if the console cannot connect. ## Controls @@ -42,7 +75,7 @@ A jailbroken PS5 with kstuff (fake-signed executables) and | Triangle / touchpad | Automap (Square: follow mode) | | | Options | Menu | Close menu | -Saves and settings stay on the console. Holding **L1+R1** at launch uses the CPU scaler instead of +Saves and settings stay on the console, and each game keeps its own saves. Holding **L1+R1** at launch uses the CPU scaler instead of the GPU, should a firmware ever disagree with the GPU path. ## Building and internals diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 44fb375..afbcd82 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,14 +1,18 @@ # Third-party notices -Everything in this project that came from someone else, what it is used for, and its license. +Everything in this project that came from someone else, what it is used for, and its license. The +release package carries these notices and the license texts in `PPSA99666/licenses/`. ## Shipped in the release | Component | Used as | 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`) | -| [Freedoom](https://freedoom.github.io/) 0.13.0, Freedoom project | Bundled game data: `wads/freedoom1.wad`, `wads/freedoom2.wad` | BSD 3-Clause (`wads/FREEDOOM-COPYING.txt`, credits in `wads/FREEDOOM-CREDITS.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 | +| DOOM shareware IWAD v1.9 (`DOOM1.WAD`, episode 1), id Software | Bundled game data, `wads/doom1.wad`, unmodified; fetched by `tools/fetch-shareware.sh` | 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`) | +| [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`) | +| `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 @@ -17,7 +21,7 @@ Everything in this project that came from someone else, what it is used for, and | [ps5-native-app-boilerplate](https://github.com/blackbearreloaded/ps5-native-app-boilerplate) `b1315a9` | `ps5-native-tool` (PIE to PS5 module conversion, FSELF signing) and its intermediate linker script `ps5-pie.ld`, fetched by `tools/fetch-native-tools.sh`, not copied into this repository | GNU GPL v3 or later | | [PS5 Payload SDK](https://github.com/ps5-payload-dev/sdk) v0.42, John Törnblom | C library headers, system import stubs, `prospero-lld`; the SDK compiler wrapper for the test launcher payload | GNU GPL v3 | | [LLVM/Clang](https://llvm.org/) 18 | C compiler for the x86-64 PS5 target and the AMDGPU compute kernel | Apache 2.0 with LLVM exception | -| DOOM shareware IWAD (`DOOM1.WAD`), id Software | Regression data for the PC test build only (demo playback); fetched by `tools/fetch-shareware.sh`, not shipped | id Software shareware terms | +| [libcurl](https://curl.se/) | HTTP for the PC test build only (`src/host/http.c`); the console uses the system's `sceHttp` | curl license | ## Studied for platform facts (no code copied) @@ -27,10 +31,13 @@ not copied; the facts they document are hardware and system behaviour. | Project | What was learned | License | | --- | --- | --- | | [ps5-opengl](https://github.com/blackbearreloaded/ps5-opengl) | AGC call sequence (wait-for-safe-render, command buffer layout, dispatch, flip, suspend point), compute register offsets, direct memory type and cache maintenance, the RELEASE_MEM/WAIT_REG_MEM fence pattern and its packet constants | GNU GPL v3 or later | -| [SDL PS5 port](https://github.com/ps5-payload-dev/SDL) | VideoOut, ScePad and AudioOut call contracts and the tiled scanout pixel layout | zlib | -| [ps5-native-app-boilerplate](https://github.com/blackbearreloaded/ps5-native-app-boilerplate) | Native title startup contract, `param.json` fields, sandbox notes | GNU GPL v3 or later | +| [SDL PS5 port](https://github.com/ps5-payload-dev/SDL) | VideoOut, ScePad and AudioOut call contracts, the tiled scanout pixel layout, the IME dialog structures | zlib | +| [ps5-native-app-boilerplate](https://github.com/blackbearreloaded/ps5-native-app-boilerplate) | Native title startup contract, `param.json` fields, sandbox notes, the `sceHttp`/`sceSsl` setup that works in a sandboxed title | GNU GPL v3 or later | +| [ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight) | Opening the system keyboard from a native title (`sceCommonDialogInitialize`, the IME dialog module ID and parameter layout) | GNU GPL v3 | +| [SharpProspero](https://github.com/SvenGDK/SharpProspero) | The `sceKernelGetdents` record layout for listing folders in the sandbox | GNU GPL v3 | | [ps5-yamagi-quake2](https://github.com/blackbearreloaded/ps5-yamagi-quake2) | Native app heap limits and the system exit path | GNU GPL v3 or later | | [websrv](https://github.com/ps5-payload-dev/websrv), [ps5upload](https://github.com/phantomptr/ps5upload) | How homebrew is launched and how files reach the console (the test tools use ps5upload's helper) | GNU GPL v3 | DOOM is a registered trademark of id Software / ZeniMax Media. This project is not affiliated with -or endorsed by them. No commercial game data is included. +or endorsed by them. The only game data included is id Software's freely distributable shareware +episode; the full games are not included and the app does not point to any source for them. diff --git a/docker/Dockerfile b/docker/Dockerfile index 7c265a4..d8f5317 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates wget unzip make ninja-build python3 git file xxd gdb \ + ca-certificates wget unzip make ninja-build python3 git file xxd gdb libcurl4-openssl-dev \ clang-18 lld-18 llvm-18 libclang-rt-18-dev \ && rm -rf /var/lib/apt/lists/* ENV PATH=/usr/lib/llvm-18/bin:$PATH diff --git a/src/doom/d_main.c b/src/doom/d_main.c index a16419b..a61e83e 100644 --- a/src/doom/d_main.c +++ b/src/doom/d_main.c @@ -124,6 +124,7 @@ boolean advancedemo; char wadfile[1024]; // primary wad file char mapdir[1024]; // directory of development maps char basedefault[1024]; // default file +char* savegameprefix; // save game path up to the slot number void D_CheckNetGame (void); @@ -545,201 +546,30 @@ void D_AddFile (char *file) wadfiles[numwadfiles] = newfile; } -static char* FindWad (char* name) -{ - char** dir; - char* path; - char* base; - int upper; - int handle; - - for (dir = I_GetWadDirs (); *dir; dir++) - { - path = malloc (strlen(*dir)+1+strlen(name)+1); - for (upper = 0; upper < 2; upper++) - { - sprintf (path, "%s/%s", *dir, name); - for (base = path+strlen(*dir)+1; upper && *base; base++) - *base = toupper (*base); - if ((handle = open (path, O_RDONLY)) >= 0) - { - close (handle); - return path; - } - } - free (path); - } - return NULL; -} - // // IdentifyVersion -// Checks availability of IWAD files by name, -// to determine whether registered/commercial features -// should be executed (notably loading PWAD's). +// Takes the game chosen in the launcher and sets up +// registered/commercial features to match its IWAD. // void IdentifyVersion (void) { - - char* doom1wad; - char* doomwad; - char* doomuwad; - char* doom2wad; - - char* doom2fwad; - char* plutoniawad; - char* tntwad; - char* freedoom1wad; - char* freedoom2wad; - - doom2wad = FindWad ("doom2.wad"); - doomuwad = FindWad ("doomu.wad"); - doomwad = FindWad ("doom.wad"); - doom1wad = FindWad ("doom1.wad"); - plutoniawad = FindWad ("plutonia.wad"); - tntwad = FindWad ("tnt.wad"); - doom2fwad = FindWad ("doom2f.wad"); - freedoom1wad = FindWad ("freedoom1.wad"); - freedoom2wad = FindWad ("freedoom2.wad"); + iwad_t* iwad; sprintf (basedefault, "%s/default.cfg", I_GetSaveDir ()); - if (M_CheckParm ("-shdev")) + iwad = I_ChooseIwad (); + if (!iwad) { - gamemode = shareware; - devparm = true; - D_AddFile (DEVDATA"doom1.wad"); - D_AddFile (DEVMAPS"data_se/texture1.lmp"); - D_AddFile (DEVMAPS"data_se/pnames.lmp"); - strcpy (basedefault,DEVDATA"default.cfg"); + printf("Game mode indeterminate.\n"); + gamemode = indetermined; return; } - if (M_CheckParm ("-regdev")) - { - gamemode = registered; - devparm = true; - D_AddFile (DEVDATA"doom.wad"); - D_AddFile (DEVMAPS"data_se/texture1.lmp"); - D_AddFile (DEVMAPS"data_se/texture2.lmp"); - D_AddFile (DEVMAPS"data_se/pnames.lmp"); - strcpy (basedefault,DEVDATA"default.cfg"); - return; - } - - if (M_CheckParm ("-comdev")) - { - gamemode = commercial; - devparm = true; - /* I don't bother - if(plutonia) - D_AddFile (DEVDATA"plutonia.wad"); - else if(tnt) - D_AddFile (DEVDATA"tnt.wad"); - else*/ - D_AddFile (DEVDATA"doom2.wad"); - - D_AddFile (DEVMAPS"cdata/texture1.lmp"); - D_AddFile (DEVMAPS"cdata/pnames.lmp"); - strcpy (basedefault,DEVDATA"default.cfg"); - return; - } - - if (I_PreferDoom1 ()) - { - if (doomuwad) - { - gamemode = retail; - D_AddFile (doomuwad); - return; - } - if (doomwad) - { - gamemode = registered; - D_AddFile (doomwad); - return; - } - if (freedoom1wad) - { - gamemode = retail; - D_AddFile (freedoom1wad); - return; - } - } - - if (doom2fwad) - { - gamemode = commercial; - // C'est ridicule! - // Let's handle languages in config files, okay? - language = french; - printf("French version\n"); - D_AddFile (doom2fwad); - return; - } - - if (doom2wad) - { - gamemode = commercial; - D_AddFile (doom2wad); - return; - } - - if (plutoniawad) - { - gamemode = commercial; - D_AddFile (plutoniawad); - return; - } - - if (tntwad) - { - gamemode = commercial; - D_AddFile (tntwad); - return; - } - - if (doomuwad) - { - gamemode = retail; - D_AddFile (doomuwad); - return; - } - - if (doomwad) - { - gamemode = registered; - D_AddFile (doomwad); - return; - } - - if (freedoom2wad) - { - gamemode = commercial; - D_AddFile (freedoom2wad); - return; - } - - if (freedoom1wad) - { - gamemode = retail; - D_AddFile (freedoom1wad); - return; - } - - if (doom1wad) - { - gamemode = shareware; - D_AddFile (doom1wad); - return; - } - - printf("Game mode indeterminate.\n"); - gamemode = indetermined; - - // We don't abort. Let's see what the PWAD contains. - //exit(1); - //I_Error ("Game mode indeterminate\n"); + gamemode = iwad->mode; + gamemission = iwad->mission; + language = iwad->language; + savegameprefix = iwad->savename; + D_AddFile (iwad->path); } // @@ -1158,7 +988,7 @@ void D_DoomMain (void) p = M_CheckParm ("-loadgame"); if (p && p < myargc-1) { - sprintf(file, "%s/"SAVEGAMENAME"%c.dsg",I_GetSaveDir(),myargv[p+1][0]); + sprintf(file, "%s%c.dsg",savegameprefix,myargv[p+1][0]); G_LoadGame (file); } diff --git a/src/doom/doomstat.h b/src/doom/doomstat.h index 65f5225..5a3e88c 100644 --- a/src/doom/doomstat.h +++ b/src/doom/doomstat.h @@ -242,6 +242,7 @@ extern int maxammo[NUMAMMO]; // File handling stuff. extern char basedefault[1024]; +extern char* savegameprefix; extern FILE* debugfile; // if true, load all graphics at level load diff --git a/src/doom/dstrings.h b/src/doom/dstrings.h index 291767b..2a5e96b 100644 --- a/src/doom/dstrings.h +++ b/src/doom/dstrings.h @@ -37,8 +37,6 @@ #include "d_englsh.h" #endif -// Misc. other strings. -#define SAVEGAMENAME "doomsav" // @@ -47,7 +45,6 @@ // Path names are OS-sensitive. // #define DEVMAPS "devmaps" -#define DEVDATA "devdata" // Not done in french? diff --git a/src/doom/f_finale.c b/src/doom/f_finale.c index a6e74dd..e3229ec 100644 --- a/src/doom/f_finale.c +++ b/src/doom/f_finale.c @@ -61,26 +61,9 @@ char* e2text = E2TEXT; char* e3text = E3TEXT; char* e4text = E4TEXT; -char* c1text = C1TEXT; -char* c2text = C2TEXT; -char* c3text = C3TEXT; -char* c4text = C4TEXT; -char* c5text = C5TEXT; -char* c6text = C6TEXT; - -char* p1text = P1TEXT; -char* p2text = P2TEXT; -char* p3text = P3TEXT; -char* p4text = P4TEXT; -char* p5text = P5TEXT; -char* p6text = P6TEXT; - -char* t1text = T1TEXT; -char* t2text = T2TEXT; -char* t3text = T3TEXT; -char* t4text = T4TEXT; -char* t5text = T5TEXT; -char* t6text = T6TEXT; +char* doom2text[] = {C1TEXT, C2TEXT, C3TEXT, C4TEXT, C5TEXT, C6TEXT}; +char* plutoniatext[] = {P1TEXT, P2TEXT, P3TEXT, P4TEXT, P5TEXT, P6TEXT}; +char* tnttext[] = {T1TEXT, T2TEXT, T3TEXT, T4TEXT, T5TEXT, T6TEXT}; char* finaletext; char* finaleflat; @@ -141,33 +124,36 @@ void F_StartFinale (void) // DOOM II and missions packs with E1, M34 case commercial: { + char** text = gamemission == pack_tnt ? tnttext + : gamemission == pack_plut ? plutoniatext : doom2text; + S_ChangeMusic(mus_read_m, true); switch (gamemap) { case 6: finaleflat = "SLIME16"; - finaletext = c1text; + finaletext = text[0]; break; case 11: finaleflat = "RROCK14"; - finaletext = c2text; + finaletext = text[1]; break; case 20: finaleflat = "RROCK07"; - finaletext = c3text; + finaletext = text[2]; break; case 30: finaleflat = "RROCK17"; - finaletext = c4text; + finaletext = text[3]; break; case 15: finaleflat = "RROCK13"; - finaletext = c5text; + finaletext = text[4]; break; case 31: finaleflat = "RROCK19"; - finaletext = c6text; + finaletext = text[5]; break; default: // Ouch. @@ -181,7 +167,7 @@ void F_StartFinale (void) default: S_ChangeMusic(mus_read_m, true); finaleflat = "F_SKY1"; // Not used anywhere else. - finaletext = c1text; // FIXME - other text, music? + finaletext = doom2text[0]; // FIXME - other text, music? break; } diff --git a/src/doom/g_game.c b/src/doom/g_game.c index d38fcff..d14a091 100644 --- a/src/doom/g_game.c +++ b/src/doom/g_game.c @@ -454,9 +454,7 @@ void G_DoLoadLevel (void) // DOOM determines the sky texture to be used // depending on the current episode, and the game version. - if ( (gamemode == commercial) - || ( gamemode == pack_tnt ) - || ( gamemode == pack_plut ) ) + if (gamemode == commercial) { skytexture = R_TextureNumForName ("SKY3"); if (gamemap < 12) @@ -1272,7 +1270,7 @@ void G_DoSaveGame (void) int length; int i; - sprintf (name,"%s/"SAVEGAMENAME"%d.dsg",I_GetSaveDir(),savegameslot); + sprintf (name,"%s%d.dsg",savegameprefix,savegameslot); description = savedescription; save_p = savebuffer = Z_Malloc (SAVEGAMESIZE, PU_STATIC, NULL); diff --git a/src/doom/hu_stuff.c b/src/doom/hu_stuff.c index efb1396..5b98a2f 100644 --- a/src/doom/hu_stuff.c +++ b/src/doom/hu_stuff.c @@ -451,18 +451,10 @@ void HU_Start(void) s = HU_TITLE; break; -/* FIXME - case pack_plut: - s = HU_TITLEP; - break; - case pack_tnt: - s = HU_TITLET; - break; -*/ - case commercial: default: - s = HU_TITLE2; + s = gamemission == pack_plut ? HU_TITLEP + : gamemission == pack_tnt ? HU_TITLET : HU_TITLE2; break; } diff --git a/src/doom/i_system.h b/src/doom/i_system.h index 848ef91..7937bb2 100644 --- a/src/doom/i_system.h +++ b/src/doom/i_system.h @@ -25,6 +25,7 @@ #include "d_ticcmd.h" #include "d_event.h" +#include "doomdef.h" #ifdef __GNUG__ #pragma interface @@ -88,14 +89,21 @@ void I_Tactile (int on, int off, int total); void I_Error (char *error, ...) __attribute__((noreturn)); -// NULL-terminated list of directories searched for IWADs. -char** I_GetWadDirs (void); - // Directory for the config file and save games. char* I_GetSaveDir (void); -// True when the player asked for the Doom 1 style game at startup. -boolean I_PreferDoom1 (void); +// The game to play: its IWAD and how the engine treats it. +typedef struct +{ + char* path; + char* savename; // save game path up to the slot number + GameMode_t mode; + GameMission_t mission; + Language_t language; +} iwad_t; + +// Runs the launcher and returns the chosen game, or NULL when there is none. +iwad_t* I_ChooseIwad (void); #endif diff --git a/src/doom/m_menu.c b/src/doom/m_menu.c index bcc805d..68be1cc 100644 --- a/src/doom/m_menu.c +++ b/src/doom/m_menu.c @@ -517,7 +517,7 @@ void M_ReadSaveStrings(void) for (i = 0;i < load_end;i++) { - sprintf(name,"%s/"SAVEGAMENAME"%d.dsg",I_GetSaveDir(),i); + sprintf(name,"%s%d.dsg",savegameprefix,i); handle = open (name, O_RDONLY | 0, 0666); if (handle == -1) @@ -577,7 +577,7 @@ void M_LoadSelect(int choice) { char name[256]; - sprintf(name,"%s/"SAVEGAMENAME"%d.dsg",I_GetSaveDir(),choice); + sprintf(name,"%s%d.dsg",savegameprefix,choice); G_LoadGame (name); M_ClearMenus (); } diff --git a/src/doom/v_video.c b/src/doom/v_video.c index 9b30e64..3a3d540 100644 --- a/src/doom/v_video.c +++ b/src/doom/v_video.c @@ -216,13 +216,17 @@ V_DrawPatch byte* source; int w; - y -= SHORT(patch->topoffset); - x -= SHORT(patch->leftoffset); -#ifdef RANGECHECK - if (x<0 - ||x+SHORT(patch->width) >SCREENWIDTH + y -= SHORT(patch->topoffset); + x -= SHORT(patch->leftoffset); + w = SHORT(patch->width); + + // Widescreen pictures from the newer releases: show their middle. + if (w > SCREENWIDTH) + x = (SCREENWIDTH - w) / 2; +#ifdef RANGECHECK + if ((w <= SCREENWIDTH && (x<0 || x+w >SCREENWIDTH)) || y<0 - || y+SHORT(patch->height)>SCREENHEIGHT + || y+SHORT(patch->height)>SCREENHEIGHT || (unsigned)scrn>4) { fprintf( stderr, "Patch at %d,%d exceeds LFB\n", x,y ); @@ -230,19 +234,17 @@ V_DrawPatch fprintf( stderr, "V_DrawPatch: bad patch (ignored)\n"); return; } -#endif - +#endif + if (!scrn) - V_MarkRect (x, y, SHORT(patch->width), SHORT(patch->height)); + V_MarkRect (x < 0 ? 0 : x, y, w < SCREENWIDTH ? w : SCREENWIDTH, SHORT(patch->height)); - col = 0; - desttop = screens[scrn]+y*SCREENWIDTH+x; - - w = SHORT(patch->width); - - for ( ; colcolumnofs[col])); + for (col = 0; col= SCREENWIDTH) + continue; + desttop = screens[scrn]+y*SCREENWIDTH+x; + column = (column_t *)((byte *)patch + LONG(patch->columnofs[col])); // step through the posts in a column while (column->topdelta != 0xff ) diff --git a/src/host/host.c b/src/host/host.c index ac9b593..3e5b14e 100644 --- a/src/host/host.c +++ b/src/host/host.c @@ -1,4 +1,5 @@ #define _GNU_SOURCE +#include #include #include #include @@ -14,7 +15,7 @@ #define AUDIO_BLOCK 480 -static const char *wad_dirs[2]; +static const char *wad_dirs[3]; static const char *save_dir; static const char *out_dir; static int frame; @@ -39,12 +40,14 @@ int plat_init(int argc, char **argv) (void)argc; (void)argv; wad_dirs[0] = realpath(env_or("DOOM_WADDIR", "wads"), NULL); - wad_dirs[1] = NULL; save_dir = env_or("DOOM_SAVEDIR", "/tmp/doom-save"); + wad_dirs[1] = save_dir; out_dir = realpath(env_or("DOOM_OUT", "out"), NULL); test_plan_limit(atoi(env_or("DOOM_FRAMES", "0"))); test_plan_captures(getenv("DOOM_CAPTURE")); test_plan_input(getenv("DOOM_INPUT")); + test_plan_set_game(getenv("DOOM_IWAD")); + test_plan_set_text(getenv("DOOM_TEXT")); mkdir(save_dir, 0755); if (!wad_dirs[0] || !out_dir) { @@ -102,6 +105,39 @@ const char *plat_save_dir(void) return save_dir; } +const char *plat_wad_folder(void) +{ + return wad_dirs[0]; +} + +int plat_list_dir(const char *path, plat_dir_fn fn, void *user) +{ + DIR *dir = opendir(path); + struct dirent *entry; + + if (!dir) + return -1; + while ((entry = readdir(dir))) + if (strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) + fn(entry->d_name, user); + closedir(dir); + return 0; +} + +int plat_text_open(const char *title, const char *text) +{ + fprintf(stderr, "host: keyboard \"%s\" opened with \"%s\"\n", title, text); + return 0; +} + +int plat_text_poll(char *text, int size) +{ + if (!test_plan_text()) + return -1; + snprintf(text, size, "%s", test_plan_text()); + return 1; +} + int plat_video_init(int width, int height) { video_width = width; diff --git a/src/host/http.c b/src/host/http.c new file mode 100644 index 0000000..2d746cc --- /dev/null +++ b/src/host/http.c @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include + +#include "platform.h" + +#define BUFFER_LIMIT (1 << 20) + +struct plat_http +{ + CURL *easy; + CURLM *multi; + char *data; + size_t used, read_at; + int paused, finished; + CURLcode result; +}; + +static pthread_once_t once = PTHREAD_ONCE_INIT; + +static void start_curl(void) +{ + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +static size_t receive(char *chunk, size_t size, size_t count, void *user) +{ + plat_http_t *http = user; + size_t bytes = size * count; + char *grown; + + if (http->used - http->read_at >= BUFFER_LIMIT) + { + http->paused = 1; + return CURL_WRITEFUNC_PAUSE; + } + if (http->read_at) + { + memmove(http->data, http->data + http->read_at, http->used - http->read_at); + http->used -= http->read_at; + http->read_at = 0; + } + grown = realloc(http->data, http->used + bytes); + if (!grown) + return 0; + http->data = grown; + memcpy(http->data + http->used, chunk, bytes); + http->used += bytes; + return bytes; +} + +static void pump(plat_http_t *http) +{ + CURLMsg *message; + int running, left; + + if (http->paused) + { + http->paused = 0; + curl_easy_pause(http->easy, CURLPAUSE_CONT); + } + curl_multi_perform(http->multi, &running); + while ((message = curl_multi_info_read(http->multi, &left))) + if (message->msg == CURLMSG_DONE) + { + http->finished = 1; + http->result = message->data.result; + } + if (!http->finished && http->used == http->read_at) + curl_multi_poll(http->multi, NULL, 0, 100, NULL); +} + +plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *info, + char *error, int error_size) +{ + plat_http_t *http = calloc(1, sizeof(*http)); + char range[32]; + long status = 0; + curl_off_t length = -1; + const char *type = NULL; + + pthread_once(&once, start_curl); + if (!http) + return NULL; + http->easy = curl_easy_init(); + http->multi = curl_multi_init(); + curl_easy_setopt(http->easy, CURLOPT_URL, url); + curl_easy_setopt(http->easy, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(http->easy, CURLOPT_MAXREDIRS, 8L); + curl_easy_setopt(http->easy, CURLOPT_CONNECTTIMEOUT, 10L); + curl_easy_setopt(http->easy, CURLOPT_LOW_SPEED_LIMIT, 1L); + curl_easy_setopt(http->easy, CURLOPT_LOW_SPEED_TIME, 30L); + curl_easy_setopt(http->easy, CURLOPT_USERAGENT, "DOOM-PS5"); + curl_easy_setopt(http->easy, CURLOPT_WRITEFUNCTION, receive); + curl_easy_setopt(http->easy, CURLOPT_WRITEDATA, http); + if (offset) + { + snprintf(range, sizeof(range), "%llu-", (unsigned long long)offset); + curl_easy_setopt(http->easy, CURLOPT_RANGE, range); + } + curl_multi_add_handle(http->multi, http->easy); + + while (!http->finished && !http->used) + pump(http); + curl_easy_getinfo(http->easy, CURLINFO_RESPONSE_CODE, &status); + if (http->finished && http->result != CURLE_OK) + { + snprintf(error, error_size, "%s", curl_easy_strerror(http->result)); + plat_http_close(http); + return NULL; + } + if (status != 200 && status != 206) + { + snprintf(error, error_size, "the server answered %ld", status); + plat_http_close(http); + return NULL; + } + curl_easy_getinfo(http->easy, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &length); + curl_easy_getinfo(http->easy, CURLINFO_CONTENT_TYPE, &type); + info->status = (int)status; + info->length = length; + snprintf(info->type, sizeof(info->type), "%s", type ? type : ""); + return http; +} + +int plat_http_read(plat_http_t *http, void *buffer, int size) +{ + size_t available; + + while (http->used == http->read_at && !http->finished) + pump(http); + available = http->used - http->read_at; + if (!available) + return http->result == CURLE_OK ? 0 : -1; + if (available > (size_t)size) + available = (size_t)size; + memcpy(buffer, http->data + http->read_at, available); + http->read_at += available; + return (int)available; +} + +void plat_http_close(plat_http_t *http) +{ + if (!http) + return; + curl_multi_remove_handle(http->multi, http->easy); + curl_easy_cleanup(http->easy); + curl_multi_cleanup(http->multi); + free(http->data); + free(http); +} diff --git a/src/launcher/games.c b/src/launcher/games.c new file mode 100644 index 0000000..2df2398 --- /dev/null +++ b/src/launcher/games.c @@ -0,0 +1,208 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "games.h" +#include "platform.h" + +#define LUMP_ENTRY 16 +#define MAX_LUMPS 100000 + +static const struct +{ + const char *file; + game_kind_t kind; + const char *title; +} known[] = { + {"doomu.wad", GAME_ULTIMATE, "THE ULTIMATE DOOM"}, + {"doom.wad", GAME_DOOM, "DOOM"}, + {"doom2.wad", GAME_DOOM2, "DOOM II: HELL ON EARTH"}, + {"doom2f.wad", GAME_DOOM2_FRENCH, "DOOM II: L'ENFER SUR TERRE"}, + {"tnt.wad", GAME_TNT, "FINAL DOOM: TNT EVILUTION"}, + {"plutonia.wad", GAME_PLUTONIA, "FINAL DOOM: THE PLUTONIA EXPERIMENT"}, + {"freedoom1.wad", GAME_FREEDOOM1, "FREEDOOM: PHASE 1"}, + {"freedoom2.wad", GAME_FREEDOOM2, "FREEDOOM: PHASE 2"}, + {"doom1.wad", GAME_SHAREWARE, "DOOM SHAREWARE: EPISODE 1"}, +}; + +#define KNOWN (int)(sizeof(known) / sizeof(known[0])) + +typedef struct +{ + game_list_t *list; + const char *dir; +} scan_t; + +const char *games_known_file(const char *name) +{ + int i; + + for (i = 0; i < KNOWN; i++) + if (!strcasecmp(name, known[i].file)) + return known[i].file; + return NULL; +} + +static int has_suffix(const char *name, const char *suffix) +{ + size_t length = strlen(name), suffix_length = strlen(suffix); + + return length > suffix_length && !strcasecmp(name + length - suffix_length, suffix); +} + +int games_is_archive(const char *name) +{ + return has_suffix(name, ".zip") || has_suffix(name, ".7z") || has_suffix(name, ".rar"); +} + +static uint32_t read_le32(const uint8_t *p) +{ + return p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24; +} + +int games_check_wad(const char *path, int *has_episode4) +{ + uint8_t header[12]; + uint8_t *directory = NULL; + uint32_t lumps, offset; + off_t size; + int fd = open(path, O_RDONLY), ok = 0; + + if (fd < 0) + return -1; + size = lseek(fd, 0, SEEK_END); + if (lseek(fd, 0, SEEK_SET) == 0 && read(fd, header, sizeof(header)) == sizeof(header) && + !memcmp(header, "IWAD", 4)) + { + lumps = read_le32(header + 4); + offset = read_le32(header + 8); + ok = lumps > 0 && lumps < MAX_LUMPS && (off_t)offset + (off_t)lumps * LUMP_ENTRY <= size; + } + if (ok && has_episode4) + { + uint32_t i; + + *has_episode4 = 0; + directory = malloc((size_t)lumps * LUMP_ENTRY); + ok = directory && lseek(fd, offset, SEEK_SET) == (off_t)offset && + read(fd, directory, (size_t)lumps * LUMP_ENTRY) == (ssize_t)lumps * LUMP_ENTRY; + for (i = 0; ok && i < lumps; i++) + if (!strncmp((const char *)directory + i * LUMP_ENTRY + 8, "E4M1", 8)) + *has_episode4 = 1; + free(directory); + } + close(fd); + return ok ? 0 : -1; +} + +static void add_game(game_list_t *list, int index, const char *path) +{ + game_kind_t kind = known[index].kind; + const char *title = known[index].title; + int episode4 = 0, i; + + if (list->count == MAX_GAMES) + return; + if (games_check_wad(path, kind == GAME_DOOM ? &episode4 : NULL)) + { + plat_log("launcher: %s is not a usable IWAD\n", path); + return; + } + if (episode4) + { + kind = GAME_ULTIMATE; + title = known[0].title; + } + for (i = 0; i < list->count; i++) + if (list->games[i].kind == kind) + return; + list->games[list->count].kind = kind; + list->games[list->count].file = known[index].file; + list->games[list->count].title = title; + snprintf(list->games[list->count].path, GAME_PATH, "%s", path); + list->count++; +} + +static void add_archive(game_list_t *list, const char *path) +{ + int fd; + + if (list->archive_count == MAX_ARCHIVES || (fd = open(path, O_RDONLY)) < 0) + return; + list->archive_sizes[list->archive_count] = lseek(fd, 0, SEEK_END); + close(fd); + snprintf(list->archives[list->archive_count], GAME_PATH, "%s", path); + list->archive_count++; +} + +static void on_entry(const char *name, void *user) +{ + scan_t *scan = user; + char path[GAME_PATH]; + int i; + + snprintf(path, sizeof(path), "%s/%s", scan->dir, name); + for (i = 0; i < KNOWN; i++) + if (!strcasecmp(name, known[i].file)) + add_game(scan->list, i, path); + if (games_is_archive(name)) + add_archive(scan->list, path); +} + +static void probe_known(scan_t *scan) +{ + char path[GAME_PATH]; + int i, upper, fd; + + for (i = 0; i < KNOWN; i++) + for (upper = 0; upper < 2; upper++) + { + char *c; + + snprintf(path, sizeof(path), "%s/%s", scan->dir, known[i].file); + for (c = path + strlen(scan->dir) + 1; upper && *c; c++) + *c = (char)toupper((unsigned char)*c); + if ((fd = open(path, O_RDONLY)) >= 0) + { + close(fd); + add_game(scan->list, i, path); + break; + } + } +} + +static int by_kind(const void *a, const void *b) +{ + return (int)((const game_t *)a)->kind - (int)((const game_t *)b)->kind; +} + +void games_scan(game_list_t *list) +{ + const char *const *dir; + scan_t scan; + + memset(list, 0, sizeof(*list)); + scan.list = list; + for (dir = plat_wad_dirs(); *dir; dir++) + { + scan.dir = *dir; + if (plat_list_dir(*dir, on_entry, &scan)) + probe_known(&scan); + } + qsort(list->games, (size_t)list->count, sizeof(list->games[0]), by_kind); +} + +const game_t *games_find(const game_list_t *list, const char *file) +{ + int i; + + for (i = 0; file && i < list->count; i++) + if (!strcasecmp(list->games[i].file, file)) + return &list->games[i]; + return NULL; +} diff --git a/src/launcher/games.h b/src/launcher/games.h new file mode 100644 index 0000000..da735e4 --- /dev/null +++ b/src/launcher/games.h @@ -0,0 +1,44 @@ +#ifndef GAMES_H +#define GAMES_H + +#define MAX_GAMES 16 +#define MAX_ARCHIVES 16 +#define GAME_PATH 512 + +typedef enum +{ + GAME_ULTIMATE, + GAME_DOOM, + GAME_DOOM2, + GAME_DOOM2_FRENCH, + GAME_TNT, + GAME_PLUTONIA, + GAME_FREEDOOM1, + GAME_FREEDOOM2, + GAME_SHAREWARE, +} game_kind_t; + +typedef struct +{ + game_kind_t kind; + const char *file; + const char *title; + char path[GAME_PATH]; +} game_t; + +typedef struct +{ + game_t games[MAX_GAMES]; + int count; + char archives[MAX_ARCHIVES][GAME_PATH]; + long long archive_sizes[MAX_ARCHIVES]; + int archive_count; +} game_list_t; + +const char *games_known_file(const char *name); +int games_is_archive(const char *name); +int games_check_wad(const char *path, int *has_episode4); +void games_scan(game_list_t *list); +const game_t *games_find(const game_list_t *list, const char *file); + +#endif diff --git a/src/launcher/import.c b/src/launcher/import.c new file mode 100644 index 0000000..46bfe71 --- /dev/null +++ b/src/launcher/import.c @@ -0,0 +1,540 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "games.h" +#include "import.h" +#include "platform.h" + +#define CHUNK (256 * 1024) +#define PEEK (64 * 1024) +#define LISTING_LIMIT (2 * 1024 * 1024) +#define DISCARD_LIMIT (2 * 1024 * 1024) + +typedef struct +{ + int remote; + const char *url; + plat_http_t *http; + int fd; + long long position, size; + int ranges; + char type[64]; + uint8_t *pending; + int pending_length, pending_offset; +} source_t; + +typedef int (*fill_fn)(void *context, void *buffer, int size); + +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_t worker; +static int worker_running; +static volatile int cancelled; +static import_status_t status; +static listing_t listing; +static char sources[MAX_SOURCES][LINK_URL]; +static int source_count; +static char dest_dir[GAME_PATH]; +static uint8_t peek_buffer[PEEK], read_buffer[CHUNK], data_buffer[CHUNK], discard_buffer[CHUNK]; + +static void fail(const char *format, ...) +{ + va_list args; + + pthread_mutex_lock(&lock); + va_start(args, format); + vsnprintf(status.message, sizeof(status.message), format, args); + va_end(args); + pthread_mutex_unlock(&lock); + plat_log("import: %s\n", status.message); +} + +static void upper(char *out, size_t size, const char *text) +{ + size_t i; + + for (i = 0; text[i] && i + 1 < size; i++) + out[i] = (char)toupper((unsigned char)text[i]); + out[i] = 0; +} + +static void add_found(const char *file) +{ + char name[32]; + + upper(name, sizeof(name), file); + pthread_mutex_lock(&lock); + if (!strstr(status.found, name)) + { + if (status.found[0]) + strncat(status.found, " ", sizeof(status.found) - strlen(status.found) - 1); + strncat(status.found, name, sizeof(status.found) - strlen(status.found) - 1); + status.found_count++; + } + status.current[0] = 0; + pthread_mutex_unlock(&lock); +} + +static long long logical(const source_t *s) +{ + return s->position - (s->pending_length - s->pending_offset); +} + +static int open_remote(source_t *s, long long offset) +{ + plat_http_info_t info; + char error[128]; + + plat_http_close(s->http); + s->http = plat_http_get(s->url, (uint64_t)offset, &info, error, sizeof(error)); + if (!s->http) + { + fail("%s", error); + return -1; + } + if (info.status == 206) + { + s->ranges = 1; + s->position = offset; + if (s->size < 0 && info.length >= 0) + s->size = offset + info.length; + } + else + { + if (offset) + s->ranges = 0; + s->position = 0; + if (info.length >= 0) + s->size = info.length; + } + if (!offset) + snprintf(s->type, sizeof(s->type), "%s", info.type); + plat_log("import: request from %lld answered %d with %lld bytes\n", offset, info.status, + (long long)info.length); + return 0; +} + +static int raw_read(source_t *s, void *out, int size) +{ + int got; + + if (cancelled) + return -1; + if (s->pending_offset < s->pending_length) + { + got = s->pending_length - s->pending_offset; + if (got > size) + got = size; + memcpy(out, s->pending + s->pending_offset, (size_t)got); + s->pending_offset += got; + return got; + } + if (s->remote) + got = s->http ? plat_http_read(s->http, out, size) : 0; + else + got = (int)read(s->fd, out, (size_t)size); + if (got > 0) + { + s->position += got; + pthread_mutex_lock(&lock); + status.done = s->position; + pthread_mutex_unlock(&lock); + } + return got; +} + +static int discard(source_t *s, long long count) +{ + while (count > 0) + { + int got = raw_read(s, discard_buffer, count < CHUNK ? (int)count : CHUNK); + + if (got <= 0) + return -1; + count -= got; + } + return 0; +} + +static int source_seek(source_t *s, long long target) +{ + long long here = logical(s); + + if (target == here) + return 0; + if (!s->remote) + { + s->pending_length = s->pending_offset = 0; + if (lseek(s->fd, target, SEEK_SET) != target) + return -1; + s->position = target; + return 0; + } + if (target > here && (target - here <= DISCARD_LIMIT || s->ranges == 0)) + return discard(s, target - here); + s->pending_length = s->pending_offset = 0; + if (s->size >= 0 && target >= s->size) + { + plat_http_close(s->http); + s->http = NULL; + s->position = target; + return 0; + } + if (open_remote(s, s->ranges == 0 ? 0 : target)) + return -1; + return discard(s, target - s->position); +} + +static la_ssize_t on_read(struct archive *a, void *user, const void **buffer) +{ + int got = raw_read(user, read_buffer, CHUNK); + + if (got < 0) + { + archive_set_error(a, EIO, cancelled ? "cancelled" : "read failed"); + return -1; + } + *buffer = read_buffer; + return got; +} + +static la_int64_t on_seek(struct archive *a, void *user, la_int64_t offset, int whence) +{ + source_t *s = user; + long long target = offset; + + (void)a; + if (whence == SEEK_CUR) + target += logical(s); + else if (whence == SEEK_END) + { + if (s->size < 0) + return ARCHIVE_FATAL; + target += s->size; + } + if (target < 0 || source_seek(s, target)) + return ARCHIVE_FATAL; + return target; +} + +static int fill_raw(void *context, void *buffer, int size) +{ + return raw_read(context, buffer, size); +} + +static int fill_archive(void *context, void *buffer, int size) +{ + la_ssize_t got = archive_read_data(context, buffer, (size_t)size); + + if (got < 0) + plat_log("import: %s\n", archive_error_string(context) ? archive_error_string(context) : "read error"); + return got < 0 ? -1 : (int)got; +} + +static int write_all(int fd, const uint8_t *data, int size) +{ + while (size > 0) + { + ssize_t written = write(fd, data, (size_t)size); + + if (written <= 0) + return -1; + data += written; + size -= (int)written; + } + return 0; +} + +static int already_have(const char *path, long long size) +{ + int fd = open(path, O_RDONLY); + long long existing; + + if (fd < 0) + return 0; + existing = lseek(fd, 0, SEEK_END); + close(fd); + return size >= 0 && existing == size && games_check_wad(path, NULL) == 0; +} + +static int write_game(const char *file, fill_fn fill, void *context, long long size) +{ + char path[GAME_PATH], part[GAME_PATH + 8], name[32]; + int fd, got; + + snprintf(path, sizeof(path), "%s/%s", dest_dir, file); + snprintf(part, sizeof(part), "%s.part", path); + upper(name, sizeof(name), file); + if (already_have(path, size)) + { + add_found(file); + return 0; + } + pthread_mutex_lock(&lock); + snprintf(status.current, sizeof(status.current), "%s", name); + pthread_mutex_unlock(&lock); + + fd = open(part, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + { + fail("cannot write %s (error %d)", part, errno); + return -1; + } + while ((got = fill(context, data_buffer, CHUNK)) > 0) + if (write_all(fd, data_buffer, got)) + { + fail(errno == ENOSPC ? "not enough space for %s" : "writing %s failed", name); + close(fd); + unlink(part); + return -1; + } + close(fd); + if (got < 0) + { + if (!cancelled) + fail("reading %s failed", name); + unlink(part); + return -1; + } + if (games_check_wad(part, NULL)) + { + fail("%s is not a valid game wad", name); + unlink(part); + return -1; + } + unlink(path); + if (rename(part, path)) + { + fail("cannot rename %s (error %d)", part, errno); + unlink(part); + return -1; + } + plat_log("import: wrote %s\n", path); + add_found(file); + return 0; +} + +static int read_listing(source_t *s) +{ + char base[LINK_URL]; + char *html = malloc(LISTING_LIMIT + 1); + int used = 0, got; + + if (!html) + return -1; + while (used < LISTING_LIMIT && (got = raw_read(s, html + used, LISTING_LIMIT - used)) > 0) + used += got; + html[used] = 0; + url_directory(base, sizeof(base), s->url); + listing_parse(&listing, base, html, (size_t)used); + free(html); + pthread_mutex_lock(&lock); + status.state = IMPORT_LISTING; + pthread_mutex_unlock(&lock); + return 0; +} + +static int extract(source_t *s, const char *name) +{ + struct archive *a = archive_read_new(); + struct archive_entry *entry; + int result = 0, r; + + archive_read_support_format_zip(a); + archive_read_support_format_7zip(a); + archive_read_support_format_rar(a); + archive_read_support_format_rar5(a); + archive_read_set_read_callback(a, on_read); + archive_read_set_seek_callback(a, on_seek); + archive_read_set_callback_data(a, s); + if (archive_read_open1(a) != ARCHIVE_OK) + { + if (!cancelled) + fail("%s is not a wad, zip, 7z or rar file", name); + archive_read_free(a); + return -1; + } + while (!cancelled && (r = archive_read_next_header(a, &entry)) == ARCHIVE_OK) + { + const char *path = archive_entry_pathname_utf8(entry); + const char *base, *file; + + if (!path) + path = archive_entry_pathname(entry); + if (!path || archive_entry_filetype(entry) != AE_IFREG) + continue; + base = path + strlen(path); + while (base > path && base[-1] != '/' && base[-1] != '\\') + base--; + file = games_known_file(base); + if (!file || !strcmp(file, "doom1.wad")) + continue; + if (write_game(file, fill_archive, a, + archive_entry_size_is_set(entry) ? archive_entry_size(entry) : -1)) + { + result = -1; + break; + } + } + if (!result && !cancelled && r != ARCHIVE_EOF) + { + fail("%s: %s", name, archive_error_string(a) ? archive_error_string(a) : "unreadable"); + result = -1; + } + archive_read_free(a); + return result; +} + +static int run_source(const char *location, int single) +{ + source_t s; + char name[LINK_NAME]; + const char *file; + int result = -1, i; + + memset(&s, 0, sizeof(s)); + s.fd = -1; + s.size = -1; + s.ranges = -1; + s.remote = strstr(location, "://") != NULL; + s.url = location; + if (s.remote) + url_file_name(name, sizeof(name), location); + else + snprintf(name, sizeof(name), "%s", strrchr(location, '/') ? strrchr(location, '/') + 1 : location); + + pthread_mutex_lock(&lock); + upper(status.source, sizeof(status.source), name[0] ? name : location); + status.done = 0; + status.total = -1; + pthread_mutex_unlock(&lock); + + if (s.remote) + { + if (open_remote(&s, 0)) + return -1; + } + else + { + if ((s.fd = open(location, O_RDONLY)) < 0) + { + fail("cannot open %s", location); + return -1; + } + s.size = lseek(s.fd, 0, SEEK_END); + lseek(s.fd, 0, SEEK_SET); + } + pthread_mutex_lock(&lock); + status.total = s.size; + pthread_mutex_unlock(&lock); + + s.pending = peek_buffer; + s.pending_length = raw_read(&s, peek_buffer, PEEK); + for (i = 0; i < s.pending_length && isspace(peek_buffer[i]); i++) + ; + if (s.pending_length < 0) + { + if (!cancelled) + fail("reading %s failed", name); + } + else if (s.remote && single && (strstr(s.type, "text/html") || (i < s.pending_length && peek_buffer[i] == '<'))) + result = read_listing(&s); + else if (s.pending_length >= 4 && !memcmp(peek_buffer, "PWAD", 4)) + fail("%s is an add-on (pwad), not a game", name); + else if (s.pending_length >= 4 && !memcmp(peek_buffer, "IWAD", 4)) + { + if (!(file = games_known_file(name))) + fail("%s is not a game doom knows", name); + else + result = write_game(file, fill_raw, &s, s.size); + } + else + result = extract(&s, name); + + plat_http_close(s.http); + if (s.fd >= 0) + close(s.fd); + return result; +} + +static void *work(void *arg) +{ + int failed = 0, i; + + (void)arg; + for (i = 0; i < source_count && !cancelled; i++) + { + pthread_mutex_lock(&lock); + status.source_index = i; + pthread_mutex_unlock(&lock); + if (run_source(sources[i], source_count == 1)) + failed = 1; + if (status.state == IMPORT_LISTING) + return NULL; + } + pthread_mutex_lock(&lock); + status.current[0] = 0; + status.state = cancelled ? IMPORT_CANCELLED + : failed && !status.found_count ? IMPORT_FAILED : IMPORT_DONE; + pthread_mutex_unlock(&lock); + return NULL; +} + +void import_start(char locations[][LINK_URL], int count, const char *dest) +{ + int i; + + import_finish(); + memset(&status, 0, sizeof(status)); + for (i = 0; i < count && i < MAX_SOURCES; i++) + snprintf(sources[i], LINK_URL, "%s", locations[i]); + source_count = i; + snprintf(dest_dir, sizeof(dest_dir), "%s", dest); + status.state = IMPORT_RUNNING; + status.source_count = source_count; + cancelled = 0; + if (pthread_create(&worker, NULL, work, NULL)) + { + status.state = IMPORT_FAILED; + snprintf(status.message, sizeof(status.message), "cannot start the import"); + return; + } + worker_running = 1; +} + +void import_status(import_status_t *out) +{ + pthread_mutex_lock(&lock); + *out = status; + pthread_mutex_unlock(&lock); +} + +const listing_t *import_listing(void) +{ + return &listing; +} + +void import_cancel(void) +{ + cancelled = 1; +} + +void import_finish(void) +{ + if (worker_running) + { + pthread_join(worker, NULL); + worker_running = 0; + } + status.state = IMPORT_IDLE; +} diff --git a/src/launcher/import.h b/src/launcher/import.h new file mode 100644 index 0000000..d5d72f6 --- /dev/null +++ b/src/launcher/import.h @@ -0,0 +1,36 @@ +#ifndef IMPORT_H +#define IMPORT_H + +#include "listing.h" + +#define MAX_SOURCES MAX_LINKS + +typedef enum +{ + IMPORT_IDLE, + IMPORT_RUNNING, + IMPORT_LISTING, + IMPORT_DONE, + IMPORT_FAILED, + IMPORT_CANCELLED, +} import_state_t; + +typedef struct +{ + import_state_t state; + long long done, total; + int source_index, source_count; + char source[LINK_NAME]; + char current[32]; + char found[192]; + int found_count; + char message[192]; +} import_status_t; + +void import_start(char sources[][LINK_URL], int count, const char *dest); +void import_status(import_status_t *status); +const listing_t *import_listing(void); +void import_cancel(void); +void import_finish(void); + +#endif diff --git a/src/launcher/import_memory.c b/src/launcher/import_memory.c new file mode 100644 index 0000000..584fdc4 --- /dev/null +++ b/src/launcher/import_memory.c @@ -0,0 +1,94 @@ +#include +#include +#include + +#define MAPPED_FROM (256 * 1024) +#define HEADER 16 + +typedef struct +{ + size_t size; + size_t mapped; +} header_t; + +_Static_assert(sizeof(header_t) <= HEADER, "allocation header must fit its slot"); + +void *import_malloc(size_t size) +{ + header_t *header; + + if (size > (size_t)-1 - HEADER) + return NULL; + if (size + HEADER >= MAPPED_FROM) + { + header = mmap(NULL, size + HEADER, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (header == MAP_FAILED) + return NULL; + header->mapped = 1; + } + else + { + if (!(header = malloc(size + HEADER))) + return NULL; + header->mapped = 0; + } + header->size = size; + return (char *)header + HEADER; +} + +void import_free(void *pointer) +{ + header_t *header; + + if (!pointer) + return; + header = (header_t *)((char *)pointer - HEADER); + if (header->mapped) + munmap(header, header->size + HEADER); + else + free(header); +} + +void *import_calloc(size_t count, size_t size) +{ + void *pointer; + + if (size && count > (size_t)-1 / size) + return NULL; + pointer = import_malloc(count * size); + if (pointer && count * size + HEADER < MAPPED_FROM) + memset(pointer, 0, count * size); + return pointer; +} + +void *import_realloc(void *pointer, size_t size) +{ + header_t *header; + void *grown; + + if (!pointer) + return import_malloc(size); + if (!size) + { + import_free(pointer); + return NULL; + } + header = (header_t *)((char *)pointer - HEADER); + if (size <= header->size) + return pointer; + if (!(grown = import_malloc(size))) + return NULL; + memcpy(grown, pointer, header->size); + import_free(pointer); + return grown; +} + +char *import_strdup(const char *text) +{ + size_t length = strlen(text) + 1; + char *copy = import_malloc(length); + + if (copy) + memcpy(copy, text, length); + return copy; +} diff --git a/src/launcher/launcher.c b/src/launcher/launcher.c new file mode 100644 index 0000000..98d2f78 --- /dev/null +++ b/src/launcher/launcher.c @@ -0,0 +1,534 @@ +#include +#include +#include +#include +#include + +#include "games.h" +#include "import.h" +#include "launcher.h" +#include "listing.h" +#include "platform.h" +#include "screen.h" +#include "settings.h" +#include "test_plan.h" + +#define FRAME_US 16667 +#define REPEAT_DELAY 18 +#define REPEAT_RATE 5 +#define STICK 16000 +#define ROW 12 +#define SKULL_FRAMES 14 +#define GAME_ROWS 7 +#define BROWSE_ROWS 12 +#define FOOTER_Y 188 +#define MB (1024.0 * 1024.0) + +typedef enum +{ + VIEW_GAMES, + VIEW_ADD, + VIEW_NOTICE, + VIEW_BROWSE, + VIEW_IMPORT, +} view_t; + +static game_list_t games; +static settings_t settings; +static listing_t browse; +static char import_dir[GAME_PATH]; +static char status_line[192]; +static ink_t status_ink; +static char pending[MAX_ARCHIVES][LINK_URL]; +static long long pending_sizes[MAX_ARCHIVES]; +static int pending_count; +static view_t view, import_return; +static int game_row, add_row, notice_row, browse_row; +static int frame, typing, understood, auto_import; +static uint32_t held, pressed; +static int repeat_timer, input_locked = 1; +static const game_t *chosen; + +static void read_input(void) +{ + pad_state_t pad; + uint32_t now = 0; + + if (plat_pad_read(&pad) == 0) + { + now = pad.buttons; + if (pad.ly < -STICK) + now |= PAD_UP; + if (pad.ly > STICK) + now |= PAD_DOWN; + } + pressed = 0; + if (input_locked) + { + input_locked = now != 0; + held = now; + return; + } + pressed = now & ~held; + if (now & (PAD_UP | PAD_DOWN)) + { + if (pressed & (PAD_UP | PAD_DOWN)) + repeat_timer = REPEAT_DELAY; + else if (--repeat_timer <= 0) + { + pressed |= now & (PAD_UP | PAD_DOWN); + repeat_timer = REPEAT_RATE; + } + } + held = now; +} + +static void move(int *row, int count) +{ + if (count <= 0) + return; + if (pressed & PAD_UP) + *row = (*row + count - 1) % count; + if (pressed & PAD_DOWN) + *row = (*row + 1) % count; +} + +static void present(void) +{ + static uint64_t next; + uint64_t now; + + screen_present(); + now = plat_ticks_us(); + if (next > now) + plat_sleep_us((uint32_t)(next - now)); + next = (next > now ? next : now) + FRAME_US; + frame++; +} + +static void draw_row(int x, int y, const char *text, int selected, int enabled) +{ + if (selected) + screen_skull(x - 14, y - 2, frame / SKULL_FRAMES); + screen_text(x, y, text, !enabled ? INK_GRAY : selected ? INK_WHITE : INK_RED, 1); +} + +static void set_status(const char *text, ink_t ink) +{ + snprintf(status_line, sizeof(status_line), "%s", text); + status_ink = ink; +} + +static void draw_status(int y, int wrap) +{ + if (!status_line[0]) + return; + if (wrap) + screen_wrap(16, y, SCREEN_WIDTH - 32, status_line, status_ink); + else + screen_text_fit(16, y, SCREEN_WIDTH - 32, status_line, status_ink, 0); +} + +static void choose_import_dir(void) +{ + const char *const *dir; + char probe[GAME_PATH + 16]; + int fd; + + for (dir = plat_wad_dirs(); *dir; dir++) + { + snprintf(probe, sizeof(probe), "%s/.write-test", *dir); + if ((fd = open(probe, O_WRONLY | O_CREAT | O_TRUNC, 0644)) >= 0) + { + close(fd); + unlink(probe); + snprintf(import_dir, sizeof(import_dir), "%s", *dir); + plat_log("launcher: imports go to %s\n", import_dir); + return; + } + } + snprintf(import_dir, sizeof(import_dir), "%s", plat_save_dir()); + plat_log("launcher: no writable game folder, imports go to %s\n", import_dir); +} + +static void start_import(char sources[][LINK_URL], int count, view_t back, int automatic) +{ + import_start(sources, count, import_dir); + import_return = back; + auto_import = automatic; + view = VIEW_IMPORT; +} + +static void start_auto_import(void) +{ + int i; + + pending_count = 0; + for (i = 0; i < games.archive_count; i++) + if (!settings_was_imported(&settings, games.archives[i], games.archive_sizes[i])) + { + snprintf(pending[pending_count], LINK_URL, "%s", games.archives[i]); + pending_sizes[pending_count++] = games.archive_sizes[i]; + } + if (pending_count) + start_import(pending, pending_count, VIEW_GAMES, 1); +} + +static void games_view(void) +{ + int count = games.count + 1, width = 0, first, x, i; + + move(&game_row, count); + if (pressed & PAD_CROSS) + { + if (game_row < games.count) + { + chosen = &games.games[game_row]; + snprintf(settings.game, sizeof(settings.game), "%s", chosen->file); + settings_save(&settings); + return; + } + add_row = 0; + view = VIEW_ADD; + } + if (pressed & PAD_CIRCLE) + plat_exit(0); + + screen_background(); + screen_logo(4); + for (i = 0; i < games.count; i++) + if (screen_text_width(games.games[i].title, 1) > width) + width = screen_text_width(games.games[i].title, 1); + x = (SCREEN_WIDTH - width) / 2 > 28 ? (SCREEN_WIDTH - width) / 2 : 28; + first = game_row >= GAME_ROWS ? game_row - GAME_ROWS + 1 : 0; + for (i = first; i < count && i < first + GAME_ROWS; i++) + draw_row(x, 76 + (i - first) * ROW, i < games.count ? games.games[i].title : "Add games...", + i == game_row, 1); + if (status_line[0]) + draw_status(160, 1); + else if (games.count == 1 && games.games[0].kind == GAME_SHAREWARE) + { + screen_text_center(160, "Own Doom, Doom II, TNT or Plutonia?", INK_GRAY, 1); + screen_text_center(170, "Add them under Add games.", INK_GRAY, 1); + } + screen_text_center(FOOTER_Y, "X start O quit", INK_GRAY, 1); +} + +static void add_view(void) +{ + char text[SETTINGS_URL], line[SETTINGS_URL + 8]; + int y, result; + + if (typing) + { + result = plat_text_poll(text, sizeof(text)); + if (result != 0) + { + typing = 0; + input_locked = 1; + if (result > 0 && text[0]) + { + url_clean(settings.url, sizeof(settings.url), text); + settings_save(&settings); + add_row = 1; + } + } + } + else + { + move(&add_row, 3); + if ((pressed & PAD_CROSS) && add_row == 0) + { + if (plat_text_open("Link to a WAD, ZIP, 7Z or RAR file, or a folder", + settings.url[0] ? settings.url : "http://") == 0) + typing = 1; + else + set_status("The keyboard is not available", INK_RED); + } + else if ((pressed & PAD_CROSS) && add_row == 1 && settings.url[0]) + { + if (settings.accepted) + { + char source[1][LINK_URL]; + + snprintf(source[0], LINK_URL, "%s", settings.url); + start_import(source, 1, VIEW_ADD, 0); + } + else + { + understood = 0; + notice_row = 0; + view = VIEW_NOTICE; + } + } + else if (((pressed & PAD_CROSS) && add_row == 2) || (pressed & PAD_CIRCLE)) + view = VIEW_GAMES; + } + + screen_background(); + screen_text_center(6, "Add games", INK_GOLD, 2); + y = screen_wrap(16, 28, SCREEN_WIDTH - 32, + "Copy game WADs, or ZIP, 7Z or RAR files with them inside, to this folder, " + "for example over FTP:", INK_RED); + screen_text_fit(16, y + 1, SCREEN_WIDTH - 32, plat_wad_folder(), INK_GOLD, 1); + y = screen_wrap(16, y + 12, SCREEN_WIDTH - 32, "Doom adds them the next time it starts.", INK_RED); + y = screen_wrap(16, y + 3, SCREEN_WIDTH - 32, + "DOOM.WAD DOOM2.WAD TNT.WAD PLUTONIA.WAD DOOMU.WAD FREEDOOM1.WAD FREEDOOM2.WAD", + INK_GRAY); + y = screen_wrap(16, y + 5, SCREEN_WIDTH - 32, + "Or download a WAD, ZIP, 7Z or RAR, or a folder your PC shares over HTTP:", INK_RED); + snprintf(line, sizeof(line), "Link: %s", settings.url[0] ? settings.url : "not set"); + if (add_row == 0) + screen_skull(16, y + 4, frame / SKULL_FRAMES); + screen_text_fit(30, y + 6, SCREEN_WIDTH - 46, line, add_row == 0 ? INK_WHITE : INK_RED, 1); + draw_row(30, y + 6 + ROW, "Download", add_row == 1, settings.url[0] != 0); + draw_row(30, y + 6 + ROW * 2, "Back", add_row == 2, 1); + 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); +} + +static void notice_view(void) +{ + int y; + + move(¬ice_row, 3); + if (pressed & PAD_CROSS) + { + if (notice_row == 0) + understood = !understood; + else if (notice_row == 1 && understood) + { + char source[1][LINK_URL]; + + settings.accepted = 1; + settings_save(&settings); + snprintf(source[0], LINK_URL, "%s", settings.url); + start_import(source, 1, VIEW_ADD, 0); + return; + } + else if (notice_row == 2) + view = VIEW_ADD; + } + if (pressed & PAD_CIRCLE) + view = VIEW_ADD; + + screen_background(); + screen_text_center(10, "Before you download", INK_GOLD, 2); + y = screen_wrap(20, 40, SCREEN_WIDTH - 40, + "Only download game files you own, or files that are free to share such as " + "Freedoom. You are responsible for what you download and for following the " + "laws where you live.", INK_RED); + draw_row(52, y + 16, "I understand", notice_row == 0, 1); + screen_box(40, y + 15, 8, 8, 0); + if (understood) + screen_box(42, y + 17, 4, 4, 1); + draw_row(40, y + 16 + ROW, "Continue", notice_row == 1, understood); + draw_row(40, y + 16 + ROW * 2, "Back", notice_row == 2, 1); + screen_text_center(FOOTER_Y, "X select O back", INK_GRAY, 1); +} + +static int browse_files(void) +{ + int files = 0, i; + + for (i = 0; i < browse.count; i++) + files += !browse.links[i].directory; + return files; +} + +static void browse_view(void) +{ + static char sources[MAX_LINKS][LINK_URL]; + int files = browse_files(), offset = files ? 1 : 0, count = browse.count + offset, first, i; + + move(&browse_row, count); + if ((pressed & PAD_CROSS) && count) + { + int n = 0; + + if (offset && browse_row == 0) + { + for (i = 0; i < browse.count; i++) + if (!browse.links[i].directory) + snprintf(sources[n++], LINK_URL, "%s", browse.links[i].url); + } + else + snprintf(sources[n++], LINK_URL, "%s", browse.links[browse_row - offset].url); + start_import(sources, n, VIEW_BROWSE, 0); + return; + } + if (pressed & PAD_CIRCLE) + view = VIEW_ADD; + + screen_background(); + screen_text_fit(16, 8, SCREEN_WIDTH - 32, browse.base, INK_GOLD, 1); + if (!count) + screen_text_center(60, "No WAD, ZIP, 7Z or RAR files here", INK_RED, 1); + first = browse_row >= BROWSE_ROWS ? browse_row - BROWSE_ROWS + 1 : 0; + for (i = first; i < count && i < first + BROWSE_ROWS; i++) + { + char line[LINK_NAME + 32]; + int y = 28 + (i - first) * ROW; + + if (offset && i == 0 && files == 1) + snprintf(line, sizeof(line), "Import this file"); + else if (offset && i == 0) + snprintf(line, sizeof(line), "Import all %d files", files); + else + snprintf(line, sizeof(line), "%s%s", browse.links[i - offset].name, + browse.links[i - offset].directory ? "/" : ""); + if (i == browse_row) + screen_skull(16, y - 2, frame / SKULL_FRAMES); + screen_text_fit(30, y, SCREEN_WIDTH - 46, line, i == browse_row ? INK_WHITE : INK_RED, 0); + } + draw_status(FOOTER_Y - 11, 0); + screen_text_center(FOOTER_Y, "X open O back", INK_GRAY, 1); +} + +static void finish_import(const import_status_t *status) +{ + char text[256]; + int i; + + if (auto_import && status->state == IMPORT_DONE) + for (i = 0; i < pending_count; i++) + settings_mark_imported(&settings, pending[i], pending_sizes[i]); + settings_save(&settings); + import_finish(); + games_scan(&games); + if (status->found_count) + { + snprintf(text, sizeof(text), "Added %s", status->found); + set_status(text, INK_GREEN); + } + else if (status->message[0]) + set_status(status->message, INK_RED); + else if (status->state == IMPORT_DONE) + set_status("No game WADs found to add", INK_RED); + view = status->found_count ? VIEW_GAMES : import_return; + game_row = 0; + auto_import = 0; +} + +static void draw_progress(const import_status_t *status) +{ + char line[160]; + int y = 112; + + screen_text_center(20, auto_import ? "Adding your games" : "Downloading", INK_GOLD, 2); + screen_text_fit(20, 52, SCREEN_WIDTH - 40, status->source, INK_WHITE, 0); + if (status->source_count > 1) + { + snprintf(line, sizeof(line), "File %d of %d", status->source_index + 1, status->source_count); + screen_text(20, 64, line, INK_GRAY, 1); + } + screen_box(20, 80, SCREEN_WIDTH - 40, 10, 0); + if (status->total > 0) + { + long long done = status->done < status->total ? status->done : status->total; + + screen_box(22, 82, (int)((SCREEN_WIDTH - 44) * done / status->total), 6, 1); + snprintf(line, sizeof(line), "%.1f / %.1f MB", status->done / MB, status->total / MB); + } + else + snprintf(line, sizeof(line), "%.1f MB", status->done / MB); + screen_text(20, 96, line, INK_RED, 1); + if (status->current[0]) + { + snprintf(line, sizeof(line), "Writing %s", status->current); + screen_text(20, y, line, INK_WHITE, 1); + y += LINE_HEIGHT + 2; + } + if (status->found[0]) + { + snprintf(line, sizeof(line), "Found: %s", status->found); + screen_wrap(20, y, SCREEN_WIDTH - 40, line, INK_GREEN); + } + screen_text_center(FOOTER_Y, "O cancel", INK_GRAY, 1); +} + +static void import_view(void) +{ + import_status_t status; + + import_status(&status); + if (status.state == IMPORT_LISTING) + { + browse = *import_listing(); + import_finish(); + browse_row = 0; + view = VIEW_BROWSE; + return; + } + if (status.state != IMPORT_RUNNING) + { + finish_import(&status); + return; + } + if (pressed & PAD_CIRCLE) + import_cancel(); + screen_background(); + draw_progress(&status); +} + +static const game_t *preselected(void) +{ + const game_t *game = games_find(&games, test_plan_game()); + + if (test_plan_game() && !game) + plat_log("launcher: test plan game %s is not installed\n", test_plan_game()); + return game; +} + +const game_t *launcher_choose(void) +{ + const game_t *assets, *game; + int i; + + games_scan(&games); + settings_load(&settings); + if ((game = preselected())) + return game; + assets = games_find(&games, "doom1.wad"); + if (!assets && games.count) + assets = &games.games[0]; + if (!assets || screen_init(assets->path) || plat_video_init(SCREEN_WIDTH, SCREEN_HEIGHT)) + { + plat_alert("No game data found. Reinstall DOOM."); + return NULL; + } + choose_import_dir(); + for (i = 0; i < games.count; i++) + if (!strcasecmp(games.games[i].file, settings.game)) + game_row = i; + view = VIEW_GAMES; + start_auto_import(); + + while (!chosen) + { + read_input(); + if (view != VIEW_IMPORT && pressed) + status_line[0] = 0; + switch (view) + { + case VIEW_GAMES: + games_view(); + break; + case VIEW_ADD: + add_view(); + break; + case VIEW_NOTICE: + notice_view(); + break; + case VIEW_BROWSE: + browse_view(); + break; + case VIEW_IMPORT: + import_view(); + break; + } + if (!chosen) + present(); + } + return chosen; +} diff --git a/src/launcher/launcher.h b/src/launcher/launcher.h new file mode 100644 index 0000000..bc35270 --- /dev/null +++ b/src/launcher/launcher.h @@ -0,0 +1,8 @@ +#ifndef LAUNCHER_H +#define LAUNCHER_H + +#include "games.h" + +const game_t *launcher_choose(void); + +#endif diff --git a/src/launcher/listing.c b/src/launcher/listing.c new file mode 100644 index 0000000..8b1bf3c --- /dev/null +++ b/src/launcher/listing.c @@ -0,0 +1,200 @@ +#include +#include +#include +#include + +#include "games.h" +#include "listing.h" + +static const char *path_start(const char *url) +{ + const char *scheme = strstr(url, "://"); + const char *slash; + + if (!scheme) + return url; + slash = strchr(scheme + 3, '/'); + return slash ? slash : url + strlen(url); +} + +void url_clean(char *out, size_t size, const char *typed) +{ + size_t used = 0; + const char *end; + + while (*typed == ' ') + typed++; + end = typed + strlen(typed); + while (end > typed && end[-1] == ' ') + end--; + if (!strstr(typed, "://")) + used = (size_t)snprintf(out, size, "http://"); + for (; typed < end && used + 4 < size; typed++) + { + if (*typed == ' ') + used += (size_t)snprintf(out + used, size - used, "%%20"); + else + out[used++] = *typed; + } + out[used < size ? used : size - 1] = 0; +} + +void url_directory(char *out, size_t size, const char *url) +{ + const char *path = path_start(url); + const char *last = strrchr(path, '/'); + + snprintf(out, size, "%s", url); + if (!*path || (last && last[1] && !strchr(last + 1, '.'))) + strncat(out, "/", size - strlen(out) - 1); +} + +void url_resolve(char *out, size_t size, const char *base, const char *href) +{ + const char *path = path_start(base); + const char *last; + + if (strstr(href, "://")) + snprintf(out, size, "%s", href); + else if (href[0] == '/' && href[1] == '/') + snprintf(out, size, "%.*s%s", (int)(strstr(base, "//") - base), base, href); + else if (href[0] == '/') + snprintf(out, size, "%.*s%s", (int)(path - base), base, href); + else + { + last = strrchr(path, '/'); + if (last) + snprintf(out, size, "%.*s%s", (int)(last + 1 - base), base, href); + else + snprintf(out, size, "%s/%s", base, href); + } +} + +static int hex(int c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + c = tolower(c); + return c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; +} + +void url_file_name(char *out, size_t size, const char *url) +{ + const char *path = path_start(url); + const char *end = path + strcspn(path, "?#"); + const char *start; + size_t used = 0; + + if (end > path && end[-1] == '/') + end--; + for (start = end; start > path && start[-1] != '/'; start--) + ; + while (start < end && used + 1 < size) + { + if (*start == '%' && end - start >= 3 && hex(start[1]) >= 0 && hex(start[2]) >= 0) + { + out[used++] = (char)(hex(start[1]) * 16 + hex(start[2])); + start += 3; + } + else + out[used++] = *start++; + } + out[used] = 0; +} + +static void decode_entities(char *text) +{ + static const struct + { + const char *entity; + char c; + } entities[] = {{"&", '&'}, {""", '"'}, {"'", '\''}, {"<", '<'}, {">", '>'}}; + char *read = text, *write = text; + size_t i; + + while (*read) + { + for (i = 0; i < sizeof(entities) / sizeof(entities[0]); i++) + if (!strncmp(read, entities[i].entity, strlen(entities[i].entity))) + break; + if (i < sizeof(entities) / sizeof(entities[0])) + { + *write++ = entities[i].c; + read += strlen(entities[i].entity); + } + else + *write++ = *read++; + } + *write = 0; +} + +static int has_suffix(const char *name, const char *suffix) +{ + size_t length = strlen(name), suffix_length = strlen(suffix); + + return length > suffix_length && !strcasecmp(name + length - suffix_length, suffix); +} + +static void add_link(listing_t *listing, const char *href) +{ + link_t *link; + char url[LINK_URL]; + size_t base_length = strlen(listing->base); + int i; + + if (!*href || strchr("?#", href[0]) || strchr(href, '?') || !strncasecmp(href, "mailto:", 7) || + !strncasecmp(href, "javascript:", 11)) + return; + url_resolve(url, sizeof(url), listing->base, href); + if (strstr(url, "/../") || strstr(url, "/./")) + return; + if (strncmp(url, listing->base, base_length) || strlen(url) == base_length || + listing->count == MAX_LINKS) + return; + for (i = 0; i < listing->count; i++) + if (!strcmp(listing->links[i].url, url)) + return; + + link = &listing->links[listing->count]; + snprintf(link->url, sizeof(link->url), "%s", url); + url_file_name(link->name, sizeof(link->name), url); + link->directory = url[strlen(url) - 1] == '/'; + if (link->directory || has_suffix(link->name, ".wad") || games_is_archive(link->name)) + listing->count++; +} + +void listing_parse(listing_t *listing, const char *base, const char *html, size_t length) +{ + const char *p = html, *end = html + length; + + memset(listing, 0, sizeof(*listing)); + snprintf(listing->base, sizeof(listing->base), "%s", base); + while (p + 5 < end) + { + char href[LINK_URL]; + size_t used = 0; + char quote = 0; + + if (strncasecmp(p, "href", 4)) + { + p++; + continue; + } + p += 4; + while (p < end && isspace((unsigned char)*p)) + p++; + if (p >= end || *p != '=') + continue; + p++; + while (p < end && isspace((unsigned char)*p)) + p++; + if (p < end && (*p == '"' || *p == '\'')) + quote = *p++; + while (p < end && used + 1 < sizeof(href) && + (quote ? *p != quote : !isspace((unsigned char)*p) && *p != '>')) + href[used++] = *p++; + href[used] = 0; + decode_entities(href); + add_link(listing, href); + } +} diff --git a/src/launcher/listing.h b/src/launcher/listing.h new file mode 100644 index 0000000..7132c77 --- /dev/null +++ b/src/launcher/listing.h @@ -0,0 +1,30 @@ +#ifndef LISTING_H +#define LISTING_H + +#include + +#define MAX_LINKS 128 +#define LINK_URL 768 +#define LINK_NAME 128 + +typedef struct +{ + char url[LINK_URL]; + char name[LINK_NAME]; + int directory; +} link_t; + +typedef struct +{ + char base[LINK_URL]; + link_t links[MAX_LINKS]; + int count; +} listing_t; + +void url_clean(char *out, size_t size, const char *typed); +void url_directory(char *out, size_t size, const char *url); +void url_resolve(char *out, size_t size, const char *base, const char *href); +void url_file_name(char *out, size_t size, const char *url); +void listing_parse(listing_t *listing, const char *base, const char *html, size_t length); + +#endif diff --git a/src/launcher/screen.c b/src/launcher/screen.c new file mode 100644 index 0000000..f39cc6a --- /dev/null +++ b/src/launcher/screen.c @@ -0,0 +1,330 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform.h" +#include "screen.h" + +#define FONT_FIRST '!' +#define FONT_LAST '_' +#define FONT_COUNT (FONT_LAST - FONT_FIRST + 1) +#define SPACE_WIDTH 4 +#define LUMP_ENTRY 16 +#define BACKGROUND_SHADE 29 +#define COLORMAP_SIZE (34 * 256) +#define MAX_PATCH_HEIGHT 256 + +static uint8_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT]; +static uint32_t palette[256]; +static uint8_t rgb[768]; +static uint8_t shade[256]; +static uint8_t inks[INKS][256]; +static uint8_t *font[FONT_COUNT]; +static uint8_t *title, *logo, *skulls[2]; +static uint8_t outline_color, fill_color; + +static uint32_t le32(const uint8_t *p) +{ + return p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24; +} + +static int s16(const uint8_t *p) +{ + return (int16_t)(p[0] | p[1] << 8); +} + +static uint8_t *load(int fd, const uint8_t *directory, uint32_t count, const char *name, uint32_t *size) +{ + uint32_t i = count; + + while (i--) + { + const uint8_t *entry = directory + i * LUMP_ENTRY; + uint32_t length = le32(entry + 4); + uint8_t *data; + + if (strncasecmp((const char *)entry + 8, name, 8)) + continue; + data = malloc(length ? length : 1); + if (!data || lseek(fd, le32(entry), SEEK_SET) < 0 || read(fd, data, length) != (ssize_t)length) + { + free(data); + return NULL; + } + if (size) + *size = length; + return data; + } + return NULL; +} + +static uint8_t nearest(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 build_inks(void) +{ + int i; + + for (i = 0; i < 256; i++) + { + int r = rgb[i * 3], g = rgb[i * 3 + 1], b = rgb[i * 3 + 2]; + int level = r > g ? (r > b ? r : b) : (g > b ? g : b); + int bright = level * 5 / 4 + 30 > 255 ? 255 : level * 5 / 4 + 30; + + inks[INK_RED][i] = (uint8_t)i; + inks[INK_WHITE][i] = nearest(bright, bright, bright); + inks[INK_GOLD][i] = nearest(bright, bright * 3 / 4, bright / 5); + inks[INK_GRAY][i] = nearest(level * 3 / 5, level * 3 / 5, level * 3 / 5); + inks[INK_GREEN][i] = nearest(level / 4, bright, level / 4); + palette[i] = 0xff000000u | (uint32_t)r << 16 | (uint32_t)g << 8 | (uint32_t)b; + } + outline_color = nearest(150, 150, 150); + fill_color = nearest(190, 30, 30); +} + +int screen_init(const char *wad_path) +{ + uint8_t header[12], *directory, *playpal, *colormap; + uint32_t count, size = 0; + char name[9]; + int fd = open(wad_path, O_RDONLY), i; + + if (fd < 0 || read(fd, header, sizeof(header)) != sizeof(header)) + { + if (fd >= 0) + close(fd); + return -1; + } + count = le32(header + 4); + directory = malloc((size_t)count * LUMP_ENTRY); + if (!directory || lseek(fd, le32(header + 8), SEEK_SET) < 0 || + read(fd, directory, (size_t)count * LUMP_ENTRY) != (ssize_t)count * LUMP_ENTRY) + { + free(directory); + close(fd); + return -1; + } + + playpal = load(fd, directory, count, "PLAYPAL", &size); + if (playpal && size >= sizeof(rgb)) + memcpy(rgb, playpal, sizeof(rgb)); + colormap = load(fd, directory, count, "COLORMAP", &size); + for (i = 0; i < 256; i++) + shade[i] = colormap && size >= COLORMAP_SIZE ? colormap[BACKGROUND_SHADE * 256 + i] : 0; + for (i = 0; i < FONT_COUNT; i++) + { + snprintf(name, sizeof(name), "STCFN%03d", FONT_FIRST + i); + font[i] = load(fd, directory, count, name, NULL); + } + title = load(fd, directory, count, "TITLEPIC", NULL); + logo = load(fd, directory, count, "M_DOOM", NULL); + skulls[0] = load(fd, directory, count, "M_SKULL1", NULL); + skulls[1] = load(fd, directory, count, "M_SKULL2", NULL); + free(colormap); + free(directory); + close(fd); + if (!playpal || !font['A' - FONT_FIRST]) + { + free(playpal); + return -1; + } + free(playpal); + build_inks(); + return 0; +} + +static void draw_patch(int x, int y, const uint8_t *patch, const uint8_t *map, int num, int den) +{ + int width = s16(patch), height = s16(patch + 2); + int target_width = width * num / den, target_height = height * num / den; + uint8_t column[MAX_PATCH_HEIGHT], mask[MAX_PATCH_HEIGHT]; + int tx, ty; + + if (height > MAX_PATCH_HEIGHT) + return; + x -= s16(patch + 4) * num / den; + y -= s16(patch + 6) * num / den; + for (tx = 0; tx < target_width; tx++) + { + const uint8_t *post; + int px = x + tx; + + if (px < 0 || px >= SCREEN_WIDTH) + continue; + memset(mask, 0, sizeof(mask)); + post = patch + le32(patch + 8 + (tx * den / num) * 4); + while (*post != 0xff) + { + int top = post[0], length = post[1], i; + + for (i = 0; i < length && top + i < MAX_PATCH_HEIGHT; i++) + { + column[top + i] = post[3 + i]; + mask[top + i] = 1; + } + post += length + 4; + } + for (ty = 0; ty < target_height; ty++) + { + int sy = ty * den / num, py = y + ty; + + if (mask[sy] && py >= 0 && py < SCREEN_HEIGHT) + pixels[py * SCREEN_WIDTH + px] = map ? map[column[sy]] : column[sy]; + } + } +} + +void screen_background(void) +{ + int i; + + memset(pixels, 0, sizeof(pixels)); + if (title) + draw_patch(0, 0, title, NULL, 1, 1); + for (i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++) + pixels[i] = shade[pixels[i]]; +} + +void screen_logo(int y) +{ + if (logo) + draw_patch((SCREEN_WIDTH - s16(logo)) / 2 + s16(logo + 4), y + s16(logo + 6), logo, NULL, 1, 1); +} + +void screen_skull(int x, int y, int frame) +{ + const uint8_t *skull = skulls[frame & 1] ? skulls[frame & 1] : skulls[0]; + + if (skull) + draw_patch(x + s16(skull + 4) / 2, y + s16(skull + 6) / 2, skull, NULL, 1, 2); +} + +void screen_box(int x, int y, int width, int height, int filled) +{ + int i, j; + + for (j = y; j < y + height; j++) + for (i = x; i < x + width; i++) + { + int edge = i == x || j == y || i == x + width - 1 || j == y + height - 1; + + if (i >= 0 && i < SCREEN_WIDTH && j >= 0 && j < SCREEN_HEIGHT && (filled || edge)) + pixels[j * SCREEN_WIDTH + i] = filled ? fill_color : outline_color; + } +} + +static const uint8_t *glyph(char c) +{ + c = (char)toupper((unsigned char)c); + return c >= FONT_FIRST && c <= FONT_LAST ? font[c - FONT_FIRST] : NULL; +} + +int screen_text_width(const char *text, int scale) +{ + int width = 0; + + for (; *text && *text != '\n'; text++) + width += (glyph(*text) ? s16(glyph(*text)) : SPACE_WIDTH) * scale; + return width; +} + +void screen_text(int x, int y, const char *text, ink_t ink, int scale) +{ + for (; *text && *text != '\n'; text++) + { + const uint8_t *g = glyph(*text); + + if (g) + draw_patch(x, y, g, inks[ink], scale, 1); + x += (g ? s16(g) : SPACE_WIDTH) * scale; + } +} + +void screen_text_center(int y, const char *text, ink_t ink, int scale) +{ + screen_text((SCREEN_WIDTH - screen_text_width(text, scale)) / 2, y, text, ink, scale); +} + +void screen_text_fit(int x, int y, int width, const char *text, ink_t ink, int keep_end) +{ + char shown[512]; + size_t length = strlen(text), cut; + + if (screen_text_width(text, 1) <= width) + { + screen_text(x, y, text, ink, 1); + return; + } + for (cut = 1; cut < length; cut++) + { + if (keep_end) + snprintf(shown, sizeof(shown), "...%s", text + cut); + else + snprintf(shown, sizeof(shown), "%.*s...", (int)(length - cut), text); + if (screen_text_width(shown, 1) <= width) + break; + } + screen_text(x, y, shown, ink, 1); +} + +int screen_wrap(int x, int y, int width, const char *text, ink_t ink) +{ + char line[256]; + size_t used = 0; + + while (*text) + { + const char *word = text; + size_t word_length = strcspn(word, " \n"); + char candidate[256]; + + snprintf(candidate, sizeof(candidate), "%.*s%s%.*s", (int)used, line, used ? " " : "", + (int)word_length, word); + if (used && screen_text_width(candidate, 1) > width) + { + line[used] = 0; + screen_text(x, y, line, ink, 1); + y += LINE_HEIGHT; + used = 0; + continue; + } + used = strlen(candidate) < sizeof(line) - 1 ? strlen(candidate) : sizeof(line) - 1; + memcpy(line, candidate, used); + text = word + word_length; + if (*text == '\n' || !*text) + { + line[used] = 0; + screen_text(x, y, line, ink, 1); + y += LINE_HEIGHT; + used = 0; + } + if (*text) + text++; + } + return y; +} + +void screen_present(void) +{ + plat_video_present(pixels, palette); +} diff --git a/src/launcher/screen.h b/src/launcher/screen.h new file mode 100644 index 0000000..2fc6611 --- /dev/null +++ b/src/launcher/screen.h @@ -0,0 +1,30 @@ +#ifndef SCREEN_H +#define SCREEN_H + +#define SCREEN_WIDTH 320 +#define SCREEN_HEIGHT 200 +#define LINE_HEIGHT 10 + +typedef enum +{ + INK_RED, + INK_WHITE, + INK_GOLD, + INK_GRAY, + INK_GREEN, + INKS +} ink_t; + +int screen_init(const char *wad_path); +void screen_background(void); +void screen_logo(int y); +void screen_skull(int x, int y, int frame); +void screen_box(int x, int y, int width, int height, int filled); +int screen_text_width(const char *text, int scale); +void screen_text(int x, int y, const char *text, ink_t ink, int scale); +void screen_text_center(int y, const char *text, ink_t ink, int scale); +void screen_text_fit(int x, int y, int width, const char *text, ink_t ink, int keep_end); +int screen_wrap(int x, int y, int width, const char *text, ink_t ink); +void screen_present(void); + +#endif diff --git a/src/launcher/settings.c b/src/launcher/settings.c new file mode 100644 index 0000000..5e125e5 --- /dev/null +++ b/src/launcher/settings.c @@ -0,0 +1,80 @@ +#include +#include +#include + +#include "platform.h" +#include "settings.h" + +static void settings_path(char *path, size_t size) +{ + snprintf(path, size, "%s/launcher.cfg", plat_save_dir()); +} + +void settings_load(settings_t *settings) +{ + char path[GAME_PATH], line[GAME_PATH + 64]; + FILE *file; + + memset(settings, 0, sizeof(*settings)); + settings_path(path, sizeof(path)); + if (!(file = fopen(path, "r"))) + return; + while (fgets(line, sizeof(line), file)) + { + line[strcspn(line, "\r\n")] = 0; + if (!strncmp(line, "game ", 5)) + snprintf(settings->game, sizeof(settings->game), "%s", line + 5); + else if (!strncmp(line, "url ", 4)) + snprintf(settings->url, sizeof(settings->url), "%s", line + 4); + else if (!strncmp(line, "accepted ", 9)) + settings->accepted = atoi(line + 9); + else if (!strncmp(line, "imported ", 9) && settings->imported_count < MAX_IMPORTED) + snprintf(settings->imported[settings->imported_count++], sizeof(settings->imported[0]), + "%s", line + 9); + } + fclose(file); +} + +void settings_save(const settings_t *settings) +{ + char path[GAME_PATH]; + FILE *file; + int i; + + settings_path(path, sizeof(path)); + if (!(file = fopen(path, "w"))) + { + plat_log("launcher: cannot write %s\n", path); + return; + } + fprintf(file, "game %s\nurl %s\naccepted %d\n", settings->game, settings->url, settings->accepted); + for (i = 0; i < settings->imported_count; i++) + fprintf(file, "imported %s\n", settings->imported[i]); + fclose(file); +} + +int settings_was_imported(const settings_t *settings, const char *path, long long size) +{ + char key[GAME_PATH + 24]; + int i; + + snprintf(key, sizeof(key), "%lld %s", size, path); + for (i = 0; i < settings->imported_count; i++) + if (!strcmp(settings->imported[i], key)) + return 1; + return 0; +} + +void settings_mark_imported(settings_t *settings, const char *path, long long size) +{ + if (settings_was_imported(settings, path, size)) + return; + if (settings->imported_count == MAX_IMPORTED) + { + memmove(settings->imported, settings->imported + 1, + sizeof(settings->imported[0]) * (MAX_IMPORTED - 1)); + settings->imported_count--; + } + snprintf(settings->imported[settings->imported_count++], sizeof(settings->imported[0]), + "%lld %s", size, path); +} diff --git a/src/launcher/settings.h b/src/launcher/settings.h new file mode 100644 index 0000000..b3ee985 --- /dev/null +++ b/src/launcher/settings.h @@ -0,0 +1,23 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include "games.h" + +#define SETTINGS_URL 512 +#define MAX_IMPORTED 32 + +typedef struct +{ + char game[64]; + char url[SETTINGS_URL]; + int accepted; + char imported[MAX_IMPORTED][GAME_PATH + 24]; + int imported_count; +} settings_t; + +void settings_load(settings_t *settings); +void settings_save(const settings_t *settings); +int settings_was_imported(const settings_t *settings, const char *path, long long size); +void settings_mark_imported(settings_t *settings, const char *path, long long size); + +#endif diff --git a/src/platform/platform.h b/src/platform/platform.h index 3e0e50e..c62c521 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -40,8 +40,29 @@ void plat_alert(const char *message); uint64_t plat_ticks_us(void); void plat_sleep_us(uint32_t us); +typedef void (*plat_dir_fn)(const char *name, void *user); + +typedef struct +{ + int status; + int64_t length; + char type[64]; +} plat_http_info_t; + +typedef struct plat_http plat_http_t; + const char *const *plat_wad_dirs(void); const char *plat_save_dir(void); +const char *plat_wad_folder(void); +int plat_list_dir(const char *path, plat_dir_fn fn, void *user); + +plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *info, + char *error, int error_size); +int plat_http_read(plat_http_t *http, void *buffer, int size); +void plat_http_close(plat_http_t *http); + +int plat_text_open(const char *title, const char *text); +int plat_text_poll(char *text, int size); int plat_video_init(int width, int height); void plat_video_present(const uint8_t *pixels, const uint32_t *palette); diff --git a/src/platform/test_plan.c b/src/platform/test_plan.c index f8b2f45..0d0aed0 100644 --- a/src/platform/test_plan.c +++ b/src/platform/test_plan.c @@ -1,3 +1,4 @@ +#include #include #include @@ -30,6 +31,8 @@ static int step_count; static int captures[MAX_CAPTURES]; static int capture_count; static int frame_limit; +static char game[64]; +static char text[512]; static void parse_token(step_t *step, const char *token) { @@ -144,3 +147,23 @@ int test_plan_finished(int frame) { return frame_limit && frame >= frame_limit; } + +void test_plan_set_game(const char *file) +{ + snprintf(game, sizeof(game), "%s", file ? file : ""); +} + +const char *test_plan_game(void) +{ + return game[0] ? game : NULL; +} + +void test_plan_set_text(const char *value) +{ + snprintf(text, sizeof(text), "%s", value ? value : ""); +} + +const char *test_plan_text(void) +{ + return text[0] ? text : NULL; +} diff --git a/src/platform/test_plan.h b/src/platform/test_plan.h index bb71ede..23b7db9 100644 --- a/src/platform/test_plan.h +++ b/src/platform/test_plan.h @@ -10,5 +10,9 @@ void test_plan_apply(int frame, pad_state_t *state); int test_plan_captures_frame(int frame); void test_plan_limit(int frames); int test_plan_finished(int frame); +void test_plan_set_game(const char *file); +const char *test_plan_game(void); +void test_plan_set_text(const char *text); +const char *test_plan_text(void); #endif diff --git a/src/port/i_system.c b/src/port/i_system.c index 7270d27..6b21a54 100644 --- a/src/port/i_system.c +++ b/src/port/i_system.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include "doomdef.h" @@ -12,6 +13,7 @@ #include "i_system.h" #include "i_video.h" +#include "launcher.h" #include "platform.h" #define ZONE_SIZE (64 * 1024 * 1024) @@ -82,21 +84,50 @@ byte *I_AllocLow(int length) return mem; } -char **I_GetWadDirs(void) -{ - return (char **)plat_wad_dirs(); -} - char *I_GetSaveDir(void) { return (char *)plat_save_dir(); } -boolean I_PreferDoom1(void) +iwad_t *I_ChooseIwad(void) { - pad_state_t pad; + static const struct + { + game_kind_t kind; + GameMode_t mode; + GameMission_t mission; + Language_t language; + } modes[] = { + {GAME_ULTIMATE, retail, doom, english}, + {GAME_DOOM, registered, doom, english}, + {GAME_DOOM2, commercial, doom2, english}, + {GAME_DOOM2_FRENCH, commercial, doom2, french}, + {GAME_TNT, commercial, pack_tnt, english}, + {GAME_PLUTONIA, commercial, pack_plut, english}, + {GAME_FREEDOOM1, retail, doom, english}, + {GAME_FREEDOOM2, commercial, doom2, english}, + {GAME_SHAREWARE, shareware, doom, english}, + }; + static iwad_t iwad; + static char savename[GAME_PATH]; + const game_t *game = launcher_choose(); + size_t i; - return plat_pad_read(&pad) == 0 && (pad.buttons & PAD_L2); + if (!game) + return NULL; + for (i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) + if (modes[i].kind == game->kind) + { + iwad.mode = modes[i].mode; + iwad.mission = modes[i].mission; + iwad.language = modes[i].language; + } + snprintf(savename, sizeof(savename), "%s/%.*ssav", plat_save_dir(), (int)(strlen(game->file) - 4), + game->file); + iwad.path = (char *)game->path; + iwad.savename = savename; + plat_log("launcher: starting %s from %s\n", game->title, game->path); + return &iwad; } static void shutdown_all(void) diff --git a/src/ps5/http.c b/src/ps5/http.c new file mode 100644 index 0000000..deca9ee --- /dev/null +++ b/src/ps5/http.c @@ -0,0 +1,138 @@ +#include +#include +#include +#include + +#include "platform.h" +#include "sce.h" + +#define NET_POOL (1024 * 1024) +#define SSL_POOL (304 * 1024) +#define HTTP_POOL (4 * 1024 * 1024) +#define HTTP_1_1 2 +#define METHOD_GET 0 +#define HEADER_OVERWRITE 0 +#define TIMEOUT_US 15000000 +#define VERIFY_FLAGS (0x01 | 0x04 | 0x08 | 0x10 | 0x20 | 0x80) + +struct plat_http +{ + int connection, request; +}; + +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static int template_id = -1; + +static int start_http(void) +{ + int pool, ssl, http, result; + + pool = sceNetPoolCreate("doom", NET_POOL, 0); + if (pool < 0) + return pool; + ssl = sceSslInit(SSL_POOL); + if (ssl < 0) + return ssl; + http = sceHttpInit(pool, ssl, HTTP_POOL); + if (http < 0) + return http; + template_id = sceHttpCreateTemplate(http, "DOOM-PS5", HTTP_1_1, 0); + if (template_id < 0) + return template_id; + if ((result = sceHttpSetAutoRedirect(template_id, 1)) < 0 || + (result = sceHttpSetResolveTimeOut(template_id, TIMEOUT_US)) < 0 || + (result = sceHttpSetConnectTimeOut(template_id, TIMEOUT_US)) < 0 || + (result = sceHttpSetSendTimeOut(template_id, TIMEOUT_US)) < 0 || + (result = sceHttpSetRecvTimeOut(template_id, TIMEOUT_US)) < 0 || + (result = sceHttpsEnableOption(template_id, VERIFY_FLAGS)) < 0) + return result; + return 0; +} + +static void header_value(int request, const char *name, char *out, size_t size) +{ + char *headers = NULL; + const char *value = NULL; + size_t headers_size = 0, value_size = 0; + + out[0] = 0; + if (sceHttpGetAllResponseHeaders(request, &headers, &headers_size) < 0 || + sceHttpParseResponseHeader(headers, headers_size, name, &value, &value_size) < 0) + return; + if (value_size >= size) + value_size = size - 1; + memcpy(out, value, value_size); + out[value_size] = 0; +} + +plat_http_t *plat_http_get(const char *url, uint64_t offset, plat_http_info_t *info, + char *error, int error_size) +{ + plat_http_t *http; + char range[40]; + uint64_t length = 0; + int status = 0, has_length = -1, result; + + pthread_mutex_lock(&lock); + result = template_id < 0 ? start_http() : 0; + pthread_mutex_unlock(&lock); + if (result < 0) + { + snprintf(error, error_size, "network setup failed (0x%08x)", (unsigned)result); + return NULL; + } + + http = calloc(1, sizeof(*http)); + if (!http) + return NULL; + http->request = -1; + http->connection = result = sceHttpCreateConnectionWithURL(template_id, url, 1); + if (result >= 0) + http->request = result = sceHttpCreateRequestWithURL(http->connection, METHOD_GET, url, 0); + if (result >= 0 && offset) + { + 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); + if (result < 0) + { + snprintf(error, error_size, "connection failed (0x%08x)", (unsigned)result); + plat_http_close(http); + return NULL; + } + if (status != 200 && status != 206) + { + snprintf(error, error_size, "the server answered %d", status); + plat_http_close(http); + return NULL; + } + sceHttpGetResponseContentLength(http->request, &has_length, &length); + info->status = status; + info->length = has_length == 0 ? (int64_t)length : -1; + header_value(http->request, "Content-Type", info->type, sizeof(info->type)); + return http; +} + +int plat_http_read(plat_http_t *http, void *buffer, int size) +{ + int got = sceHttpReadData(http->request, buffer, (size_t)size); + + if (got < 0) + plat_log("http: read failed (0x%08x)\n", (unsigned)got); + return got < 0 ? -1 : got; +} + +void plat_http_close(plat_http_t *http) +{ + if (!http) + return; + if (http->request >= 0) + sceHttpDeleteRequest(http->request); + if (http->connection >= 0) + sceHttpDeleteConnection(http->connection); + free(http); +} diff --git a/src/ps5/libc_shim.c b/src/ps5/libc_shim.c new file mode 100644 index 0000000..d9b710d --- /dev/null +++ b/src/ps5/libc_shim.c @@ -0,0 +1,9 @@ +#include + +int _Getmbcurmax(void); + +/* The SDK's FreeBSD headers expand MB_CUR_MAX to this; the console's C library names it _Getmbcurmax. */ +size_t ___mb_cur_max(void) +{ + return (size_t)_Getmbcurmax(); +} diff --git a/src/ps5/sce.h b/src/ps5/sce.h index d7f3a19..50ee605 100644 --- a/src/ps5/sce.h +++ b/src/ps5/sce.h @@ -122,6 +122,67 @@ int sceSystemServiceLoadExec(const char *path, char *const *argv); int sceUserServiceInitialize(void *params); int sceUserServiceGetInitialUser(int *user); +int sceUserServiceGetForegroundUser(int *user); + +int sceKernelOpen(const char *path, int flags, int mode); +int sceKernelClose(int fd); +int sceKernelGetdents(int fd, char *buffer, int size); + +int sceNetPoolCreate(const char *name, int size, int flags); +int sceSslInit(size_t pool_size); +int sceHttpInit(int net_pool, int ssl_context, size_t pool_size); +int sceHttpCreateTemplate(int context, const char *user_agent, int version, int auto_proxy); +int sceHttpSetAutoRedirect(int id, int enabled); +int sceHttpSetResolveTimeOut(int id, uint32_t usec); +int sceHttpSetConnectTimeOut(int id, uint32_t usec); +int sceHttpSetSendTimeOut(int id, uint32_t usec); +int sceHttpSetRecvTimeOut(int id, uint32_t usec); +int sceHttpsEnableOption(int id, uint32_t flags); +int sceHttpCreateConnectionWithURL(int template_id, const char *url, int keep_alive); +int sceHttpCreateRequestWithURL(int connection, int method, const char *url, + uint64_t content_length); +int sceHttpAddRequestHeader(int id, const char *name, const char *value, uint32_t mode); +int sceHttpSendRequest(int request, const void *data, size_t size); +int sceHttpGetStatusCode(int request, int *status); +int sceHttpGetResponseContentLength(int request, int *result, uint64_t *length); +int sceHttpGetAllResponseHeaders(int request, char **headers, size_t *size); +int sceHttpParseResponseHeader(const char *headers, size_t size, const char *name, + const char **value, size_t *value_size); +int sceHttpReadData(int request, void *data, size_t size); +int sceHttpDeleteRequest(int request); +int sceHttpDeleteConnection(int connection); + +typedef struct +{ + int32_t user; + int32_t type; + uint64_t languages; + int32_t enter_label; + int32_t input_method; + void *filter; + uint32_t option; + uint32_t max_length; + uint16_t *text; + float x, y; + int32_t horizontal_alignment; + int32_t vertical_alignment; + const uint16_t *placeholder; + const uint16_t *title; + int8_t reserved[16]; +} SceImeDialogParam; + +typedef struct +{ + int32_t outcome; + int8_t reserved[12]; +} SceImeDialogResult; + +int sceSysmoduleLoadModule(uint16_t id); +int sceCommonDialogInitialize(void); +int sceImeDialogInit(const SceImeDialogParam *param, const void *extended); +int sceImeDialogGetStatus(void); +int sceImeDialogGetResult(SceImeDialogResult *result); +int sceImeDialogTerm(void); int sceVideoOutOpen(int user, int bus, int index, const void *params); int sceVideoOutClose(int handle); diff --git a/src/ps5/stubs/libSceCommonDialog.txt b/src/ps5/stubs/libSceCommonDialog.txt new file mode 100644 index 0000000..5014a5f --- /dev/null +++ b/src/ps5/stubs/libSceCommonDialog.txt @@ -0,0 +1 @@ +sceCommonDialogInitialize diff --git a/src/ps5/system.c b/src/ps5/system.c index 62dfc22..dee42e1 100644 --- a/src/ps5/system.c +++ b/src/ps5/system.c @@ -16,11 +16,17 @@ #define SAVE_DIR "/download0" #define LOG_PATH SAVE_DIR "/doom.log" #define WAD_DIR "/app0/wads" +#define WAD_FOLDER "/data/homebrew/PPSA99666/wads" #define TEST_PLAN_PATH "/app0/test.cfg" #define TEST_PLAN_BYTES 8192 #define LOG_DRAIN_US 50000 +#define LOG_SEND_BYTES (1024 * 1024) +#define LOG_SEND_US 10000000 +#define DIRENT_BUFFER 65536 +#define DIRENT_HEADER 8 +#define OPEN_DIRECTORY 0x00020000 -static const char *const wad_dirs[] = {WAD_DIR, NULL}; +static const char *const wad_dirs[] = {WAD_DIR, SAVE_DIR, NULL}; static int user = SCE_USER_SYSTEM; static int log_file = -1; static int test_cpu_present; @@ -120,6 +126,10 @@ static void load_test_plan(void) ps5_capture_serve(atoi(line + 6)); else if (!strcmp(line, "present cpu")) test_cpu_present = 1; + else if (!strncmp(line, "game ", 5)) + test_plan_set_game(line + 5); + else if (!strncmp(line, "text ", 5)) + test_plan_set_text(line + 5); } plat_log("ps5: test plan loaded\n"); } @@ -148,11 +158,32 @@ void plat_shutdown(void) ps5_pad_close(); } +static void send_log(void) +{ + static char text[LOG_SEND_BYTES]; + ssize_t length; + off_t size; + int fd = open(LOG_PATH, O_RDONLY); + + if (fd < 0) + return; + size = lseek(fd, 0, SEEK_END); + lseek(fd, size > LOG_SEND_BYTES ? size - LOG_SEND_BYTES : 0, SEEK_SET); + length = read(fd, text, sizeof(text)); + close(fd); + if (length > 0) + { + ps5_capture_add("doom.log", text, (size_t)length); + ps5_capture_drain(LOG_SEND_US); + } +} + _Noreturn void plat_exit(int code) { plat_log("ps5: exit %d\n", code); fflush(NULL); sceKernelUsleep(LOG_DRAIN_US); + send_log(); sceSystemServiceLoadExec("exit", NULL); for (;;) sceKernelUsleep(1000000); @@ -200,3 +231,46 @@ const char *plat_save_dir(void) { return SAVE_DIR; } + +const char *plat_wad_folder(void) +{ + return WAD_FOLDER; +} + +int plat_list_dir(const char *path, plat_dir_fn fn, void *user) +{ + static uint8_t buffer[DIRENT_BUFFER]; + int fd = sceKernelOpen(path, O_RDONLY | OPEN_DIRECTORY, 0); + int length; + + if (fd < 0) + { + plat_log("ps5: cannot open %s for listing (0x%08x)\n", path, (unsigned)fd); + return -1; + } + while ((length = sceKernelGetdents(fd, (char *)buffer, sizeof(buffer))) > 0) + { + int offset = 0; + + while (offset + DIRENT_HEADER <= length) + { + int record = buffer[offset + 4] | buffer[offset + 5] << 8; + int name_length = buffer[offset + 7]; + char name[256]; + + if (record < DIRENT_HEADER || offset + record > length) + break; + if (name_length > record - DIRENT_HEADER) + name_length = record - DIRENT_HEADER; + memcpy(name, buffer + offset + DIRENT_HEADER, (size_t)name_length); + name[name_length] = 0; + if (name[0] && strcmp(name, ".") && strcmp(name, "..")) + fn(name, user); + offset += record; + } + } + sceKernelClose(fd); + if (length < 0) + plat_log("ps5: listing %s failed (0x%08x)\n", path, (unsigned)length); + return length < 0 ? -1 : 0; +} diff --git a/src/ps5/text.c b/src/ps5/text.c new file mode 100644 index 0000000..3caa0ef --- /dev/null +++ b/src/ps5/text.c @@ -0,0 +1,120 @@ +#include +#include + +#include "platform.h" +#include "ps5.h" +#include "sce.h" +#include "test_plan.h" + +#define SYSMODULE_IME_DIALOG 0x0096 +#define COMMON_DIALOG_READY 0x80b80002u +#define IME_TYPE_URL 2 +#define IME_ENTER_GO 3 +#define IME_NO_AUTO_CAPITALS 0x0002 +#define IME_ALIGN_CENTER 1 +#define IME_RUNNING 1 +#define IME_FINISHED 2 +#define IME_ACCEPTED 0 +#define MAX_TEXT 255 +#define START_GRACE_US 1000000 + +static uint16_t buffer[MAX_TEXT + 1]; +static uint16_t title_text[64]; +static int loaded, forced; +static uint64_t opened_at; + +static void to_utf16(uint16_t *out, int capacity, const char *text) +{ + int i; + + for (i = 0; text[i] && i < capacity - 1; i++) + out[i] = (uint8_t)text[i]; + out[i] = 0; +} + +static int load(void) +{ + int result; + + if (loaded) + return 0; + result = sceCommonDialogInitialize(); + if (result < 0 && (uint32_t)result != COMMON_DIALOG_READY) + { + plat_log("text: common dialog unavailable (0x%08x)\n", (unsigned)result); + return -1; + } + result = sceSysmoduleLoadModule(SYSMODULE_IME_DIALOG); + if (result < 0) + { + plat_log("text: keyboard module unavailable (0x%08x)\n", (unsigned)result); + return -1; + } + loaded = 1; + return 0; +} + +int plat_text_open(const char *title, const char *text) +{ + SceImeDialogParam param; + int user = ps5_user(), result; + + if (test_plan_text()) + { + forced = 1; + return 0; + } + if (load()) + return -1; + sceUserServiceGetForegroundUser(&user); + to_utf16(buffer, MAX_TEXT + 1, text); + to_utf16(title_text, 64, title); + + memset(¶m, 0, sizeof(param)); + param.user = user; + param.type = IME_TYPE_URL; + param.enter_label = IME_ENTER_GO; + param.option = IME_NO_AUTO_CAPITALS; + param.max_length = MAX_TEXT; + param.text = buffer; + param.horizontal_alignment = IME_ALIGN_CENTER; + param.vertical_alignment = IME_ALIGN_CENTER; + param.title = title_text; + result = sceImeDialogInit(¶m, NULL); + if (result < 0) + { + plat_log("text: keyboard did not open (0x%08x)\n", (unsigned)result); + return -1; + } + opened_at = plat_ticks_us(); + return 0; +} + +int plat_text_poll(char *text, int size) +{ + SceImeDialogResult result; + int status, i; + + if (forced) + { + forced = 0; + strncpy(text, test_plan_text(), size - 1); + text[size - 1] = 0; + return 1; + } + status = sceImeDialogGetStatus(); + if (status == IME_RUNNING || (status != IME_FINISHED && plat_ticks_us() - opened_at < START_GRACE_US)) + return 0; + + memset(&result, 0, sizeof(result)); + if (status != IME_FINISHED || sceImeDialogGetResult(&result) < 0 || result.outcome != IME_ACCEPTED) + { + sceImeDialogTerm(); + return -1; + } + for (i = 0; buffer[i] && i < size - 1; i++) + text[i] = buffer[i] < 0x80 ? (char)buffer[i] : '?'; + text[i] = 0; + sceImeDialogTerm(); + return 1; +} diff --git a/src/ps5/video.c b/src/ps5/video.c index a3e51fa..6684c9f 100644 --- a/src/ps5/video.c +++ b/src/ps5/video.c @@ -106,6 +106,8 @@ int plat_video_init(int width, int height) gpu_config_t config; int i; + if (video >= 0) + return width == src_width && height == src_height ? 0 : -1; src_width = width; src_height = height; for (i = 0; i < VIEW_WIDTH; i++) diff --git a/test/console-autoimport.cfg b/test/console-autoimport.cfg new file mode 100644 index 0000000..507afda --- /dev/null +++ b/test/console-autoimport.cfg @@ -0,0 +1,2 @@ +capture 20,120,300 +frames 320 diff --git a/test/console-cpu.cfg b/test/console-cpu.cfg index 83c12af..b91ab55 100644 --- a/test/console-cpu.cfg +++ b/test/console-cpu.cfg @@ -1,3 +1,4 @@ +game doom1.wad present cpu input 40:OPTIONS:2;60:CROSS:2;80:CROSS:2;100:CROSS:2;130:LY=-32000:70 capture 30,200 diff --git a/test/console-import.cfg b/test/console-import.cfg new file mode 100644 index 0000000..4631e73 --- /dev/null +++ b/test/console-import.cfg @@ -0,0 +1,4 @@ +text http://192.168.0.10:8666/ +input 30:DOWN:2;60:CROSS:2;90:CROSS:2;130:CROSS:2;160:CROSS:2;190:DOWN:2;220:CROSS:2;300:CROSS:2 +capture 25,80,120,180,250,400,1200,2400,3600,4800,6000,7100 +frames 7200 diff --git a/test/console-play.cfg b/test/console-play.cfg index e5c0d73..34fa3a6 100644 --- a/test/console-play.cfg +++ b/test/console-play.cfg @@ -1,3 +1,4 @@ +game doom1.wad input 40:OPTIONS:2;60:CROSS:2;80:CROSS:2;100:CROSS:2;130:LY=-32000:70;210:RX=20000:50;270:R2:6;300:TRIANGLE:2;318:R1:12;340:TRIANGLE:2;360:R1:2;400:OPTIONS:2;410:DOWN:2;416:DOWN:2;422:DOWN:2;430:CROSS:2;445:CROSS:2;455:CROSS:2;500:OPTIONS:2;510:UP:2;520:CROSS:2;535:CROSS:2 capture 30,95,200,276,330,440,528,600 frames 900 diff --git a/test/console-soak.cfg b/test/console-soak.cfg index 1baec7b..3669ea1 100644 --- a/test/console-soak.cfg +++ b/test/console-soak.cfg @@ -1,2 +1,3 @@ +game doom1.wad capture 2000,6000,12000 frames 12600 diff --git a/test/console-tnt.cfg b/test/console-tnt.cfg new file mode 100644 index 0000000..0534296 --- /dev/null +++ b/test/console-tnt.cfg @@ -0,0 +1,4 @@ +game tnt.wad +input 40:OPTIONS:2;60:CROSS:2;80:CROSS:2;240:TRIANGLE:2;320:TRIANGLE:2;400:OPTIONS:2;410:DOWN:2;416:DOWN:2;422:DOWN:2;430:CROSS:2;445:CROSS:2;455:CROSS:2 +capture 70,200,270,440,520 +frames 560 diff --git a/test/console-ultimate.cfg b/test/console-ultimate.cfg new file mode 100644 index 0000000..071ae8f --- /dev/null +++ b/test/console-ultimate.cfg @@ -0,0 +1,4 @@ +game doom.wad +input 40:OPTIONS:2;60:CROSS:2;80:DOWN:2;86:DOWN:2;92:DOWN:2;110:CROSS:2;130:CROSS:2;300:TRIANGLE:2 +capture 75,100,280,330 +frames 360 diff --git a/third_party/libarchive/config.h b/third_party/libarchive/config.h new file mode 100644 index 0000000..877becb --- /dev/null +++ b/third_party/libarchive/config.h @@ -0,0 +1,42 @@ +/* libarchive configuration for the importer: reading only, zlib and liblzma, no iconv or crypto. */ +#define __LIBARCHIVE_CONFIG_H_INCLUDED 1 +#define HAVE_STDINT_H 1 +#define HAVE_INTTYPES_H 1 +#define HAVE_STDLIB_H 1 +#define HAVE_STRING_H 1 +#define HAVE_ERRNO_H 1 +#define HAVE_EILSEQ 1 +#define HAVE_LIMITS_H 1 +#define HAVE_UNISTD_H 1 +#define HAVE_FCNTL_H 1 +#define HAVE_FCNTL 1 +#define HAVE_SYS_TIME_H 1 +#define HAVE_SYS_TYPES_H 1 +#define HAVE_SYS_STAT_H 1 +#define HAVE_WCHAR_H 1 + +#define HAVE_DECL_SIZE_MAX 1 +#define HAVE_DECL_SSIZE_MAX 1 +#define HAVE_DECL_INT32_MAX 1 +#define HAVE_DECL_INT32_MIN 1 +#define HAVE_DECL_UINT32_MAX 1 +#define HAVE_DECL_INT64_MAX 1 +#define HAVE_DECL_INT64_MIN 1 +#define HAVE_DECL_UINT64_MAX 1 +#define HAVE_DECL_INTMAX_MAX 1 +#define HAVE_DECL_INTMAX_MIN 1 +#define HAVE_DECL_UINTMAX_MAX 1 + +#define HAVE_WCSCPY 1 +#define HAVE_WCSLEN 1 +#define HAVE_WMEMCMP 1 +#define HAVE_WMEMCPY 1 +#define HAVE_WMEMMOVE 1 +#define HAVE_MBRTOWC 1 +#define HAVE_WCRTOMB 1 + +#define HAVE_ZLIB_H 1 +#define HAVE_LZMA_H 1 +#define HAVE_LIBLZMA 1 + +#define LZMA_API_STATIC 1 diff --git a/third_party/third_party.mk b/third_party/third_party.mk new file mode 100644 index 0000000..97a3232 --- /dev/null +++ b/third_party/third_party.mk @@ -0,0 +1,41 @@ +ARCHIVE_DEPS := .deps/archive +ARCHIVE_STAMP := $(ARCHIVE_DEPS)/zlib-1.3.2/.complete +LA_DIR := $(ARCHIVE_DEPS)/libarchive-3.8.9/libarchive +XZ_DIR := $(ARCHIVE_DEPS)/xz-5.8.4/src/liblzma +ZL_DIR := $(ARCHIVE_DEPS)/zlib-1.3.2 + +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_virtual.c archive_read_support_format_zip.c archive_read_support_format_7zip.c \ + archive_read_support_format_rar.c archive_read_support_format_rar5.c archive_ppmd7.c \ + archive_ppmd8.c archive_blake2s_ref.c archive_blake2sp_ref.c archive_cryptor.c archive_hmac.c \ + archive_read_add_passphrase.c archive_time.c archive_acl.c archive_entry_xattr.c \ + archive_entry_sparse.c archive_rb.c archive_random.c) +XZ_SRC := $(addprefix $(XZ_DIR)/, common/common.c common/alone_decoder.c common/block_decoder.c \ + common/block_header_decoder.c common/block_util.c common/filter_common.c \ + common/filter_decoder.c common/filter_flags_decoder.c common/index_hash.c \ + common/stream_decoder.c common/stream_flags_common.c common/stream_flags_decoder.c \ + common/vli_decoder.c common/vli_size.c common/easy_preset.c check/check.c check/crc32_fast.c \ + check/crc64_fast.c check/sha256.c lz/lz_decoder.c lzma/lzma_decoder.c lzma/lzma2_decoder.c \ + lzma/lzma_encoder_presets.c delta/delta_common.c delta/delta_decoder.c simple/simple_coder.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) +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) + +ARCHIVE_INCLUDES := -I$(LA_DIR) +LA_FLAGS := -DHAVE_CONFIG_H -Ithird_party/libarchive -I$(LA_DIR) -I$(XZ_DIR)/api -I$(ZL_DIR) +XZ_FLAGS := -DHAVE_CONFIG_H -Ithird_party/xz -I$(XZ_DIR)/api -I$(XZ_DIR)/common -I$(XZ_DIR)/check \ + -I$(XZ_DIR)/lz -I$(XZ_DIR)/rangecoder -I$(XZ_DIR)/lzma -I$(XZ_DIR)/delta -I$(XZ_DIR)/simple \ + -I$(XZ_DIR)/../common +ZL_FLAGS := -I$(ZL_DIR) +THIRD_FLAGS := -std=gnu11 -O2 -w -fno-strict-aliasing -DNDEBUG -Dmalloc=import_malloc \ + -Dcalloc=import_calloc -Drealloc=import_realloc -Dfree=import_free -Dstrdup=import_strdup + +third_obj = $(patsubst $(ARCHIVE_DEPS)/%.c,$(1)/third_party/%.o,$(THIRD_SRC)) +third_flags = $(if $(findstring /libarchive-,$(1)),$(LA_FLAGS),$(if $(findstring /xz-,$(1)),$(XZ_FLAGS),$(ZL_FLAGS))) + +$(THIRD_SRC): | $(ARCHIVE_STAMP) + +$(ARCHIVE_STAMP): + bash tools/fetch-archive-libs.sh $(ARCHIVE_DEPS) diff --git a/third_party/xz/config.h b/third_party/xz/config.h new file mode 100644 index 0000000..f2d84f6 --- /dev/null +++ b/third_party/xz/config.h @@ -0,0 +1,28 @@ +/* liblzma configuration for the importer: decoders only, single-threaded, portable C. */ +#define HAVE_STDBOOL_H 1 +#define HAVE__BOOL 1 +#define HAVE_STDINT_H 1 +#define HAVE_INTTYPES_H 1 +#define HAVE_VISIBILITY 0 +#define SIZEOF_SIZE_T 8 + +#define HAVE_DECODERS 1 +#define HAVE_DECODER_LZMA1 1 +#define HAVE_DECODER_LZMA2 1 +#define HAVE_DECODER_DELTA 1 +#define HAVE_DECODER_X86 1 +#define HAVE_DECODER_ARM 1 +#define HAVE_DECODER_ARMTHUMB 1 +#define HAVE_DECODER_ARM64 1 +#define HAVE_DECODER_POWERPC 1 +#define HAVE_DECODER_IA64 1 +#define HAVE_DECODER_SPARC 1 +#define HAVE_DECODER_RISCV 1 + +#define HAVE_CHECK_CRC32 1 +#define HAVE_CHECK_CRC64 1 +#define HAVE_CHECK_SHA256 1 +#define HAVE_INTERNAL_SHA256 1 + +#define TUKLIB_FAST_UNALIGNED_ACCESS 1 +#define HAVE___BUILTIN_BSWAPXX 1 diff --git a/tools/console_test.py b/tools/console_test.py index 6a98f76..c6cba0b 100644 --- a/tools/console_test.py +++ b/tools/console_test.py @@ -5,8 +5,8 @@ Writes the plan to the title folder as test.cfg (plus a capture port), launches PPSA99666 with the doomlaunch payload (tools/launcher, loaded through the Payload Manager), pulls the frames the plan captures from the running title over TCP, converts them to PNG under build/test/run/, -prints the title's log lines from the kernel log (klogsrv capture in build/test/klog.txt) and -removes test.cfg again. The PS5Upload helper must be running. +receives the title's log over the same connection when it exits (build/test/run/doom.log), +prints it and removes test.cfg again. The PS5Upload helper must be running. """ import argparse import base64 @@ -28,12 +28,8 @@ LAUNCHER = ROOT / "build" / "launcher" / "doomlaunch.elf" LAUNCHER_DIR = "/data/pldmgr/payloads/doomlaunch" TEST_PLAN = f"/data/homebrew/{TITLE}/test.cfg" CAPTURE_PORT = 9119 -KLOG = ROOT / "build" / "test" / "klog.txt" OUT = ROOT / "build" / "test" / "run" FRAME = (1920, 1080) -NOISE = ("PFAuth", "FMEM", "PSM.UI", "catalog", "SHELLFLAG", "LOGIN MGR", "CloudMessaging", - "ResArbitrator", "PlayGo", "AsyncStorage", "a53mm", "BAPM", "<118>", "[KERNEL]", "###", - "num clients", "Amm config", "wt_application", "appmgr_create") def get(url, timeout=10): @@ -112,8 +108,11 @@ def pull_captures(seen): name = receive_exactly(sock, name_length).decode() (size,) = struct.unpack("&2 + exit 1 +} + +fetch libarchive-3.8.9 f5a6539059cf5e597dbeda37bfa4874b1e8dea063c8d93bf85a2b44af90a5bd4 \ + https://www.libarchive.org/downloads/libarchive-3.8.9.tar.gz \ + https://github.com/libarchive/libarchive/releases/download/v3.8.9/libarchive-3.8.9.tar.gz +fetch xz-5.8.4 0014c7886930454fe8bd4228665b51af55eeae560ea135c9c4cd33f55b2591d9 \ + https://github.com/tukaani-project/xz/releases/download/v5.8.4/xz-5.8.4.tar.gz \ + https://downloads.sourceforge.net/project/lzmautils/xz-5.8.4.tar.gz +fetch zlib-1.3.2 bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 \ + https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz \ + https://zlib.net/zlib-1.3.2.tar.gz diff --git a/tools/fetch-freedoom.sh b/tools/fetch-freedoom.sh deleted file mode 100644 index 155bd02..0000000 --- a/tools/fetch-freedoom.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -# Downloads Freedoom (free, BSD-licensed Doom-compatible game data) and checks it. -set -euo pipefail - -version=0.13.0 -url="https://github.com/freedoom/freedoom/releases/download/v$version/freedoom-$version.zip" -sha256=3f9b264f3e3ce503b4fb7f6bdcb1f419d93c7b546f4df3e874dd878db9688f59 -out=${1:-wads} -tmp=$(mktemp -d) -trap 'rm -rf -- "$tmp"' EXIT - -mkdir -p "$out" -wget -q -O "$tmp/freedoom.zip" "$url" -echo "$sha256 $tmp/freedoom.zip" | sha256sum --check --quiet -unzip -q -o "$tmp/freedoom.zip" -d "$tmp" -mv "$tmp/freedoom-$version/freedoom1.wad" "$tmp/freedoom-$version/freedoom2.wad" "$out/" -mv "$tmp/freedoom-$version/COPYING.txt" "$out/FREEDOOM-COPYING.txt" -mv "$tmp/freedoom-$version/CREDITS.txt" "$out/FREEDOOM-CREDITS.txt"