Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ jobs:
run: |
source emscripten/get_emscripten.sh
emmake emscripten/build.sh
echo "::group::Module sizes (what WebKit must compile on the phone)"
ls -l build/install/*.so build/install/hl2_launcher.wasm | awk '{ printf "%9.2f MB %s\n", $5/1048576, $9 }' | sort -rn
du -cb build/install/*.so | tail -n 1 | awk '{ printf "total side modules: %.1f MB\n", $1/1048576 }'
echo "::endgroup::"
cd build/install/
zip -r ../../out.zip *
# "." rather than "*": the glob skips dotfiles, which would drop
# .nojekyll and leave a manual GitHub Pages deploy broken.
zip -r ../../out.zip .
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
Expand Down
78 changes: 78 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Deploy to GitHub Pages

# GitHub Pages cannot send Cross-Origin-Opener-Policy / Cross-Origin-Embedder-
# Policy, which the threaded wasm build needs for SharedArrayBuffer. The bundle
# ships coi-serviceworker.js, which adds those headers from a service worker, so
# the published site ends up cross-origin isolated anyway.
#
# Run it by hand from any branch: Actions -> Deploy to GitHub Pages -> Run
# workflow. Pages must be set to "GitHub Actions" as its source, under
# Settings -> Pages.

on:
push:
branches: [master]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

# Never let two deploys race; queue them instead.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Build wasm
run: |
source emscripten/get_emscripten.sh
emmake emscripten/build.sh

# Optional. Set the CHUNKS_BASE_URL repository variable (Settings ->
# Secrets and variables -> Actions -> Variables) to publish the game data
# alongside the engine. Mind the limits: GitHub Pages refuses to publish a
# site over 1GB, so for the full game you usually want to leave this unset
# and host the chunks elsewhere, pointing at them with ?chunks=<url>.
- name: Fetch game data
if: vars.CHUNKS_BASE_URL != ''
run: |
python3 emscripten/fetch_chunks.py \
--dir build/install \
--base-url "${{ vars.CHUNKS_BASE_URL }}"

- name: Report bundle size
run: |
du -sh build/install
echo "--- largest files ---"
find build/install -type f -printf '%s\t%p\n' | sort -rn | head -10 | \
awk -F'\t' '{ printf "%8.1f MB %s\n", $1/1048576, $2 }'
total=$(du -sb build/install | cut -f1)
if [ "$total" -gt 1073741824 ]; then
echo "::error::Site is $((total/1048576))MB. GitHub Pages will not publish a site over 1GB."
exit 1
fi

- uses: actions/configure-pages@v5

- uses: actions/upload-pages-artifact@v3
with:
path: build/install

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
78 changes: 76 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,76 @@
+ sound
+ saving/loading (works, TODO: save to browser storage)
+ sometimes render breaks (something related to lightmaps?)
+ fullscreen html button (works through game settings)
+ fullscreen html button (works through game settings; iPhone Safari has no
fullscreen API at all -- use Add to Home Screen)

## running it

Pushing to GitHub is enough to **build** it -- the `Build` workflow compiles the
wasm bundle and uploads `release.zip` -- but a build on its own is not a playable
game. Three things have to come together:

**1. Get the engine.** Open the repo's Actions tab, pick the latest green `Build`
run, and download the `release.zip` artifact. Unzip it.

**2. Add the game data.** The zip contains the engine and *no* Portal content.
The engine fetches `chunks/<map>.data` at runtime; without those files the page
stops with "Could not load game data". You need to own Portal.

```sh
cd <unzipped release.zip>
python3 fetch_chunks.py --dir .
```

Or build the chunks from your own Portal install with `emscripten/repackage.js`
(see [packing game data](#packing-game-data)).

**3. Serve it cross-origin isolated, over HTTPS or localhost.** This is not
optional: the engine uses threads, threads need `SharedArrayBuffer`, and browsers
only hand one out to a page sending `Cross-Origin-Opener-Policy` and
`Cross-Origin-Embedder-Policy`. Opening `hl2_launcher.html` as a `file://` URL
will never work.

```sh
python3 serve.py --dir .
# http://localhost:8080/
```

### GitHub Pages

Pages cannot send those headers, so the bundle ships `coi-serviceworker.js`,
which installs them from a service worker instead. Set **Settings > Pages >
Source** to **GitHub Actions**, then run **Actions > Deploy to GitHub Pages**.
The first visit reloads itself once to pick the worker up.

The catch is size, not headers: GitHub blocks files over 100MB and will not
publish a Pages site over 1GB, so the game data usually has to live elsewhere.
Host the chunks anywhere (a GitHub Release, R2, any static host) and point at
them:

```
https://<user>.github.io/<repo>/?chunks=https://your-host.example/portal/chunks
```

Then open it, tap/click **Tap to play**, and use the main menu to start a new
game. If something is missing, the page now says what on screen rather than
showing a black screen.

To play on an actual iPhone you need an HTTPS address, so put the server behind
a tunnel (`cloudflared tunnel --url http://localhost:8080`) or deploy to a host
that respects the bundled `_headers` file, such as Netlify or Cloudflare Pages.
Full details in [emscripten/README-hosting.md](emscripten/README-hosting.md).

## phones (iPhone / iPad / Android)

Touch controls are turned on automatically on touch devices: drag the left half
of the screen to move, the right half to look, and use the on-screen buttons for
jump / crouch / use / both portals. Play in landscape.

iPhone needs iOS 17+ with Lockdown Mode off. See
[emscripten/README-hosting.md](emscripten/README-hosting.md) for the full
requirements, HTTPS tunnelling for on-device testing, fullscreen on iPhone, and
the memory knobs.

## building

Expand Down Expand Up @@ -36,11 +105,16 @@ embuilder --force --pic build sdl2 sdl2-mt
# patch glMapBufferRange to allow some "unsupported" parameters
patch /emsdk/upstream/emscripten/src/lib/libwebgl.js emscripten/libwebgl.patch

emmake ./build_emscripten.sh
emmake ./emscripten/build.sh
```
then download packed game data (yikes.pw/portal/chunks/mapName.data for each map) and put it to ./build/install/chunks/

`build.sh` reads `EM_INITIAL_MEMORY`, `EM_MAXIMUM_MEMORY` and
`EM_PTHREAD_POOL_SIZE` from the environment if you want to tune it for a
specific target.

## packing game data
<a id="packing-game-data"></a>
first of all, you'll need to build engine from https://github.com/nillerusr/source-engine for your native arch

and after that you should add that printf to ./filesystem/basefilesystem.cpp, to dump all files that engine would access (textures/models that map needs)
Expand Down
26 changes: 22 additions & 4 deletions appframework/sdlmgr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#ifdef __EMSCRIPTEN__
#include <dlfcn.h>
#include <emscripten.h>
#include <emscripten/html5.h>
#endif

Expand Down Expand Up @@ -1198,10 +1199,27 @@ void CSDLMgr::OnFrameRendered()
SDL_SetWindowGrab( m_Window, bWindowGrab );
SDL_SetRelativeMouseMode( bRelativeMouseMode );
#ifdef __EMSCRIPTEN__
if (bWindowGrab)
emscripten_request_pointerlock("canvas", true);
else
emscripten_exit_pointerlock();
// iOS has never shipped the Pointer Lock API, and on a touch device the
// engine's on-screen touch controls drive the view anyway. Requesting a
// lock there throws out of the deferred-call handler on every click, so
// only ask where the browser actually supports it.
//
// This runs on the proxied main pthread, which has no DOM of its own --
// the probe has to be evaluated on the browser main thread.
static const bool bPointerLockSupported = MAIN_THREAD_EM_ASM_INT({
var body = document.body;
if (!body) return 0;
if (navigator.maxTouchPoints > 0 && !window.matchMedia('(pointer: fine)').matches) return 0;
return (body.requestPointerLock || body.webkitRequestPointerLock) ? 1 : 0;
}) != 0;

if ( bPointerLockSupported )
{
if (bWindowGrab)
emscripten_request_pointerlock("canvas", true);
else
emscripten_exit_pointerlock();
}
#endif

SDL_ShowCursor( m_bCursorVisible ? 1 : 0 );
Expand Down
Loading