mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 18:00:25 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0d454ae9b | ||
|
|
96a75df218 | ||
|
|
1a732f577f | ||
|
|
66c0d33e34 | ||
|
|
6b77fe428a |
No files matched your search
@@ -19,6 +19,9 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_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) -->
|
<!-- installing a new version of the game (Updater) -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.halo.decomp;
|
package com.halo.decomp;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -35,6 +38,7 @@ import java.nio.channels.FileChannel;
|
|||||||
*/
|
*/
|
||||||
public class LauncherActivity extends Activity {
|
public class LauncherActivity extends Activity {
|
||||||
private static final int PICK_IMAGE = 1;
|
private static final int PICK_IMAGE = 1;
|
||||||
|
private static final int ASK_MICROPHONE = 2;
|
||||||
|
|
||||||
private File dataRoot;
|
private File dataRoot;
|
||||||
private TextView status;
|
private TextView status;
|
||||||
@@ -53,12 +57,37 @@ public class LauncherActivity extends Activity {
|
|||||||
passOnHardwareId();
|
passOnHardwareId();
|
||||||
passOnInvite(getIntent());
|
passOnInvite(getIntent());
|
||||||
if (haveData()) {
|
if (haveData()) {
|
||||||
startGame();
|
if (!askForMicrophone())
|
||||||
|
startGame();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
buildInterface();
|
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
|
* 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
|
* (port/linux/src/p2p.c) picks it up from join_link.txt, whether it is
|
||||||
@@ -111,6 +140,23 @@ public class LauncherActivity extends Activity {
|
|||||||
partial.delete();
|
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). */
|
/** The game data, or the marker file of the VR self-test (host_xr.c). */
|
||||||
private boolean haveData() {
|
private boolean haveData() {
|
||||||
return dataRoot != null && (new File(dataRoot, "maps/ui.map").isFile() ||
|
return dataRoot != null && (new File(dataRoot, "maps/ui.map").isFile() ||
|
||||||
@@ -118,6 +164,7 @@ public class LauncherActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startGame() {
|
private void startGame() {
|
||||||
|
passOnMicrophonePermission();
|
||||||
startActivity(new Intent(this, HaloActivity.class));
|
startActivity(new Intent(this, HaloActivity.class));
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,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) */
|
/* darkens the eye images from their edges, strength 0..1 (0: off) */
|
||||||
void host_xr_set_vignette(float strength);
|
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 */
|
/* ---------- Android */
|
||||||
|
|
||||||
/* the storage directories the port uses, copied into buffer */
|
/* the storage directories the port uses, copied into buffer */
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -58,6 +58,11 @@ host_xr_copy_layer_region
|
|||||||
host_xr_end_frame
|
host_xr_end_frame
|
||||||
host_xr_haptic
|
host_xr_haptic
|
||||||
host_xr_set_vignette
|
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_xr_copy_layer_array
|
||||||
host_android_path
|
host_android_path
|
||||||
|
|
||||||
|
|||||||
@@ -3051,6 +3051,7 @@ void network_distributed_new_game(
|
|||||||
network_objects_new_game();
|
network_objects_new_game();
|
||||||
network_damage_new_game();
|
network_damage_new_game();
|
||||||
network_vr_new_game();
|
network_vr_new_game();
|
||||||
|
network_voice_new_game();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* after each tick (game_time.c) */
|
/* after each tick (game_time.c) */
|
||||||
@@ -3093,6 +3094,7 @@ void network_distributed_tick(
|
|||||||
distributed_statistics_due = FALSE;
|
distributed_statistics_due = FALSE;
|
||||||
distributed_host_send_players();
|
distributed_host_send_players();
|
||||||
network_vr_tick();
|
network_vr_tick();
|
||||||
|
network_voice_tick();
|
||||||
distributed_send_pickups();
|
distributed_send_pickups();
|
||||||
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
|
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
|
||||||
distributed_send_game_state(NONE);
|
distributed_send_game_state(NONE);
|
||||||
@@ -3102,6 +3104,7 @@ void network_distributed_tick(
|
|||||||
distributed_note_own_positions();
|
distributed_note_own_positions();
|
||||||
distributed_client_send_inputs();
|
distributed_client_send_inputs();
|
||||||
network_vr_tick();
|
network_vr_tick();
|
||||||
|
network_voice_tick();
|
||||||
distributed_client_send_predictions();
|
distributed_client_send_predictions();
|
||||||
network_objects_client_tick();
|
network_objects_client_tick();
|
||||||
network_damage_client_tick();
|
network_damage_client_tick();
|
||||||
@@ -3577,6 +3580,7 @@ void network_distributed_handle_message(
|
|||||||
case _distributed_message_client_ready: entry_size = 0; break;
|
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_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_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_damage_events:
|
||||||
case _distributed_message_hit_reports: entry_size = network_damage_entry_size(header.type); break;
|
case _distributed_message_hit_reports: entry_size = network_damage_entry_size(header.type); break;
|
||||||
default: entry_size = network_objects_entry_size(header.type); break;
|
default: entry_size = network_objects_entry_size(header.type); break;
|
||||||
@@ -3590,6 +3594,16 @@ void network_distributed_handle_message(
|
|||||||
|
|
||||||
/* (each kind from the host, or from a client; a VR player's hands
|
/* (each kind from the host, or from a client; a VR player's hands
|
||||||
both ways) */
|
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 (header.type == _distributed_message_vr_poses)
|
||||||
{
|
{
|
||||||
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
|
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ enum
|
|||||||
the host, the host's of the others to each client (unreliable;
|
the host, the host's of the others to each client (unreliable;
|
||||||
network_vr.c). Last, so that a build without it drops it. */
|
network_vr.c). Last, so that a build without it drops it. */
|
||||||
_distributed_message_vr_poses,
|
_distributed_message_vr_poses,
|
||||||
|
/* (the VR port's) 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
|
NUMBER_OF_DISTRIBUTED_MESSAGES
|
||||||
};
|
};
|
||||||
@@ -236,4 +240,10 @@ void network_vr_tick(void);
|
|||||||
word network_vr_entry_size(void);
|
word network_vr_entry_size(void);
|
||||||
void network_vr_handle_poses(long machine_index, void const *entries, short count);
|
void network_vr_handle_poses(long machine_index, void const *entries, short count);
|
||||||
|
|
||||||
|
/* 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
|
#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
|
||||||
@@ -331,6 +331,8 @@ static struct
|
|||||||
unsigned char join_host_hash[P2P_KEY_HASH_SIZE];
|
unsigned char join_host_hash[P2P_KEY_HASH_SIZE];
|
||||||
unsigned char join_token[P2P_TOKEN_SIZE];
|
unsigned char join_token[P2P_TOKEN_SIZE];
|
||||||
unsigned long join_time;
|
unsigned long join_time;
|
||||||
|
/* how the latest join goes, for the menus (p2p_join_state) */
|
||||||
|
int join_state;
|
||||||
|
|
||||||
char clipboard[P2P_LINK_SIZE];
|
char clipboard[P2P_LINK_SIZE];
|
||||||
int has_clipboard;
|
int has_clipboard;
|
||||||
@@ -840,6 +842,12 @@ static void drop_peer(struct peer *peer, const char *reason)
|
|||||||
struct retired_session *retired = &p2p.retired[p2p.retired_next++ % MAXIMUM_RETIRED_SESSIONS];
|
struct retired_session *retired = &p2p.retired[p2p.retired_next++ % MAXIMUM_RETIRED_SESSIONS];
|
||||||
int ask_again = p2p.joining && peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE);
|
int ask_again = p2p.joining && peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE);
|
||||||
|
|
||||||
|
if (peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE) &&
|
||||||
|
p2p.join_state != P2P_JOIN_NONE)
|
||||||
|
{
|
||||||
|
p2p.join_state = peer->connected ? P2P_JOIN_LOST : P2P_JOIN_BLOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
platform_log("Internet play: %s %s: %s", peer->is_host ? "host" : "player", peer->name, reason);
|
platform_log("Internet play: %s %s: %s", peer->is_host ? "host" : "player", peer->name, reason);
|
||||||
if (peer->connected)
|
if (peer->connected)
|
||||||
{
|
{
|
||||||
@@ -1029,6 +1037,8 @@ static void peer_heard(struct peer *peer, unsigned long address, unsigned short
|
|||||||
address_text(address, port, text));
|
address_text(address, port, text));
|
||||||
if (peer->is_host)
|
if (peer->is_host)
|
||||||
{
|
{
|
||||||
|
if (!memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE) && p2p.join_state != P2P_JOIN_NONE)
|
||||||
|
p2p.join_state = P2P_JOIN_CONNECTED;
|
||||||
if (p2p.joining && !memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE))
|
if (p2p.joining && !memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE))
|
||||||
{
|
{
|
||||||
p2p.joining = 0;
|
p2p.joining = 0;
|
||||||
@@ -2298,6 +2308,7 @@ static int join_invite(const char *text)
|
|||||||
memcpy(p2p.join_host_hash, hash, sizeof(hash));
|
memcpy(p2p.join_host_hash, hash, sizeof(hash));
|
||||||
memcpy(p2p.join_token, token, sizeof(token));
|
memcpy(p2p.join_token, token, sizeof(token));
|
||||||
p2p.join_requested = 1;
|
p2p.join_requested = 1;
|
||||||
|
p2p.join_state = P2P_JOIN_REACHING;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2344,6 +2355,51 @@ int p2p_join_code_status(void)
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void p2p_set_public_game(int public_game)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&p2p_lock);
|
||||||
|
p2p_list_set_public(public_game);
|
||||||
|
pthread_mutex_unlock(&p2p_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
int p2p_public_game(void)
|
||||||
|
{
|
||||||
|
int result;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&p2p_lock);
|
||||||
|
result = p2p_list_public();
|
||||||
|
pthread_mutex_unlock(&p2p_lock);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
|
||||||
|
int maximum_players)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&p2p_lock);
|
||||||
|
p2p_list_set_description(name, map, mode, players, maximum_players);
|
||||||
|
pthread_mutex_unlock(&p2p_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
int p2p_public_games(struct p2p_public_game *games, int maximum)
|
||||||
|
{
|
||||||
|
int count;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&p2p_lock);
|
||||||
|
count = p2p.running ? p2p_list_games(games, maximum) : 0;
|
||||||
|
pthread_mutex_unlock(&p2p_lock);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int p2p_join_state(void)
|
||||||
|
{
|
||||||
|
int state;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&p2p_lock);
|
||||||
|
state = p2p.running ? p2p.join_state : P2P_JOIN_NONE;
|
||||||
|
pthread_mutex_unlock(&p2p_lock);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
void p2p_join_code_clear_status(void)
|
void p2p_join_code_clear_status(void)
|
||||||
{
|
{
|
||||||
pthread_mutex_lock(&p2p_lock);
|
pthread_mutex_lock(&p2p_lock);
|
||||||
@@ -2381,6 +2437,8 @@ static void update_joining(void)
|
|||||||
{
|
{
|
||||||
p2p.joining = 0;
|
p2p.joining = 0;
|
||||||
p2p_signal_stop_joining();
|
p2p_signal_stop_joining();
|
||||||
|
if (p2p.join_state == P2P_JOIN_REACHING)
|
||||||
|
p2p.join_state = P2P_JOIN_NO_ANSWER;
|
||||||
platform_log("Internet play: no answer from the invite's host; it may have stopped hosting or quit");
|
platform_log("Internet play: no answer from the invite's host; it may have stopped hosting or quit");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,43 @@ int p2p_join_code(char *text, int size);
|
|||||||
int p2p_join_with_code(const char *typed);
|
int p2p_join_with_code(const char *typed);
|
||||||
int p2p_join_code_status(void);
|
int p2p_join_code_status(void);
|
||||||
void p2p_join_code_clear_status(void);
|
void p2p_join_code_clear_status(void);
|
||||||
|
/* how the latest join of an invite or code goes (the VR port's, for the
|
||||||
|
menus): reaching the host, connected (its game is in the system link list),
|
||||||
|
or why not */
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
P2P_JOIN_NONE,
|
||||||
|
P2P_JOIN_REACHING,
|
||||||
|
P2P_JOIN_CONNECTED,
|
||||||
|
P2P_JOIN_NO_ANSWER,
|
||||||
|
P2P_JOIN_BLOCKED,
|
||||||
|
P2P_JOIN_LOST,
|
||||||
|
};
|
||||||
|
int p2p_join_state(void);
|
||||||
|
|
||||||
|
/* Public games (p2p_list.c): the hosted game listed for anyone or not, and
|
||||||
|
what it is (the game's server, as it changes); and the games others list,
|
||||||
|
joined by their codes (p2p_join_with_code) */
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
P2P_PUBLIC_NAME_SIZE = 16,
|
||||||
|
P2P_PUBLIC_MAP_SIZE = 32,
|
||||||
|
P2P_PUBLIC_MODE_SIZE = 24,
|
||||||
|
};
|
||||||
|
struct p2p_public_game
|
||||||
|
{
|
||||||
|
char name[P2P_PUBLIC_NAME_SIZE];
|
||||||
|
char map[P2P_PUBLIC_MAP_SIZE];
|
||||||
|
char mode[P2P_PUBLIC_MODE_SIZE];
|
||||||
|
char code[12];
|
||||||
|
int players;
|
||||||
|
int maximum_players;
|
||||||
|
};
|
||||||
|
void p2p_set_public_game(int public_game);
|
||||||
|
int p2p_public_game(void);
|
||||||
|
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
|
||||||
|
int maximum_players);
|
||||||
|
int p2p_public_games(struct p2p_public_game *games, int maximum);
|
||||||
|
|
||||||
/* this machine's identifier, which its XNADDR carries (6 bytes) */
|
/* this machine's identifier, which its XNADDR carries (6 bytes) */
|
||||||
const unsigned char *p2p_identifier(void);
|
const unsigned char *p2p_identifier(void);
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ int p2p_signal_connected(void);
|
|||||||
a message to a topic through every broker */
|
a message to a topic through every broker */
|
||||||
void p2p_signal_sync_topics(void);
|
void p2p_signal_sync_topics(void);
|
||||||
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size);
|
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size);
|
||||||
|
/* the same, kept by the brokers for whoever listens later (empty: no
|
||||||
|
longer) */
|
||||||
|
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size);
|
||||||
|
|
||||||
/* ---------- p2p_code.c: join codes (under p2p_lock) */
|
/* ---------- p2p_code.c: join codes (under p2p_lock) */
|
||||||
|
|
||||||
@@ -136,6 +139,21 @@ void p2p_code_topics(const char **host_topic, const char **look_topic);
|
|||||||
int p2p_code_received(const char *topic, const unsigned char *payload, int size);
|
int p2p_code_received(const char *topic, const unsigned char *payload, int size);
|
||||||
void p2p_code_update(void);
|
void p2p_code_update(void);
|
||||||
|
|
||||||
|
/* ---------- p2p_list.c: public games (under p2p_lock) */
|
||||||
|
|
||||||
|
/* hosting: whether the game is listed, and what it is */
|
||||||
|
void p2p_list_set_public(int public_game);
|
||||||
|
int p2p_list_public(void);
|
||||||
|
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
|
||||||
|
int maximum_players);
|
||||||
|
/* the games listed lately; asking keeps this machine listening a while */
|
||||||
|
int p2p_list_games(struct p2p_public_game *games, int maximum);
|
||||||
|
/* (p2p_signal.c) the topic to listen on (empty: none); a message on one
|
||||||
|
(nonzero if it was a listing's); and each pass, whether hosting */
|
||||||
|
const char *p2p_list_topic(void);
|
||||||
|
int p2p_list_received(const char *topic, const unsigned char *payload, int size);
|
||||||
|
void p2p_list_update(int hosting);
|
||||||
|
|
||||||
/* ---------- p2p_crypto.c */
|
/* ---------- p2p_crypto.c */
|
||||||
|
|
||||||
enum
|
enum
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
/*
|
||||||
|
P2P_LIST.C
|
||||||
|
|
||||||
|
Public games (the VR port's): a host may list its game for anyone, who
|
||||||
|
finds it in a list of online games and joins it by its join code
|
||||||
|
(p2p_code.c), without a server of this project's.
|
||||||
|
|
||||||
|
- A public game's host announces it through the signalling brokers
|
||||||
|
(p2p_signal.c) every ANNOUNCE_INTERVAL, retained, on a topic of its own
|
||||||
|
under hcvr/1/g<network version>/: its name, map, game type, players, its
|
||||||
|
join code, and the time. Retained, a machine that starts listening gets
|
||||||
|
every game's latest at once. A host that stops (it closes its game, or
|
||||||
|
makes it private) clears its topic; one that quits without that leaves
|
||||||
|
its last announcement, which listeners drop once it is older than
|
||||||
|
STALE_SECONDS (by the machines' clocks).
|
||||||
|
- A machine that shows the list listens on the topics of its network
|
||||||
|
version (a wildcard), and keeps the games heard of lately.
|
||||||
|
- An announcement carries no address: a machine reaches a public game's
|
||||||
|
host only by its code, as anyone given the code does. Anyone can announce
|
||||||
|
anything, so what is shown is checked to be printable and in bounds, and
|
||||||
|
a game that is not what it says is only a code that does not answer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "platform.h"
|
||||||
|
#include "posix.h"
|
||||||
|
#include "port_config.h"
|
||||||
|
#include "p2p_internal.h"
|
||||||
|
#include "halo_port_limits.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
ANNOUNCEMENT_VERSION = 1,
|
||||||
|
/* milliseconds */
|
||||||
|
ANNOUNCE_INTERVAL = 10000,
|
||||||
|
/* a listener's interest lapses this long after the menu last asked */
|
||||||
|
WATCH_TIME = 3000,
|
||||||
|
STALE_SECONDS = 45,
|
||||||
|
MAXIMUM_LISTED_GAMES = 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* what goes to the brokers (byte for byte) */
|
||||||
|
struct announcement
|
||||||
|
{
|
||||||
|
char magic[4];
|
||||||
|
unsigned char version;
|
||||||
|
unsigned char players;
|
||||||
|
unsigned char maximum_players;
|
||||||
|
unsigned char flags;
|
||||||
|
unsigned char time[8];
|
||||||
|
char code[8];
|
||||||
|
char name[P2P_PUBLIC_NAME_SIZE];
|
||||||
|
char map[P2P_PUBLIC_MAP_SIZE];
|
||||||
|
char mode[P2P_PUBLIC_MODE_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct listed_game
|
||||||
|
{
|
||||||
|
int used;
|
||||||
|
char topic[P2P_CODE_TOPIC_SIZE];
|
||||||
|
unsigned long heard_time;
|
||||||
|
long long announced_time;
|
||||||
|
struct p2p_public_game game;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct
|
||||||
|
{
|
||||||
|
/* hosting (whether public: network.public_game, until Y changes it) */
|
||||||
|
int initialized;
|
||||||
|
int public_game;
|
||||||
|
char topic[P2P_CODE_TOPIC_SIZE];
|
||||||
|
int announced;
|
||||||
|
unsigned long announced_time;
|
||||||
|
char name[P2P_PUBLIC_NAME_SIZE];
|
||||||
|
char map[P2P_PUBLIC_MAP_SIZE];
|
||||||
|
char mode[P2P_PUBLIC_MODE_SIZE];
|
||||||
|
int players;
|
||||||
|
int maximum_players;
|
||||||
|
|
||||||
|
/* listening */
|
||||||
|
unsigned long watched_time;
|
||||||
|
int watching;
|
||||||
|
char watch_topic[P2P_CODE_TOPIC_SIZE];
|
||||||
|
struct listed_game games[MAXIMUM_LISTED_GAMES];
|
||||||
|
} list;
|
||||||
|
|
||||||
|
static int list_elapsed(unsigned long since, unsigned long time)
|
||||||
|
{
|
||||||
|
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* text as shown: printable ASCII only, ended */
|
||||||
|
static void list_text(char *destination, int size, const char *source, int source_size)
|
||||||
|
{
|
||||||
|
int index;
|
||||||
|
|
||||||
|
for (index = 0; index < size - 1 && index < source_size && source[index]; index++)
|
||||||
|
destination[index] = source[index] >= 32 && source[index] < 127 ? source[index] : '?';
|
||||||
|
destination[index] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static long long list_time_now(void)
|
||||||
|
{
|
||||||
|
return (long long)time(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void list_topics_make(void)
|
||||||
|
{
|
||||||
|
if (!list.topic[0])
|
||||||
|
{
|
||||||
|
unsigned char random[8];
|
||||||
|
char hex[17];
|
||||||
|
|
||||||
|
posix_random_bytes(random, sizeof(random));
|
||||||
|
p2p_hex(random, sizeof(random), hex);
|
||||||
|
snprintf(list.topic, sizeof(list.topic), "hcvr/1/g%d/%s", HALO_PORT_NETWORK_VERSION, hex);
|
||||||
|
}
|
||||||
|
if (!list.watch_topic[0])
|
||||||
|
snprintf(list.watch_topic, sizeof(list.watch_topic), "hcvr/1/g%d/+", HALO_PORT_NETWORK_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void list_announce(void)
|
||||||
|
{
|
||||||
|
struct announcement announcement;
|
||||||
|
char code[16];
|
||||||
|
long long now = list_time_now();
|
||||||
|
int index;
|
||||||
|
|
||||||
|
if (!p2p_code_hosted(code, sizeof(code)))
|
||||||
|
return;
|
||||||
|
memset(&announcement, 0, sizeof(announcement));
|
||||||
|
memcpy(announcement.magic, "HCVR", 4);
|
||||||
|
announcement.version = ANNOUNCEMENT_VERSION;
|
||||||
|
announcement.players = (unsigned char)(list.players < 0 ? 0 : list.players > 255 ? 255 : list.players);
|
||||||
|
announcement.maximum_players =
|
||||||
|
(unsigned char)(list.maximum_players < 0 ? 0 : list.maximum_players > 255 ? 255 : list.maximum_players);
|
||||||
|
for (index = 0; index < 8; index++)
|
||||||
|
announcement.time[index] = (unsigned char)(now >> (8 * index));
|
||||||
|
/* (the code without its dash) */
|
||||||
|
memcpy(announcement.code, code, 4);
|
||||||
|
memcpy(announcement.code + 4, code + 5, 4);
|
||||||
|
memcpy(announcement.name, list.name, sizeof(announcement.name));
|
||||||
|
memcpy(announcement.map, list.map, sizeof(announcement.map));
|
||||||
|
memcpy(announcement.mode, list.mode, sizeof(announcement.mode));
|
||||||
|
p2p_signal_publish_retained(list.topic, (const unsigned char *)&announcement, sizeof(announcement));
|
||||||
|
if (!list.announced)
|
||||||
|
platform_log("Internet play: the game is listed as public (%s, %s, %s)", list.name, list.map, list.mode);
|
||||||
|
list.announced = 1;
|
||||||
|
list.announced_time = p2p_now();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void list_withdraw(void)
|
||||||
|
{
|
||||||
|
if (!list.announced)
|
||||||
|
return;
|
||||||
|
p2p_signal_publish_retained(list.topic, NULL, 0);
|
||||||
|
platform_log("Internet play: the game is no longer listed as public");
|
||||||
|
list.announced = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- p2p.c's side (under p2p_lock) */
|
||||||
|
|
||||||
|
void p2p_list_set_public(int public_game)
|
||||||
|
{
|
||||||
|
if (public_game == list.public_game)
|
||||||
|
return;
|
||||||
|
list.public_game = public_game;
|
||||||
|
list_topics_make();
|
||||||
|
if (!public_game)
|
||||||
|
list_withdraw();
|
||||||
|
else
|
||||||
|
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int p2p_list_public(void)
|
||||||
|
{
|
||||||
|
return list.public_game;
|
||||||
|
}
|
||||||
|
|
||||||
|
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
|
||||||
|
int maximum_players)
|
||||||
|
{
|
||||||
|
int changed = strncmp(list.name, name, sizeof(list.name) - 1) || strncmp(list.map, map, sizeof(list.map) - 1) ||
|
||||||
|
strncmp(list.mode, mode, sizeof(list.mode) - 1) || players != list.players ||
|
||||||
|
maximum_players != list.maximum_players;
|
||||||
|
|
||||||
|
list_text(list.name, sizeof(list.name), name, P2P_PUBLIC_NAME_SIZE);
|
||||||
|
list_text(list.map, sizeof(list.map), map, P2P_PUBLIC_MAP_SIZE);
|
||||||
|
list_text(list.mode, sizeof(list.mode), mode, P2P_PUBLIC_MODE_SIZE);
|
||||||
|
list.players = players;
|
||||||
|
list.maximum_players = maximum_players;
|
||||||
|
/* (a change goes out soon, not at the next interval) */
|
||||||
|
if (changed && list.announced && list_elapsed(list.announced_time, 1000))
|
||||||
|
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int p2p_list_games(struct p2p_public_game *games, int maximum)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
int index;
|
||||||
|
|
||||||
|
list.watched_time = p2p_now();
|
||||||
|
list.watching = 1;
|
||||||
|
for (index = 0; index < MAXIMUM_LISTED_GAMES && count < maximum; index++)
|
||||||
|
{
|
||||||
|
if (list.games[index].used)
|
||||||
|
games[count++] = list.games[index].game;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- p2p_signal.c's side (under p2p_lock) */
|
||||||
|
|
||||||
|
const char *p2p_list_topic(void)
|
||||||
|
{
|
||||||
|
return list.watching ? list.watch_topic : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
int p2p_list_received(const char *topic, const unsigned char *payload, int size)
|
||||||
|
{
|
||||||
|
struct announcement announcement;
|
||||||
|
struct listed_game *slot = NULL;
|
||||||
|
long long announced = 0;
|
||||||
|
char code[16];
|
||||||
|
int index;
|
||||||
|
|
||||||
|
if (!list.watch_topic[0] || strncmp(topic, list.watch_topic, strlen(list.watch_topic) - 1))
|
||||||
|
return 0;
|
||||||
|
/* (this machine's own, as the broker echoes it) */
|
||||||
|
if (!strcmp(topic, list.topic))
|
||||||
|
return 1;
|
||||||
|
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
|
||||||
|
{
|
||||||
|
if (list.games[index].used && !strcmp(list.games[index].topic, topic))
|
||||||
|
slot = &list.games[index];
|
||||||
|
}
|
||||||
|
if (size != (int)sizeof(announcement))
|
||||||
|
{
|
||||||
|
/* (empty: withdrawn) */
|
||||||
|
if (slot && size == 0)
|
||||||
|
slot->used = 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
memcpy(&announcement, payload, sizeof(announcement));
|
||||||
|
if (memcmp(announcement.magic, "HCVR", 4) || announcement.version != ANNOUNCEMENT_VERSION ||
|
||||||
|
!announcement.maximum_players || announcement.players > announcement.maximum_players)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (index = 0; index < 8; index++)
|
||||||
|
announced |= (long long)announcement.time[index] << (8 * index);
|
||||||
|
if (announced < list_time_now() - STALE_SECONDS || announced > list_time_now() + STALE_SECONDS)
|
||||||
|
{
|
||||||
|
if (slot)
|
||||||
|
slot->used = 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
snprintf(code, sizeof(code), "%.4s-%.4s", announcement.code, announcement.code + 4);
|
||||||
|
for (index = 0; code[index]; index++)
|
||||||
|
{
|
||||||
|
if (index != 4 && !((code[index] >= '0' && code[index] <= '9') || (code[index] >= 'A' && code[index] <= 'Z')))
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!slot)
|
||||||
|
{
|
||||||
|
int oldest = -1;
|
||||||
|
|
||||||
|
for (index = 0; index < MAXIMUM_LISTED_GAMES && !slot; index++)
|
||||||
|
{
|
||||||
|
if (!list.games[index].used)
|
||||||
|
slot = &list.games[index];
|
||||||
|
else if (oldest < 0 || list.games[index].announced_time < list.games[oldest].announced_time)
|
||||||
|
oldest = index;
|
||||||
|
}
|
||||||
|
if (!slot)
|
||||||
|
slot = &list.games[oldest];
|
||||||
|
memset(slot, 0, sizeof(*slot));
|
||||||
|
snprintf(slot->topic, sizeof(slot->topic), "%s", topic);
|
||||||
|
}
|
||||||
|
slot->used = 1;
|
||||||
|
slot->heard_time = p2p_now();
|
||||||
|
slot->announced_time = announced;
|
||||||
|
list_text(slot->game.name, sizeof(slot->game.name), announcement.name, sizeof(announcement.name));
|
||||||
|
list_text(slot->game.map, sizeof(slot->game.map), announcement.map, sizeof(announcement.map));
|
||||||
|
list_text(slot->game.mode, sizeof(slot->game.mode), announcement.mode, sizeof(announcement.mode));
|
||||||
|
snprintf(slot->game.code, sizeof(slot->game.code), "%s", code);
|
||||||
|
slot->game.players = announcement.players;
|
||||||
|
slot->game.maximum_players = announcement.maximum_players;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void p2p_list_update(int hosting)
|
||||||
|
{
|
||||||
|
int index;
|
||||||
|
|
||||||
|
if (!list.initialized)
|
||||||
|
{
|
||||||
|
list.initialized = 1;
|
||||||
|
if (!list.public_game)
|
||||||
|
list.public_game = config_boolean("network.public_game");
|
||||||
|
}
|
||||||
|
list_topics_make();
|
||||||
|
/* hosting a public game: announced every so often; withdrawn when it
|
||||||
|
is not */
|
||||||
|
if (hosting && list.public_game)
|
||||||
|
{
|
||||||
|
p2p_signal_start();
|
||||||
|
if (p2p_signal_connected() && list_elapsed(list.announced_time, ANNOUNCE_INTERVAL))
|
||||||
|
list_announce();
|
||||||
|
}
|
||||||
|
else if (list.announced)
|
||||||
|
{
|
||||||
|
list_withdraw();
|
||||||
|
}
|
||||||
|
/* listening while the menu asks */
|
||||||
|
if (list.watching && list_elapsed(list.watched_time, WATCH_TIME))
|
||||||
|
{
|
||||||
|
list.watching = 0;
|
||||||
|
memset(list.games, 0, sizeof(list.games));
|
||||||
|
p2p_signal_sync_topics();
|
||||||
|
}
|
||||||
|
else if (list.watching)
|
||||||
|
{
|
||||||
|
p2p_signal_start();
|
||||||
|
p2p_signal_sync_topics();
|
||||||
|
}
|
||||||
|
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
|
||||||
|
{
|
||||||
|
if (list.games[index].used && list.games[index].announced_time < list_time_now() - STALE_SECONDS)
|
||||||
|
list.games[index].used = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,6 +157,8 @@ struct broker
|
|||||||
/* (join codes' topics: p2p_code.c) */
|
/* (join codes' topics: p2p_code.c) */
|
||||||
char code_host_topic[TOPIC_SIZE];
|
char code_host_topic[TOPIC_SIZE];
|
||||||
char code_look_topic[TOPIC_SIZE];
|
char code_look_topic[TOPIC_SIZE];
|
||||||
|
/* (public games': p2p_list.c) */
|
||||||
|
char list_topic[TOPIC_SIZE];
|
||||||
unsigned char input[BUFFER_SIZE];
|
unsigned char input[BUFFER_SIZE];
|
||||||
int input_size;
|
int input_size;
|
||||||
unsigned char output[BUFFER_SIZE];
|
unsigned char output[BUFFER_SIZE];
|
||||||
@@ -328,6 +330,7 @@ static void broker_close(struct broker *broker, int failed)
|
|||||||
broker->join_topic[0] = 0;
|
broker->join_topic[0] = 0;
|
||||||
broker->code_host_topic[0] = 0;
|
broker->code_host_topic[0] = 0;
|
||||||
broker->code_look_topic[0] = 0;
|
broker->code_look_topic[0] = 0;
|
||||||
|
broker->list_topic[0] = 0;
|
||||||
if (failed)
|
if (failed)
|
||||||
broker->failures++;
|
broker->failures++;
|
||||||
}
|
}
|
||||||
@@ -417,8 +420,8 @@ static void broker_publish(struct broker *broker, const char *topic, const unsig
|
|||||||
/* the topics a ready broker should be subscribed to */
|
/* the topics a ready broker should be subscribed to */
|
||||||
static void broker_sync_topics(struct broker *broker)
|
static void broker_sync_topics(struct broker *broker)
|
||||||
{
|
{
|
||||||
const char *wanted[4];
|
const char *wanted[5];
|
||||||
char *had[4];
|
char *had[5];
|
||||||
int index;
|
int index;
|
||||||
|
|
||||||
if (broker->state != _broker_ready)
|
if (broker->state != _broker_ready)
|
||||||
@@ -426,11 +429,13 @@ static void broker_sync_topics(struct broker *broker)
|
|||||||
wanted[0] = signalling.hosting ? signalling.host_topic : "";
|
wanted[0] = signalling.hosting ? signalling.host_topic : "";
|
||||||
wanted[1] = signalling.joining ? signalling.join_topic : "";
|
wanted[1] = signalling.joining ? signalling.join_topic : "";
|
||||||
p2p_code_topics(&wanted[2], &wanted[3]);
|
p2p_code_topics(&wanted[2], &wanted[3]);
|
||||||
|
wanted[4] = p2p_list_topic();
|
||||||
had[0] = broker->host_topic;
|
had[0] = broker->host_topic;
|
||||||
had[1] = broker->join_topic;
|
had[1] = broker->join_topic;
|
||||||
had[2] = broker->code_host_topic;
|
had[2] = broker->code_host_topic;
|
||||||
had[3] = broker->code_look_topic;
|
had[3] = broker->code_look_topic;
|
||||||
for (index = 0; index < 4; index++)
|
had[4] = broker->list_topic;
|
||||||
|
for (index = 0; index < 5; index++)
|
||||||
{
|
{
|
||||||
if (!strcmp(wanted[index], had[index]))
|
if (!strcmp(wanted[index], had[index]))
|
||||||
continue;
|
continue;
|
||||||
@@ -949,7 +954,7 @@ static void publish_received(struct broker *broker, const char *topic, const uns
|
|||||||
|
|
||||||
if (size > MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
|
if (size > MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
|
||||||
return;
|
return;
|
||||||
if (p2p_code_received(topic, payload, size))
|
if (p2p_code_received(topic, payload, size) || p2p_list_received(topic, payload, size))
|
||||||
return;
|
return;
|
||||||
if (signalling.hosting && !strcmp(topic, signalling.host_topic))
|
if (signalling.hosting && !strcmp(topic, signalling.host_topic))
|
||||||
{
|
{
|
||||||
@@ -1203,6 +1208,7 @@ void p2p_signal_update(const int *read, int read_count, const int *write, int wr
|
|||||||
if (signalling.joining && elapsed(signalling.join_sent_time, JOIN_INTERVAL))
|
if (signalling.joining && elapsed(signalling.join_sent_time, JOIN_INTERVAL))
|
||||||
send_join();
|
send_join();
|
||||||
p2p_code_update();
|
p2p_code_update();
|
||||||
|
p2p_list_update(signalling.hosting);
|
||||||
}
|
}
|
||||||
|
|
||||||
int p2p_signal_connected(void)
|
int p2p_signal_connected(void)
|
||||||
@@ -1236,6 +1242,28 @@ void p2p_signal_publish(const char *topic, const unsigned char *payload, int siz
|
|||||||
publish_everywhere(topic, payload, size);
|
publish_everywhere(topic, payload, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size)
|
||||||
|
{
|
||||||
|
unsigned char body[2 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE];
|
||||||
|
int index;
|
||||||
|
|
||||||
|
if (size > MAXIMUM_MESSAGE_SIZE)
|
||||||
|
return;
|
||||||
|
for (index = 0; index < signalling.broker_count; index++)
|
||||||
|
{
|
||||||
|
struct broker *broker = &signalling.brokers[index];
|
||||||
|
int body_size;
|
||||||
|
|
||||||
|
if (broker->state != _broker_ready)
|
||||||
|
continue;
|
||||||
|
body_size = put_string(body, topic);
|
||||||
|
if (size)
|
||||||
|
memcpy(body + body_size, payload, (size_t)size);
|
||||||
|
/* (PUBLISH, at most once, retained: an empty one clears it) */
|
||||||
|
broker_send(broker, 0x31, body, body_size + size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void p2p_signal_host(const unsigned char *token)
|
void p2p_signal_host(const unsigned char *token)
|
||||||
{
|
{
|
||||||
if (!signalling.has_nonce_key)
|
if (!signalling.has_nonce_key)
|
||||||
|
|||||||
@@ -275,6 +275,9 @@ static const struct config_setting config_settings[] =
|
|||||||
"clipboard) that lets whoever has it join over the internet; opening a\n"
|
"clipboard) that lets whoever has it join over the internet; opening a\n"
|
||||||
"link (or copying one before switching to the game) joins. Only people\n"
|
"link (or copying one before switching to the game) joins. Only people\n"
|
||||||
"with the invite can join. Off keeps system link to the local network." },
|
"with the invite can join. Off keeps system link to the local network." },
|
||||||
|
{ "network.public_game", _config_boolean, "false", "HALO_NET_PUBLIC_GAME", _environment_value, _platform_all,
|
||||||
|
"Host games as public: listed for anyone among the online games (the\n"
|
||||||
|
"system link screen's X), as Y in a hosted game's lobby makes them." },
|
||||||
{ "network.join_from_clipboard", _config_boolean, "true", "HALO_NET_JOIN_FROM_CLIPBOARD", _environment_value,
|
{ "network.join_from_clipboard", _config_boolean, "true", "HALO_NET_JOIN_FROM_CLIPBOARD", _environment_value,
|
||||||
_platform_all,
|
_platform_all,
|
||||||
"Join the game of an invite link found on the clipboard when the game\n"
|
"Join the game of an invite link found on the clipboard when the game\n"
|
||||||
@@ -301,6 +304,17 @@ static const struct config_setting config_settings[] =
|
|||||||
"The Discord application internet play invites go through while the\n"
|
"The Discord application internet play invites go through while the\n"
|
||||||
"Discord desktop client runs; empty for none." },
|
"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." },
|
||||||
{ "update.auto", _config_boolean, "true", "HALO_UPDATE_AUTO", _environment_value, _platform_all,
|
{ "update.auto", _config_boolean, "true", "HALO_UPDATE_AUTO", _environment_value, _platform_all,
|
||||||
"Look for a new version when the game starts, and offer to update to it;\n"
|
"Look for a new version when the game starts, and offer to update to it;\n"
|
||||||
"false never looks (the game's \"Do not ask again\" writes false here)." },
|
"false never looks (the game's \"Do not ask again\" writes false here)." },
|
||||||
|
|||||||
@@ -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
|
||||||
+304
-27
@@ -5593,15 +5593,18 @@ static boolean ui_mouse_menus_active(
|
|||||||
#ifdef HALO_ANDROID
|
#ifdef HALO_ANDROID
|
||||||
/* ---------- join codes (the VR port's)
|
/* ---------- join codes (the VR port's)
|
||||||
|
|
||||||
Internet play's join codes (port/linux/src/p2p_code.c): the host's lobby
|
Internet play's join codes (port/linux/src/p2p_code.c) and public games
|
||||||
shows its game's code, and on the screen of games to join, X opens the
|
(p2p_list.c): the host's lobby shows its game's code, and Y there lists the
|
||||||
keyboard to type one in, which then joins as an invite link does (the game
|
game for anyone or not; on the screen of games to join, X opens a panel of
|
||||||
shows up in the list once its host is reached). */
|
the public games and a way to type a code in, either of which then joins as
|
||||||
|
an invite link does (the game shows up in the list once its host is
|
||||||
|
reached). */
|
||||||
|
|
||||||
int p2p_join_code(char *text, int size);
|
int p2p_join_code(char *text, int size);
|
||||||
int p2p_join_with_code(char const *typed);
|
int p2p_join_with_code(char const *typed);
|
||||||
int p2p_join_code_status(void);
|
int p2p_join_code_status(void);
|
||||||
void p2p_join_code_clear_status(void);
|
void p2p_join_code_clear_status(void);
|
||||||
|
int p2p_join_state(void);
|
||||||
|
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
@@ -5615,12 +5618,78 @@ enum
|
|||||||
_join_code_offline,
|
_join_code_offline,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
/* p2p.h's */
|
||||||
|
_join_state_none,
|
||||||
|
_join_state_reaching,
|
||||||
|
_join_state_connected,
|
||||||
|
_join_state_no_answer,
|
||||||
|
_join_state_blocked,
|
||||||
|
_join_state_lost,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ui_public_game
|
||||||
|
{
|
||||||
|
/* p2p.h's struct p2p_public_game */
|
||||||
|
char name[16];
|
||||||
|
char map[32];
|
||||||
|
char mode[24];
|
||||||
|
char code[12];
|
||||||
|
int players;
|
||||||
|
int maximum_players;
|
||||||
|
};
|
||||||
|
|
||||||
|
void p2p_set_public_game(int public_game);
|
||||||
|
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);
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
|
||||||
|
/* the online games panel's rows: "enter a code", the voice chat's
|
||||||
|
switches, and the games */
|
||||||
|
ONLINE_PANEL_ROWS = 11,
|
||||||
|
ONLINE_PANEL_FIXED_ROWS = 3,
|
||||||
|
};
|
||||||
|
|
||||||
static struct
|
static struct
|
||||||
{
|
{
|
||||||
boolean keyboard_launched;
|
boolean keyboard_launched;
|
||||||
wchar_t typed[12];
|
wchar_t typed[12];
|
||||||
|
/* the panel of online games, over the screen of games to join */
|
||||||
|
boolean panel_open;
|
||||||
|
short selection;
|
||||||
|
short scroll;
|
||||||
|
short game_count;
|
||||||
|
struct ui_public_game games[MAXIMUM_SHOWN_PUBLIC_GAMES];
|
||||||
} ui_join_code_globals;
|
} ui_join_code_globals;
|
||||||
|
|
||||||
|
/* the multiplayer maps' names, as the menus show them */
|
||||||
|
static char const *ui_join_code_map_name(
|
||||||
|
char const *map)
|
||||||
|
{
|
||||||
|
static char const *const names[][2] = {
|
||||||
|
{ "beavercreek", "Battle Creek" }, { "bloodgulch", "Blood Gulch" }, { "boardingaction", "Boarding Action" },
|
||||||
|
{ "carousel", "Derelict" }, { "chillout", "Chill Out" }, { "damnation", "Damnation" },
|
||||||
|
{ "dangercanyon", "Danger Canyon" }, { "deathisland", "Death Island" }, { "gephyrophobia", "Gephyrophobia" },
|
||||||
|
{ "hangemhigh", "Hang 'Em High" }, { "icefields", "Ice Fields" }, { "infinity", "Infinity" },
|
||||||
|
{ "longest", "Longest" }, { "prisoner", "Prisoner" }, { "putput", "Chiron TL-34" },
|
||||||
|
{ "ratrace", "Rat Race" }, { "sidewinder", "Sidewinder" }, { "timberland", "Timberland" },
|
||||||
|
{ "wizard", "Wizard" },
|
||||||
|
};
|
||||||
|
short index;
|
||||||
|
|
||||||
|
for (index = 0; index < (short)NUMBEROF(names); index++)
|
||||||
|
{
|
||||||
|
if (!strcmp(map, names[index][0]))
|
||||||
|
return names[index][1];
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
static boolean ui_join_code_screen_active(
|
static boolean ui_join_code_screen_active(
|
||||||
char const *name)
|
char const *name)
|
||||||
{
|
{
|
||||||
@@ -5628,28 +5697,122 @@ static boolean ui_join_code_screen_active(
|
|||||||
strcmp(widget_globals.active_widgets[0]->name, name) == 0;
|
strcmp(widget_globals.active_widgets[0]->name, name) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* X on the screen of games to join: the keyboard, for a code (TRUE: the
|
static void ui_join_code_launch_keyboard(
|
||||||
event is taken) */
|
void)
|
||||||
static boolean ui_join_code_take_event(
|
|
||||||
struct widget_instance *widget,
|
|
||||||
struct event_record const *event)
|
|
||||||
{
|
{
|
||||||
if (event->type != _event_type_button || event->data.button.value != 1 ||
|
|
||||||
event->data.button.index != _gamepad_analog_button_x || widget != widget_globals.active_widgets[0] ||
|
|
||||||
!ui_join_code_screen_active("server_list_screen"))
|
|
||||||
{
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
ui_join_code_globals.typed[0] = 0;
|
ui_join_code_globals.typed[0] = 0;
|
||||||
virtual_keyboard_set_caption(L"Enter the join code of an online game");
|
virtual_keyboard_set_caption(L"Enter the join code of an online game");
|
||||||
if (!virtual_keyboard_launch(ui_join_code_globals.typed, sizeof(ui_join_code_globals.typed), 8))
|
if (!virtual_keyboard_launch(ui_join_code_globals.typed, sizeof(ui_join_code_globals.typed), 8))
|
||||||
{
|
{
|
||||||
virtual_keyboard_set_caption(NULL);
|
virtual_keyboard_set_caption(NULL);
|
||||||
return FALSE;
|
return;
|
||||||
}
|
}
|
||||||
p2p_join_code_clear_status();
|
p2p_join_code_clear_status();
|
||||||
ui_join_code_globals.keyboard_launched = TRUE;
|
ui_join_code_globals.keyboard_launched = TRUE;
|
||||||
return TRUE;
|
}
|
||||||
|
|
||||||
|
/* the panel's input: the stick or the pad's arrows choose a row, A takes
|
||||||
|
it, B closes it */
|
||||||
|
static void ui_join_code_panel_event(
|
||||||
|
struct event_record const *event)
|
||||||
|
{
|
||||||
|
short rows = (short)(ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS);
|
||||||
|
short move = 0;
|
||||||
|
|
||||||
|
if (event->type == _event_type_left_stick)
|
||||||
|
{
|
||||||
|
if (event->data.stick.y == SHORT_MAX)
|
||||||
|
move = -1;
|
||||||
|
else if (event->data.stick.y == SHORT_MIN)
|
||||||
|
move = 1;
|
||||||
|
}
|
||||||
|
else if (event->type == _event_type_button && event->data.button.value == 1)
|
||||||
|
{
|
||||||
|
switch (event->data.button.index)
|
||||||
|
{
|
||||||
|
case _gamepad_binary_button_dpad_up: move = -1; break;
|
||||||
|
case _gamepad_binary_button_dpad_down: move = 1; break;
|
||||||
|
case _gamepad_analog_button_b:
|
||||||
|
case _gamepad_binary_button_back:
|
||||||
|
ui_join_code_globals.panel_open = FALSE;
|
||||||
|
break;
|
||||||
|
case _gamepad_analog_button_x:
|
||||||
|
ui_join_code_globals.panel_open = FALSE;
|
||||||
|
ui_join_code_launch_keyboard();
|
||||||
|
break;
|
||||||
|
case _gamepad_analog_button_a:
|
||||||
|
case _gamepad_binary_button_start:
|
||||||
|
/* (the voice chat's switches, kept in config.toml) */
|
||||||
|
if (ui_join_code_globals.selection == 1)
|
||||||
|
{
|
||||||
|
config_write_boolean("voice.enabled", !config_boolean("voice.enabled"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ui_join_code_globals.selection == 2)
|
||||||
|
{
|
||||||
|
config_write_boolean("voice.microphone", !config_boolean("voice.microphone"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ui_join_code_globals.panel_open = FALSE;
|
||||||
|
if (ui_join_code_globals.selection == 0 ||
|
||||||
|
ui_join_code_globals.selection >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
|
||||||
|
{
|
||||||
|
ui_join_code_launch_keyboard();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
p2p_join_code_clear_status();
|
||||||
|
p2p_join_with_code(
|
||||||
|
ui_join_code_globals.games[ui_join_code_globals.selection - ONLINE_PANEL_FIXED_ROWS].code);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (move)
|
||||||
|
{
|
||||||
|
ui_join_code_globals.selection = (short)PIN(ui_join_code_globals.selection + move, 0, rows - 1);
|
||||||
|
if (ui_join_code_globals.selection < ui_join_code_globals.scroll)
|
||||||
|
ui_join_code_globals.scroll = ui_join_code_globals.selection;
|
||||||
|
else if (ui_join_code_globals.selection >= ui_join_code_globals.scroll + ONLINE_PANEL_ROWS)
|
||||||
|
ui_join_code_globals.scroll = (short)(ui_join_code_globals.selection - ONLINE_PANEL_ROWS + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* X on the screen of games to join: the panel of online games (and in it,
|
||||||
|
everything); Y in a hosted game's lobby: whether the game is public. TRUE:
|
||||||
|
the event is taken. */
|
||||||
|
static boolean ui_join_code_take_event(
|
||||||
|
struct widget_instance *widget,
|
||||||
|
struct event_record const *event)
|
||||||
|
{
|
||||||
|
if (widget != widget_globals.active_widgets[0])
|
||||||
|
return FALSE;
|
||||||
|
if (ui_join_code_globals.panel_open)
|
||||||
|
{
|
||||||
|
if (!ui_join_code_screen_active("server_list_screen"))
|
||||||
|
{
|
||||||
|
ui_join_code_globals.panel_open = FALSE;
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
ui_join_code_panel_event(event);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
if (event->type != _event_type_button || event->data.button.value != 1)
|
||||||
|
return FALSE;
|
||||||
|
if (event->data.button.index == _gamepad_analog_button_x && ui_join_code_screen_active("server_list_screen"))
|
||||||
|
{
|
||||||
|
ui_join_code_globals.panel_open = TRUE;
|
||||||
|
ui_join_code_globals.selection = 0;
|
||||||
|
ui_join_code_globals.scroll = 0;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
if (event->data.button.index == _gamepad_analog_button_y &&
|
||||||
|
ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get())
|
||||||
|
{
|
||||||
|
p2p_set_public_game(!p2p_public_game());
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* the keyboard closed: the code typed, looked up */
|
/* the keyboard closed: the code typed, looked up */
|
||||||
@@ -5675,17 +5838,20 @@ static void ui_join_code_update(
|
|||||||
p2p_join_with_code(text);
|
p2p_join_with_code(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ui_join_code_draw_line(
|
/* text in bounds, left, right or centred (0, 1, 2) */
|
||||||
|
static void ui_join_code_draw_text(
|
||||||
|
short x0,
|
||||||
|
short x1,
|
||||||
short y0,
|
short y0,
|
||||||
char const *text,
|
char const *text,
|
||||||
boolean icons,
|
boolean icons,
|
||||||
boolean centered)
|
short justification,
|
||||||
|
real_argb_color const *color)
|
||||||
{
|
{
|
||||||
rectangle2d bounds;
|
rectangle2d bounds;
|
||||||
wchar_t wide[96];
|
wchar_t wide[96];
|
||||||
short index;
|
short index;
|
||||||
long font_index = tag_loaded(FONT_GROUP_TAG, "ui\\small_ui");
|
long font_index = tag_loaded(FONT_GROUP_TAG, "ui\\small_ui");
|
||||||
real_argb_color color = { 1.0f, 0.85f, 0.85f, 0.85f };
|
|
||||||
|
|
||||||
if (font_index == NONE)
|
if (font_index == NONE)
|
||||||
return;
|
return;
|
||||||
@@ -5694,15 +5860,108 @@ static void ui_join_code_draw_line(
|
|||||||
wide[index] = 0;
|
wide[index] = 0;
|
||||||
bounds.y0 = y0;
|
bounds.y0 = y0;
|
||||||
bounds.y1 = (short)(y0 + 20);
|
bounds.y1 = (short)(y0 + 20);
|
||||||
bounds.x0 = centered ? 0 : 48;
|
bounds.x0 = x0;
|
||||||
bounds.x1 = centered ? 640 : 600;
|
bounds.x1 = x1;
|
||||||
draw_string_set_draw_mode(font_index, NONE, centered ? 2 : 0, 0, &color);
|
draw_string_set_draw_mode(font_index, NONE, justification, 0, color);
|
||||||
if (icons)
|
if (icons)
|
||||||
draw_string_and_hack_in_icons(&bounds, &bounds, NULL, 0, wide, FALSE);
|
draw_string_and_hack_in_icons(&bounds, &bounds, NULL, 0, wide, FALSE);
|
||||||
else
|
else
|
||||||
rasterizer_draw_unicode_string(&bounds, &bounds, NULL, 0, wide);
|
rasterizer_draw_unicode_string(&bounds, &bounds, NULL, 0, wide);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ui_join_code_draw_line(
|
||||||
|
short y0,
|
||||||
|
char const *text,
|
||||||
|
boolean icons,
|
||||||
|
boolean centered)
|
||||||
|
{
|
||||||
|
real_argb_color color = { 1.0f, 0.85f, 0.85f, 0.85f };
|
||||||
|
|
||||||
|
ui_join_code_draw_text((short)(centered ? 0 : 48), (short)(centered ? 640 : 600), y0, text, icons,
|
||||||
|
(short)(centered ? 2 : 0), &color);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ui_join_code_draw_quad(
|
||||||
|
short x0,
|
||||||
|
short y0,
|
||||||
|
short x1,
|
||||||
|
short y1,
|
||||||
|
unsigned long color)
|
||||||
|
{
|
||||||
|
rectangle2d bounds;
|
||||||
|
|
||||||
|
bounds.x0 = x0;
|
||||||
|
bounds.y0 = y0;
|
||||||
|
bounds.x1 = x1;
|
||||||
|
bounds.y1 = y1;
|
||||||
|
draw_quad(&bounds, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the panel of online games: "enter a code", then the public games */
|
||||||
|
static void ui_join_code_render_panel(
|
||||||
|
void)
|
||||||
|
{
|
||||||
|
real_argb_color title_color = { 1.0f, 0.55f, 0.75f, 1.0f };
|
||||||
|
real_argb_color text_color = { 1.0f, 0.85f, 0.85f, 0.85f };
|
||||||
|
real_argb_color dim_color = { 1.0f, 0.6f, 0.6f, 0.6f };
|
||||||
|
char line[64];
|
||||||
|
short row;
|
||||||
|
|
||||||
|
ui_join_code_globals.game_count =
|
||||||
|
(short)p2p_public_games(ui_join_code_globals.games, MAXIMUM_SHOWN_PUBLIC_GAMES);
|
||||||
|
ui_join_code_globals.selection =
|
||||||
|
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS - 1);
|
||||||
|
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);
|
||||||
|
snprintf(line, sizeof(line), "ONLINE GAMES (%d public)", (int)ui_join_code_globals.game_count);
|
||||||
|
ui_join_code_draw_text(56, 584, 80, line, FALSE, 0, &title_color);
|
||||||
|
ui_join_code_draw_text(56, 216, 104, "Name", FALSE, 0, &dim_color);
|
||||||
|
ui_join_code_draw_text(216, 376, 104, "Map", FALSE, 0, &dim_color);
|
||||||
|
ui_join_code_draw_text(376, 516, 104, "Game", FALSE, 0, &dim_color);
|
||||||
|
ui_join_code_draw_text(516, 584, 104, "Players", FALSE, 1, &dim_color);
|
||||||
|
for (row = 0; row < ONLINE_PANEL_ROWS; row++)
|
||||||
|
{
|
||||||
|
short index = (short)(ui_join_code_globals.scroll + row);
|
||||||
|
short y = (short)(128 + row * 22);
|
||||||
|
|
||||||
|
if (index >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
|
||||||
|
break;
|
||||||
|
if (index == ui_join_code_globals.selection)
|
||||||
|
ui_join_code_draw_quad(48, y, 592, (short)(y + 21), 0x803A6EA5);
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
ui_join_code_draw_text(56, 584, (short)(y + 1), "Enter a join code...", FALSE, 0, &text_color);
|
||||||
|
}
|
||||||
|
else if (index == 1)
|
||||||
|
{
|
||||||
|
ui_join_code_draw_text(56, 584, (short)(y + 1),
|
||||||
|
config_boolean("voice.enabled") ? "Voice chat: On" : "Voice chat: Off", FALSE, 0, &text_color);
|
||||||
|
}
|
||||||
|
else if (index == 2)
|
||||||
|
{
|
||||||
|
ui_join_code_draw_text(56, 584, (short)(y + 1),
|
||||||
|
config_boolean("voice.microphone") ? "Microphone: On (sent while you speak)" :
|
||||||
|
"Microphone: Off (listening only)", FALSE, 0, &text_color);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
struct ui_public_game const *game = &ui_join_code_globals.games[index - ONLINE_PANEL_FIXED_ROWS];
|
||||||
|
|
||||||
|
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,
|
||||||
|
&text_color);
|
||||||
|
ui_join_code_draw_text(376, 512, (short)(y + 1), game->mode, FALSE, 0, &text_color);
|
||||||
|
snprintf(line, sizeof(line), "%d/%d", game->players, game->maximum_players);
|
||||||
|
ui_join_code_draw_text(516, 584, (short)(y + 1), line, FALSE, 1, &text_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ui_join_code_globals.game_count)
|
||||||
|
ui_join_code_draw_text(56, 584, 196, "No public games right now", FALSE, 0, &dim_color);
|
||||||
|
ui_join_code_draw_text(56, 584, 384, "%a-button Join or switch %x-button Enter a code %b-button Back", TRUE,
|
||||||
|
0, &text_color);
|
||||||
|
}
|
||||||
|
|
||||||
/* over the screens, as they are drawn */
|
/* over the screens, as they are drawn */
|
||||||
static void ui_join_code_render(
|
static void ui_join_code_render(
|
||||||
void)
|
void)
|
||||||
@@ -5713,7 +5972,12 @@ static void ui_join_code_render(
|
|||||||
{
|
{
|
||||||
char const *status = NULL;
|
char const *status = NULL;
|
||||||
|
|
||||||
ui_join_code_draw_line(372, "%x-button Join an online game with its code", TRUE, FALSE);
|
if (ui_join_code_globals.panel_open)
|
||||||
|
{
|
||||||
|
ui_join_code_render_panel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui_join_code_draw_line(372, "%x-button Online games: public ones, or join by a code", TRUE, FALSE);
|
||||||
switch (p2p_join_code_status())
|
switch (p2p_join_code_status())
|
||||||
{
|
{
|
||||||
case _join_code_looking: status = "Looking for the game of that code..."; break;
|
case _join_code_looking: status = "Looking for the game of that code..."; break;
|
||||||
@@ -5723,17 +5987,30 @@ static void ui_join_code_render(
|
|||||||
case _join_code_own: status = "That is this game's own code"; break;
|
case _join_code_own: status = "That is this game's own code"; break;
|
||||||
case _join_code_offline: status = "Online play is off (network.online in config.toml)"; break;
|
case _join_code_offline: status = "Online play is off (network.online in config.toml)"; break;
|
||||||
}
|
}
|
||||||
|
/* (the code found, or a link: how reaching its host goes) */
|
||||||
|
if (!status || p2p_join_code_status() == _join_code_found)
|
||||||
|
{
|
||||||
|
switch (p2p_join_state())
|
||||||
|
{
|
||||||
|
case _join_state_reaching: status = "Reaching the online game's host..."; break;
|
||||||
|
case _join_state_connected: status = "Connected: the online game is in the list above"; break;
|
||||||
|
case _join_state_no_answer: status = "The host did not answer: it may have stopped hosting"; break;
|
||||||
|
case _join_state_blocked: status = "Blocked by the routers: allow UPnP on yours or the host's"; break;
|
||||||
|
case _join_state_lost: status = "Lost the connection to the online game's host"; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (status)
|
if (status)
|
||||||
ui_join_code_draw_line(392, status, FALSE, FALSE);
|
ui_join_code_draw_line(392, status, FALSE, FALSE);
|
||||||
}
|
}
|
||||||
else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get() &&
|
else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get() &&
|
||||||
p2p_join_code(code, sizeof(code)))
|
p2p_join_code(code, sizeof(code)))
|
||||||
{
|
{
|
||||||
char line[48];
|
char line[96];
|
||||||
|
|
||||||
snprintf(line, sizeof(line), "Online join code: %s", code);
|
snprintf(line, sizeof(line), "Online join code: %s %%y-button Public game: %s", code,
|
||||||
|
p2p_public_game() ? "Yes" : "No");
|
||||||
/* (under the buttons' key) */
|
/* (under the buttons' key) */
|
||||||
ui_join_code_draw_line(448, line, FALSE, TRUE);
|
ui_join_code_draw_line(448, line, TRUE, TRUE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -475,6 +475,10 @@ symbols in this file:
|
|||||||
|
|
||||||
/* port: internet play's Discord presence (port/linux/src/p2p.c) */
|
/* port: internet play's Discord presence (port/linux/src/p2p.c) */
|
||||||
void p2p_set_game_player_counts(int count, int maximum);
|
void p2p_set_game_player_counts(int count, int maximum);
|
||||||
|
#ifdef HALO_ANDROID
|
||||||
|
void p2p_set_game_description(char const *name, char const *map, char const *mode, int players,
|
||||||
|
int maximum_players);
|
||||||
|
#endif
|
||||||
|
|
||||||
/* ---------- constants */
|
/* ---------- constants */
|
||||||
|
|
||||||
@@ -1308,6 +1312,42 @@ boolean network_game_server_idle(
|
|||||||
|
|
||||||
/* (what Discord shows of a game hosted for internet play) */
|
/* (what Discord shows of a game hosted for internet play) */
|
||||||
p2p_set_game_player_counts(server->game.player_count, server->game.maximum_players);
|
p2p_set_game_player_counts(server->game.player_count, server->game.maximum_players);
|
||||||
|
#ifdef HALO_ANDROID
|
||||||
|
/* (and what a public game's listing says: port/linux/src/p2p_list.c) */
|
||||||
|
{
|
||||||
|
char name[16], map[32], mode[24];
|
||||||
|
char const *map_name = server->game.map.name;
|
||||||
|
char const *separator;
|
||||||
|
short index;
|
||||||
|
|
||||||
|
for (index = 0; index < (short)(NUMBEROF(name) - 1) && server->game.name[index]; index++)
|
||||||
|
name[index] = server->game.name[index] < 128 ? (char)server->game.name[index] : '?';
|
||||||
|
name[index] = 0;
|
||||||
|
/* (the scenario's name, the last of its path) */
|
||||||
|
while ((separator = strpbrk(map_name, "\\/")) != NULL)
|
||||||
|
map_name = separator + 1;
|
||||||
|
csstrncpy(map, map_name, sizeof(map) - 1);
|
||||||
|
map[sizeof(map) - 1] = 0;
|
||||||
|
for (index = 0; index < (short)(NUMBEROF(mode) - 1) && index < 12 &&
|
||||||
|
server->game.variant.human_readable_game_description[index]; index++)
|
||||||
|
{
|
||||||
|
wchar_t character = server->game.variant.human_readable_game_description[index];
|
||||||
|
|
||||||
|
mode[index] = character < 128 ? (char)character : '?';
|
||||||
|
}
|
||||||
|
mode[index] = 0;
|
||||||
|
/* (a game type without a name of its own: its kind's) */
|
||||||
|
if (!mode[0])
|
||||||
|
{
|
||||||
|
static char const *const kinds[] = { "", "CTF", "Slayer", "Oddball", "King of the Hill", "Race" };
|
||||||
|
long kind = server->game.variant.game_engine_index;
|
||||||
|
|
||||||
|
csstrncpy(mode, kind > 0 && kind < (long)NUMBEROF(kinds) ? kinds[kind] : "Custom", sizeof(mode) - 1);
|
||||||
|
mode[sizeof(mode) - 1] = 0;
|
||||||
|
}
|
||||||
|
p2p_set_game_description(name, map, mode, server->game.player_count, server->game.maximum_players);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (network_game_server_game_is_valid(server))
|
if (network_game_server_game_is_valid(server))
|
||||||
{
|
{
|
||||||
|
|||||||
+28
-3
@@ -44,6 +44,10 @@ MUSL_URL = f"https://musl.libc.org/releases/musl-{MUSL_VERSION}.tar.gz"
|
|||||||
SDL_TAG = "release-3.4.16"
|
SDL_TAG = "release-3.4.16"
|
||||||
SDL_DIR = THIRD_PARTY / "SDL3"
|
SDL_DIR = THIRD_PARTY / "SDL3"
|
||||||
SDL_URL = "https://github.com/libsdl-org/SDL.git"
|
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
|
# 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
|
# headers and library are linked from here; the app packages the same AAR
|
||||||
# through Gradle (port/android/app/build.gradle), which also merges the
|
# through Gradle (port/android/app/build.gradle), which also merges the
|
||||||
@@ -178,6 +182,10 @@ def fetch_third_party() -> None:
|
|||||||
print(f"Cloning SDL3 {SDL_TAG}")
|
print(f"Cloning SDL3 {SDL_TAG}")
|
||||||
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(SDL_DIR)],
|
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(SDL_DIR)],
|
||||||
check=True)
|
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)
|
||||||
if not OPENXR_DIR.is_dir():
|
if not OPENXR_DIR.is_dir():
|
||||||
print(f"Downloading {OPENXR_URL}")
|
print(f"Downloading {OPENXR_URL}")
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -509,6 +517,23 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
|||||||
)
|
)
|
||||||
n.build(outputs=libsdl, rule="android_sdl3", implicit=[SDL_DIR / "CMakeLists.txt"])
|
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
|
# ---------- the host library
|
||||||
|
|
||||||
host_objects: List[Path] = []
|
host_objects: List[Path] = []
|
||||||
@@ -523,7 +548,7 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
|||||||
host_cflags = " ".join([
|
host_cflags = " ".join([
|
||||||
"-O2", "-g", "-fPIC", "-Wall", "-Wno-unused-function", "-D_GNU_SOURCE",
|
"-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{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")) + [
|
host_sources = sorted((PORT_DIR / "host").glob("*.c")) + [
|
||||||
LINUX_DIR / "src" / "posix_files.c", LINUX_DIR / "src" / "posix_net.c",
|
LINUX_DIR / "src" / "posix_files.c", LINUX_DIR / "src" / "posix_net.c",
|
||||||
@@ -549,12 +574,12 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
|||||||
host_objects.append(table_obj)
|
host_objects.append(table_obj)
|
||||||
n.rule(
|
n.rule(
|
||||||
name="android_host_link",
|
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)
|
+ " ".join(f"-l{lib}" for lib in HOST_LIBRARIES)
|
||||||
+ " -Wl,-z,max-page-size=16384 -Wl,--no-undefined"),
|
+ " -Wl,-z,max-page-size=16384 -Wl,--no-undefined"),
|
||||||
description="ANDROID HOST LINK $out",
|
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])
|
||||||
|
|
||||||
# ---------- staging for Gradle
|
# ---------- staging for Gradle
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user