mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 19:00:25 +02:00
Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9304daf2e8 | ||
|
|
8e8713681d | ||
|
|
d7099f3e40 | ||
|
|
4ba36d7305 | ||
|
|
0a62ad4610 | ||
|
|
855f083117 | ||
|
|
6f3786de91 | ||
|
|
493713b0a6 | ||
|
|
bf356fc58a | ||
|
|
6a7b919522 | ||
|
|
c1305fa94f | ||
|
|
0fc85d6c67 | ||
|
|
c0a08f395e | ||
|
|
42dc1f546f | ||
|
|
79a6482c16 | ||
|
|
bfd92ec17d | ||
|
|
852ee322cc | ||
|
|
f0d4fb2608 | ||
|
|
736c217347 | ||
|
|
30edb22f36 | ||
|
|
abc1c12f1e | ||
|
|
3f02527c68 | ||
|
|
ceecb039e3 | ||
|
|
a34685574f | ||
|
|
eb83e9d6d1 | ||
|
|
144063871a | ||
|
|
7165d40f90 | ||
|
|
65d81ad1ba | ||
|
|
d356c0abab | ||
|
|
1536c30870 | ||
|
|
458b4642b6 | ||
|
|
2af2702bfa | ||
|
|
7a1c5e6239 | ||
|
|
a9f3ce2647 | ||
|
|
1cebe431d1 | ||
|
|
ec2ab73acc | ||
|
|
2f5fa771f5 | ||
|
|
3aa88244fc | ||
|
|
a4e1bdb80d | ||
|
|
ceb0260389 | ||
|
|
bdfdef9bd4 | ||
|
|
f02317ca25 | ||
|
|
d7fa443718 | ||
|
|
4414d7606d | ||
|
|
824f79aa70 | ||
|
|
25a947faac | ||
|
|
bf545439b4 | ||
|
|
201f69a35a | ||
|
|
96a75df218 | ||
|
|
1a732f577f |
No files matched your search
@@ -32,18 +32,64 @@ What differs is found at run time, not by a build of its own.
|
||||
|
||||
| # | Stage | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Base: the fixes above on the current main | in progress |
|
||||
| 2 | The game's data where Lepton keeps it | |
|
||||
| 3 | Menus, HUD and text visible (quad layers) | |
|
||||
| 4 | The freeze at the cryo bay's calibration | |
|
||||
| 5 | The Frame's controllers | |
|
||||
| 6 | Frame rate, resolution | |
|
||||
| 1 | Base: the fixes above on the current main | done |
|
||||
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
|
||||
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
|
||||
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
|
||||
| 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 | |
|
||||
|
||||
## The game's data
|
||||
|
||||
Lepton uninstalls the app and empties its folders whenever it starts again.
|
||||
The game's data lives in the SteamOS user's Documents instead: when
|
||||
`/home/steamos/Documents/HaloCE` holds `maps/ui.map` and is writable, the
|
||||
launcher and the game take it as their data folder (config.toml, saves and
|
||||
debug.txt go there too). Once:
|
||||
|
||||
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
|
||||
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
|
||||
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
|
||||
|
||||
After a Lepton restart only the APK is installed again. (Use the path under
|
||||
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
|
||||
app and the latter.)
|
||||
|
||||
## The controllers
|
||||
|
||||
SteamVR offers the Frame's controllers as themselves
|
||||
(`XR_VALVE_frame_controller_interaction`,
|
||||
`/interaction_profiles/valve/frame_controller_valve`; Valve's "Steam Frame
|
||||
Input" page). Without bindings for it, SteamVR maps the Quest's onto them,
|
||||
and the left Touch controller's X and Y land on the D-pad. With them the
|
||||
game gets the Xbox's layout, as the right controller has it: A jump, B
|
||||
flashlight (held: switch grenades), X action and reload, Y switch weapon,
|
||||
menu pause; the left one's View is Back (scores), its D-pad the D-pad, the
|
||||
bumpers switch grenades (left) and the flashlight (right). The VR options'
|
||||
page of buttons shows this layout on the Frame.
|
||||
|
||||
## Findings
|
||||
|
||||
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
|
||||
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
|
||||
(menus, HUD, instructions) are not shown; the projection layer is. Froze
|
||||
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
|
||||
- 2026-10-04: SteamVR draws a quad's texels by their alpha alone; the
|
||||
game's menus and HUD have none in their additive parts, so nothing showed.
|
||||
On SteamVR (by the runtime's name) the overlays' alpha now comes from their
|
||||
colour (host_xr.c; `HALO_XR_QUAD_ALPHA=colour|keep` overrides). With it,
|
||||
menus, HUD and the campaign ran well (the user: "lief 1a"); no freeze.
|
||||
- 2026-10-04: no online games found: the public MQTT brokers had banned the
|
||||
home connection for a while (too many connections from tests that day);
|
||||
broker.emqx.io came back within minutes. Lepton's network is the Frame's
|
||||
own (same IP), DNS works.
|
||||
- 2026-10-04: the refresh rate is SteamOS's setting; the runtime offers
|
||||
only that one (the game asks for the highest at or below
|
||||
`vr.refresh_rate`, and logs the rates offered). At 72 Hz the game holds
|
||||
72 fps (about 11.6 ms a frame with multiview, eyes 7.6 ms; without it
|
||||
eyes about 9 ms). At 90 Hz it cannot (11.1 ms a frame), and SteamVR's
|
||||
making up of the missed frames showed as a doubled picture when turning
|
||||
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
|
||||
0.85) for 90.
|
||||
@@ -19,6 +19,9 @@
|
||||
<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,7 +1,10 @@
|
||||
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;
|
||||
@@ -35,6 +38,8 @@ 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;
|
||||
private TextView status;
|
||||
@@ -46,6 +51,12 @@ public class LauncherActivity extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
dataRoot = getExternalFilesDir(null);
|
||||
// the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
// starts again; the game's data lives in the SteamOS user's
|
||||
// Documents instead, when it is there (port/android/STEAM_FRAME.md;
|
||||
// the same choice as host_main.c's)
|
||||
if (new File(SHARED_DATA_ROOT, "maps/ui.map").isFile() && new File(SHARED_DATA_ROOT).canWrite())
|
||||
dataRoot = new File(SHARED_DATA_ROOT);
|
||||
// created by the app, so that files pushed into it with adb stay
|
||||
// readable (a directory adb creates there belongs to the shell user)
|
||||
if (dataRoot != null)
|
||||
@@ -53,12 +64,37 @@ public class LauncherActivity extends Activity {
|
||||
passOnHardwareId();
|
||||
passOnInvite(getIntent());
|
||||
if (haveData()) {
|
||||
startGame();
|
||||
if (!askForMicrophone())
|
||||
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
|
||||
@@ -111,6 +147,23 @@ 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() ||
|
||||
@@ -118,6 +171,7 @@ public class LauncherActivity extends Activity {
|
||||
}
|
||||
|
||||
private void startGame() {
|
||||
passOnMicrophonePermission();
|
||||
startActivity(new Intent(this, HaloActivity.class));
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -134,6 +134,21 @@ 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 */
|
||||
|
||||
@@ -90,6 +90,8 @@ int host_errno(void)
|
||||
|
||||
static char data_root[512];
|
||||
static char save_root[512];
|
||||
/* where a device that empties the app's folders keeps the game's data */
|
||||
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
|
||||
|
||||
void host_android_path(int which, char *buffer, uint32_t size)
|
||||
{
|
||||
@@ -269,6 +271,12 @@ static void *game_main(void *unused)
|
||||
external = SDL_GetAndroidExternalStoragePath();
|
||||
if (!external)
|
||||
host_fatal("Android storage is unavailable: %s", SDL_GetError());
|
||||
/* the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
starts again; the game's data lives in the SteamOS user's Documents
|
||||
instead, when it is there (port/android/STEAM_FRAME.md; the same choice
|
||||
as LauncherActivity's) */
|
||||
if (directory_has_maps(SHARED_DATA_ROOT) && access(SHARED_DATA_ROOT, W_OK) == 0)
|
||||
external = SHARED_DATA_ROOT;
|
||||
snprintf(data_root, sizeof(data_root), "%s", external);
|
||||
snprintf(save_root, sizeof(save_root), "%s/save", external);
|
||||
/* readable by adb (the shell user), for managing saves */
|
||||
|
||||
@@ -0,0 +1,494 @@
|
||||
/*
|
||||
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();
|
||||
}
|
||||
+157
-1
@@ -102,6 +102,14 @@ static struct
|
||||
XrAction button_b; /* B on the right controller, Y on the left */
|
||||
XrAction menu;
|
||||
XrAction thumbrest;
|
||||
/* the Steam Frame's controllers' own (XR_VALVE_frame_controller_interaction) */
|
||||
int has_frame_controller;
|
||||
int frame_layout;
|
||||
XrAction button_x;
|
||||
XrAction button_y;
|
||||
XrAction view;
|
||||
XrAction bumper;
|
||||
XrAction dpad[4];
|
||||
XrAction haptic;
|
||||
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
|
||||
|
||||
@@ -122,6 +130,11 @@ static struct
|
||||
GLint rect_uniform;
|
||||
GLint cut_disc_uniform;
|
||||
GLint vignette_uniform;
|
||||
GLint alpha_from_colour_uniform;
|
||||
/* the runtime draws a quad's texels by their alpha alone, ignoring the
|
||||
colour of those with none (SteamVR on the Steam Frame): the game's
|
||||
overlays, whose additive parts have none, get alpha from their colour */
|
||||
int quad_alpha_from_colour;
|
||||
GLint tangents_uniform;
|
||||
float vignette;
|
||||
/* the same copy from a layer of a texture array (the eyes drawn at
|
||||
@@ -223,6 +236,7 @@ static const char copy_fragment_shader[] =
|
||||
"precision highp float;\n"
|
||||
"uniform sampler2D source;\n"
|
||||
"uniform float cut_disc;\n"
|
||||
"uniform float alpha_from_colour; /* 1: as opaque as it is bright (a quad's, SteamVR) */\n"
|
||||
"uniform float vignette; /* 0..1: the eye images darken from their edges */\n"
|
||||
"uniform vec4 tangents; /* the eye's edges: left, right, down, up */\n"
|
||||
"in vec2 coordinate;\n"
|
||||
@@ -236,6 +250,8 @@ static const char copy_fragment_shader[] =
|
||||
"{\n"
|
||||
" vec4 texel = texture(source, coordinate);\n"
|
||||
" color = vec4(to_linear(texel.rgb), texel.a);\n"
|
||||
" if (alpha_from_colour > 0.5)\n"
|
||||
" color.a = max(color.a, max(color.r, max(color.g, color.b)));\n"
|
||||
" if (cut_disc > 0.5)\n"
|
||||
" {\n"
|
||||
" float inside = 1.0 - smoothstep(0.95, 1.0, length(disc));\n"
|
||||
@@ -326,6 +342,7 @@ static int copy_initialize(void)
|
||||
xr.rect_uniform = glGetUniformLocation(xr.program, "rect");
|
||||
xr.cut_disc_uniform = glGetUniformLocation(xr.program, "cut_disc");
|
||||
xr.vignette_uniform = glGetUniformLocation(xr.program, "vignette");
|
||||
xr.alpha_from_colour_uniform = glGetUniformLocation(xr.program, "alpha_from_colour");
|
||||
xr.tangents_uniform = glGetUniformLocation(xr.program, "tangents");
|
||||
xr.copy_array_layer = -1;
|
||||
{
|
||||
@@ -580,6 +597,14 @@ static int actions_initialize(void)
|
||||
xr.button_b = action_create("button_upper", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.menu = action_create("menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.thumbrest = action_create("thumbrest", "Thumb rest", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.button_x = action_create("button_x", "X", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.button_y = action_create("button_y", "Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.view = action_create("view", "View", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.bumper = action_create("bumper", "Bumper", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[0] = action_create("dpad_up", "D-pad up", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[1] = action_create("dpad_down", "D-pad down", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[2] = action_create("dpad_left", "D-pad left", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[3] = action_create("dpad_right", "D-pad right", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.haptic = action_create("haptic", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT);
|
||||
|
||||
{
|
||||
@@ -644,6 +669,44 @@ static int actions_initialize(void)
|
||||
{ &xr.haptic, "/user/hand/right/output/haptic" },
|
||||
};
|
||||
|
||||
/* the Steam Frame's controllers, as themselves: the right one has
|
||||
A, B, X, Y and a menu button, the left one a D-pad and a View button
|
||||
(port/android/STEAM_FRAME.md) */
|
||||
static const struct binding frame_controller[] =
|
||||
{
|
||||
{ &xr.grip_pose, "/user/hand/left/input/grip/pose" },
|
||||
{ &xr.grip_pose, "/user/hand/right/input/grip/pose" },
|
||||
{ &xr.aim_pose, "/user/hand/left/input/aim/pose" },
|
||||
{ &xr.aim_pose, "/user/hand/right/input/aim/pose" },
|
||||
{ &xr.trigger, "/user/hand/left/input/trigger/value" },
|
||||
{ &xr.trigger, "/user/hand/right/input/trigger/value" },
|
||||
{ &xr.squeeze, "/user/hand/left/input/squeeze/value" },
|
||||
{ &xr.squeeze, "/user/hand/right/input/squeeze/value" },
|
||||
{ &xr.thumbstick, "/user/hand/left/input/thumbstick" },
|
||||
{ &xr.thumbstick, "/user/hand/right/input/thumbstick" },
|
||||
{ &xr.thumbstick_click, "/user/hand/left/input/thumbstick/click" },
|
||||
{ &xr.thumbstick_click, "/user/hand/right/input/thumbstick/click" },
|
||||
{ &xr.button_a, "/user/hand/right/input/a/click" },
|
||||
{ &xr.button_b, "/user/hand/right/input/b/click" },
|
||||
{ &xr.button_x, "/user/hand/right/input/x/click" },
|
||||
{ &xr.button_y, "/user/hand/right/input/y/click" },
|
||||
{ &xr.menu, "/user/hand/right/input/menu/click" },
|
||||
{ &xr.view, "/user/hand/left/input/view/click" },
|
||||
{ &xr.bumper, "/user/hand/left/input/bumper/click" },
|
||||
{ &xr.bumper, "/user/hand/right/input/bumper/click" },
|
||||
{ &xr.dpad[0], "/user/hand/left/input/dpad_up/click" },
|
||||
{ &xr.dpad[1], "/user/hand/left/input/dpad_down/click" },
|
||||
{ &xr.dpad[2], "/user/hand/left/input/dpad_left/click" },
|
||||
{ &xr.dpad[3], "/user/hand/left/input/dpad_right/click" },
|
||||
{ &xr.haptic, "/user/hand/left/output/haptic" },
|
||||
{ &xr.haptic, "/user/hand/right/output/haptic" },
|
||||
};
|
||||
|
||||
if (xr.has_frame_controller)
|
||||
{
|
||||
suggest("/interaction_profiles/valve/frame_controller_valve", frame_controller,
|
||||
sizeof(frame_controller) / sizeof(frame_controller[0]));
|
||||
}
|
||||
suggest("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
|
||||
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]));
|
||||
suggest("/interaction_profiles/khr/simple_controller", simple, sizeof(simple) / sizeof(simple[0]));
|
||||
@@ -713,6 +776,30 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
sync.activeActionSets = &active;
|
||||
if (!XR_SUCCEEDED(xrSyncActions(xr.session, &sync)))
|
||||
return;
|
||||
{
|
||||
/* (the controllers' profile the runtime chose, in the log as it
|
||||
changes) */
|
||||
static XrPath logged[HALO_XR_HAND_COUNT];
|
||||
|
||||
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
|
||||
{
|
||||
XrInteractionProfileState state = { XR_TYPE_INTERACTION_PROFILE_STATE };
|
||||
|
||||
if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(xr.session, xr.hands[hand].path, &state)) &&
|
||||
state.interactionProfile != logged[hand])
|
||||
{
|
||||
char name[XR_MAX_PATH_LENGTH] = "none";
|
||||
uint32_t length = 0;
|
||||
|
||||
logged[hand] = state.interactionProfile;
|
||||
if (state.interactionProfile != XR_NULL_PATH)
|
||||
xrPathToString(xr.instance, state.interactionProfile, sizeof(name), &length, name);
|
||||
if (hand == HALO_XR_HAND_RIGHT)
|
||||
xr.frame_layout = strstr(name, "frame_controller") != NULL;
|
||||
host_logf(HOST_LOG_INFO, "xr: the %s hand's controller: %s", hand ? "right" : "left", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
|
||||
{
|
||||
struct halo_xr_hand *out = &frame->hands[hand];
|
||||
@@ -757,7 +844,31 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
(action_boolean(xr.menu, hand) ? HALO_XR_BUTTON_MENU : 0) |
|
||||
(action_boolean(xr.thumbrest, hand) ? HALO_XR_BUTTON_THUMBREST : 0) |
|
||||
(out->trigger > 0.5f ? HALO_XR_BUTTON_TRIGGER : 0) |
|
||||
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0);
|
||||
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0) |
|
||||
(action_boolean(xr.view, hand) ? HALO_XR_BUTTON_VIEW : 0) |
|
||||
(action_boolean(xr.bumper, hand) ? HALO_XR_BUTTON_BUMPER : 0) |
|
||||
(action_boolean(xr.dpad[0], hand) ? HALO_XR_BUTTON_DPAD_UP : 0) |
|
||||
(action_boolean(xr.dpad[1], hand) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
|
||||
(action_boolean(xr.dpad[2], hand) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
|
||||
(action_boolean(xr.dpad[3], hand) ? HALO_XR_BUTTON_DPAD_RIGHT : 0);
|
||||
}
|
||||
frame->controller_layout = xr.frame_layout ? HALO_XR_LAYOUT_FRAME : HALO_XR_LAYOUT_TOUCH;
|
||||
if (xr.frame_layout)
|
||||
{
|
||||
/* (the Frame's right X, Y and menu button: the game's X, Y and Start,
|
||||
which the Quest has on the left) */
|
||||
struct halo_xr_hand *left = &frame->hands[HALO_XR_HAND_LEFT];
|
||||
struct halo_xr_hand *right = &frame->hands[HALO_XR_HAND_RIGHT];
|
||||
|
||||
if (action_boolean(xr.button_x, HALO_XR_HAND_RIGHT))
|
||||
left->buttons |= HALO_XR_BUTTON_A;
|
||||
if (action_boolean(xr.button_y, HALO_XR_HAND_RIGHT))
|
||||
left->buttons |= HALO_XR_BUTTON_B;
|
||||
if (right->buttons & HALO_XR_BUTTON_MENU)
|
||||
{
|
||||
right->buttons &= ~HALO_XR_BUTTON_MENU;
|
||||
left->buttons |= HALO_XR_BUTTON_MENU;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,6 +885,13 @@ static void refresh_rate_choose(float wanted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
char line[128] = "";
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
snprintf(line + strlen(line), sizeof(line) - strlen(line), " %.0f", rates[index]);
|
||||
host_logf(HOST_LOG_INFO, "xr: refresh rates offered:%s Hz", line);
|
||||
}
|
||||
/* the highest rate not above the one wanted (0: the highest) */
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
@@ -928,6 +1046,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
extensions[index].next = NULL;
|
||||
}
|
||||
xrEnumerateInstanceExtensionProperties(NULL, extension_count, &extension_count, extensions);
|
||||
{
|
||||
/* (the runtime's extensions, in the log: what a headset offers) */
|
||||
char line[512] = "";
|
||||
|
||||
for (index = 0; index < extension_count; index++)
|
||||
{
|
||||
if (strlen(line) + strlen(extensions[index].extensionName) + 2 >= sizeof(line))
|
||||
{
|
||||
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
|
||||
line[0] = 0;
|
||||
}
|
||||
strcat(line, " ");
|
||||
strcat(line, extensions[index].extensionName);
|
||||
}
|
||||
if (line[0])
|
||||
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
|
||||
}
|
||||
if (!has_extension(extensions, extension_count, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME))
|
||||
{
|
||||
host_logf(HOST_LOG_ERROR, "xr: the runtime has no OpenGL ES support");
|
||||
@@ -939,6 +1074,9 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
xr.has_performance_settings = has_extension(extensions, extension_count, XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
|
||||
if (xr.has_performance_settings)
|
||||
enabled[enabled_count++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME;
|
||||
xr.has_frame_controller = has_extension(extensions, extension_count, "XR_VALVE_frame_controller_interaction");
|
||||
if (xr.has_frame_controller)
|
||||
enabled[enabled_count++] = "XR_VALVE_frame_controller_interaction";
|
||||
xr.has_refresh_rate = has_extension(extensions, extension_count, XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
|
||||
if (xr.has_refresh_rate)
|
||||
enabled[enabled_count++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
|
||||
@@ -966,6 +1104,22 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
}
|
||||
xrGetSystemProperties(xr.instance, xr.system, &properties);
|
||||
strncpy(xr.system_name, properties.systemName, sizeof(xr.system_name) - 1);
|
||||
{
|
||||
/* (SteamVR: the quads' alpha from their colour; HALO_XR_QUAD_ALPHA
|
||||
"colour" or "keep" says otherwise, for trying) */
|
||||
XrInstanceProperties instance = { XR_TYPE_INSTANCE_PROPERTIES };
|
||||
const char *choice = getenv("HALO_XR_QUAD_ALPHA");
|
||||
|
||||
if (XR_SUCCEEDED(xrGetInstanceProperties(xr.instance, &instance)))
|
||||
{
|
||||
xr.quad_alpha_from_colour = strstr(instance.runtimeName, "SteamVR") != NULL;
|
||||
host_logf(HOST_LOG_INFO, "xr: runtime %s", instance.runtimeName);
|
||||
}
|
||||
if (choice)
|
||||
xr.quad_alpha_from_colour = !strcmp(choice, "colour");
|
||||
if (xr.quad_alpha_from_colour)
|
||||
host_logf(HOST_LOG_INFO, "xr: the overlays' alpha from their colour");
|
||||
}
|
||||
|
||||
views[0].type = views[1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
|
||||
views[0].next = views[1].next = NULL;
|
||||
@@ -1322,6 +1476,8 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
glUniform1i(xr.source_uniform, 0);
|
||||
glUniform4f(xr.rect_uniform, left, top, right, bottom);
|
||||
glUniform1f(xr.cut_disc_uniform, clear_alpha == HALO_XR_COPY_DISC ? 1.0f : 0.0f);
|
||||
glUniform1f(xr.alpha_from_colour_uniform, xr.quad_alpha_from_colour && clear_alpha == HALO_XR_COPY_ALPHA &&
|
||||
layer != HALO_XR_LAYER_EYE_LEFT && layer != HALO_XR_LAYER_EYE_RIGHT ? 1.0f : 0.0f);
|
||||
if ((layer == HALO_XR_LAYER_EYE_LEFT || layer == HALO_XR_LAYER_EYE_RIGHT) && xr.vignette > 0.001f)
|
||||
{
|
||||
const XrFovf *fov = &xr.views[layer - HALO_XR_LAYER_EYE_LEFT].fov;
|
||||
|
||||
@@ -60,6 +60,11 @@ 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
|
||||
|
||||
|
||||
@@ -53,6 +53,23 @@ enum
|
||||
HALO_XR_BUTTON_TRIGGER = 1 << 4, /* trigger past its click point */
|
||||
HALO_XR_BUTTON_SQUEEZE = 1 << 5, /* grip past its click point */
|
||||
HALO_XR_BUTTON_THUMBREST = 1 << 6, /* thumb resting (touch) */
|
||||
/* the Steam Frame's controllers' (HALO_XR_LAYOUT_FRAME): the left one's
|
||||
View button and D-pad, either one's bumper (their right one's X, Y and
|
||||
menu button come as the left hand's A, B and MENU: the game's X, Y and
|
||||
Start, as on the Quest) */
|
||||
HALO_XR_BUTTON_VIEW = 1 << 7,
|
||||
HALO_XR_BUTTON_BUMPER = 1 << 8,
|
||||
HALO_XR_BUTTON_DPAD_UP = 1 << 9,
|
||||
HALO_XR_BUTTON_DPAD_DOWN = 1 << 10,
|
||||
HALO_XR_BUTTON_DPAD_LEFT = 1 << 11,
|
||||
HALO_XR_BUTTON_DPAD_RIGHT = 1 << 12,
|
||||
};
|
||||
|
||||
/* the controllers' layout, for what the game tells the player */
|
||||
enum
|
||||
{
|
||||
HALO_XR_LAYOUT_TOUCH = 0, /* the Quest's: X, Y and menu on the left */
|
||||
HALO_XR_LAYOUT_FRAME = 1, /* the Steam Frame's: A, B, X, Y and menu on the right, a D-pad on the left */
|
||||
};
|
||||
|
||||
/* halo_xr_hand.flags, halo_xr_view.flags, halo_xr_frame.head_flags */
|
||||
@@ -112,6 +129,7 @@ struct halo_xr_frame
|
||||
struct halo_xr_pose head;
|
||||
struct halo_xr_view views[HALO_XR_EYE_COUNT];
|
||||
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
|
||||
int controller_layout; /* HALO_XR_LAYOUT_* */
|
||||
};
|
||||
|
||||
/* halo_xr_submit.overlay_mode */
|
||||
|
||||
@@ -3136,6 +3136,7 @@ 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) */
|
||||
@@ -3181,6 +3182,7 @@ 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);
|
||||
@@ -3190,6 +3192,7 @@ 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();
|
||||
@@ -3667,6 +3670,7 @@ 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;
|
||||
@@ -3680,6 +3684,16 @@ 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 :
|
||||
|
||||
@@ -72,6 +72,10 @@ 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
|
||||
};
|
||||
@@ -252,4 +256,10 @@ 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
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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
|
||||
@@ -201,6 +201,9 @@ driver's hands on it as drawn (world units, from the game's camera), or
|
||||
NULL; the player's own hands turn it. And whether they hold it. */
|
||||
void halo_vr_set_wheel(short local_player_index, const float left[3], const float right[3]);
|
||||
int halo_vr_wheel_held(void);
|
||||
/* the controllers' layout (HALO_XR_LAYOUT_*: 0 the Quest's, 1 the Steam
|
||||
Frame's), for what the game tells the player */
|
||||
int halo_vr_controller_layout(void);
|
||||
/* whether a seated player's view stays in first person (director.c) */
|
||||
int halo_vr_vehicle_first_person(void);
|
||||
/* VR test aid (debug.vehicle_spawn): both sticks held in for a moment asks
|
||||
|
||||
@@ -2863,7 +2863,15 @@ static struct program_entry *prepare_draw(BOOL immediate)
|
||||
key.count_samples = device.visibility_test_active && xgpu_capabilities.atomic_counters;
|
||||
#endif
|
||||
|
||||
entry = program_get(vertex_shader_get(program, immediate, targets_multiview), fragment_shader_get(&key));
|
||||
entry = program_get(vertex_shader_get(program, immediate,
|
||||
#ifdef HALO_ANDROID
|
||||
/* the bound targets are the layers (Android's GL_OVR_multiview2);
|
||||
on the desktop there are no layers to stand in for the back buffer */
|
||||
targets_multiview
|
||||
#else
|
||||
FALSE
|
||||
#endif
|
||||
), fragment_shader_get(&key));
|
||||
if (!entry)
|
||||
{
|
||||
stats.skipped_link++;
|
||||
@@ -4023,7 +4031,9 @@ void d3d8_clear_back_buffer(float red, float green, float blue, float alpha)
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xff);
|
||||
glClearColor(red, green, blue, alpha);
|
||||
glClearDepthf(1.0f);
|
||||
/* (glClearDepth: on Android the define above maps it to glClearDepthf,
|
||||
ES's float entry point) */
|
||||
glClearDepth(1.0f);
|
||||
glClearStencil(0);
|
||||
glClear(GL_COLOR_BUFFER_BIT | (depth_buffer ? GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT : 0));
|
||||
xgpu_gl_state_invalidate();
|
||||
|
||||
@@ -413,6 +413,17 @@ 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"
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
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
|
||||
@@ -502,6 +502,11 @@ int halo_vr_active(void)
|
||||
return vr.active;
|
||||
}
|
||||
|
||||
int halo_vr_controller_layout(void)
|
||||
{
|
||||
return vr.active ? vr.frame.controller_layout : HALO_XR_LAYOUT_TOUCH;
|
||||
}
|
||||
|
||||
int halo_vr_eye_size(int *width, int *height)
|
||||
{
|
||||
const char *eye_width = getenv("HALO_VR_EYE_WIDTH");
|
||||
@@ -2479,6 +2484,7 @@ int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menu
|
||||
|
||||
#else
|
||||
|
||||
int halo_vr_controller_layout(void) { return 0; }
|
||||
int halo_vr_active(void) { return 0; }
|
||||
int halo_vr_stereo_frame(void) { return 0; }
|
||||
int halo_vr_stereo_player(short local_player_index, int cinematic) { (void)local_player_index; (void)cinematic; return 0; }
|
||||
|
||||
@@ -862,6 +862,12 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
|
||||
value = vr_stick(left->thumbstick[1]);
|
||||
if (abs(value) > abs(pad->sThumbLY)) pad->sThumbLY = value;
|
||||
if (left->buttons & HALO_XR_BUTTON_MENU) pad->wButtons |= XINPUT_GAMEPAD_START;
|
||||
/* (the Steam Frame's controllers: the left one's View button and D-pad) */
|
||||
if (left->buttons & HALO_XR_BUTTON_VIEW) pad->wButtons |= XINPUT_GAMEPAD_BACK;
|
||||
if (left->buttons & HALO_XR_BUTTON_DPAD_UP) pad->wButtons |= XINPUT_GAMEPAD_DPAD_UP;
|
||||
if (left->buttons & HALO_XR_BUTTON_DPAD_DOWN) pad->wButtons |= XINPUT_GAMEPAD_DPAD_DOWN;
|
||||
if (left->buttons & HALO_XR_BUTTON_DPAD_LEFT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_LEFT;
|
||||
if (left->buttons & HALO_XR_BUTTON_DPAD_RIGHT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_RIGHT;
|
||||
if (left->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_LEFT_THUMB;
|
||||
if (right->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_RIGHT_THUMB;
|
||||
/* (in the menus the triggers click the pointer: halo_vr_menu_pointer) */
|
||||
@@ -897,6 +903,10 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
|
||||
merge_button(pad, XINPUT_GAMEPAD_B,
|
||||
((right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 && !halo_vr_wheel_held()) || halo_vr_melee());
|
||||
merge_button(pad, XINPUT_GAMEPAD_Y, (left->buttons & HALO_XR_BUTTON_B) != 0);
|
||||
/* (the Steam Frame's bumpers: the left switches grenades, the right is
|
||||
the flashlight) */
|
||||
merge_button(pad, XINPUT_GAMEPAD_BLACK, (left->buttons & HALO_XR_BUTTON_BUMPER) != 0);
|
||||
merge_button(pad, XINPUT_GAMEPAD_WHITE, (right->buttons & HALO_XR_BUTTON_BUMPER) != 0);
|
||||
{
|
||||
/* B: a tap is the flashlight, holding it switches grenades; the
|
||||
game sees a short press of either */
|
||||
|
||||
@@ -5990,16 +5990,18 @@ 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" and the games */
|
||||
/* the online games panel's rows: "enter a code", the voice chat's
|
||||
switches, and the games */
|
||||
ONLINE_PANEL_ROWS = 11,
|
||||
/* (the rows before the games: "enter a code") */
|
||||
ONLINE_PANEL_FIXED_ROWS = 1,
|
||||
ONLINE_PANEL_FIXED_ROWS = 3,
|
||||
};
|
||||
|
||||
static struct
|
||||
@@ -6073,7 +6075,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 + 1);
|
||||
short rows = (short)(ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS);
|
||||
short move = 0;
|
||||
|
||||
if (event->type == _event_type_left_stick)
|
||||
@@ -6099,9 +6101,20 @@ 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)
|
||||
ui_join_code_globals.selection >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
|
||||
{
|
||||
ui_join_code_launch_keyboard();
|
||||
}
|
||||
@@ -6403,7 +6416,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);
|
||||
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS - 1);
|
||||
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);
|
||||
@@ -6418,7 +6431,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)
|
||||
if (index >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
|
||||
break;
|
||||
if (index == ui_join_code_globals.selection)
|
||||
ui_join_code_draw_quad(48, y, 592, (short)(y + 21), 0x803A6EA5);
|
||||
@@ -6426,9 +6439,20 @@ 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 - 1];
|
||||
struct ui_public_game const *game = &ui_join_code_globals.games[index - ONLINE_PANEL_FIXED_ROWS];
|
||||
|
||||
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,
|
||||
@@ -6439,9 +6463,9 @@ static void ui_join_code_render_panel(
|
||||
}
|
||||
}
|
||||
if (!ui_join_code_globals.game_count)
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
/* over the screens, as they are drawn */
|
||||
@@ -6875,6 +6899,63 @@ static void ui_vr_controls_render(
|
||||
ui_join_code_draw_text(left, (short)(left + 270), 86, "LEFT CONTROLLER", FALSE, 0, &title_color);
|
||||
ui_join_code_draw_text(right, (short)(right + 270), 86, "RIGHT CONTROLLER", FALSE, 0, &title_color);
|
||||
ui_join_code_draw_quad(320, 86, 321, 300, 0xFF3A6EA5);
|
||||
if (halo_vr_controller_layout() == 1)
|
||||
{
|
||||
/* the Steam Frame's controllers: A, B, X, Y and a menu button on the
|
||||
right one, a D-pad and a View button on the left one */
|
||||
switch (ui_vr_options_globals.page)
|
||||
{
|
||||
case _vr_controls_on_foot:
|
||||
ui_vr_controls_line(left, y, "Stick", "Move; click: crouch");
|
||||
ui_vr_controls_line(left, y + 22, "Trigger", "Grenade: hold it,");
|
||||
ui_vr_controls_line(left, y + 40, "", "let go as you throw");
|
||||
ui_vr_controls_line(left, y + 62, "Grip", "Weapon in both hands");
|
||||
ui_vr_controls_line(left, y + 84, "Bumper", "Switch grenades");
|
||||
ui_vr_controls_line(left, y + 106, "View", "Scores");
|
||||
ui_vr_controls_line(left, y + 128, "D-pad", "In the menus");
|
||||
ui_vr_controls_line(right, y, "Stick", "Turn; click: zoom");
|
||||
ui_vr_controls_line(right, y + 22, "Trigger", "Fire");
|
||||
ui_vr_controls_line(right, y + 44, "Grip", "Melee (or punch)");
|
||||
ui_vr_controls_line(right, y + 66, "A", "Jump");
|
||||
ui_vr_controls_line(right, y + 88, "B", "Light; held: grenades");
|
||||
ui_vr_controls_line(right, y + 110, "X", "Action, reload");
|
||||
ui_vr_controls_line(right, y + 132, "Y", "Switch weapon");
|
||||
ui_vr_controls_line(right, y + 154, "Menu", "Pause");
|
||||
ui_vr_controls_note(320, "Motion tracker: turn the back of your left wrist towards you.");
|
||||
ui_vr_controls_note(340, "Scope: zoom, and look through the disc above the weapon.");
|
||||
ui_vr_controls_note(360, "Right bumper: the flashlight too.");
|
||||
break;
|
||||
case _vr_controls_vehicles:
|
||||
ui_vr_controls_line(left, y, "Stick", "Drive");
|
||||
ui_vr_controls_line(left, y + 22, "Trigger", "Second weapon");
|
||||
ui_vr_controls_line(left, y + 40, "", "(Banshee, tank)");
|
||||
ui_vr_controls_line(right, y, "Stick", "Steer; up and down:");
|
||||
ui_vr_controls_line(right, y + 18, "", "climb, dive");
|
||||
ui_vr_controls_line(right, y + 40, "Trigger", "Fire");
|
||||
ui_vr_controls_line(right, y + 62, "X", "Get in, get out");
|
||||
ui_vr_controls_line(right, y + 84, "B", "Flashlight");
|
||||
ui_vr_controls_note(300, "Warthog: grip the wheel with one hand or both and turn it.");
|
||||
ui_vr_controls_note(320, "Holding it, the right trigger accelerates, the left one brakes");
|
||||
ui_vr_controls_note(338, "and reverses. Let go, and the right stick steers.");
|
||||
ui_vr_controls_note(360, "At a gun or as a passenger: aim with the right controller.");
|
||||
break;
|
||||
default:
|
||||
ui_vr_controls_line(left, y, "Stick", "Choose");
|
||||
ui_vr_controls_line(left, y + 22, "D-pad", "Choose");
|
||||
ui_vr_controls_line(left, y + 44, "Trigger", "Click where it points");
|
||||
ui_vr_controls_line(right, y, "Stick", "Choose, scroll");
|
||||
ui_vr_controls_line(right, y + 22, "A", "Accept");
|
||||
ui_vr_controls_line(right, y + 44, "B", "Back");
|
||||
ui_vr_controls_line(right, y + 66, "X, Y", "As the screen shows");
|
||||
ui_vr_controls_line(right, y + 88, "Trigger", "Click where it points");
|
||||
ui_vr_controls_note(300, "Point at the menus with either controller; its trigger clicks.");
|
||||
ui_vr_controls_note(320, "On the keyboard, BACK and ENTER under it can be clicked too.");
|
||||
ui_vr_controls_note(340, "Y on the main menu or the pause menu: these VR options.");
|
||||
break;
|
||||
}
|
||||
ui_join_code_draw_text(44, 596, 412, "%b-button Back Left, right: the other pages", TRUE, 0, &tab_color);
|
||||
return;
|
||||
}
|
||||
switch (ui_vr_options_globals.page)
|
||||
{
|
||||
case _vr_controls_on_foot:
|
||||
|
||||
@@ -231,6 +231,7 @@ symbols in this file:
|
||||
#include "units/units.h"
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#include "physics/collisions.h"
|
||||
#endif
|
||||
|
||||
/* port/linux/game/pal_tags.c's */
|
||||
@@ -2205,6 +2206,10 @@ static void trigger_create_projectiles(
|
||||
long target_object_index= NONE;
|
||||
long projectile_definition_index;
|
||||
short projectile_count;
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: fired from the muzzle of the weapon in the hand */
|
||||
boolean vr_from_muzzle= FALSE;
|
||||
#endif
|
||||
|
||||
if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit) &&
|
||||
unit &&
|
||||
@@ -2282,6 +2287,7 @@ static void trigger_create_projectiles(
|
||||
origin.x = vr_origin[0];
|
||||
origin.y = vr_origin[1];
|
||||
origin.z = vr_origin[2];
|
||||
vr_from_muzzle = TRUE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2383,6 +2389,26 @@ static void trigger_create_projectiles(
|
||||
real_point3d camera_position;
|
||||
|
||||
unit_get_camera_position(owner_object_index, &camera_position);
|
||||
#ifdef HALO_ANDROID
|
||||
if (vr_from_muzzle)
|
||||
{
|
||||
/* VR: from the eyes down to the hand the line runs through
|
||||
the player's own body, which the game's test hits, moving the
|
||||
shot up towards the eyes (it then flies parallel to the aim,
|
||||
above it): only the level stops it, a hand through a wall */
|
||||
struct collision_result collision;
|
||||
struct object_datum *projectile_object= object_get(projectile_object_index);
|
||||
|
||||
if (collision_test_line(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
|
||||
&camera_position, &projectile_object->object.position, owner_object_index, &collision) &&
|
||||
collision.location.cluster_index!=NONE)
|
||||
{
|
||||
object_translate(projectile_object_index, &collision.point, &collision.location);
|
||||
object_compute_node_matrices(projectile_object_index);
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
object_force_inside_bsp(projectile_object_index, &camera_position);
|
||||
}
|
||||
if (target_object_index!=NONE)
|
||||
|
||||
@@ -703,9 +703,10 @@ typedef char network_game_server_countdown_state_offset_assert[
|
||||
offsetof(struct network_game_server, countdown_state) ==
|
||||
8 + HALO_PORT_NETWORK_GAME_SIZE + MAXIMUM_NETWORK_MACHINE_COUNT * 0x10 + 0xC ? 1 : -1];
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* bots' machines (below: the VR port's bots) */
|
||||
/* bots' machines (the VR port's bots, port/linux/game/bots.c; on the
|
||||
desktop none is ever marked, the writes live behind the Android guard) */
|
||||
static boolean network_game_server_bot_machines[MAXIMUM_NETWORK_MACHINE_COUNT];
|
||||
#ifdef HALO_ANDROID
|
||||
static short network_game_server_bot_machine_count(struct network_game_server *server);
|
||||
#endif
|
||||
|
||||
@@ -4340,6 +4341,19 @@ static boolean network_game_server_have_all_machines_have_precached(
|
||||
return all_machines_have_precached;
|
||||
}
|
||||
|
||||
/* whether the machine at this index is one of the host's bots (the VR
|
||||
port's bots.c asks, on every platform: on the desktop no bot machine is
|
||||
ever marked, so this is always false there) */
|
||||
boolean network_game_server_is_bot_machine(
|
||||
long machine_index)
|
||||
{
|
||||
struct network_game_server *server = global_network_game_server_get();
|
||||
|
||||
return server && VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
|
||||
network_game_server_bot_machines[machine_index] &&
|
||||
server->game.machines[machine_index].machine_index == machine_index;
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* ---------- bots (the VR port's: port/linux/game/bots.c)
|
||||
|
||||
@@ -4363,16 +4377,6 @@ static void network_game_server_bot_copy_name(
|
||||
destination[index] = 0;
|
||||
}
|
||||
|
||||
boolean network_game_server_is_bot_machine(
|
||||
long machine_index)
|
||||
{
|
||||
struct network_game_server *server = global_network_game_server_get();
|
||||
|
||||
return server && VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
|
||||
network_game_server_bot_machines[machine_index] &&
|
||||
server->game.machines[machine_index].machine_index == machine_index;
|
||||
}
|
||||
|
||||
static short network_game_server_bot_machine_count(
|
||||
struct network_game_server *server)
|
||||
{
|
||||
|
||||
+28
-3
@@ -48,6 +48,10 @@ 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
|
||||
@@ -221,6 +225,10 @@ 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}")
|
||||
@@ -564,6 +572,23 @@ 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] = []
|
||||
@@ -578,7 +603,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{TOML_DIR}", f"-I{OPENXR_DIR}/include", f"-I{OPUS_DIR}/include",
|
||||
])
|
||||
host_sources = sorted((PORT_DIR / "host").glob("*.c")) + [
|
||||
LINUX_DIR / "src" / "posix_files.c", LINUX_DIR / "src" / "posix_net.c",
|
||||
@@ -604,12 +629,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 -L{sdl_build} -L{OPENXR_DIR}/lib -lopenxr_loader "
|
||||
command=(f"$android_host_cc -shared -o $out $in {libopus} -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])
|
||||
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl, libopus])
|
||||
|
||||
# a do-nothing OpenXR layer under the Steam Frame's implicit layer's
|
||||
# library name (port/android/xr_layer/fdm_injection_stub.c)
|
||||
|
||||
Reference in new issue
Block a user