Compare commits

..
Author SHA1 Message Date
Stefan Maier 9304daf2e8 Merge branch 'main' into test/bots-voice 2026-10-04 23:00:24 +02:00
Stefan Maier 8e8713681d Merge remote-tracking branch 'origin/fix/multiview-desktop-compile' 2026-10-04 22:59:55 +02:00
Stefan Maier d7099f3e40 Fix desktop link: bot machine check and glClearDepthf were Android-only
network_game_server_is_bot_machine (cea7abfc, Bots as players of the
host) was written into the HALO_ANDROID block of network_server_manager.c
while bots.c calls it on every platform, and so was the array it reads;
both move out of the guard (on the desktop nothing marks a bot machine,
so the check is always false there, the writes stay Android-only).

glClearDepthf (dc21f4b4, Play in VR on OpenXR headsets) is ES-only:
GL_FUNCTIONS declares it for Android's loader and the define at the top
maps glClearDepth to it, but the desktop context links against GL's
glClearDepth. The clear now calls glClearDepth, which the Android define
turns into glClearDepthf as before.
2026-10-04 22:37:17 +02:00
Stefan Maier 4ba36d7305 VR: fix desktop build, targets_multiview was Android-only
6c3f4ce2 (VR: draw both eyes in one pass) changed prepare_draw to pass
targets_multiview to vertex_shader_get, but the variable is declared and
kept inside the HALO_ANDROID guard (L887): Linux and Windows failed to
compile with 'use of undeclared identifier targets_multiview', Android
was fine. On the desktop there are no multiview layers to stand in for
the back buffer, so the desktop call passes FALSE.
2026-10-04 22:25:21 +02:00
Stefan Maier 0a62ad4610 Merge branch 'main' into test/bots-voice
# Conflicts:
#	port/android/app/src/main/java/com/halo/decomp/LauncherActivity.java
#	tools/android_build.py
2026-10-04 22:05:55 +02:00
Stefan Maier 855f083117 Merge branch 'frame/performance' 2026-10-04 22:04:33 +02:00
Stefan Maier 6f3786de91 Merge branch 'fix/vr-shot-origin' 2026-10-04 22:04:33 +02:00
Stefan MaierandClaude Opus 5.5 493713b0a6 Steam Frame: the refresh rates offered in the log; its rate and speed noted
The runtime offers only the rate SteamOS is set to. At 72 Hz the game holds
it with multiview (about 11.6 ms a frame); at 90 Hz it cannot, and the
frames SteamVR makes up showed as a doubled picture when the head turned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:47:56 +02:00
Stefan Maier 42dc1f546f Merge branch 'main' into test/bots-voice 2026-10-04 17:51:25 +02:00
Stefan Maier 79a6482c16 Merge branch 'merge/upstream-2026-10-04' into test/bots-voice 2026-10-04 13:47:58 +02:00
Stefan Maier bfd92ec17d Merge branch 'merge/upstream-2026-10-04' into test/bots-voice
# Conflicts:
#	source/interface/ui_widget.c
2026-10-04 13:47:56 +02:00
Stefan Maier 852ee322cc Merge branch 'feature/vr-hud-fixes' into test/bots-voice 2026-10-04 13:25:17 +02:00
Stefan Maier f0d4fb2608 Merge branch 'feature/bots-vehicles' into test/bots-voice 2026-10-04 13:25:17 +02:00
Stefan Maier 736c217347 Merge branch 'feature/vr-hud-fixes' into test/bots-voice 2026-10-04 12:00:43 +02:00
Stefan Maier 30edb22f36 Merge branch 'feature/vr-menu-lists' into test/bots-voice 2026-10-04 11:41:42 +02:00
Stefan Maier abc1c12f1e Merge branch 'feature/bots-vehicles' into test/bots-voice 2026-10-04 11:41:41 +02:00
Stefan Maier 3f02527c68 Merge branch 'feature/bots-vehicles' into test/bots-voice 2026-10-04 11:23:51 +02:00
Stefan Maier ceecb039e3 Merge branch 'feature/bots-vehicles' into test/bots-voice 2026-10-03 16:49:01 +02:00
Stefan Maier a34685574f Merge branch 'feature/vr-options' into test/bots-voice 2026-10-03 15:41:58 +02:00
Stefan Maier eb83e9d6d1 Merge branch 'feature/vr-options' into test/bots-voice 2026-10-03 15:38:13 +02:00
Stefan Maier 144063871a Merge branch 'main' into test/bots-voice 2026-10-03 15:38:13 +02:00
Stefan Maier 7165d40f90 Merge branch 'merge/upstream-v11' into test/bots-voice 2026-10-03 15:11:17 +02:00
Stefan Maier 65d81ad1ba Merge branch 'feature/warthog-wheel' into test/bots-voice 2026-10-03 13:50:43 +02:00
Stefan Maier d356c0abab Merge branch 'feature/warthog-wheel' into test/bots-voice 2026-10-03 13:00:27 +02:00
Stefan Maier 1536c30870 Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:47:53 +02:00
Stefan Maier 458b4642b6 Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:42:59 +02:00
Stefan Maier 2af2702bfa Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:30:21 +02:00
Stefan Maier 7a1c5e6239 Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:27:30 +02:00
Stefan Maier a9f3ce2647 Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:23:29 +02:00
Stefan Maier 1cebe431d1 Merge branch 'feature/vr-menu-pointer' into test/bots-voice
# Conflicts:
#	source/interface/ui_widget.c
2026-10-03 11:56:21 +02:00
Stefan Maier ec2ab73acc Merge branch 'main' into test/bots-voice 2026-10-03 11:56:06 +02:00
Stefan Maier 2f5fa771f5 Merge branches 'feature/vr-names' and 'feature/bots-team' into test/bots-voice 2026-10-03 10:55:30 +02:00
Stefan Maier 3aa88244fc Merge branches 'feature/vr-names' and 'feature/bots-team' into test/bots-voice 2026-10-03 10:41:10 +02:00
Stefan Maier a4e1bdb80d Merge branch 'feature/bots-modes' into test/bots-voice 2026-10-03 09:34:00 +02:00
Stefan Maier ceb0260389 Merge branches 'feature/bots-modes', 'merge/upstream-2026-10-03' and 'feature/vr-scope' into test/bots-voice 2026-10-03 08:27:26 +02:00
Stefan Maier bdfdef9bd4 Merge branch 'merge/upstream-2026-10-03' into test/bots-voice
# Conflicts:
#	port/linux/game/network_distributed.h
2026-10-03 07:40:34 +02:00
Stefan Maier f02317ca25 Merge branch 'feature/bots-modes' into test/bots-voice 2026-10-02 21:22:33 +02:00
Stefan Maier d7fa443718 Merge branch 'feature/bots-items' into test/bots-voice 2026-10-02 20:41:50 +02:00
Stefan Maier 4414d7606d Merge branch 'feature/vr-scope' into test/bots-voice 2026-10-02 18:14:36 +02:00
Stefan Maier 824f79aa70 Merge branch 'feature/vr-scope' into test/bots-voice 2026-10-02 18:08:07 +02:00
Stefan Maier 25a947faac Merge bots host options into the test build 2026-10-02 18:08:07 +02:00
Stefan MaierandClaude Opus 5.5 bf545439b4 Keep the voice and bots settings out of the network table
The settings file is written in the table's order, so voice.* and bots.*
between network.* entries wrote [network] twice, and the whole file was
then refused (defaults used). They now follow the network settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:52:12 +02:00
Stefan Maier 201f69a35a Merge voice chat into the bots test build 2026-10-02 17:06:16 +02:00
Stefan MaierandClaude Opus 5.5 96a75df218 Switch the voice chat and the microphone in the online games panel
Two rows under 'Enter a join code' toggle voice.enabled and
voice.microphone with A, written to config.toml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:53:04 +02:00
Stefan MaierandClaude Opus 5.5 1a732f577f Voice chat in multiplayer
The microphone (SDL3 recording with Android's voice communication preset:
echo cancelling, noise suppression) is encoded with Opus (VOIP, 16 kbit/s,
20 ms frames) while its level is over voice.threshold, and sent every
tick in a new distributed message (_distributed_message_voice): clients
to the host, which plays it and passes it on; the host its own to every
client. Each speaker is decoded into a jitter buffer and mixed into a
stream of its own, teammates over the radio, everyone else by distance
(20 world units) and from their side of where the listener looks.
Devices open on a thread of their own so the game never waits; the
launcher asks for the microphone once and tells the game whether it may
use it. Opus v1.5.2 is fetched and built statically into the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:43:36 +02:00
17 changed files with 1152 additions and 31 deletions

No files matched your search

+10 -2
View File
@@ -36,8 +36,8 @@ What differs is found at run time, not by a build of its own.
| 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 | in test: their own profile, the Xbox's layout |
| 6 | Frame rate, resolution | |
| 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 | |
@@ -85,3 +85,11 @@ page of buttons shows this layout on the Frame.
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,7 @@ 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;
@@ -60,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
@@ -118,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() ||
@@ -125,6 +171,7 @@ public class LauncherActivity extends Activity {
}
private void startGame() {
passOnMicrophonePermission();
startActivity(new Intent(this, HaloActivity.class));
finish();
}
+15
View File
@@ -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 */
+494
View File
@@ -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();
}
+7
View File
@@ -885,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++)
{
+5
View File
@@ -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
+14
View File
@@ -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 :
+10
View File
@@ -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
+326
View File
@@ -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));
}
}
}
+31
View File
@@ -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
+12 -2
View File
@@ -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();
+11
View File
@@ -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"
+87
View File
@@ -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
+35 -11
View File
@@ -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 */
+16 -12
View File
@@ -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
View File
@@ -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)