Compare commits

..
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 59b1fb422b Steam Frame: a library button that starts, updates and quits the game
The launcher gives Lepton what Steam gives an Android title of its own (a
compat data path, which the app's data links to after an install, and the
shader path), keeps a container id of its own, tries a start that ends at
once again (a new APK's first start only installs it), and stops when the
game was quit. Steam's "Exit game" cannot reach the game in Lepton's
container: the VR options get "Quit Halo" (asked twice), which on Android
now ends the process and leaves quit_requested.txt for the launcher. The
library's pictures are in port/android/steam_frame_grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:17:35 +02:00
Stefan MaierandClaude Opus 5.5 5ecd3075a0 Steam Frame: a launcher that starts the game as Steam starts Android titles
Lepton runs the APK in a container of its own, kept between starts, and
installs it anew whenever the APK is newer; the script starts it a second
time after a new APK's first start, which only installs it. With the APK in
~/Documents/HaloCE, updating the game is replacing that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:17:54 +02:00
20 changed files with 163 additions and 1113 deletions

No files matched your search

+31 -1
View File
@@ -39,7 +39,37 @@ What differs is found at run time, not by a build of its own.
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
| 7 | Multiplayer, voice, cross-play with the Quest | |
| 8 | Installing it, for others | |
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
## Starting it on the Frame
A button in the Steam library ("Halo VR", a non-Steam game) runs
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
anew whenever the file is newer. Updating the game is replacing that APK.
What the script does that Steam would for an Android title:
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
install Lepton links the app's data to it, a dangling link without it;
and the shader path, which Lepton reads Steam's shader dump path against;
- a container id of its own: with the shortcut's id, SteamVR's client in the
container stalled in xrCreateInstance;
- a few starts: a new APK's first start only installs it, and a start that
ends within 30 s is tried again (up to three times), unless the game was
quit (it writes `quit_requested.txt`).
Steam's "Exit game" does not reach the game (its process is the container's,
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
pause menu, twice). A container ended within 30 s of its start makes
Lepton drop the app's install, so the next start installs it again.
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
it starts; appid 3151454348 for this name and path), its pictures in
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
beside Lepton's `<frame-ip>:5555`.
## The game's data
@@ -19,9 +19,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- the voice chat in multiplayer (host_voice.c) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- installing a new version of the game (Updater) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -1,10 +1,7 @@
package com.halo.decomp;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
@@ -38,7 +35,6 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final int ASK_MICROPHONE = 2;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
private File dataRoot;
@@ -64,37 +60,12 @@ public class LauncherActivity extends Activity {
passOnHardwareId();
passOnInvite(getIntent());
if (haveData()) {
if (!askForMicrophone())
startGame();
startGame();
return;
}
buildInterface();
}
/**
* The voice chat's microphone (port/android/host/host_voice.c), asked
* for here, once, on this flat panel, before the game starts: in the
* game, the question would stop it. Returns true if it asked (the game
* starts on the answer).
*/
private boolean askForMicrophone() {
SharedPreferences preferences = getSharedPreferences("launcher", MODE_PRIVATE);
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|| preferences.getBoolean("asked_microphone", false))
return false;
preferences.edit().putBoolean("asked_microphone", true).apply();
requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, ASK_MICROPHONE);
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
super.onRequestPermissionsResult(requestCode, permissions, results);
if (requestCode == ASK_MICROPHONE)
startGame();
}
/**
* An internet play invite link the app was opened with: the game
* (port/linux/src/p2p.c) picks it up from join_link.txt, whether it is
@@ -147,23 +118,6 @@ public class LauncherActivity extends Activity {
partial.delete();
}
/**
* Whether the microphone may be used, for the game (port/linux/src/
* voice.c), which opens it only then: opening it otherwise would have
* Android ask in the middle of the game.
*/
private void passOnMicrophonePermission() {
if (dataRoot == null)
return;
boolean granted =
checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
try (OutputStream out = new FileOutputStream(new File(dataRoot, "microphone_permission.txt"))) {
out.write((granted ? "1" : "0").getBytes("UTF-8"));
} catch (java.io.IOException e) {
// the game then does without the microphone
}
}
/** The game data, or the marker file of the VR self-test (host_xr.c). */
private boolean haveData() {
return dataRoot != null && (new File(dataRoot, "maps/ui.map").isFile() ||
@@ -171,7 +125,6 @@ public class LauncherActivity extends Activity {
}
private void startGame() {
passOnMicrophonePermission();
startActivity(new Intent(this, HaloActivity.class));
finish();
}
-15
View File
@@ -134,21 +134,6 @@ int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
/* darkens the eye images from their edges, strength 0..1 (0: off) */
void host_xr_set_vignette(float strength);
/* ---------- the voice chat (host_voice.c) */
/* opens the voices' playback, and the microphone if asked (else closes
it): their volume, and the level (0..1 of full scale) the microphone's
sound must reach to be sent */
int host_voice_start(int microphone, float volume, float threshold);
void host_voice_stop(void);
/* the microphone's speech since asked: Opus packets, each after a byte of
its length; returns the bytes */
int host_voice_take_packets(void *buffer, int size);
/* the microphone's level (0..1000) */
int host_voice_level(void);
/* a speaker's packet (sequence numbered), played at these gains */
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right);
/* ---------- Android */
/* the storage directories the port uses, copied into buffer */
-494
View File
@@ -1,494 +0,0 @@
/*
HOST_VOICE.C
The voice chat's sound (the VR port's; port/linux/game/network_voice.c
sends and receives it): the microphone, Opus, and the voices played. It
runs here, natively, where SDL's audio threads may call it.
- The microphone: SDL's default recording device, mono at 16 kHz, with
Android's voice communication preset (its echo cancelling and noise
suppression: SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET). Every 20 ms of it
that is loud enough to be speech (or follows speech closely, VAD_HANGOVER)
is encoded (Opus, VOIP, 16 kbit/s) for the guest to send.
- The voices: each speaker's packets, decoded as they come (a gap in their
numbers concealed by Opus's packet loss concealment), wait in the
speaker's buffer until JITTER_START of them is there, and are mixed, each
at the gains its latest packet was given (left and right: where the
speaker is), into a stream of their own.
The devices are opened and closed on a thread of their own (voice_worker):
opening a microphone can take a long while (an emulator's took a minute),
which the game, whose clock the other machines follow, must not wait for.
*/
#include "host.h"
#include <SDL3/SDL.h>
#include <opus.h>
#include <pthread.h>
#include <string.h>
enum
{
VOICE_RATE = 16000,
FRAME_SAMPLES = VOICE_RATE / 50,
MAXIMUM_PACKET = 200,
MAXIMUM_SPEAKERS = 32,
/* a speaker's buffer: two seconds */
SPEAKER_BUFFER = VOICE_RATE * 2,
/* played from once this much is there, after silence */
JITTER_START = FRAME_SAMPLES * 3,
/* more than this behind: the oldest dropped */
JITTER_MAXIMUM = FRAME_SAMPLES * 15,
/* frames sent on after the voice drops below the threshold */
VAD_HANGOVER = 15,
BITRATE = 16000,
};
struct speaker
{
int used;
int id;
OpusDecoder *decoder;
int has_sequence;
unsigned short sequence;
short buffer[SPEAKER_BUFFER];
int read;
int count;
int playing;
float gain_left;
float gain_right;
unsigned long long heard_ticks;
};
static struct
{
SDL_Mutex *lock;
/* the devices wanted, which the worker opens and closes to match
(under worker_lock); the recording stream is read under it too */
pthread_mutex_t worker_lock;
pthread_cond_t worker_wake;
int worker_started;
int want_playback;
int want_recording;
/* the microphone */
SDL_AudioStream *recording;
OpusEncoder *encoder;
short frame[FRAME_SAMPLES];
int frame_count;
int hangover;
float threshold;
int level;
/* the voices */
SDL_AudioStream *playback;
struct speaker speakers[MAXIMUM_SPEAKERS];
float volume;
} voice;
static void voice_lock(void)
{
if (!voice.lock)
voice.lock = SDL_CreateMutex();
SDL_LockMutex(voice.lock);
}
static void voice_unlock(void)
{
SDL_UnlockMutex(voice.lock);
}
/* the voices mixed, as SDL asks for them (on its audio thread) */
static void SDLCALL voice_playback_callback(void *userdata, SDL_AudioStream *stream, int additional, int total)
{
short mixed[2 * 512];
(void)userdata;
(void)total;
while (additional > 0)
{
int frames = additional / 4;
int frame;
int index;
if (frames > 512)
frames = 512;
if (frames <= 0)
frames = 1;
memset(mixed, 0, sizeof(mixed));
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
{
struct speaker *speaker = &voice.speakers[index];
if (!speaker->used)
continue;
if (!speaker->playing && speaker->count >= JITTER_START)
speaker->playing = 1;
if (!speaker->playing)
continue;
for (frame = 0; frame < frames && speaker->count > 0; frame++)
{
float sample = (float)speaker->buffer[speaker->read] * voice.volume;
int left = mixed[2 * frame] + (int)(sample * speaker->gain_left);
int right = mixed[2 * frame + 1] + (int)(sample * speaker->gain_right);
mixed[2 * frame] = (short)(left > 32767 ? 32767 : left < -32768 ? -32768 : left);
mixed[2 * frame + 1] = (short)(right > 32767 ? 32767 : right < -32768 ? -32768 : right);
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
speaker->count--;
}
/* (run dry: wait for enough again) */
if (!speaker->count)
speaker->playing = 0;
}
voice_unlock();
SDL_PutAudioStreamData(stream, mixed, frames * 4);
additional -= frames * 4;
}
}
static int voice_open_playback(void)
{
SDL_AudioSpec spec;
if (voice.playback)
return 1;
spec.format = SDL_AUDIO_S16;
spec.channels = 2;
spec.freq = VOICE_RATE;
voice.playback = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, voice_playback_callback,
NULL);
if (!voice.playback)
{
host_logf(HOST_LOG_WARN, "voice: cannot open the playback stream: %s", SDL_GetError());
return 0;
}
SDL_ResumeAudioStreamDevice(voice.playback);
return 1;
}
static SDL_AudioStream *voice_open_recording(void)
{
SDL_AudioSpec spec;
SDL_AudioStream *recording;
int error;
if (!voice.encoder)
{
voice.encoder = opus_encoder_create(VOICE_RATE, 1, OPUS_APPLICATION_VOIP, &error);
if (!voice.encoder)
{
host_logf(HOST_LOG_WARN, "voice: cannot make the encoder (%d)", error);
return NULL;
}
opus_encoder_ctl(voice.encoder, OPUS_SET_BITRATE(BITRATE));
opus_encoder_ctl(voice.encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
opus_encoder_ctl(voice.encoder, OPUS_SET_INBAND_FEC(1));
opus_encoder_ctl(voice.encoder, OPUS_SET_PACKET_LOSS_PERC(10));
}
/* Android's voice communication preset: echo cancelling, noise
suppression (AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION) */
SDL_SetHint(SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET, "7");
spec.format = SDL_AUDIO_S16;
spec.channels = 1;
spec.freq = VOICE_RATE;
recording = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, NULL);
if (!recording)
{
host_logf(HOST_LOG_WARN, "voice: cannot open the microphone: %s", SDL_GetError());
return NULL;
}
SDL_ResumeAudioStreamDevice(recording);
host_logf(HOST_LOG_INFO, "voice: the microphone is open");
return recording;
}
static void voice_close_recording(void)
{
SDL_AudioStream *recording;
pthread_mutex_lock(&voice.worker_lock);
recording = voice.recording;
voice.recording = NULL;
voice.level = 0;
pthread_mutex_unlock(&voice.worker_lock);
if (!recording)
return;
SDL_DestroyAudioStream(recording);
host_logf(HOST_LOG_INFO, "voice: the microphone is closed");
}
static void voice_close_playback(void)
{
SDL_AudioStream *playback = voice.playback;
voice.playback = NULL;
if (playback)
SDL_DestroyAudioStream(playback);
}
/* opens and closes the devices as wanted, for as long as the app runs */
static void *voice_worker(void *unused)
{
(void)unused;
pthread_mutex_lock(&voice.worker_lock);
for (;;)
{
int want_playback = voice.want_playback;
int want_recording = voice.want_recording;
int have_playback = voice.playback != NULL;
int have_recording = voice.recording != NULL;
if (want_playback == have_playback && want_recording == have_recording)
{
pthread_cond_wait(&voice.worker_wake, &voice.worker_lock);
continue;
}
pthread_mutex_unlock(&voice.worker_lock);
if (want_recording && !have_recording)
{
/* (opened outside the lock, which the game reads it under) */
SDL_AudioStream *recording = voice_open_recording();
pthread_mutex_lock(&voice.worker_lock);
voice.recording = recording;
pthread_mutex_unlock(&voice.worker_lock);
/* (a microphone that will not open is not tried again until
wanted anew) */
if (!recording)
{
pthread_mutex_lock(&voice.worker_lock);
voice.want_recording = 0;
pthread_mutex_unlock(&voice.worker_lock);
}
}
else if (!want_recording && have_recording)
{
voice_close_recording();
}
if (want_playback && !have_playback)
{
if (!voice_open_playback())
{
pthread_mutex_lock(&voice.worker_lock);
voice.want_playback = 0;
pthread_mutex_unlock(&voice.worker_lock);
}
}
else if (!want_playback && have_playback)
{
voice_close_playback();
}
pthread_mutex_lock(&voice.worker_lock);
}
return NULL;
}
static void voice_want(int playback, int recording)
{
if (!voice.worker_started)
{
voice.worker_started = 1;
pthread_mutex_init(&voice.worker_lock, NULL);
pthread_cond_init(&voice.worker_wake, NULL);
if (host_native_thread_create(voice_worker, NULL, 256 * 1024) != 0)
{
host_logf(HOST_LOG_WARN, "voice: cannot start the device thread");
return;
}
}
pthread_mutex_lock(&voice.worker_lock);
voice.want_playback = playback;
voice.want_recording = recording;
pthread_cond_signal(&voice.worker_wake);
pthread_mutex_unlock(&voice.worker_lock);
}
/* ---------- the guest's side */
int host_voice_start(int microphone, float volume, float threshold)
{
voice_lock();
voice.volume = volume < 0.0f ? 0.0f : volume > 4.0f ? 4.0f : volume;
voice.threshold = threshold;
voice_unlock();
voice_want(1, microphone);
return 1;
}
void host_voice_stop(void)
{
int index;
voice_want(0, 0);
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
{
if (voice.speakers[index].decoder)
opus_decoder_destroy(voice.speakers[index].decoder);
}
memset(voice.speakers, 0, sizeof(voice.speakers));
voice_unlock();
}
int host_voice_take_packets(void *buffer, int size)
{
unsigned char *out = buffer;
int used = 0;
if (!voice.worker_started)
return 0;
pthread_mutex_lock(&voice.worker_lock);
if (!voice.recording)
{
voice.frame_count = 0;
voice.hangover = 0;
pthread_mutex_unlock(&voice.worker_lock);
return 0;
}
for (;;)
{
int wanted = (FRAME_SAMPLES - voice.frame_count) * (int)sizeof(short);
int got = SDL_GetAudioStreamData(voice.recording, voice.frame + voice.frame_count, wanted);
double energy = 0.0;
int index;
if (got <= 0)
break;
voice.frame_count += got / (int)sizeof(short);
if (voice.frame_count < FRAME_SAMPLES)
break;
voice.frame_count = 0;
for (index = 0; index < FRAME_SAMPLES; index++)
energy += (double)voice.frame[index] * voice.frame[index];
energy = SDL_sqrt(energy / FRAME_SAMPLES) / 32768.0;
voice.level = (int)(energy * 1000.0);
if (energy >= voice.threshold)
voice.hangover = VAD_HANGOVER;
else if (voice.hangover > 0)
voice.hangover--;
else
continue;
if (used + 1 + MAXIMUM_PACKET > size)
continue;
{
int length = opus_encode(voice.encoder, voice.frame, FRAME_SAMPLES, out + used + 1, MAXIMUM_PACKET);
if (length > 0 && length < 256)
{
out[used] = (unsigned char)length;
used += 1 + length;
}
}
}
pthread_mutex_unlock(&voice.worker_lock);
return used;
}
int host_voice_level(void)
{
return voice.level;
}
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right)
{
struct speaker *speaker = NULL;
struct speaker *oldest = NULL;
short decoded[FRAME_SAMPLES * 6];
int frames[4];
int frame_count = 0;
int index;
if (length <= 0 || length > MAXIMUM_PACKET)
return;
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
{
if (voice.speakers[index].used && voice.speakers[index].id == id)
speaker = &voice.speakers[index];
}
if (!speaker)
{
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
{
if (!voice.speakers[index].used)
speaker = &voice.speakers[index];
else if (!oldest || voice.speakers[index].heard_ticks < oldest->heard_ticks)
oldest = &voice.speakers[index];
}
if (!speaker)
speaker = oldest;
if (!speaker->decoder)
{
int error;
speaker->decoder = opus_decoder_create(VOICE_RATE, 1, &error);
if (!speaker->decoder)
{
voice_unlock();
return;
}
}
else
{
opus_decoder_ctl(speaker->decoder, OPUS_RESET_STATE);
}
speaker->used = 1;
speaker->id = id;
speaker->has_sequence = 0;
speaker->read = speaker->count = 0;
speaker->playing = 0;
}
speaker->heard_ticks = SDL_GetTicks();
speaker->gain_left = gain_left;
speaker->gain_right = gain_right;
{
unsigned short number = (unsigned short)sequence;
if (speaker->has_sequence)
{
short gap = (short)(number - speaker->sequence);
/* (late, or again: dropped) */
if (gap <= 0)
{
voice_unlock();
return;
}
/* (lost ones: concealed, a few) */
if (gap > 1 && gap <= 4 && speaker->count > 0)
{
int missing;
for (missing = 1; missing < gap && frame_count < 3; missing++)
{
frames[frame_count] = opus_decode(speaker->decoder, NULL, 0, decoded + frame_count * FRAME_SAMPLES,
FRAME_SAMPLES, 0);
frame_count++;
}
}
}
speaker->has_sequence = 1;
speaker->sequence = number;
}
frames[frame_count] = opus_decode(speaker->decoder, data, length, decoded + frame_count * FRAME_SAMPLES,
FRAME_SAMPLES * 2, 0);
frame_count++;
for (index = 0; index < frame_count; index++)
{
int sample;
for (sample = 0; sample < frames[index] && sample < FRAME_SAMPLES * 2; sample++)
{
if (speaker->count >= JITTER_MAXIMUM)
{
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
speaker->count--;
}
speaker->buffer[(speaker->read + speaker->count) % SPEAKER_BUFFER] =
decoded[index * FRAME_SAMPLES + sample];
speaker->count++;
}
}
voice_unlock();
}
-5
View File
@@ -60,11 +60,6 @@ host_xr_copy_layer_region
host_xr_end_frame
host_xr_haptic
host_xr_set_vignette
host_voice_start
host_voice_stop
host_voice_take_packets
host_voice_level
host_voice_play
host_xr_copy_layer_array
host_android_path
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
# and installs it anew whenever the APK is newer than the installed one
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
# (a container of its own, not the shortcut's id: with that, SteamVR's
# client in the container waited on xrCreateInstance)
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
# (what Steam gives an Android title of its own, and a non-Steam game not:
# the app's data in the container links to the compat data path's
# internal/, a dangling link without it; and the shader paths: Lepton reads
# the dump path against the other)
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
APK="$HOME/Documents/HaloCE/HaloVR.apk"
LOG="$HOME/Documents/HaloCE/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
rm -f "$QUIT"
CONTEXT="steamlaunch-$SteamAppId"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
log "start (pid $$, parent $PPID)"
# Steam's "Exit game" ends the game's processes as Steam knows them (this
# script, under Steam's reaper), not the container, which podman runs: a
# signal here is passed on to Lepton, and a watcher of its own takes the
# container down once this script is gone (killed)
stopping=0
child=0
stop()
{
log "signal $1"
stopping=1
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
}
trap 'stop TERM' TERM
trap 'stop INT' INT
trap 'stop HUP' HUP
script=$$
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
' </dev/null >/dev/null 2>&1 &
run()
{
"$LEPTON" waitforexitandrun -- "$APK" &
child=$!
# (wait returns early when a signal comes: until Lepton is gone)
while kill -0 "$child" 2>/dev/null; do
wait "$child"
done
child=0
}
# (a new APK is only installed on its first start, not started; and a start
# right after a game that was ended hard can fail in SteamVR, which then
# needs a moment: again, up to three times, while it ends within 30 s)
for attempt in 1 2 3; do
started=$SECONDS
run
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
break
fi
log "ended within $((SECONDS - started)) s: again"
sleep 5
done
rm -f "$QUIT"
log "end"
-14
View File
@@ -3136,7 +3136,6 @@ void network_distributed_new_game(
network_damage_new_game();
network_vr_new_game();
bots_new_game();
network_voice_new_game();
}
/* after each tick (game_time.c) */
@@ -3182,7 +3181,6 @@ void network_distributed_tick(
distributed_host_send_players();
network_vr_tick();
bots_tick();
network_voice_tick();
distributed_send_pickups();
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
distributed_send_game_state(NONE);
@@ -3192,7 +3190,6 @@ void network_distributed_tick(
distributed_note_own_positions();
distributed_client_send_inputs();
network_vr_tick();
network_voice_tick();
distributed_client_send_predictions();
network_objects_client_tick();
network_damage_client_tick();
@@ -3670,7 +3667,6 @@ void network_distributed_handle_message(
case _distributed_message_client_ready: entry_size = 0; break;
case _distributed_message_client_identity: entry_size = sizeof(struct distributed_client_identity); break;
case _distributed_message_vr_poses: entry_size = network_vr_entry_size(); break;
case _distributed_message_voice: entry_size = network_voice_entry_size(); break;
case _distributed_message_damage_events:
case _distributed_message_hit_reports: entry_size = network_damage_entry_size(header.type); break;
default: entry_size = network_objects_entry_size(header.type); break;
@@ -3684,16 +3680,6 @@ void network_distributed_handle_message(
/* (each kind from the host, or from a client; a VR player's hands
both ways) */
if (header.type == _distributed_message_voice)
{
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
game_connection() != _game_connection_network_server)
{
return;
}
network_voice_handle(machine_index, message, size);
return;
}
if (header.type == _distributed_message_vr_poses)
{
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
-10
View File
@@ -72,10 +72,6 @@ enum
/* a VR player's hands, every tick: a client's own to the host, the
host's of the others to each client (unreliable; network_vr.c) */
_distributed_message_vr_poses = 240,
/* a player's speech, every tick while they speak: a client's own to
the host, the host's and the other clients' to each client (unreliable;
network_voice.c) */
_distributed_message_voice,
NUMBER_OF_DISTRIBUTED_MESSAGES
};
@@ -256,10 +252,4 @@ void network_vr_handle_poses(long machine_index, void const *entries, short coun
void bots_new_game(void);
void bots_tick(void);
/* the voice chat (network_voice.c) */
void network_voice_new_game(void);
void network_voice_tick(void);
word network_voice_entry_size(void);
void network_voice_handle(long machine_index, void const *message, word size);
#endif // __NETWORK_DISTRIBUTED_H
-326
View File
@@ -1,326 +0,0 @@
/*
NETWORK_VOICE.C
The voice chat (the VR port's): an extension of the distributed netcode
(port/linux/NETCODE.md, network_distributed.c), as the VR players' hands
are (network_vr.c).
- Every tick, a machine sends what its microphone heard of its player's
speech (halo_voice.h: Opus packets of 20 ms, only while they speak) in
_distributed_message_voice (unreliable): a client to the host, which
plays it and passes it on to its other clients; the host its own to
every client.
- A machine plays each speaker's voice from where they are: a teammate
(in a game of teams) over the radio, as loud anywhere and only a little
to the side they are on; anyone else as near as they are (no further off
than VOICE_RANGE), from their side. A speaker with no unit (dead) is
heard over the radio by their team, and not by the rest.
A machine that does not know the message (a build without this, of the same
network version) drops it, as it drops any type past its own.
*/
#include "cseries.h"
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
#include "units/units.h"
#include "network_distributed.h"
#include "halo_voice.h"
#include "halo_vr.h"
#include <math.h>
void platform_log(char const *format, ...);
/* ---------- constants */
enum
{
/* an entry: the speaker, its sequence number and the packet's length,
and the packet */
VOICE_ENTRY_HEADER = 4,
VOICE_MAXIMUM_PACKET = 200,
VOICE_MAXIMUM_TAKEN = 1024,
};
/* how far off someone not on the radio is heard (world units: 60 metres) */
#define VOICE_RANGE 20.0f
/* how far to the side a voice is put: on the radio, and near */
#define VOICE_RADIO_PAN 0.3f
#define VOICE_NEAR_PAN 0.8f
/* ---------- globals */
static struct
{
boolean active;
word sequence;
long sent;
long played;
long log_time;
} network_voice;
/* ---------- private code */
static boolean network_voice_in_network_game(
void)
{
short connection = game_connection();
return (connection == _game_connection_network_server || connection == _game_connection_network_client) &&
game_in_progress();
}
/* where a player's head is, and whether they have a unit */
static boolean network_voice_player_head(
struct player_datum const *player,
real_point3d *head)
{
long unit_index = distributed_living_unit(player);
if (unit_index == NONE)
return FALSE;
unit_get_camera_position(unit_index, head);
return TRUE;
}
/* the gains a speaker is heard at here (FALSE: not heard) */
static boolean network_voice_gains(
struct player_datum const *speaker,
real *left,
real *right)
{
long listener_index = local_player_get_player_index(0);
struct player_datum *listener = listener_index != NONE ? player_try_and_get(listener_index) : NULL;
boolean teammate = listener && game_engine_has_teams() && listener->team_index == speaker->team_index;
real_point3d speaker_head;
real_point3d listener_head;
real gain = 1.0f;
real pan = 0.0f;
if (!listener)
return FALSE;
if (!network_voice_player_head(speaker, &speaker_head))
{
/* (dead: on the radio, to the team) */
if (!teammate)
return FALSE;
*left = *right = 1.0f;
return TRUE;
}
if (network_voice_player_head(listener, &listener_head))
{
real dx = speaker_head.x - listener_head.x;
real dy = speaker_head.y - listener_head.y;
real dz = speaker_head.z - listener_head.z;
real distance = (real)sqrt(dx * dx + dy * dy + dz * dz);
real flat = (real)sqrt(dx * dx + dy * dy);
float look[3];
real forward_x;
real forward_y;
real length;
struct unit_datum *unit = unit_try_and_get(distributed_living_unit(listener));
/* (where the listener looks: the head, in VR) */
if (halo_vr_look_vector(0, look))
{
forward_x = look[0];
forward_y = look[1];
}
else
{
forward_x = unit ? unit->unit.looking_vector.i : 1.0f;
forward_y = unit ? unit->unit.looking_vector.j : 0.0f;
}
length = (real)sqrt(forward_x * forward_x + forward_y * forward_y);
if (length > 0.0001f && flat > 0.0001f)
{
/* (the right: forward turned clockwise) */
pan = (dx * forward_y - dy * forward_x) / (flat * length);
}
if (!teammate)
{
gain = 1.0f - distance / VOICE_RANGE;
if (gain <= 0.0f)
return FALSE;
gain *= gain;
}
pan *= teammate ? VOICE_RADIO_PAN : VOICE_NEAR_PAN;
}
else if (!teammate)
{
/* (the listener dead: the others as from where the body is not:
not at all) */
return FALSE;
}
*left = gain * (pan > 0.0f ? 1.0f - pan : 1.0f);
*right = gain * (pan < 0.0f ? 1.0f + pan : 1.0f);
return TRUE;
}
static void network_voice_play_entries(
byte const *entries,
byte const *end,
short count,
long machine_index)
{
short index;
for (index = 0; index < count && entries + VOICE_ENTRY_HEADER <= end; index++)
{
byte player_byte = entries[0];
word sequence = (word)(entries[1] | entries[2] << 8);
short length = entries[3];
long player_index;
struct player_datum *player;
real left;
real right;
if (length <= 0 || entries + VOICE_ENTRY_HEADER + length > end)
break;
player_index = distributed_player_from_byte(player_byte);
player = player_index != NONE ? player_try_and_get(player_index) : NULL;
/* (the host: only a client's own players') */
if (player && (machine_index == NONE || distributed_machine_has_player(machine_index, player_byte)) &&
player->local_player_index == NONE && network_voice_gains(player, &left, &right))
{
halo_voice_play(player_byte, sequence, entries + VOICE_ENTRY_HEADER, length, left, right);
network_voice.played++;
}
entries += VOICE_ENTRY_HEADER + length;
}
}
/* ---------- public code */
void network_voice_new_game(
void)
{
network_voice.sequence = 0;
}
word network_voice_entry_size(
void)
{
return VOICE_ENTRY_HEADER;
}
/* after each tick (network_distributed_tick): this machine's speech sent */
void network_voice_tick(
void)
{
struct
{
struct distributed_message_header header;
byte entries[VOICE_MAXIMUM_TAKEN + 64];
} message;
byte taken[VOICE_MAXIMUM_TAKEN];
long player_index = local_player_get_player_index(0);
long size;
long offset = 0;
long used = 0;
short count = 0;
if (!network_voice_in_network_game() || !halo_voice_enabled())
{
if (network_voice.active)
{
network_voice.active = FALSE;
halo_voice_end();
}
return;
}
if (!network_voice.active)
{
network_voice.active = TRUE;
halo_voice_begin();
}
if (game_time_get() - network_voice.log_time >= 150 && (network_voice.sent || network_voice.played))
{
platform_log("voice: sent %ld, played %ld packets (in 150 ticks)", network_voice.sent, network_voice.played);
network_voice.sent = network_voice.played = 0;
network_voice.log_time = game_time_get();
}
size = halo_voice_take(taken, sizeof(taken));
if (player_index == NONE)
return;
while (offset < size)
{
short length = taken[offset];
if (length <= 0 || offset + 1 + length > size || length > VOICE_MAXIMUM_PACKET)
break;
if (used + VOICE_ENTRY_HEADER + length > (long)sizeof(message.entries))
break;
message.entries[used] = distributed_player_to_byte(player_index);
message.entries[used + 1] = (byte)(network_voice.sequence & 0xFF);
message.entries[used + 2] = (byte)(network_voice.sequence >> 8);
message.entries[used + 3] = (byte)length;
csmemcpy(message.entries + used + VOICE_ENTRY_HEADER, taken + offset + 1, length);
used += VOICE_ENTRY_HEADER + length;
offset += 1 + length;
network_voice.sequence++;
count++;
}
if (!count)
return;
network_voice.sent += count;
distributed_send(&message, _distributed_message_voice, count, (word)(sizeof(message.header) + used),
game_connection() == _game_connection_network_server ? _distributed_to_clients : _distributed_to_host);
}
/* a machine's speech: on the host a client's, played and passed on to the
other clients; on a client the host's of any other */
void network_voice_handle(
long machine_index,
void const *message,
word size)
{
struct distributed_message_header header;
byte const *entries = (byte const *)message + sizeof(header);
byte const *end = (byte const *)message + size;
if (!halo_voice_enabled() || !network_voice.active)
return;
csmemcpy(&header, message, sizeof(header));
network_voice_play_entries(entries, end, header.count, machine_index);
if (machine_index != NONE)
{
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
short number;
struct
{
struct distributed_message_header header;
byte entries[VOICE_MAXIMUM_TAKEN + 64];
} relayed;
word entries_size = (word)(end - entries);
byte const *entry = entries;
short index;
/* (only the sender's own players' speech is passed on, whole) */
if (entries_size < VOICE_ENTRY_HEADER || entries_size > sizeof(relayed.entries))
return;
for (index = 0; index < header.count; index++)
{
if (entry + VOICE_ENTRY_HEADER > end || entry[3] == 0 || entry + VOICE_ENTRY_HEADER + entry[3] > end ||
!distributed_machine_has_player(machine_index, entry[0]))
{
return;
}
entry += VOICE_ENTRY_HEADER + entry[3];
}
if (entry != end)
return;
for (number = 0; number < machine_count; number++)
{
if (machine_indices[number] == machine_index)
continue;
csmemcpy(relayed.entries, entries, entries_size);
distributed_send_to_machine(machine_indices[number], &relayed, _distributed_message_voice, header.count,
(word)(sizeof(relayed.header) + entries_size));
}
}
}
-31
View File
@@ -1,31 +0,0 @@
/*
HALO_VOICE.H
The voice chat's sound for the game (port/linux/src/voice.c: on Android the
host's microphone, Opus and mixer, port/android/host/host_voice.c; elsewhere
nothing). port/linux/game/network_voice.c sends and receives it.
*/
#ifndef __HALO_VOICE_H
#define __HALO_VOICE_H
#ifdef __cplusplus
extern "C" {
#endif
/* whether the voice chat is on (voice.enabled) */
int halo_voice_enabled(void);
/* starts playing voices, and the microphone if voice.microphone; stops */
int halo_voice_begin(void);
void halo_voice_end(void);
/* the microphone's speech since asked: Opus packets, each after a byte of
its length; returns the bytes */
int halo_voice_take(void *buffer, int size);
/* a speaker's packet, played at these gains (left, right) */
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right);
#ifdef __cplusplus
}
#endif
#endif
-11
View File
@@ -413,17 +413,6 @@ static const struct config_setting config_settings[] =
"The Discord application internet play invites go through while the\n"
"Discord desktop client runs; empty for none." },
{ "voice.enabled", _config_boolean, "true", "HALO_VOICE_ENABLED", _environment_value, _platform_android,
"Voice chat in multiplayer: hear the other players (teammates over the\n"
"radio, the rest when near, from where they are)." },
{ "voice.microphone", _config_boolean, "true", "HALO_VOICE_MICROPHONE", _environment_value, _platform_android,
"Send your voice in multiplayer, whenever you speak (the headset's\n"
"microphone; Android asks for it once). False only listens." },
{ "voice.volume", _config_real, "1.0", "HALO_VOICE_VOLUME", _environment_value, _platform_android,
"The other players' voices' volume (1 as they come)." },
{ "voice.threshold", _config_real, "0.02", "HALO_VOICE_THRESHOLD", _environment_value, _platform_android,
"How loud the microphone must be to send (0..1 of full scale): higher\n"
"if it sends noise, lower if it cuts words off." },
{ "bots.count", _config_integer, "0", "HALO_BOTS_COUNT", _environment_value, _platform_android,
"Bots in the multiplayer games this machine hosts (0 to 15; 0: none):\n"
"players it drives itself, which every machine sees as players. Set in\n"
+18 -2
View File
@@ -925,8 +925,10 @@ static void platform_show_pending_message(void)
/* ---------- events */
/* quits as closing the window does, when the events are next read (the
menus' Quit: port/linux/game/menu_functions.c); Android's menus have none,
as the system closes its apps */
menus' Quit: port/linux/game/menu_functions.c); on Android, where the
system closes its apps, the VR options' Quit (ui_widget.c) ends the process
at once: on the Steam Frame, Steam cannot end the game, which runs in
Lepton's container (port/android/STEAM_FRAME.md) */
void platform_request_quit(void)
{
#ifndef HALO_ANDROID
@@ -935,6 +937,20 @@ void platform_request_quit(void)
memset(&event, 0, sizeof(event));
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#else
{
/* (and says so beside the game's data: the Steam Frame's launcher
does not then take it for a start that failed and start it again) */
char path[512];
FILE *file;
snprintf(path, sizeof(path), "%s/quit_requested.txt", platform_data_root());
file = fopen(path, "wb");
if (file)
fclose(file);
}
platform_log("quit: the player asked");
exit(EXIT_SUCCESS);
#endif
}
-87
View File
@@ -1,87 +0,0 @@
/*
VOICE.C
The voice chat's sound (halo_voice.h): on Android, the host's
(port/android/host/host_voice.c); elsewhere none.
*/
#include "halo_voice.h"
#include "platform.h"
#include "port_config.h"
#ifdef HALO_ANDROID
#include "guest_host.h"
#include <stdio.h>
static int voice_started;
/* whether the app may use the microphone, as the launcher found
(LauncherActivity.java): opened without it, Android would ask in the middle
of the game */
static int voice_microphone_allowed(void)
{
char path[512];
FILE *file;
int allowed = 0;
snprintf(path, sizeof(path), "%s/microphone_permission.txt", platform_data_root());
file = fopen(path, "rb");
if (file)
{
allowed = fgetc(file) == '1';
fclose(file);
}
return allowed;
}
int halo_voice_enabled(void)
{
return config_boolean("voice.enabled");
}
int halo_voice_begin(void)
{
int microphone;
if (voice_started)
return 1;
voice_started = 1;
microphone = config_boolean("voice.microphone") && voice_microphone_allowed();
platform_log("voice: on, %s", microphone ? "with the microphone" :
config_boolean("voice.microphone") ? "without the microphone (not allowed)" : "listening only");
return host_voice_start(microphone, (float)config_real("voice.volume"), (float)config_real("voice.threshold"));
}
void halo_voice_end(void)
{
if (!voice_started)
return;
voice_started = 0;
host_voice_stop();
platform_log("voice: off");
}
int halo_voice_take(void *buffer, int size)
{
return voice_started ? host_voice_take_packets(buffer, size) : 0;
}
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
{
if (voice_started)
host_voice_play(speaker, sequence, data, length, left, right);
}
#else
int halo_voice_enabled(void) { return 0; }
int halo_voice_begin(void) { return 0; }
void halo_voice_end(void) {}
int halo_voice_take(void *buffer, int size) { (void)buffer; (void)size; return 0; }
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
{
(void)speaker; (void)sequence; (void)data; (void)length; (void)left; (void)right;
}
#endif
+36 -38
View File
@@ -5990,18 +5990,16 @@ void p2p_set_public_game(int public_game);
int p2p_join_invite(char const *text);
int p2p_public_game(void);
int p2p_public_games(struct ui_public_game *games, int maximum);
int config_boolean(char const *name);
int config_write_boolean(char const *name, int value);
long config_integer(char const *name);
int config_write_integer(char const *name, long value);
enum
{
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
/* the online games panel's rows: "enter a code", the voice chat's
switches, and the games */
/* the online games panel's rows: "enter a code" and the games */
ONLINE_PANEL_ROWS = 11,
ONLINE_PANEL_FIXED_ROWS = 3,
/* (the rows before the games: "enter a code") */
ONLINE_PANEL_FIXED_ROWS = 1,
};
static struct
@@ -6075,7 +6073,7 @@ it, B closes it */
static void ui_join_code_panel_event(
struct event_record const *event)
{
short rows = (short)(ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS);
short rows = (short)(ui_join_code_globals.game_count + 1);
short move = 0;
if (event->type == _event_type_left_stick)
@@ -6101,20 +6099,9 @@ static void ui_join_code_panel_event(
break;
case _gamepad_analog_button_a:
case _gamepad_binary_button_start:
/* (the voice chat's switches, kept in config.toml) */
if (ui_join_code_globals.selection == 1)
{
config_write_boolean("voice.enabled", !config_boolean("voice.enabled"));
break;
}
if (ui_join_code_globals.selection == 2)
{
config_write_boolean("voice.microphone", !config_boolean("voice.microphone"));
break;
}
ui_join_code_globals.panel_open = FALSE;
if (ui_join_code_globals.selection == 0 ||
ui_join_code_globals.selection >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
ui_join_code_globals.selection > ui_join_code_globals.game_count)
{
ui_join_code_launch_keyboard();
}
@@ -6416,7 +6403,7 @@ static void ui_join_code_render_panel(
ui_join_code_globals.game_count =
(short)p2p_public_games(ui_join_code_globals.games, MAXIMUM_SHOWN_PUBLIC_GAMES);
ui_join_code_globals.selection =
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS - 1);
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count);
ui_join_code_draw_quad(40, 72, 600, 410, 0xF0050A18);
ui_join_code_draw_quad(40, 72, 600, 74, 0xFF3A6EA5);
ui_join_code_draw_quad(40, 408, 600, 410, 0xFF3A6EA5);
@@ -6431,7 +6418,7 @@ static void ui_join_code_render_panel(
short index = (short)(ui_join_code_globals.scroll + row);
short y = (short)(128 + row * 22);
if (index >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
if (index > ui_join_code_globals.game_count)
break;
if (index == ui_join_code_globals.selection)
ui_join_code_draw_quad(48, y, 592, (short)(y + 21), 0x803A6EA5);
@@ -6439,20 +6426,9 @@ static void ui_join_code_render_panel(
{
ui_join_code_draw_text(56, 584, (short)(y + 1), "Enter a join code...", FALSE, 0, &text_color);
}
else if (index == 1)
{
ui_join_code_draw_text(56, 584, (short)(y + 1),
config_boolean("voice.enabled") ? "Voice chat: On" : "Voice chat: Off", FALSE, 0, &text_color);
}
else if (index == 2)
{
ui_join_code_draw_text(56, 584, (short)(y + 1),
config_boolean("voice.microphone") ? "Microphone: On (sent while you speak)" :
"Microphone: Off (listening only)", FALSE, 0, &text_color);
}
else
{
struct ui_public_game const *game = &ui_join_code_globals.games[index - ONLINE_PANEL_FIXED_ROWS];
struct ui_public_game const *game = &ui_join_code_globals.games[index - 1];
ui_join_code_draw_text(56, 212, (short)(y + 1), game->name, FALSE, 0, &text_color);
ui_join_code_draw_text(216, 372, (short)(y + 1), ui_join_code_map_name(game->map), FALSE, 0,
@@ -6463,9 +6439,9 @@ static void ui_join_code_render_panel(
}
}
if (!ui_join_code_globals.game_count)
ui_join_code_draw_text(56, 584, 196, "No public games right now", FALSE, 0, &dim_color);
ui_join_code_draw_text(56, 584, 384, "%a-button Join or switch %x-button Enter a code %b-button Back", TRUE,
0, &text_color);
ui_join_code_draw_text(56, 584, 152, "No public games right now", FALSE, 0, &dim_color);
ui_join_code_draw_text(56, 584, 384, "%a-button Join %x-button Enter a code %b-button Back", TRUE, 0,
&text_color);
}
/* over the screens, as they are drawn */
@@ -6594,10 +6570,11 @@ enum
_vr_option_crosshair,
_vr_option_motion_tracker,
_vr_option_controls,
_vr_option_quit,
NUMBER_OF_VR_OPTIONS,
VR_OPTIONS_TOP = 102,
VR_OPTIONS_ROW_HEIGHT = 28,
VR_OPTIONS_TOP = 98,
VR_OPTIONS_ROW_HEIGHT = 26,
VR_OPTIONS_FOOTER = 362,
_vr_controls_on_foot = 0,
@@ -6612,8 +6589,12 @@ static struct
boolean controls;
short selection;
short page;
/* "Quit Halo" chosen once: a second time quits */
boolean quit_armed;
} ui_vr_options_globals;
void platform_request_quit(void);
static real const vr_snap_angles[] = { 30.0f, 45.0f, 60.0f, 90.0f };
static real const vr_smooth_speeds[] = { 60.0f, 90.0f, 120.0f, 150.0f, 180.0f };
static real const vr_vignettes[] = { 0.0f, 0.35f, 0.7f, 1.0f };
@@ -6685,6 +6666,13 @@ static void ui_vr_options_change(
ui_vr_options_globals.controls = TRUE;
ui_vr_options_globals.page = _vr_controls_on_foot;
break;
case _vr_option_quit:
/* (the Steam Frame's Steam cannot end the game, which runs in
Lepton's container: it ends itself; asked twice) */
if (ui_vr_options_globals.quit_armed)
platform_request_quit();
ui_vr_options_globals.quit_armed = TRUE;
break;
}
ui_play_audio_feedback_sound(_ui_audio_feedback_forward);
}
@@ -6692,6 +6680,7 @@ static void ui_vr_options_change(
static void ui_vr_options_close(
void)
{
ui_vr_options_globals.quit_armed = FALSE;
ui_vr_options_globals.open = FALSE;
ui_vr_options_globals.controls = FALSE;
}
@@ -6778,6 +6767,7 @@ static boolean ui_vr_options_take_event(
else if (move)
{
ui_vr_options_globals.selection = (short)PIN(ui_vr_options_globals.selection + move, 0, NUMBER_OF_VR_OPTIONS - 1);
ui_vr_options_globals.quit_armed = FALSE;
ui_play_audio_feedback_sound(_ui_audio_feedback_cursor);
}
else if (accept)
@@ -6837,7 +6827,11 @@ static boolean ui_vr_options_take_pointer(
if (x >= 118 && x < 522 && y >= VR_OPTIONS_TOP - 4 &&
y < VR_OPTIONS_TOP - 4 + NUMBER_OF_VR_OPTIONS * VR_OPTIONS_ROW_HEIGHT)
{
ui_vr_options_globals.selection = (short)((y - (VR_OPTIONS_TOP - 4)) / VR_OPTIONS_ROW_HEIGHT);
short row = (short)((y - (VR_OPTIONS_TOP - 4)) / VR_OPTIONS_ROW_HEIGHT);
if (row != ui_vr_options_globals.selection)
ui_vr_options_globals.quit_armed = FALSE;
ui_vr_options_globals.selection = row;
if (pointer->left_clicks)
ui_vr_options_change(1);
}
@@ -7090,6 +7084,10 @@ static void ui_vr_options_render(
case _vr_option_controls:
label = "Controller buttons...";
break;
case _vr_option_quit:
label = ui_vr_options_globals.quit_armed && ui_vr_options_globals.selection == _vr_option_quit ?
"Quit Halo? Once more to quit" : "Quit Halo";
break;
}
if (row == ui_vr_options_globals.selection)
ui_join_code_draw_quad(118, (short)(y - 4), 522, (short)(y + VR_OPTIONS_ROW_HEIGHT - 6), 0x803A6EA5);
+3 -28
View File
@@ -48,10 +48,6 @@ MUSL_URL = f"https://musl.libc.org/releases/musl-{MUSL_VERSION}.tar.gz"
SDL_TAG = "release-3.4.16"
SDL_DIR = THIRD_PARTY / "SDL3"
SDL_URL = "https://github.com/libsdl-org/SDL.git"
# Opus, the voice chat's codec (host_voice.c), built into the host library
OPUS_TAG = "v1.5.2"
OPUS_DIR = THIRD_PARTY / "opus"
OPUS_URL = "https://github.com/xiph/opus.git"
# the Khronos OpenXR loader for Android (the VR build, host_xr.c): its
# headers and library are linked from here; the app packages the same AAR
# through Gradle (port/android/app/build.gradle), which also merges the
@@ -225,10 +221,6 @@ def fetch_third_party() -> None:
print(f"Cloning SDL3 {SDL_TAG}")
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(SDL_DIR)],
check=True)
if not OPUS_DIR.is_dir():
print(f"Cloning Opus {OPUS_TAG}")
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", OPUS_TAG, OPUS_URL, str(OPUS_DIR)],
check=True)
patch_sdl_java()
if not OPENXR_DIR.is_dir():
print(f"Downloading {OPENXR_URL}")
@@ -572,23 +564,6 @@ def generate_android_build(n: Writer, sln: Any) -> None:
)
n.build(outputs=libsdl, rule="android_sdl3", implicit=[SDL_DIR / "CMakeLists.txt"])
# ---------- Opus (the voice chat's codec, host_voice.c)
opus_build = BUILD / "opus-build"
libopus = opus_build / "libopus.a"
n.rule(
name="android_opus",
command=(f"cmake -S {OPUS_DIR} -B {opus_build} -G Ninja "
f"-DCMAKE_TOOLCHAIN_FILE={ndk}/build/cmake/android.toolchain.cmake "
f"-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-{ANDROID_API} -DCMAKE_BUILD_TYPE=Release "
f"-DCMAKE_POSITION_INDEPENDENT_CODE=ON -DOPUS_BUILD_SHARED_LIBRARY=OFF -DOPUS_BUILD_TESTING=OFF "
f"-DOPUS_BUILD_PROGRAMS=OFF -DOPUS_INSTALL_PKG_CONFIG_MODULE=OFF -DOPUS_INSTALL_CMAKE_CONFIG_MODULE=OFF "
f"> {BUILD}/opus-configure.log && ninja -C {opus_build} > {BUILD}/opus-build.log"),
description="ANDROID OPUS",
pool="console",
)
n.build(outputs=libopus, rule="android_opus", implicit=[OPUS_DIR / "CMakeLists.txt"])
# ---------- the host library
host_objects: List[Path] = []
@@ -603,7 +578,7 @@ def generate_android_build(n: Writer, sln: Any) -> None:
host_cflags = " ".join([
"-O2", "-g", "-fPIC", "-Wall", "-Wno-unused-function", "-D_GNU_SOURCE",
f"-I{PORT_DIR}/include", f"-I{PORT_DIR}/host", f"-I{SDL_DIR}/include", f"-I{LINUX_DIR}/src",
f"-I{TOML_DIR}", f"-I{OPENXR_DIR}/include", f"-I{OPUS_DIR}/include",
f"-I{TOML_DIR}", f"-I{OPENXR_DIR}/include",
])
host_sources = sorted((PORT_DIR / "host").glob("*.c")) + [
LINUX_DIR / "src" / "posix_files.c", LINUX_DIR / "src" / "posix_net.c",
@@ -629,12 +604,12 @@ def generate_android_build(n: Writer, sln: Any) -> None:
host_objects.append(table_obj)
n.rule(
name="android_host_link",
command=(f"$android_host_cc -shared -o $out $in {libopus} -L{sdl_build} -L{OPENXR_DIR}/lib -lopenxr_loader "
command=(f"$android_host_cc -shared -o $out $in -L{sdl_build} -L{OPENXR_DIR}/lib -lopenxr_loader "
+ " ".join(f"-l{lib}" for lib in HOST_LIBRARIES)
+ " -Wl,-z,max-page-size=16384 -Wl,--no-undefined"),
description="ANDROID HOST LINK $out",
)
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl, libopus])
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl])
# a do-nothing OpenXR layer under the Steam Frame's implicit layer's
# library name (port/android/xr_layer/fdm_injection_stub.c)