mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 16:00:26 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30f362ff3d | ||
|
|
f6c2322df9 | ||
|
|
39fbaefaac | ||
|
|
c6397cb515 | ||
|
|
5ba6c13097 |
No files matched your search
@@ -19,9 +19,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<!-- the voice chat in multiplayer (host_voice.c) -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<!-- installing a new version of the game (Updater) -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.halo.decomp;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -38,7 +35,6 @@ import java.nio.channels.FileChannel;
|
||||
*/
|
||||
public class LauncherActivity extends Activity {
|
||||
private static final int PICK_IMAGE = 1;
|
||||
private static final int ASK_MICROPHONE = 2;
|
||||
|
||||
private File dataRoot;
|
||||
private TextView status;
|
||||
@@ -57,37 +53,12 @@ public class LauncherActivity extends Activity {
|
||||
passOnHardwareId();
|
||||
passOnInvite(getIntent());
|
||||
if (haveData()) {
|
||||
if (!askForMicrophone())
|
||||
startGame();
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
buildInterface();
|
||||
}
|
||||
|
||||
/**
|
||||
* The voice chat's microphone (port/android/host/host_voice.c), asked
|
||||
* for here, once, on this flat panel, before the game starts: in the
|
||||
* game, the question would stop it. Returns true if it asked (the game
|
||||
* starts on the answer).
|
||||
*/
|
||||
private boolean askForMicrophone() {
|
||||
SharedPreferences preferences = getSharedPreferences("launcher", MODE_PRIVATE);
|
||||
|
||||
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|
||||
|| preferences.getBoolean("asked_microphone", false))
|
||||
return false;
|
||||
preferences.edit().putBoolean("asked_microphone", true).apply();
|
||||
requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, ASK_MICROPHONE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, results);
|
||||
if (requestCode == ASK_MICROPHONE)
|
||||
startGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* An internet play invite link the app was opened with: the game
|
||||
* (port/linux/src/p2p.c) picks it up from join_link.txt, whether it is
|
||||
@@ -140,23 +111,6 @@ public class LauncherActivity extends Activity {
|
||||
partial.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the microphone may be used, for the game (port/linux/src/
|
||||
* voice.c), which opens it only then: opening it otherwise would have
|
||||
* Android ask in the middle of the game.
|
||||
*/
|
||||
private void passOnMicrophonePermission() {
|
||||
if (dataRoot == null)
|
||||
return;
|
||||
boolean granted =
|
||||
checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
||||
try (OutputStream out = new FileOutputStream(new File(dataRoot, "microphone_permission.txt"))) {
|
||||
out.write((granted ? "1" : "0").getBytes("UTF-8"));
|
||||
} catch (java.io.IOException e) {
|
||||
// the game then does without the microphone
|
||||
}
|
||||
}
|
||||
|
||||
/** The game data, or the marker file of the VR self-test (host_xr.c). */
|
||||
private boolean haveData() {
|
||||
return dataRoot != null && (new File(dataRoot, "maps/ui.map").isFile() ||
|
||||
@@ -164,7 +118,6 @@ public class LauncherActivity extends Activity {
|
||||
}
|
||||
|
||||
private void startGame() {
|
||||
passOnMicrophonePermission();
|
||||
startActivity(new Intent(this, HaloActivity.class));
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -131,21 +131,6 @@ int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
|
||||
/* darkens the eye images from their edges, strength 0..1 (0: off) */
|
||||
void host_xr_set_vignette(float strength);
|
||||
|
||||
/* ---------- the voice chat (host_voice.c) */
|
||||
|
||||
/* opens the voices' playback, and the microphone if asked (else closes
|
||||
it): their volume, and the level (0..1 of full scale) the microphone's
|
||||
sound must reach to be sent */
|
||||
int host_voice_start(int microphone, float volume, float threshold);
|
||||
void host_voice_stop(void);
|
||||
/* the microphone's speech since asked: Opus packets, each after a byte of
|
||||
its length; returns the bytes */
|
||||
int host_voice_take_packets(void *buffer, int size);
|
||||
/* the microphone's level (0..1000) */
|
||||
int host_voice_level(void);
|
||||
/* a speaker's packet (sequence numbered), played at these gains */
|
||||
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right);
|
||||
|
||||
/* ---------- Android */
|
||||
|
||||
/* the storage directories the port uses, copied into buffer */
|
||||
|
||||
@@ -1,494 +0,0 @@
|
||||
/*
|
||||
HOST_VOICE.C
|
||||
|
||||
The voice chat's sound (the VR port's; port/linux/game/network_voice.c
|
||||
sends and receives it): the microphone, Opus, and the voices played. It
|
||||
runs here, natively, where SDL's audio threads may call it.
|
||||
|
||||
- The microphone: SDL's default recording device, mono at 16 kHz, with
|
||||
Android's voice communication preset (its echo cancelling and noise
|
||||
suppression: SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET). Every 20 ms of it
|
||||
that is loud enough to be speech (or follows speech closely, VAD_HANGOVER)
|
||||
is encoded (Opus, VOIP, 16 kbit/s) for the guest to send.
|
||||
- The voices: each speaker's packets, decoded as they come (a gap in their
|
||||
numbers concealed by Opus's packet loss concealment), wait in the
|
||||
speaker's buffer until JITTER_START of them is there, and are mixed, each
|
||||
at the gains its latest packet was given (left and right: where the
|
||||
speaker is), into a stream of their own.
|
||||
|
||||
The devices are opened and closed on a thread of their own (voice_worker):
|
||||
opening a microphone can take a long while (an emulator's took a minute),
|
||||
which the game, whose clock the other machines follow, must not wait for.
|
||||
*/
|
||||
|
||||
#include "host.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <opus.h>
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
|
||||
enum
|
||||
{
|
||||
VOICE_RATE = 16000,
|
||||
FRAME_SAMPLES = VOICE_RATE / 50,
|
||||
MAXIMUM_PACKET = 200,
|
||||
MAXIMUM_SPEAKERS = 32,
|
||||
/* a speaker's buffer: two seconds */
|
||||
SPEAKER_BUFFER = VOICE_RATE * 2,
|
||||
/* played from once this much is there, after silence */
|
||||
JITTER_START = FRAME_SAMPLES * 3,
|
||||
/* more than this behind: the oldest dropped */
|
||||
JITTER_MAXIMUM = FRAME_SAMPLES * 15,
|
||||
/* frames sent on after the voice drops below the threshold */
|
||||
VAD_HANGOVER = 15,
|
||||
BITRATE = 16000,
|
||||
};
|
||||
|
||||
struct speaker
|
||||
{
|
||||
int used;
|
||||
int id;
|
||||
OpusDecoder *decoder;
|
||||
int has_sequence;
|
||||
unsigned short sequence;
|
||||
short buffer[SPEAKER_BUFFER];
|
||||
int read;
|
||||
int count;
|
||||
int playing;
|
||||
float gain_left;
|
||||
float gain_right;
|
||||
unsigned long long heard_ticks;
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
SDL_Mutex *lock;
|
||||
/* the devices wanted, which the worker opens and closes to match
|
||||
(under worker_lock); the recording stream is read under it too */
|
||||
pthread_mutex_t worker_lock;
|
||||
pthread_cond_t worker_wake;
|
||||
int worker_started;
|
||||
int want_playback;
|
||||
int want_recording;
|
||||
/* the microphone */
|
||||
SDL_AudioStream *recording;
|
||||
OpusEncoder *encoder;
|
||||
short frame[FRAME_SAMPLES];
|
||||
int frame_count;
|
||||
int hangover;
|
||||
float threshold;
|
||||
int level;
|
||||
/* the voices */
|
||||
SDL_AudioStream *playback;
|
||||
struct speaker speakers[MAXIMUM_SPEAKERS];
|
||||
float volume;
|
||||
} voice;
|
||||
|
||||
static void voice_lock(void)
|
||||
{
|
||||
if (!voice.lock)
|
||||
voice.lock = SDL_CreateMutex();
|
||||
SDL_LockMutex(voice.lock);
|
||||
}
|
||||
|
||||
static void voice_unlock(void)
|
||||
{
|
||||
SDL_UnlockMutex(voice.lock);
|
||||
}
|
||||
|
||||
/* the voices mixed, as SDL asks for them (on its audio thread) */
|
||||
static void SDLCALL voice_playback_callback(void *userdata, SDL_AudioStream *stream, int additional, int total)
|
||||
{
|
||||
short mixed[2 * 512];
|
||||
(void)userdata;
|
||||
(void)total;
|
||||
|
||||
while (additional > 0)
|
||||
{
|
||||
int frames = additional / 4;
|
||||
int frame;
|
||||
int index;
|
||||
|
||||
if (frames > 512)
|
||||
frames = 512;
|
||||
if (frames <= 0)
|
||||
frames = 1;
|
||||
memset(mixed, 0, sizeof(mixed));
|
||||
voice_lock();
|
||||
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
|
||||
{
|
||||
struct speaker *speaker = &voice.speakers[index];
|
||||
|
||||
if (!speaker->used)
|
||||
continue;
|
||||
if (!speaker->playing && speaker->count >= JITTER_START)
|
||||
speaker->playing = 1;
|
||||
if (!speaker->playing)
|
||||
continue;
|
||||
for (frame = 0; frame < frames && speaker->count > 0; frame++)
|
||||
{
|
||||
float sample = (float)speaker->buffer[speaker->read] * voice.volume;
|
||||
int left = mixed[2 * frame] + (int)(sample * speaker->gain_left);
|
||||
int right = mixed[2 * frame + 1] + (int)(sample * speaker->gain_right);
|
||||
|
||||
mixed[2 * frame] = (short)(left > 32767 ? 32767 : left < -32768 ? -32768 : left);
|
||||
mixed[2 * frame + 1] = (short)(right > 32767 ? 32767 : right < -32768 ? -32768 : right);
|
||||
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
|
||||
speaker->count--;
|
||||
}
|
||||
/* (run dry: wait for enough again) */
|
||||
if (!speaker->count)
|
||||
speaker->playing = 0;
|
||||
}
|
||||
voice_unlock();
|
||||
SDL_PutAudioStreamData(stream, mixed, frames * 4);
|
||||
additional -= frames * 4;
|
||||
}
|
||||
}
|
||||
|
||||
static int voice_open_playback(void)
|
||||
{
|
||||
SDL_AudioSpec spec;
|
||||
|
||||
if (voice.playback)
|
||||
return 1;
|
||||
spec.format = SDL_AUDIO_S16;
|
||||
spec.channels = 2;
|
||||
spec.freq = VOICE_RATE;
|
||||
voice.playback = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, voice_playback_callback,
|
||||
NULL);
|
||||
if (!voice.playback)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "voice: cannot open the playback stream: %s", SDL_GetError());
|
||||
return 0;
|
||||
}
|
||||
SDL_ResumeAudioStreamDevice(voice.playback);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static SDL_AudioStream *voice_open_recording(void)
|
||||
{
|
||||
SDL_AudioSpec spec;
|
||||
SDL_AudioStream *recording;
|
||||
int error;
|
||||
|
||||
if (!voice.encoder)
|
||||
{
|
||||
voice.encoder = opus_encoder_create(VOICE_RATE, 1, OPUS_APPLICATION_VOIP, &error);
|
||||
if (!voice.encoder)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "voice: cannot make the encoder (%d)", error);
|
||||
return NULL;
|
||||
}
|
||||
opus_encoder_ctl(voice.encoder, OPUS_SET_BITRATE(BITRATE));
|
||||
opus_encoder_ctl(voice.encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
|
||||
opus_encoder_ctl(voice.encoder, OPUS_SET_INBAND_FEC(1));
|
||||
opus_encoder_ctl(voice.encoder, OPUS_SET_PACKET_LOSS_PERC(10));
|
||||
}
|
||||
/* Android's voice communication preset: echo cancelling, noise
|
||||
suppression (AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION) */
|
||||
SDL_SetHint(SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET, "7");
|
||||
spec.format = SDL_AUDIO_S16;
|
||||
spec.channels = 1;
|
||||
spec.freq = VOICE_RATE;
|
||||
recording = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, NULL);
|
||||
if (!recording)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "voice: cannot open the microphone: %s", SDL_GetError());
|
||||
return NULL;
|
||||
}
|
||||
SDL_ResumeAudioStreamDevice(recording);
|
||||
host_logf(HOST_LOG_INFO, "voice: the microphone is open");
|
||||
return recording;
|
||||
}
|
||||
|
||||
static void voice_close_recording(void)
|
||||
{
|
||||
SDL_AudioStream *recording;
|
||||
|
||||
pthread_mutex_lock(&voice.worker_lock);
|
||||
recording = voice.recording;
|
||||
voice.recording = NULL;
|
||||
voice.level = 0;
|
||||
pthread_mutex_unlock(&voice.worker_lock);
|
||||
if (!recording)
|
||||
return;
|
||||
SDL_DestroyAudioStream(recording);
|
||||
host_logf(HOST_LOG_INFO, "voice: the microphone is closed");
|
||||
}
|
||||
|
||||
static void voice_close_playback(void)
|
||||
{
|
||||
SDL_AudioStream *playback = voice.playback;
|
||||
|
||||
voice.playback = NULL;
|
||||
if (playback)
|
||||
SDL_DestroyAudioStream(playback);
|
||||
}
|
||||
|
||||
/* opens and closes the devices as wanted, for as long as the app runs */
|
||||
static void *voice_worker(void *unused)
|
||||
{
|
||||
(void)unused;
|
||||
pthread_mutex_lock(&voice.worker_lock);
|
||||
for (;;)
|
||||
{
|
||||
int want_playback = voice.want_playback;
|
||||
int want_recording = voice.want_recording;
|
||||
int have_playback = voice.playback != NULL;
|
||||
int have_recording = voice.recording != NULL;
|
||||
|
||||
if (want_playback == have_playback && want_recording == have_recording)
|
||||
{
|
||||
pthread_cond_wait(&voice.worker_wake, &voice.worker_lock);
|
||||
continue;
|
||||
}
|
||||
pthread_mutex_unlock(&voice.worker_lock);
|
||||
if (want_recording && !have_recording)
|
||||
{
|
||||
/* (opened outside the lock, which the game reads it under) */
|
||||
SDL_AudioStream *recording = voice_open_recording();
|
||||
|
||||
pthread_mutex_lock(&voice.worker_lock);
|
||||
voice.recording = recording;
|
||||
pthread_mutex_unlock(&voice.worker_lock);
|
||||
/* (a microphone that will not open is not tried again until
|
||||
wanted anew) */
|
||||
if (!recording)
|
||||
{
|
||||
pthread_mutex_lock(&voice.worker_lock);
|
||||
voice.want_recording = 0;
|
||||
pthread_mutex_unlock(&voice.worker_lock);
|
||||
}
|
||||
}
|
||||
else if (!want_recording && have_recording)
|
||||
{
|
||||
voice_close_recording();
|
||||
}
|
||||
if (want_playback && !have_playback)
|
||||
{
|
||||
if (!voice_open_playback())
|
||||
{
|
||||
pthread_mutex_lock(&voice.worker_lock);
|
||||
voice.want_playback = 0;
|
||||
pthread_mutex_unlock(&voice.worker_lock);
|
||||
}
|
||||
}
|
||||
else if (!want_playback && have_playback)
|
||||
{
|
||||
voice_close_playback();
|
||||
}
|
||||
pthread_mutex_lock(&voice.worker_lock);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void voice_want(int playback, int recording)
|
||||
{
|
||||
if (!voice.worker_started)
|
||||
{
|
||||
voice.worker_started = 1;
|
||||
pthread_mutex_init(&voice.worker_lock, NULL);
|
||||
pthread_cond_init(&voice.worker_wake, NULL);
|
||||
if (host_native_thread_create(voice_worker, NULL, 256 * 1024) != 0)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "voice: cannot start the device thread");
|
||||
return;
|
||||
}
|
||||
}
|
||||
pthread_mutex_lock(&voice.worker_lock);
|
||||
voice.want_playback = playback;
|
||||
voice.want_recording = recording;
|
||||
pthread_cond_signal(&voice.worker_wake);
|
||||
pthread_mutex_unlock(&voice.worker_lock);
|
||||
}
|
||||
|
||||
/* ---------- the guest's side */
|
||||
|
||||
int host_voice_start(int microphone, float volume, float threshold)
|
||||
{
|
||||
voice_lock();
|
||||
voice.volume = volume < 0.0f ? 0.0f : volume > 4.0f ? 4.0f : volume;
|
||||
voice.threshold = threshold;
|
||||
voice_unlock();
|
||||
voice_want(1, microphone);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void host_voice_stop(void)
|
||||
{
|
||||
int index;
|
||||
|
||||
voice_want(0, 0);
|
||||
voice_lock();
|
||||
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
|
||||
{
|
||||
if (voice.speakers[index].decoder)
|
||||
opus_decoder_destroy(voice.speakers[index].decoder);
|
||||
}
|
||||
memset(voice.speakers, 0, sizeof(voice.speakers));
|
||||
voice_unlock();
|
||||
}
|
||||
|
||||
int host_voice_take_packets(void *buffer, int size)
|
||||
{
|
||||
unsigned char *out = buffer;
|
||||
int used = 0;
|
||||
|
||||
if (!voice.worker_started)
|
||||
return 0;
|
||||
pthread_mutex_lock(&voice.worker_lock);
|
||||
if (!voice.recording)
|
||||
{
|
||||
voice.frame_count = 0;
|
||||
voice.hangover = 0;
|
||||
pthread_mutex_unlock(&voice.worker_lock);
|
||||
return 0;
|
||||
}
|
||||
for (;;)
|
||||
{
|
||||
int wanted = (FRAME_SAMPLES - voice.frame_count) * (int)sizeof(short);
|
||||
int got = SDL_GetAudioStreamData(voice.recording, voice.frame + voice.frame_count, wanted);
|
||||
double energy = 0.0;
|
||||
int index;
|
||||
|
||||
if (got <= 0)
|
||||
break;
|
||||
voice.frame_count += got / (int)sizeof(short);
|
||||
if (voice.frame_count < FRAME_SAMPLES)
|
||||
break;
|
||||
voice.frame_count = 0;
|
||||
for (index = 0; index < FRAME_SAMPLES; index++)
|
||||
energy += (double)voice.frame[index] * voice.frame[index];
|
||||
energy = SDL_sqrt(energy / FRAME_SAMPLES) / 32768.0;
|
||||
voice.level = (int)(energy * 1000.0);
|
||||
if (energy >= voice.threshold)
|
||||
voice.hangover = VAD_HANGOVER;
|
||||
else if (voice.hangover > 0)
|
||||
voice.hangover--;
|
||||
else
|
||||
continue;
|
||||
if (used + 1 + MAXIMUM_PACKET > size)
|
||||
continue;
|
||||
{
|
||||
int length = opus_encode(voice.encoder, voice.frame, FRAME_SAMPLES, out + used + 1, MAXIMUM_PACKET);
|
||||
|
||||
if (length > 0 && length < 256)
|
||||
{
|
||||
out[used] = (unsigned char)length;
|
||||
used += 1 + length;
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&voice.worker_lock);
|
||||
return used;
|
||||
}
|
||||
|
||||
int host_voice_level(void)
|
||||
{
|
||||
return voice.level;
|
||||
}
|
||||
|
||||
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right)
|
||||
{
|
||||
struct speaker *speaker = NULL;
|
||||
struct speaker *oldest = NULL;
|
||||
short decoded[FRAME_SAMPLES * 6];
|
||||
int frames[4];
|
||||
int frame_count = 0;
|
||||
int index;
|
||||
|
||||
if (length <= 0 || length > MAXIMUM_PACKET)
|
||||
return;
|
||||
voice_lock();
|
||||
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
|
||||
{
|
||||
if (voice.speakers[index].used && voice.speakers[index].id == id)
|
||||
speaker = &voice.speakers[index];
|
||||
}
|
||||
if (!speaker)
|
||||
{
|
||||
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
|
||||
{
|
||||
if (!voice.speakers[index].used)
|
||||
speaker = &voice.speakers[index];
|
||||
else if (!oldest || voice.speakers[index].heard_ticks < oldest->heard_ticks)
|
||||
oldest = &voice.speakers[index];
|
||||
}
|
||||
if (!speaker)
|
||||
speaker = oldest;
|
||||
if (!speaker->decoder)
|
||||
{
|
||||
int error;
|
||||
|
||||
speaker->decoder = opus_decoder_create(VOICE_RATE, 1, &error);
|
||||
if (!speaker->decoder)
|
||||
{
|
||||
voice_unlock();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
opus_decoder_ctl(speaker->decoder, OPUS_RESET_STATE);
|
||||
}
|
||||
speaker->used = 1;
|
||||
speaker->id = id;
|
||||
speaker->has_sequence = 0;
|
||||
speaker->read = speaker->count = 0;
|
||||
speaker->playing = 0;
|
||||
}
|
||||
speaker->heard_ticks = SDL_GetTicks();
|
||||
speaker->gain_left = gain_left;
|
||||
speaker->gain_right = gain_right;
|
||||
{
|
||||
unsigned short number = (unsigned short)sequence;
|
||||
|
||||
if (speaker->has_sequence)
|
||||
{
|
||||
short gap = (short)(number - speaker->sequence);
|
||||
|
||||
/* (late, or again: dropped) */
|
||||
if (gap <= 0)
|
||||
{
|
||||
voice_unlock();
|
||||
return;
|
||||
}
|
||||
/* (lost ones: concealed, a few) */
|
||||
if (gap > 1 && gap <= 4 && speaker->count > 0)
|
||||
{
|
||||
int missing;
|
||||
|
||||
for (missing = 1; missing < gap && frame_count < 3; missing++)
|
||||
{
|
||||
frames[frame_count] = opus_decode(speaker->decoder, NULL, 0, decoded + frame_count * FRAME_SAMPLES,
|
||||
FRAME_SAMPLES, 0);
|
||||
frame_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
speaker->has_sequence = 1;
|
||||
speaker->sequence = number;
|
||||
}
|
||||
frames[frame_count] = opus_decode(speaker->decoder, data, length, decoded + frame_count * FRAME_SAMPLES,
|
||||
FRAME_SAMPLES * 2, 0);
|
||||
frame_count++;
|
||||
for (index = 0; index < frame_count; index++)
|
||||
{
|
||||
int sample;
|
||||
|
||||
for (sample = 0; sample < frames[index] && sample < FRAME_SAMPLES * 2; sample++)
|
||||
{
|
||||
if (speaker->count >= JITTER_MAXIMUM)
|
||||
{
|
||||
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
|
||||
speaker->count--;
|
||||
}
|
||||
speaker->buffer[(speaker->read + speaker->count) % SPEAKER_BUFFER] =
|
||||
decoded[index * FRAME_SAMPLES + sample];
|
||||
speaker->count++;
|
||||
}
|
||||
}
|
||||
voice_unlock();
|
||||
}
|
||||
@@ -58,11 +58,6 @@ host_xr_copy_layer_region
|
||||
host_xr_end_frame
|
||||
host_xr_haptic
|
||||
host_xr_set_vignette
|
||||
host_voice_start
|
||||
host_voice_stop
|
||||
host_voice_take_packets
|
||||
host_voice_level
|
||||
host_voice_play
|
||||
host_xr_copy_layer_array
|
||||
host_android_path
|
||||
|
||||
|
||||
@@ -0,0 +1,949 @@
|
||||
/*
|
||||
BOTS.C
|
||||
|
||||
Bots in multiplayer (the VR port's): players of the host that it drives
|
||||
itself. They are the players of machines of the host's own that nothing
|
||||
connects to (network_server_manager.c: bots.count of them, added in the
|
||||
lobby), so every machine sees them as it sees any other machine's players,
|
||||
and their input goes where a client's does: every tick, the host makes each
|
||||
bot's player_action and takes it as a client's input
|
||||
(update_server_handle_distributed_input).
|
||||
|
||||
A bot looks for the nearest enemy it can see, turns to it no faster than a
|
||||
player would (and not quite right), closes on it weaving from side to side
|
||||
and fires while it is on target. With none in sight it hunts the nearest
|
||||
enemy, or else roams to a point picked at random near someone. It finds its
|
||||
way with the game's own pathfinding (ai/path.c: the maps' pathfinding
|
||||
surfaces), as the campaign's actors do: a path a few times a second, whose
|
||||
next point it walks to, looking where it goes; where no path is found it
|
||||
walks straight at the goal, jumping and turning off when it is stuck.
|
||||
|
||||
Out of a fight, a bot goes for what lies near that is worth having: a
|
||||
weapon better than the worse of its two (and swaps that one for it, as a
|
||||
player holding the action button does), ammunition for one it has when it
|
||||
runs low, an overshield, camouflage, a health pack when hurt, grenades.
|
||||
In a fight it changes to its other weapon when the one in hand is empty,
|
||||
and now and then lobs a grenade at an enemy some way off.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_engine.h"
|
||||
#include "game/players.h"
|
||||
#include "game/player_queues_new.h"
|
||||
#include "objects/objects.h"
|
||||
#include "physics/collisions.h"
|
||||
#include "ai/path.h"
|
||||
#include "units/bipeds.h"
|
||||
#include "items/items.h"
|
||||
#include "items/weapons.h"
|
||||
#include "tag_files/tag_files.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "scenario/scenario.h"
|
||||
#include "structures/structure_bsp_definitions.h"
|
||||
#include "units/units.h"
|
||||
#include "network_distributed.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
void platform_log(char const *format, ...);
|
||||
long config_integer(char const *name);
|
||||
boolean network_game_server_is_bot_machine(long machine_index);
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
MAXIMUM_BOTS = 16,
|
||||
/* how often a bot finds its path again (ticks) */
|
||||
PATH_REFRESH_TICKS = 15,
|
||||
/* how often it looks around for things to pick up (ticks) */
|
||||
ITEM_LOOK_TICKS = 30,
|
||||
/* the least time between its grenades (ticks) */
|
||||
GRENADE_INTERVAL_TICKS = 150,
|
||||
};
|
||||
|
||||
/* how far off it goes for things (world units) */
|
||||
#define BOT_ITEM_RANGE 25.0f
|
||||
/* a grenade's range: at least, at most (world units) */
|
||||
#define BOT_GRENADE_MINIMUM 6.0f
|
||||
#define BOT_GRENADE_MAXIMUM 20.0f
|
||||
|
||||
/* how near a path's point counts as reached, and the goal (world units) */
|
||||
#define BOT_STEP_RADIUS 0.4f
|
||||
#define BOT_GOAL_RADIUS 1.0f
|
||||
|
||||
/* how far a bot sees and shoots (world units), and how near it closes */
|
||||
#define BOT_FIRE_RANGE 30.0f
|
||||
#define BOT_CLOSE_RANGE 3.0f
|
||||
/* how far off target it still fires (radians) */
|
||||
#define BOT_FIRE_CONE 0.08f
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||
struct bot
|
||||
{
|
||||
long player_index;
|
||||
long target_unit_index;
|
||||
short target_seen_ticks;
|
||||
real yaw;
|
||||
real pitch;
|
||||
real aim_error_yaw;
|
||||
real aim_error_pitch;
|
||||
short error_ticks;
|
||||
real_point3d last_position;
|
||||
short stuck_ticks;
|
||||
real wander_yaw;
|
||||
short weave_phase;
|
||||
/* where it is going, and the way there */
|
||||
boolean has_goal;
|
||||
real_point3d goal;
|
||||
long goal_time;
|
||||
long path_time;
|
||||
struct path_result path;
|
||||
/* the thing it goes for, and when it last looked */
|
||||
long item_index;
|
||||
long item_look_time;
|
||||
long grenade_time;
|
||||
long weapon_switch_time;
|
||||
};
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
/* (one search at a time, shared: 80 kB) */
|
||||
static struct path_state bot_path_state;
|
||||
|
||||
static struct
|
||||
{
|
||||
boolean map_logged;
|
||||
struct bot bots[MAXIMUM_BOTS];
|
||||
long seed;
|
||||
long logged_time;
|
||||
long paths_found;
|
||||
long paths_failed;
|
||||
long grenades;
|
||||
long pickups;
|
||||
long switches;
|
||||
/* how well they play (bots.skill, as the game began) */
|
||||
short reaction_ticks;
|
||||
real turn_rate;
|
||||
real sight_range;
|
||||
real aim_error;
|
||||
} bots;
|
||||
|
||||
/* easy, normal, hard: the ticks a bot takes to notice an enemy, how fast it
|
||||
turns (radians a tick), how far it sees and how far off its aim wanders */
|
||||
static struct
|
||||
{
|
||||
short reaction_ticks;
|
||||
real turn_rate;
|
||||
real sight_range;
|
||||
real aim_error;
|
||||
} const bot_skills[] =
|
||||
{
|
||||
{ 20, 2.2f / 30.0f, 25.0f, 3.0f },
|
||||
{ 13, 2.8f / 30.0f, 32.0f, 1.8f },
|
||||
{ 9, 3.5f / 30.0f, 40.0f, 1.0f },
|
||||
};
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static real bot_random(
|
||||
void)
|
||||
{
|
||||
bots.seed = bots.seed * 1103515245 + 12345;
|
||||
return (real)((bots.seed >> 16) & 0x7FFF) / 32767.0f;
|
||||
}
|
||||
|
||||
static real bot_angle_difference(
|
||||
real to,
|
||||
real from)
|
||||
{
|
||||
real difference = to - from;
|
||||
|
||||
while (difference > _pi)
|
||||
difference -= 2.0f * _pi;
|
||||
while (difference < -_pi)
|
||||
difference += 2.0f * _pi;
|
||||
return difference;
|
||||
}
|
||||
|
||||
static boolean bot_can_see(
|
||||
long unit_index,
|
||||
real_point3d const *from,
|
||||
real_point3d const *to)
|
||||
{
|
||||
struct collision_result collision;
|
||||
real_vector3d vector;
|
||||
|
||||
vector.i = to->x - from->x;
|
||||
vector.j = to->y - from->y;
|
||||
vector.k = to->z - from->z;
|
||||
return !collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
|
||||
from, &vector, unit_index, &collision);
|
||||
}
|
||||
|
||||
/* the nearest enemy unit the bot can see, or NONE */
|
||||
static long bot_find_target(
|
||||
struct player_datum const *player,
|
||||
real_point3d const *eye)
|
||||
{
|
||||
boolean teams = game_engine_has_teams();
|
||||
long best = NONE;
|
||||
real best_distance = bots.sight_range;
|
||||
short index;
|
||||
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
{
|
||||
struct player_datum *other = distributed_player(index);
|
||||
long unit_index;
|
||||
real_point3d head;
|
||||
real distance;
|
||||
|
||||
if (!other || other == player || (teams && other->team_index == player->team_index))
|
||||
continue;
|
||||
unit_index = distributed_living_unit(other);
|
||||
if (unit_index == NONE)
|
||||
continue;
|
||||
unit_get_camera_position(unit_index, &head);
|
||||
distance = distance3d(eye, &head);
|
||||
if (distance < best_distance && bot_can_see(player->unit_index, eye, &head))
|
||||
{
|
||||
best = unit_index;
|
||||
best_distance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* the way from where the bot's unit stands to the goal (FALSE: none) */
|
||||
static boolean bot_find_path(
|
||||
struct bot *bot,
|
||||
long unit_index)
|
||||
{
|
||||
struct path_input input;
|
||||
real_point3d start;
|
||||
long start_surface = biped_find_pathfinding_surface_index(unit_index, &start);
|
||||
real_point3d goal = bot->goal;
|
||||
long goal_surface = NONE;
|
||||
|
||||
bot->path.valid = FALSE;
|
||||
if (start_surface == NONE)
|
||||
return FALSE;
|
||||
/* (the goal's surface: under it, as a biped would stand there) */
|
||||
{
|
||||
struct collision_result collision;
|
||||
real_vector3d down = { 0.0f, 0.0f, -3.0f };
|
||||
real_point3d above = goal;
|
||||
|
||||
above.z += 0.5f;
|
||||
if (collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
|
||||
&above, &down, NONE, &collision))
|
||||
{
|
||||
goal = collision.point;
|
||||
goal_surface = collision.surface_index;
|
||||
}
|
||||
}
|
||||
if (goal_surface == NONE)
|
||||
return FALSE;
|
||||
path_input_new(&input, 0.25f, FALSE, unit_index);
|
||||
path_input_set_start(&input, &start, start_surface);
|
||||
path_state_new(&input, &bot_path_state, NULL);
|
||||
path_state_destination(&bot_path_state, &goal, goal_surface, BOT_GOAL_RADIUS);
|
||||
if (!path_state_find(&bot_path_state) || !path_state_build_path(&bot_path_state, &bot->path))
|
||||
{
|
||||
bot->path.valid = FALSE;
|
||||
bots.paths_failed++;
|
||||
return FALSE;
|
||||
}
|
||||
bots.paths_found += bot->path.valid != FALSE;
|
||||
return bot->path.valid;
|
||||
}
|
||||
|
||||
/* the point it walks to now: the path's next, or the goal itself */
|
||||
static boolean bot_next_point(
|
||||
struct bot *bot,
|
||||
long unit_index,
|
||||
real_point3d const *position,
|
||||
real_point3d *point)
|
||||
{
|
||||
if (!bot->has_goal)
|
||||
return FALSE;
|
||||
if (game_time_get() - bot->path_time >= PATH_REFRESH_TICKS || game_time_get() < bot->path_time)
|
||||
{
|
||||
bot->path_time = game_time_get();
|
||||
bot_find_path(bot, unit_index);
|
||||
}
|
||||
if (bot->path.valid)
|
||||
{
|
||||
while (bot->path.step_index < bot->path.step_count)
|
||||
{
|
||||
struct path_step const *step = &bot->path.steps[(short)bot->path.step_index];
|
||||
real dx = step->point.x - position->x;
|
||||
real dy = step->point.y - position->y;
|
||||
|
||||
if (dx * dx + dy * dy > BOT_STEP_RADIUS * BOT_STEP_RADIUS)
|
||||
{
|
||||
*point = step->point;
|
||||
return TRUE;
|
||||
}
|
||||
bot->path.step_index++;
|
||||
}
|
||||
/* (the steps done: on to the end, or a path found anew soon) */
|
||||
if (!bot->path.steps_finish_path && game_time_get() - bot->path_time > 5)
|
||||
bot->path_time = game_time_get() - PATH_REFRESH_TICKS;
|
||||
*point = bot->path.endpoint.point;
|
||||
return TRUE;
|
||||
}
|
||||
*point = bot->goal;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* a goal when none is in sight: the nearest enemy, or somewhere near
|
||||
someone at random */
|
||||
static void bot_choose_goal(
|
||||
struct bot *bot,
|
||||
struct player_datum const *player,
|
||||
real_point3d const *position)
|
||||
{
|
||||
boolean teams = game_engine_has_teams();
|
||||
real best_distance = 1000000.0f;
|
||||
short index;
|
||||
boolean found = FALSE;
|
||||
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
{
|
||||
struct player_datum *other = distributed_player(index);
|
||||
long unit_index;
|
||||
real_point3d origin;
|
||||
real distance;
|
||||
|
||||
if (!other || other == player || (teams && other->team_index == player->team_index))
|
||||
continue;
|
||||
unit_index = distributed_living_unit(other);
|
||||
if (unit_index == NONE)
|
||||
continue;
|
||||
object_get_origin(unit_index, &origin);
|
||||
distance = distance3d(position, &origin);
|
||||
if (distance < best_distance)
|
||||
{
|
||||
best_distance = distance;
|
||||
bot->goal = origin;
|
||||
found = TRUE;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
/* (no enemy alive: a few metres off, somewhere) */
|
||||
real angle = bot_random() * 2.0f * _pi;
|
||||
|
||||
bot->goal = *position;
|
||||
bot->goal.x += (real)cos(angle) * 5.0f;
|
||||
bot->goal.y += (real)sin(angle) * 5.0f;
|
||||
}
|
||||
bot->has_goal = TRUE;
|
||||
bot->goal_time = game_time_get();
|
||||
bot->path_time = game_time_get() - PATH_REFRESH_TICKS;
|
||||
}
|
||||
|
||||
/* ---------- things to pick up */
|
||||
|
||||
/* how much a weapon is worth having, by its tag's name (0: not known) */
|
||||
static real bot_weapon_worth(
|
||||
long definition_index)
|
||||
{
|
||||
static struct
|
||||
{
|
||||
char const *name;
|
||||
real worth;
|
||||
} const weapons[] =
|
||||
{
|
||||
{ "rocket launcher", 9.0f }, { "sniper rifle", 8.0f }, { "fuel rod", 8.0f }, { "shotgun", 7.0f },
|
||||
{ "flamethrower", 6.0f }, { "pistol", 6.0f }, { "plasma rifle", 5.0f }, { "assault rifle", 5.0f },
|
||||
{ "needler", 4.0f }, { "plasma pistol", 4.0f },
|
||||
};
|
||||
char const *name = tag_get_name(definition_index);
|
||||
short index;
|
||||
|
||||
if (!name)
|
||||
return 0.0f;
|
||||
/* (plasma pistol before pistol: the longer name first) */
|
||||
if (strstr(name, "plasma pistol"))
|
||||
return 4.0f;
|
||||
for (index = 0; index < (short)NUMBEROF(weapons); index++)
|
||||
{
|
||||
if (strstr(name, weapons[index].name))
|
||||
return weapons[index].worth;
|
||||
}
|
||||
return 3.0f;
|
||||
}
|
||||
|
||||
/* how well a weapon suits a fight at that distance (world units) */
|
||||
static real bot_weapon_suits(
|
||||
long definition_index,
|
||||
real distance)
|
||||
{
|
||||
char const *name = tag_get_name(definition_index);
|
||||
real worth = bot_weapon_worth(definition_index);
|
||||
|
||||
if (!name)
|
||||
return worth;
|
||||
if (strstr(name, "shotgun") || strstr(name, "flamethrower"))
|
||||
return distance < 6.0f ? worth + 5.0f : distance > 10.0f ? worth - 5.0f : worth;
|
||||
if (strstr(name, "sniper rifle"))
|
||||
return distance > 12.0f ? worth + 3.0f : worth - 4.0f;
|
||||
if (strstr(name, "rocket launcher") || strstr(name, "fuel rod"))
|
||||
return distance < 4.0f ? worth - 8.0f : worth + 1.0f;
|
||||
if (strstr(name, "plasma pistol") || strstr(name, "needler"))
|
||||
return worth;
|
||||
if (strstr(name, "pistol"))
|
||||
return distance > 10.0f ? worth + 2.0f : worth;
|
||||
return worth;
|
||||
}
|
||||
|
||||
/* whether a weapon has rounds left (a battery: charge) */
|
||||
static boolean bot_weapon_has_ammunition(
|
||||
long weapon_index)
|
||||
{
|
||||
struct weapon_datum *weapon = weapon_try_and_get(weapon_index);
|
||||
|
||||
if (!weapon)
|
||||
return FALSE;
|
||||
if (weapon->weapon.magazines[0].rounds_total > 0 || weapon->weapon.magazines[0].rounds_loaded > 0)
|
||||
return TRUE;
|
||||
/* (no magazine: a battery, spent when its age reaches 1) */
|
||||
return weapon->weapon.magazines[0].rounds_loaded == 0 && weapon->weapon.magazines[0].rounds_total == 0 &&
|
||||
weapon->weapon.age < 1.0f && weapon->weapon.magazines[0].original_time == 0;
|
||||
}
|
||||
|
||||
/* how much the bot would gain by a thing lying there (0: nothing) */
|
||||
static real bot_item_worth(
|
||||
struct unit_datum const *unit,
|
||||
long item_index)
|
||||
{
|
||||
struct object_datum *object = object_get(item_index);
|
||||
char const *name = tag_get_name(object->definition_index);
|
||||
short slot;
|
||||
|
||||
if (!name)
|
||||
return 0.0f;
|
||||
if (object->object.parent_object_index != NONE)
|
||||
return 0.0f;
|
||||
if (TEST_FLAG(_object_mask_equipment, object->object.type))
|
||||
{
|
||||
if (strstr(name, "over shield") || strstr(name, "overshield"))
|
||||
return 9.0f;
|
||||
if (strstr(name, "camouflage"))
|
||||
return 8.0f;
|
||||
if (strstr(name, "health"))
|
||||
return unit->object.body_vitality < 0.7f ? 7.0f : 0.0f;
|
||||
if (strstr(name, "frag grenade"))
|
||||
return unit->unit.grenade_counts[0] < 2 ? 3.0f : 0.0f;
|
||||
if (strstr(name, "plasma grenade"))
|
||||
return NUMBER_OF_UNIT_GRENADE_TYPES > 1 && unit->unit.grenade_counts[1] < 2 ? 3.0f : 0.0f;
|
||||
return 0.0f;
|
||||
}
|
||||
if (TEST_FLAG(_object_mask_weapon, object->object.type))
|
||||
{
|
||||
real worth = bot_weapon_worth(object->definition_index);
|
||||
real worst = 1000.0f;
|
||||
short carried = 0;
|
||||
|
||||
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
|
||||
{
|
||||
long carried_index = unit->unit.weapon_object_indices[slot];
|
||||
struct object_datum *carried_weapon = carried_index != NONE ? object_try_and_get(carried_index) : NULL;
|
||||
|
||||
if (!carried_weapon)
|
||||
continue;
|
||||
carried++;
|
||||
/* (one it has: worth its ammunition when it runs low) */
|
||||
if (carried_weapon->definition_index == object->definition_index)
|
||||
return bot_weapon_has_ammunition(carried_index) ? 0.0f : 4.0f;
|
||||
worst = MIN(worst, bot_weapon_worth(carried_weapon->definition_index));
|
||||
}
|
||||
if (carried < 2)
|
||||
return worth;
|
||||
return worth - worst > 0.5f ? worth - worst : 0.0f;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
/* the thing most worth going for near the bot, or NONE */
|
||||
static long bot_find_item(
|
||||
struct unit_datum const *unit,
|
||||
real_point3d const *position)
|
||||
{
|
||||
struct object_iterator iterator;
|
||||
long best = NONE;
|
||||
real best_score = 0.0f;
|
||||
|
||||
object_iterator_new(&iterator, _object_mask_weapon | _object_mask_equipment, 0);
|
||||
while (object_iterator_next(&iterator))
|
||||
{
|
||||
real_point3d origin;
|
||||
real distance;
|
||||
real worth;
|
||||
real score;
|
||||
|
||||
object_get_origin(iterator.index, &origin);
|
||||
distance = distance3d(position, &origin);
|
||||
if (distance > BOT_ITEM_RANGE)
|
||||
continue;
|
||||
worth = bot_item_worth(unit, iterator.index);
|
||||
if (worth <= 0.0f)
|
||||
continue;
|
||||
score = worth / (1.0f + distance / 8.0f);
|
||||
if (score > best_score)
|
||||
{
|
||||
best_score = score;
|
||||
best = iterator.index;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* near a weapon it wants: the worse of its two in hand (the one a swap
|
||||
leaves), and the action held while the game offers the weapon */
|
||||
static void bot_take_item(
|
||||
struct bot *bot,
|
||||
struct player_datum const *player,
|
||||
struct unit_datum const *unit,
|
||||
struct player_action *action)
|
||||
{
|
||||
struct object_datum *item = object_try_and_get(bot->item_index);
|
||||
short slot;
|
||||
short worst_slot = NONE;
|
||||
real worst = 1000.0f;
|
||||
short carried = 0;
|
||||
|
||||
if (!item || !TEST_FLAG(_object_mask_weapon, item->object.type))
|
||||
return;
|
||||
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
|
||||
{
|
||||
long carried_index = unit->unit.weapon_object_indices[slot];
|
||||
struct object_datum *carried_weapon = carried_index != NONE ? object_try_and_get(carried_index) : NULL;
|
||||
real worth;
|
||||
|
||||
if (!carried_weapon)
|
||||
continue;
|
||||
carried++;
|
||||
worth = bot_weapon_worth(carried_weapon->definition_index);
|
||||
if (worth < worst)
|
||||
{
|
||||
worst = worth;
|
||||
worst_slot = slot;
|
||||
}
|
||||
}
|
||||
if (carried >= 2 && worst_slot != NONE)
|
||||
action->desired_weapon_index = worst_slot;
|
||||
if (player->action_object_index == bot->item_index &&
|
||||
(player->action_result == _player_action_result_swap_for_weapon ||
|
||||
player->action_result == _player_action_result_add_weapon_to_inventory) &&
|
||||
(game_time_get() & 1))
|
||||
{
|
||||
SET_FLAG(action->control_flags, _unit_control_swap_weapons_bit, TRUE);
|
||||
bots.pickups++;
|
||||
}
|
||||
}
|
||||
|
||||
/* the throttle that walks a unit facing yaw towards a point */
|
||||
static void bot_walk_to(
|
||||
struct player_action *action,
|
||||
real yaw,
|
||||
real_point3d const *position,
|
||||
real_point3d const *point)
|
||||
{
|
||||
real dx = point->x - position->x;
|
||||
real dy = point->y - position->y;
|
||||
real length = (real)sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (length < 0.001f)
|
||||
return;
|
||||
dx /= length;
|
||||
dy /= length;
|
||||
/* (forward along the facing; left, its strafe's positive) */
|
||||
action->throttle.i = dx * (real)cos(yaw) + dy * (real)sin(yaw);
|
||||
action->throttle.j = -dx * (real)sin(yaw) + dy * (real)cos(yaw);
|
||||
}
|
||||
|
||||
static void bot_think(
|
||||
struct bot *bot,
|
||||
struct player_datum *player,
|
||||
struct player_action *action)
|
||||
{
|
||||
struct unit_datum *unit = unit_try_and_get(player->unit_index);
|
||||
real_point3d eye;
|
||||
real_point3d position;
|
||||
real wanted_yaw;
|
||||
real wanted_pitch = 0.0f;
|
||||
boolean firing = FALSE;
|
||||
real moved;
|
||||
|
||||
csmemset(action, 0, sizeof(*action));
|
||||
action->desired_weapon_index = NONE;
|
||||
action->desired_grenade_index = NONE;
|
||||
action->desired_zoom_level = NONE;
|
||||
if (!unit || unit->object.parent_object_index != NONE)
|
||||
{
|
||||
bot->target_unit_index = NONE;
|
||||
return;
|
||||
}
|
||||
action->desired_weapon_index = unit->unit.current_weapon_index;
|
||||
action->desired_grenade_index = unit->unit.current_grenade_index;
|
||||
/* (the weapon in hand empty: the other) */
|
||||
if (unit->unit.current_weapon_index >= 0 &&
|
||||
!bot_weapon_has_ammunition(unit->unit.weapon_object_indices[unit->unit.current_weapon_index]))
|
||||
{
|
||||
short slot;
|
||||
|
||||
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
|
||||
{
|
||||
if (slot != unit->unit.current_weapon_index && unit->unit.weapon_object_indices[slot] != NONE &&
|
||||
bot_weapon_has_ammunition(unit->unit.weapon_object_indices[slot]))
|
||||
{
|
||||
action->desired_weapon_index = slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
unit_get_camera_position(player->unit_index, &eye);
|
||||
object_get_origin(player->unit_index, &position);
|
||||
/* (where it faces now, the first time) */
|
||||
if (bot->error_ticks == 0 && bot->yaw == 0.0f && bot->pitch == 0.0f)
|
||||
{
|
||||
bot->yaw = (real)atan2(unit->unit.aiming_vector.j, unit->unit.aiming_vector.i);
|
||||
bot->wander_yaw = bot->yaw;
|
||||
}
|
||||
|
||||
/* the enemy: noticed after a moment, kept while in sight */
|
||||
{
|
||||
long target = bot_find_target(player, &eye);
|
||||
|
||||
if (target != bot->target_unit_index)
|
||||
{
|
||||
bot->target_unit_index = target;
|
||||
bot->target_seen_ticks = 0;
|
||||
}
|
||||
else if (target != NONE && bot->target_seen_ticks < SHORT_MAX)
|
||||
{
|
||||
bot->target_seen_ticks++;
|
||||
}
|
||||
}
|
||||
/* (its aim wanders a little, anew now and then) */
|
||||
if (--bot->error_ticks <= 0)
|
||||
{
|
||||
bot->error_ticks = 10 + (short)(bot_random() * 20.0f);
|
||||
bot->aim_error_yaw = (bot_random() - 0.5f) * 0.06f * bots.aim_error;
|
||||
bot->aim_error_pitch = (bot_random() - 0.5f) * 0.04f * bots.aim_error;
|
||||
}
|
||||
|
||||
if (bot->target_unit_index != NONE && bot->target_seen_ticks >= bots.reaction_ticks)
|
||||
{
|
||||
real_point3d head;
|
||||
real dx, dy, dz, flat, distance;
|
||||
|
||||
unit_get_camera_position(bot->target_unit_index, &head);
|
||||
/* (at the chest) */
|
||||
head.z -= 0.15f;
|
||||
dx = head.x - eye.x;
|
||||
dy = head.y - eye.y;
|
||||
dz = head.z - eye.z;
|
||||
flat = (real)sqrt(dx * dx + dy * dy);
|
||||
distance = (real)sqrt(dx * dx + dy * dy + dz * dz);
|
||||
wanted_yaw = (real)atan2(dy, dx) + bot->aim_error_yaw;
|
||||
wanted_pitch = (real)atan2(dz, flat) + bot->aim_error_pitch;
|
||||
/* the weapon that suits the distance, if it has rounds (changed no
|
||||
more than every two seconds) */
|
||||
if (game_time_get() - bot->weapon_switch_time > 60 || game_time_get() < bot->weapon_switch_time)
|
||||
{
|
||||
short slot;
|
||||
short best_slot = NONE;
|
||||
real best = -1000.0f;
|
||||
|
||||
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
|
||||
{
|
||||
long weapon_index = unit->unit.weapon_object_indices[slot];
|
||||
real suits;
|
||||
|
||||
if (weapon_index == NONE || !bot_weapon_has_ammunition(weapon_index))
|
||||
continue;
|
||||
suits = bot_weapon_suits(object_get(weapon_index)->definition_index, distance) +
|
||||
(slot == unit->unit.current_weapon_index ? 1.0f : 0.0f);
|
||||
if (suits > best)
|
||||
{
|
||||
best = suits;
|
||||
best_slot = slot;
|
||||
}
|
||||
}
|
||||
if (best_slot != NONE && best_slot != unit->unit.current_weapon_index)
|
||||
{
|
||||
action->desired_weapon_index = best_slot;
|
||||
bot->weapon_switch_time = game_time_get();
|
||||
bots.switches++;
|
||||
}
|
||||
}
|
||||
/* closes on it along its way, weaving, until near */
|
||||
bot->weave_phase++;
|
||||
if (distance > BOT_CLOSE_RANGE)
|
||||
{
|
||||
real_point3d point;
|
||||
|
||||
object_get_origin(bot->target_unit_index, &bot->goal);
|
||||
bot->has_goal = TRUE;
|
||||
if (bot_next_point(bot, player->unit_index, &position, &point))
|
||||
bot_walk_to(action, bot->yaw, &position, &point);
|
||||
}
|
||||
action->throttle.j += (real)sin(bot->weave_phase * 0.08f) * 0.6f;
|
||||
firing = distance < BOT_FIRE_RANGE &&
|
||||
fabsf(bot_angle_difference(wanted_yaw, bot->yaw)) < BOT_FIRE_CONE &&
|
||||
fabsf(wanted_pitch - bot->pitch) < BOT_FIRE_CONE;
|
||||
/* (a melee blow when it is upon it) */
|
||||
if (distance < 1.0f)
|
||||
SET_FLAG(action->control_flags, _unit_control_use_equipment_bit, TRUE);
|
||||
/* (a grenade now and then, lobbed at one some way off) */
|
||||
if (distance > BOT_GRENADE_MINIMUM && distance < BOT_GRENADE_MAXIMUM &&
|
||||
game_time_get() - bot->grenade_time > GRENADE_INTERVAL_TICKS && bot_random() < 0.02f)
|
||||
{
|
||||
short type;
|
||||
|
||||
for (type = 0; type < NUMBER_OF_UNIT_GRENADE_TYPES; type++)
|
||||
{
|
||||
if (unit->unit.grenade_counts[type] > 0)
|
||||
break;
|
||||
}
|
||||
if (type < NUMBER_OF_UNIT_GRENADE_TYPES)
|
||||
{
|
||||
bot->grenade_time = game_time_get();
|
||||
bots.grenades++;
|
||||
action->desired_grenade_index = type;
|
||||
wanted_pitch += PIN(distance * 0.02f, 0.05f, 0.4f);
|
||||
SET_FLAG(action->control_flags, _unit_control_throw_grenade_bit, TRUE);
|
||||
SET_FLAG(action->control_flags, _unit_control_weapon_secondary_trigger_bit, TRUE);
|
||||
firing = FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
real_point3d point;
|
||||
|
||||
/* something worth having near: there first */
|
||||
if (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS || game_time_get() < bot->item_look_time)
|
||||
{
|
||||
long item = bot_find_item(unit, &position);
|
||||
|
||||
bot->item_look_time = game_time_get();
|
||||
if (item != bot->item_index)
|
||||
{
|
||||
bot->item_index = item;
|
||||
if (item != NONE)
|
||||
{
|
||||
object_get_origin(item, &bot->goal);
|
||||
bot->has_goal = TRUE;
|
||||
bot->goal_time = game_time_get();
|
||||
bot->path_time = game_time_get() - PATH_REFRESH_TICKS;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bot->item_index != NONE)
|
||||
{
|
||||
/* (taken or gone: on) */
|
||||
if (!object_try_and_get(bot->item_index) || bot_item_worth(unit, bot->item_index) <= 0.0f)
|
||||
{
|
||||
bot->item_index = NONE;
|
||||
bot->has_goal = FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
object_get_origin(bot->item_index, &bot->goal);
|
||||
bot->goal_time = game_time_get();
|
||||
if (distance3d(&position, &bot->goal) < 2.0f)
|
||||
bot_take_item(bot, player, unit, action);
|
||||
}
|
||||
}
|
||||
/* hunts, or roams: along its way, looking where it goes */
|
||||
if (bot->item_index == NONE &&
|
||||
(!bot->has_goal || game_time_get() - bot->goal_time > 90 ||
|
||||
distance3d(&position, &bot->goal) < BOT_GOAL_RADIUS))
|
||||
{
|
||||
bot_choose_goal(bot, player, &position);
|
||||
}
|
||||
wanted_yaw = bot->wander_yaw;
|
||||
if (bot_next_point(bot, player->unit_index, &position, &point))
|
||||
{
|
||||
real dx = point.x - position.x;
|
||||
real dy = point.y - position.y;
|
||||
|
||||
if (dx * dx + dy * dy > 0.0001f)
|
||||
bot->wander_yaw = wanted_yaw = (real)atan2(dy, dx);
|
||||
bot_walk_to(action, bot->yaw, &position, &point);
|
||||
}
|
||||
if (bot->stuck_ticks > 20)
|
||||
{
|
||||
/* (stuck: somewhere else) */
|
||||
bot_choose_goal(bot, player, &position);
|
||||
bot->goal.x += (bot_random() - 0.5f) * 6.0f;
|
||||
bot->goal.y += (bot_random() - 0.5f) * 6.0f;
|
||||
bot->stuck_ticks = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* turned no faster than a player turns */
|
||||
{
|
||||
real turn = bot_angle_difference(wanted_yaw, bot->yaw);
|
||||
|
||||
bot->yaw += PIN(turn, -bots.turn_rate, bots.turn_rate);
|
||||
bot->pitch += PIN(wanted_pitch - bot->pitch, -bots.turn_rate, bots.turn_rate);
|
||||
bot->pitch = PIN(bot->pitch, -1.4f, 1.4f);
|
||||
while (bot->yaw > _pi)
|
||||
bot->yaw -= 2.0f * _pi;
|
||||
while (bot->yaw < -_pi)
|
||||
bot->yaw += 2.0f * _pi;
|
||||
}
|
||||
action->desired_facing.yaw = bot->yaw < 0.0f ? bot->yaw + 2.0f * _pi : bot->yaw;
|
||||
action->desired_facing.pitch = bot->pitch;
|
||||
if (firing)
|
||||
{
|
||||
action->primary_trigger = 1.0f;
|
||||
SET_FLAG(action->control_flags, _unit_control_weapon_primary_trigger_bit, TRUE);
|
||||
}
|
||||
|
||||
/* stuck: hardly moved while walking (it jumps, and then turns) */
|
||||
moved = distance3d(&position, &bot->last_position);
|
||||
bot->last_position = position;
|
||||
action->throttle.i = PIN(action->throttle.i, -1.0f, 1.0f);
|
||||
action->throttle.j = PIN(action->throttle.j, -1.0f, 1.0f);
|
||||
if ((fabsf(action->throttle.i) > 0.5f || fabsf(action->throttle.j) > 0.5f) && moved < 0.02f)
|
||||
{
|
||||
bot->stuck_ticks++;
|
||||
if (bot->stuck_ticks == 8)
|
||||
SET_FLAG(action->control_flags, _unit_control_jump_bit, TRUE);
|
||||
}
|
||||
else
|
||||
{
|
||||
bot->stuck_ticks = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* the bot of a player, made when first needed */
|
||||
static struct bot *bot_get(
|
||||
long player_index)
|
||||
{
|
||||
short index;
|
||||
struct bot *free_bot = NULL;
|
||||
|
||||
for (index = 0; index < MAXIMUM_BOTS; index++)
|
||||
{
|
||||
if (bots.bots[index].player_index == player_index)
|
||||
return &bots.bots[index];
|
||||
if (bots.bots[index].player_index == NONE && !free_bot)
|
||||
free_bot = &bots.bots[index];
|
||||
}
|
||||
if (free_bot)
|
||||
{
|
||||
csmemset(free_bot, 0, sizeof(*free_bot));
|
||||
free_bot->player_index = player_index;
|
||||
free_bot->target_unit_index = NONE;
|
||||
free_bot->item_index = NONE;
|
||||
}
|
||||
return free_bot;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
void bots_new_game(
|
||||
void)
|
||||
{
|
||||
short index;
|
||||
|
||||
bots.map_logged = FALSE;
|
||||
for (index = 0; index < MAXIMUM_BOTS; index++)
|
||||
bots.bots[index].player_index = NONE;
|
||||
bots.seed = 12345;
|
||||
bots.logged_time = 0;
|
||||
{
|
||||
long skill = PIN(config_integer("bots.skill"), 0, (long)NUMBEROF(bot_skills) - 1);
|
||||
|
||||
bots.reaction_ticks = bot_skills[skill].reaction_ticks;
|
||||
bots.turn_rate = bot_skills[skill].turn_rate;
|
||||
bots.sight_range = bot_skills[skill].sight_range;
|
||||
bots.aim_error = bot_skills[skill].aim_error;
|
||||
}
|
||||
}
|
||||
|
||||
/* (the host) after each tick: each bot's input for the next */
|
||||
void bots_tick(
|
||||
void)
|
||||
{
|
||||
short index;
|
||||
short count = 0;
|
||||
|
||||
if (!bots.map_logged)
|
||||
{
|
||||
struct structure_bsp *bsp = global_structure_bsp_get();
|
||||
|
||||
bots.map_logged = TRUE;
|
||||
if (bsp)
|
||||
{
|
||||
platform_log("bots: the map's pathfinding: %ld surfaces, %ld edges",
|
||||
bsp->pathfinding_surfaces.count, bsp->pathfinding_edges.count);
|
||||
}
|
||||
}
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
{
|
||||
struct player_datum *player = distributed_player(index);
|
||||
struct bot *bot;
|
||||
struct player_action action;
|
||||
unsigned short control_flags;
|
||||
|
||||
if (!player || player->local_player_index != NONE ||
|
||||
!network_game_server_is_bot_machine(player->network_player_data.machine_index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bot = bot_get(DATUM_INDEX_NEW(index, player->identifier));
|
||||
if (!bot)
|
||||
continue;
|
||||
bot_think(bot, player, &action);
|
||||
control_flags = (unsigned short)action.control_flags;
|
||||
update_server_handle_distributed_input(DATUM_INDEX_NEW(index, player->identifier), game_time_get(), &action,
|
||||
&control_flags, 1);
|
||||
count++;
|
||||
}
|
||||
if (count && game_time_get() - bots.logged_time >= 300)
|
||||
{
|
||||
bots.logged_time = game_time_get();
|
||||
platform_log("bots: driving %d; paths found %ld, not %ld; weapon swaps asked %ld; grenades %ld; changes %ld",
|
||||
(int)count, bots.paths_found, bots.paths_failed, bots.pickups, bots.grenades, bots.switches);
|
||||
bots.paths_found = bots.paths_failed = bots.pickups = bots.grenades = bots.switches = 0;
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
{
|
||||
struct player_datum *player = distributed_player(index);
|
||||
struct unit_datum *unit;
|
||||
short slot;
|
||||
char text[160] = "";
|
||||
|
||||
if (!player || !network_game_server_is_bot_machine(player->network_player_data.machine_index) ||
|
||||
!(unit = unit_try_and_get(player->unit_index)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (slot = 0; slot < MAXIMUM_WEAPONS_PER_UNIT; slot++)
|
||||
{
|
||||
long weapon_index = unit->unit.weapon_object_indices[slot];
|
||||
char const *name = weapon_index != NONE ? tag_get_name(object_get(weapon_index)->definition_index) : NULL;
|
||||
char const *last = name ? strrchr(name, '\\') : NULL;
|
||||
|
||||
if (name)
|
||||
{
|
||||
strncat(text, last ? last + 1 : name, sizeof(text) - strlen(text) - 3);
|
||||
strncat(text, slot == unit->unit.current_weapon_index ? "* " : " ", sizeof(text) - strlen(text) - 1);
|
||||
}
|
||||
}
|
||||
platform_log("bots: player %d carries %s, grenades %d/%d", index, text, unit->unit.grenade_counts[0],
|
||||
unit->unit.grenade_counts[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3051,7 +3051,7 @@ void network_distributed_new_game(
|
||||
network_objects_new_game();
|
||||
network_damage_new_game();
|
||||
network_vr_new_game();
|
||||
network_voice_new_game();
|
||||
bots_new_game();
|
||||
}
|
||||
|
||||
/* after each tick (game_time.c) */
|
||||
@@ -3094,7 +3094,7 @@ void network_distributed_tick(
|
||||
distributed_statistics_due = FALSE;
|
||||
distributed_host_send_players();
|
||||
network_vr_tick();
|
||||
network_voice_tick();
|
||||
bots_tick();
|
||||
distributed_send_pickups();
|
||||
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
|
||||
distributed_send_game_state(NONE);
|
||||
@@ -3104,7 +3104,6 @@ void network_distributed_tick(
|
||||
distributed_note_own_positions();
|
||||
distributed_client_send_inputs();
|
||||
network_vr_tick();
|
||||
network_voice_tick();
|
||||
distributed_client_send_predictions();
|
||||
network_objects_client_tick();
|
||||
network_damage_client_tick();
|
||||
@@ -3580,7 +3579,6 @@ void network_distributed_handle_message(
|
||||
case _distributed_message_client_ready: entry_size = 0; break;
|
||||
case _distributed_message_client_identity: entry_size = sizeof(struct distributed_client_identity); break;
|
||||
case _distributed_message_vr_poses: entry_size = network_vr_entry_size(); break;
|
||||
case _distributed_message_voice: entry_size = network_voice_entry_size(); break;
|
||||
case _distributed_message_damage_events:
|
||||
case _distributed_message_hit_reports: entry_size = network_damage_entry_size(header.type); break;
|
||||
default: entry_size = network_objects_entry_size(header.type); break;
|
||||
@@ -3594,16 +3592,6 @@ void network_distributed_handle_message(
|
||||
|
||||
/* (each kind from the host, or from a client; a VR player's hands
|
||||
both ways) */
|
||||
if (header.type == _distributed_message_voice)
|
||||
{
|
||||
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
|
||||
game_connection() != _game_connection_network_server)
|
||||
{
|
||||
return;
|
||||
}
|
||||
network_voice_handle(machine_index, message, size);
|
||||
return;
|
||||
}
|
||||
if (header.type == _distributed_message_vr_poses)
|
||||
{
|
||||
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
|
||||
|
||||
@@ -66,10 +66,6 @@ enum
|
||||
the host, the host's of the others to each client (unreliable;
|
||||
network_vr.c). Last, so that a build without it drops it. */
|
||||
_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
|
||||
};
|
||||
@@ -240,10 +236,8 @@ void network_vr_tick(void);
|
||||
word network_vr_entry_size(void);
|
||||
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);
|
||||
/* bots (bots.c) */
|
||||
void bots_new_game(void);
|
||||
void bots_tick(void);
|
||||
|
||||
#endif // __NETWORK_DISTRIBUTED_H
|
||||
@@ -1,326 +0,0 @@
|
||||
/*
|
||||
NETWORK_VOICE.C
|
||||
|
||||
The voice chat (the VR port's): an extension of the distributed netcode
|
||||
(port/linux/NETCODE.md, network_distributed.c), as the VR players' hands
|
||||
are (network_vr.c).
|
||||
|
||||
- Every tick, a machine sends what its microphone heard of its player's
|
||||
speech (halo_voice.h: Opus packets of 20 ms, only while they speak) in
|
||||
_distributed_message_voice (unreliable): a client to the host, which
|
||||
plays it and passes it on to its other clients; the host its own to
|
||||
every client.
|
||||
- A machine plays each speaker's voice from where they are: a teammate
|
||||
(in a game of teams) over the radio, as loud anywhere and only a little
|
||||
to the side they are on; anyone else as near as they are (no further off
|
||||
than VOICE_RANGE), from their side. A speaker with no unit (dead) is
|
||||
heard over the radio by their team, and not by the rest.
|
||||
|
||||
A machine that does not know the message (a build without this, of the same
|
||||
network version) drops it, as it drops any type past its own.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_engine.h"
|
||||
#include "game/players.h"
|
||||
#include "units/units.h"
|
||||
#include "network_distributed.h"
|
||||
#include "halo_voice.h"
|
||||
#include "halo_vr.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
void platform_log(char const *format, ...);
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
/* an entry: the speaker, its sequence number and the packet's length,
|
||||
and the packet */
|
||||
VOICE_ENTRY_HEADER = 4,
|
||||
VOICE_MAXIMUM_PACKET = 200,
|
||||
VOICE_MAXIMUM_TAKEN = 1024,
|
||||
};
|
||||
|
||||
/* how far off someone not on the radio is heard (world units: 60 metres) */
|
||||
#define VOICE_RANGE 20.0f
|
||||
/* how far to the side a voice is put: on the radio, and near */
|
||||
#define VOICE_RADIO_PAN 0.3f
|
||||
#define VOICE_NEAR_PAN 0.8f
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
static struct
|
||||
{
|
||||
boolean active;
|
||||
word sequence;
|
||||
long sent;
|
||||
long played;
|
||||
long log_time;
|
||||
} network_voice;
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static boolean network_voice_in_network_game(
|
||||
void)
|
||||
{
|
||||
short connection = game_connection();
|
||||
|
||||
return (connection == _game_connection_network_server || connection == _game_connection_network_client) &&
|
||||
game_in_progress();
|
||||
}
|
||||
|
||||
/* where a player's head is, and whether they have a unit */
|
||||
static boolean network_voice_player_head(
|
||||
struct player_datum const *player,
|
||||
real_point3d *head)
|
||||
{
|
||||
long unit_index = distributed_living_unit(player);
|
||||
|
||||
if (unit_index == NONE)
|
||||
return FALSE;
|
||||
unit_get_camera_position(unit_index, head);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* the gains a speaker is heard at here (FALSE: not heard) */
|
||||
static boolean network_voice_gains(
|
||||
struct player_datum const *speaker,
|
||||
real *left,
|
||||
real *right)
|
||||
{
|
||||
long listener_index = local_player_get_player_index(0);
|
||||
struct player_datum *listener = listener_index != NONE ? player_try_and_get(listener_index) : NULL;
|
||||
boolean teammate = listener && game_engine_has_teams() && listener->team_index == speaker->team_index;
|
||||
real_point3d speaker_head;
|
||||
real_point3d listener_head;
|
||||
real gain = 1.0f;
|
||||
real pan = 0.0f;
|
||||
|
||||
if (!listener)
|
||||
return FALSE;
|
||||
if (!network_voice_player_head(speaker, &speaker_head))
|
||||
{
|
||||
/* (dead: on the radio, to the team) */
|
||||
if (!teammate)
|
||||
return FALSE;
|
||||
*left = *right = 1.0f;
|
||||
return TRUE;
|
||||
}
|
||||
if (network_voice_player_head(listener, &listener_head))
|
||||
{
|
||||
real dx = speaker_head.x - listener_head.x;
|
||||
real dy = speaker_head.y - listener_head.y;
|
||||
real dz = speaker_head.z - listener_head.z;
|
||||
real distance = (real)sqrt(dx * dx + dy * dy + dz * dz);
|
||||
real flat = (real)sqrt(dx * dx + dy * dy);
|
||||
float look[3];
|
||||
real forward_x;
|
||||
real forward_y;
|
||||
real length;
|
||||
struct unit_datum *unit = unit_try_and_get(distributed_living_unit(listener));
|
||||
|
||||
/* (where the listener looks: the head, in VR) */
|
||||
if (halo_vr_look_vector(0, look))
|
||||
{
|
||||
forward_x = look[0];
|
||||
forward_y = look[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
forward_x = unit ? unit->unit.looking_vector.i : 1.0f;
|
||||
forward_y = unit ? unit->unit.looking_vector.j : 0.0f;
|
||||
}
|
||||
length = (real)sqrt(forward_x * forward_x + forward_y * forward_y);
|
||||
if (length > 0.0001f && flat > 0.0001f)
|
||||
{
|
||||
/* (the right: forward turned clockwise) */
|
||||
pan = (dx * forward_y - dy * forward_x) / (flat * length);
|
||||
}
|
||||
if (!teammate)
|
||||
{
|
||||
gain = 1.0f - distance / VOICE_RANGE;
|
||||
if (gain <= 0.0f)
|
||||
return FALSE;
|
||||
gain *= gain;
|
||||
}
|
||||
pan *= teammate ? VOICE_RADIO_PAN : VOICE_NEAR_PAN;
|
||||
}
|
||||
else if (!teammate)
|
||||
{
|
||||
/* (the listener dead: the others as from where the body is not:
|
||||
not at all) */
|
||||
return FALSE;
|
||||
}
|
||||
*left = gain * (pan > 0.0f ? 1.0f - pan : 1.0f);
|
||||
*right = gain * (pan < 0.0f ? 1.0f + pan : 1.0f);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void network_voice_play_entries(
|
||||
byte const *entries,
|
||||
byte const *end,
|
||||
short count,
|
||||
long machine_index)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count && entries + VOICE_ENTRY_HEADER <= end; index++)
|
||||
{
|
||||
byte player_byte = entries[0];
|
||||
word sequence = (word)(entries[1] | entries[2] << 8);
|
||||
short length = entries[3];
|
||||
long player_index;
|
||||
struct player_datum *player;
|
||||
real left;
|
||||
real right;
|
||||
|
||||
if (length <= 0 || entries + VOICE_ENTRY_HEADER + length > end)
|
||||
break;
|
||||
player_index = distributed_player_from_byte(player_byte);
|
||||
player = player_index != NONE ? player_try_and_get(player_index) : NULL;
|
||||
/* (the host: only a client's own players') */
|
||||
if (player && (machine_index == NONE || distributed_machine_has_player(machine_index, player_byte)) &&
|
||||
player->local_player_index == NONE && network_voice_gains(player, &left, &right))
|
||||
{
|
||||
halo_voice_play(player_byte, sequence, entries + VOICE_ENTRY_HEADER, length, left, right);
|
||||
network_voice.played++;
|
||||
}
|
||||
entries += VOICE_ENTRY_HEADER + length;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
void network_voice_new_game(
|
||||
void)
|
||||
{
|
||||
network_voice.sequence = 0;
|
||||
}
|
||||
|
||||
word network_voice_entry_size(
|
||||
void)
|
||||
{
|
||||
return VOICE_ENTRY_HEADER;
|
||||
}
|
||||
|
||||
/* after each tick (network_distributed_tick): this machine's speech sent */
|
||||
void network_voice_tick(
|
||||
void)
|
||||
{
|
||||
struct
|
||||
{
|
||||
struct distributed_message_header header;
|
||||
byte entries[VOICE_MAXIMUM_TAKEN + 64];
|
||||
} message;
|
||||
byte taken[VOICE_MAXIMUM_TAKEN];
|
||||
long player_index = local_player_get_player_index(0);
|
||||
long size;
|
||||
long offset = 0;
|
||||
long used = 0;
|
||||
short count = 0;
|
||||
|
||||
if (!network_voice_in_network_game() || !halo_voice_enabled())
|
||||
{
|
||||
if (network_voice.active)
|
||||
{
|
||||
network_voice.active = FALSE;
|
||||
halo_voice_end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!network_voice.active)
|
||||
{
|
||||
network_voice.active = TRUE;
|
||||
halo_voice_begin();
|
||||
}
|
||||
if (game_time_get() - network_voice.log_time >= 150 && (network_voice.sent || network_voice.played))
|
||||
{
|
||||
platform_log("voice: sent %ld, played %ld packets (in 150 ticks)", network_voice.sent, network_voice.played);
|
||||
network_voice.sent = network_voice.played = 0;
|
||||
network_voice.log_time = game_time_get();
|
||||
}
|
||||
size = halo_voice_take(taken, sizeof(taken));
|
||||
if (player_index == NONE)
|
||||
return;
|
||||
while (offset < size)
|
||||
{
|
||||
short length = taken[offset];
|
||||
|
||||
if (length <= 0 || offset + 1 + length > size || length > VOICE_MAXIMUM_PACKET)
|
||||
break;
|
||||
if (used + VOICE_ENTRY_HEADER + length > (long)sizeof(message.entries))
|
||||
break;
|
||||
message.entries[used] = distributed_player_to_byte(player_index);
|
||||
message.entries[used + 1] = (byte)(network_voice.sequence & 0xFF);
|
||||
message.entries[used + 2] = (byte)(network_voice.sequence >> 8);
|
||||
message.entries[used + 3] = (byte)length;
|
||||
csmemcpy(message.entries + used + VOICE_ENTRY_HEADER, taken + offset + 1, length);
|
||||
used += VOICE_ENTRY_HEADER + length;
|
||||
offset += 1 + length;
|
||||
network_voice.sequence++;
|
||||
count++;
|
||||
}
|
||||
if (!count)
|
||||
return;
|
||||
network_voice.sent += count;
|
||||
distributed_send(&message, _distributed_message_voice, count, (word)(sizeof(message.header) + used),
|
||||
game_connection() == _game_connection_network_server ? _distributed_to_clients : _distributed_to_host);
|
||||
}
|
||||
|
||||
/* a machine's speech: on the host a client's, played and passed on to the
|
||||
other clients; on a client the host's of any other */
|
||||
void network_voice_handle(
|
||||
long machine_index,
|
||||
void const *message,
|
||||
word size)
|
||||
{
|
||||
struct distributed_message_header header;
|
||||
byte const *entries = (byte const *)message + sizeof(header);
|
||||
byte const *end = (byte const *)message + size;
|
||||
|
||||
if (!halo_voice_enabled() || !network_voice.active)
|
||||
return;
|
||||
csmemcpy(&header, message, sizeof(header));
|
||||
network_voice_play_entries(entries, end, header.count, machine_index);
|
||||
if (machine_index != NONE)
|
||||
{
|
||||
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
|
||||
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
|
||||
short number;
|
||||
struct
|
||||
{
|
||||
struct distributed_message_header header;
|
||||
byte entries[VOICE_MAXIMUM_TAKEN + 64];
|
||||
} relayed;
|
||||
word entries_size = (word)(end - entries);
|
||||
|
||||
byte const *entry = entries;
|
||||
short index;
|
||||
|
||||
/* (only the sender's own players' speech is passed on, whole) */
|
||||
if (entries_size < VOICE_ENTRY_HEADER || entries_size > sizeof(relayed.entries))
|
||||
return;
|
||||
for (index = 0; index < header.count; index++)
|
||||
{
|
||||
if (entry + VOICE_ENTRY_HEADER > end || entry[3] == 0 || entry + VOICE_ENTRY_HEADER + entry[3] > end ||
|
||||
!distributed_machine_has_player(machine_index, entry[0]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
entry += VOICE_ENTRY_HEADER + entry[3];
|
||||
}
|
||||
if (entry != end)
|
||||
return;
|
||||
for (number = 0; number < machine_count; number++)
|
||||
{
|
||||
if (machine_indices[number] == machine_index)
|
||||
continue;
|
||||
csmemcpy(relayed.entries, entries, entries_size);
|
||||
distributed_send_to_machine(machine_indices[number], &relayed, _distributed_message_voice, header.count,
|
||||
(word)(sizeof(relayed.header) + entries_size));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
HALO_VOICE.H
|
||||
|
||||
The voice chat's sound for the game (port/linux/src/voice.c: on Android the
|
||||
host's microphone, Opus and mixer, port/android/host/host_voice.c; elsewhere
|
||||
nothing). port/linux/game/network_voice.c sends and receives it.
|
||||
*/
|
||||
|
||||
#ifndef __HALO_VOICE_H
|
||||
#define __HALO_VOICE_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* whether the voice chat is on (voice.enabled) */
|
||||
int halo_voice_enabled(void);
|
||||
/* starts playing voices, and the microphone if voice.microphone; stops */
|
||||
int halo_voice_begin(void);
|
||||
void halo_voice_end(void);
|
||||
/* the microphone's speech since asked: Opus packets, each after a byte of
|
||||
its length; returns the bytes */
|
||||
int halo_voice_take(void *buffer, int size);
|
||||
/* a speaker's packet, played at these gains (left, right) */
|
||||
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -304,17 +304,12 @@ static const struct config_setting config_settings[] =
|
||||
"The Discord application internet play invites go through while the\n"
|
||||
"Discord desktop client runs; empty for none." },
|
||||
|
||||
{ "voice.enabled", _config_boolean, "true", "HALO_VOICE_ENABLED", _environment_value, _platform_android,
|
||||
"Voice chat in multiplayer: hear the other players (teammates over the\n"
|
||||
"radio, the rest when near, from where they are)." },
|
||||
{ "voice.microphone", _config_boolean, "true", "HALO_VOICE_MICROPHONE", _environment_value, _platform_android,
|
||||
"Send your voice in multiplayer, whenever you speak (the headset's\n"
|
||||
"microphone; Android asks for it once). False only listens." },
|
||||
{ "voice.volume", _config_real, "1.0", "HALO_VOICE_VOLUME", _environment_value, _platform_android,
|
||||
"The other players' voices' volume (1 as they come)." },
|
||||
{ "voice.threshold", _config_real, "0.02", "HALO_VOICE_THRESHOLD", _environment_value, _platform_android,
|
||||
"How loud the microphone must be to send (0..1 of full scale): higher\n"
|
||||
"if it sends noise, lower if it cuts words off." },
|
||||
{ "bots.count", _config_integer, "0", "HALO_BOTS_COUNT", _environment_value, _platform_android,
|
||||
"Bots in the multiplayer games this machine hosts (0 to 15; 0: none):\n"
|
||||
"players it drives itself, which every machine sees as players. Set in\n"
|
||||
"the lobby's host options (Y)." },
|
||||
{ "bots.skill", _config_integer, "1", "HALO_BOTS_SKILL", _environment_value, _platform_android,
|
||||
"How well the bots play: 0 easy, 1 normal, 2 hard." },
|
||||
{ "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"
|
||||
"false never looks (the game's \"Do not ask again\" writes false here)." },
|
||||
@@ -964,7 +959,9 @@ static int config_line_section(const char *line, const char *end, char *section,
|
||||
|
||||
/* sets a boolean setting, for now and in config.toml: its line there is
|
||||
changed (or added), the rest of the file kept as it is */
|
||||
int config_write_boolean(const char *name, int value)
|
||||
/* a boolean's or an integer's new value, kept and written into the file
|
||||
(the line of its key replaced, else added to its section) */
|
||||
static int config_write_value(const char *name, int type, long value)
|
||||
{
|
||||
const char *dot = strchr(name, '.');
|
||||
long index = config_setting_index(name);
|
||||
@@ -975,15 +972,24 @@ int config_write_boolean(const char *name, int value)
|
||||
const char *line;
|
||||
int written = 0, in_section = 0, succeeded;
|
||||
|
||||
if (index < 0 || config_settings[index].type != _config_boolean || !dot || (size_t)(dot - name) >= sizeof(section))
|
||||
if (index < 0 || config_settings[index].type != type || !dot || (size_t)(dot - name) >= sizeof(section))
|
||||
return 0;
|
||||
/* (the file read first, as the other settings are) */
|
||||
config_boolean(name);
|
||||
if (type == _config_boolean)
|
||||
config_boolean(name);
|
||||
else
|
||||
config_integer(name);
|
||||
pthread_mutex_lock(&config_lock);
|
||||
config_values[index].boolean = value != 0;
|
||||
if (type == _config_boolean)
|
||||
config_values[index].boolean = value != 0;
|
||||
else
|
||||
config_values[index].integer = value;
|
||||
snprintf(section, sizeof(section), "%.*s", (int)(dot - name), name);
|
||||
snprintf(key, sizeof(key), "%s", dot + 1);
|
||||
snprintf(line_text, sizeof(line_text), "%s = %s\n", key, value ? "true" : "false");
|
||||
if (type == _config_boolean)
|
||||
snprintf(line_text, sizeof(line_text), "%s = %s\n", key, value ? "true" : "false");
|
||||
else
|
||||
snprintf(line_text, sizeof(line_text), "%s = %ld\n", key, value);
|
||||
snprintf(wanted, sizeof(wanted), "%s", section);
|
||||
config_path(path, sizeof(path));
|
||||
text = config_read_file(path, &size);
|
||||
@@ -1040,6 +1046,16 @@ int config_write_boolean(const char *name, int value)
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
int config_write_boolean(const char *name, int value)
|
||||
{
|
||||
return config_write_value(name, _config_boolean, value != 0);
|
||||
}
|
||||
|
||||
int config_write_integer(const char *name, long value)
|
||||
{
|
||||
return config_write_value(name, _config_integer, value);
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
int config_boolean(const char *name)
|
||||
|
||||
@@ -21,5 +21,6 @@ const char *config_string(const char *name);
|
||||
/* sets a boolean setting, and writes it into config.toml (only its line
|
||||
changes); 1 on success */
|
||||
int config_write_boolean(const char *name, int value);
|
||||
int config_write_integer(const char *name, long value);
|
||||
|
||||
#endif
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
VOICE.C
|
||||
|
||||
The voice chat's sound (halo_voice.h): on Android, the host's
|
||||
(port/android/host/host_voice.c); elsewhere none.
|
||||
*/
|
||||
|
||||
#include "halo_voice.h"
|
||||
#include "platform.h"
|
||||
#include "port_config.h"
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
#include "guest_host.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
static int voice_started;
|
||||
|
||||
/* whether the app may use the microphone, as the launcher found
|
||||
(LauncherActivity.java): opened without it, Android would ask in the middle
|
||||
of the game */
|
||||
static int voice_microphone_allowed(void)
|
||||
{
|
||||
char path[512];
|
||||
FILE *file;
|
||||
int allowed = 0;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/microphone_permission.txt", platform_data_root());
|
||||
file = fopen(path, "rb");
|
||||
if (file)
|
||||
{
|
||||
allowed = fgetc(file) == '1';
|
||||
fclose(file);
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
int halo_voice_enabled(void)
|
||||
{
|
||||
return config_boolean("voice.enabled");
|
||||
}
|
||||
|
||||
int halo_voice_begin(void)
|
||||
{
|
||||
int microphone;
|
||||
|
||||
if (voice_started)
|
||||
return 1;
|
||||
voice_started = 1;
|
||||
microphone = config_boolean("voice.microphone") && voice_microphone_allowed();
|
||||
platform_log("voice: on, %s", microphone ? "with the microphone" :
|
||||
config_boolean("voice.microphone") ? "without the microphone (not allowed)" : "listening only");
|
||||
return host_voice_start(microphone, (float)config_real("voice.volume"), (float)config_real("voice.threshold"));
|
||||
}
|
||||
|
||||
void halo_voice_end(void)
|
||||
{
|
||||
if (!voice_started)
|
||||
return;
|
||||
voice_started = 0;
|
||||
host_voice_stop();
|
||||
platform_log("voice: off");
|
||||
}
|
||||
|
||||
int halo_voice_take(void *buffer, int size)
|
||||
{
|
||||
return voice_started ? host_voice_take_packets(buffer, size) : 0;
|
||||
}
|
||||
|
||||
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
|
||||
{
|
||||
if (voice_started)
|
||||
host_voice_play(speaker, sequence, data, length, left, right);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
int halo_voice_enabled(void) { return 0; }
|
||||
int halo_voice_begin(void) { return 0; }
|
||||
void halo_voice_end(void) {}
|
||||
int halo_voice_take(void *buffer, int size) { (void)buffer; (void)size; return 0; }
|
||||
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
|
||||
{
|
||||
(void)speaker; (void)sequence; (void)data; (void)length; (void)left; (void)right;
|
||||
}
|
||||
|
||||
#endif
|
||||
+159
-45
@@ -5643,16 +5643,14 @@ struct ui_public_game
|
||||
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);
|
||||
long config_integer(char const *name);
|
||||
int config_write_integer(char const *name, long value);
|
||||
|
||||
enum
|
||||
{
|
||||
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
|
||||
/* the online games panel's rows: "enter a code", the voice chat's
|
||||
switches, and the games */
|
||||
/* the online games panel's rows: "enter a code" and the games */
|
||||
ONLINE_PANEL_ROWS = 11,
|
||||
ONLINE_PANEL_FIXED_ROWS = 3,
|
||||
};
|
||||
|
||||
static struct
|
||||
@@ -5665,8 +5663,18 @@ static struct
|
||||
short scroll;
|
||||
short game_count;
|
||||
struct ui_public_game games[MAXIMUM_SHOWN_PUBLIC_GAMES];
|
||||
/* the host options panel, over a hosted game's lobby */
|
||||
boolean host_panel_open;
|
||||
short host_selection;
|
||||
} ui_join_code_globals;
|
||||
|
||||
enum
|
||||
{
|
||||
/* the host options: public, bots, their skill */
|
||||
HOST_PANEL_ROWS = 3,
|
||||
MAXIMUM_BOTS_SHOWN = 15,
|
||||
};
|
||||
|
||||
/* the multiplayer maps' names, as the menus show them */
|
||||
static char const *ui_join_code_map_name(
|
||||
char const *map)
|
||||
@@ -5716,7 +5724,7 @@ it, B closes it */
|
||||
static void ui_join_code_panel_event(
|
||||
struct event_record const *event)
|
||||
{
|
||||
short rows = (short)(ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS);
|
||||
short rows = (short)(ui_join_code_globals.game_count + 1);
|
||||
short move = 0;
|
||||
|
||||
if (event->type == _event_type_left_stick)
|
||||
@@ -5742,28 +5750,16 @@ static void ui_join_code_panel_event(
|
||||
break;
|
||||
case _gamepad_analog_button_a:
|
||||
case _gamepad_binary_button_start:
|
||||
/* (the voice chat's switches, kept in config.toml) */
|
||||
if (ui_join_code_globals.selection == 1)
|
||||
{
|
||||
config_write_boolean("voice.enabled", !config_boolean("voice.enabled"));
|
||||
break;
|
||||
}
|
||||
if (ui_join_code_globals.selection == 2)
|
||||
{
|
||||
config_write_boolean("voice.microphone", !config_boolean("voice.microphone"));
|
||||
break;
|
||||
}
|
||||
ui_join_code_globals.panel_open = FALSE;
|
||||
if (ui_join_code_globals.selection == 0 ||
|
||||
ui_join_code_globals.selection >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
|
||||
ui_join_code_globals.selection > ui_join_code_globals.game_count)
|
||||
{
|
||||
ui_join_code_launch_keyboard();
|
||||
}
|
||||
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);
|
||||
p2p_join_with_code(ui_join_code_globals.games[ui_join_code_globals.selection - 1].code);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -5778,15 +5774,94 @@ static void ui_join_code_panel_event(
|
||||
}
|
||||
}
|
||||
|
||||
/* the host options panel's input: the stick or the pad's arrows up and
|
||||
down choose a row, left and right (or A) change it, B or Y closes it */
|
||||
static void ui_join_code_host_panel_event(
|
||||
struct event_record const *event)
|
||||
{
|
||||
short move = 0;
|
||||
short change = 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->data.stick.x == SHORT_MAX)
|
||||
change = 1;
|
||||
else if (event->data.stick.x == SHORT_MIN)
|
||||
change = -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_binary_button_dpad_right:
|
||||
case _gamepad_analog_button_a:
|
||||
change = 1;
|
||||
break;
|
||||
case _gamepad_binary_button_dpad_left: change = -1; break;
|
||||
case _gamepad_analog_button_b:
|
||||
case _gamepad_analog_button_y:
|
||||
case _gamepad_binary_button_back:
|
||||
ui_join_code_globals.host_panel_open = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (move)
|
||||
ui_join_code_globals.host_selection = (short)PIN(ui_join_code_globals.host_selection + move, 0, HOST_PANEL_ROWS - 1);
|
||||
if (!change)
|
||||
return;
|
||||
switch (ui_join_code_globals.host_selection)
|
||||
{
|
||||
case 0:
|
||||
p2p_set_public_game(!p2p_public_game());
|
||||
break;
|
||||
case 1:
|
||||
{
|
||||
/* (A goes round: past the most, none) */
|
||||
long count = config_integer("bots.count") + change;
|
||||
|
||||
if (count > MAXIMUM_BOTS_SHOWN)
|
||||
count = event->type == _event_type_button && event->data.button.index == _gamepad_analog_button_a ? 0 :
|
||||
MAXIMUM_BOTS_SHOWN;
|
||||
config_write_integer("bots.count", PIN(count, 0, MAXIMUM_BOTS_SHOWN));
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
long skill = config_integer("bots.skill") + change;
|
||||
|
||||
if (skill > 2)
|
||||
skill = event->type == _event_type_button && event->data.button.index == _gamepad_analog_button_a ? 0 : 2;
|
||||
config_write_integer("bots.skill", PIN(skill, 0, 2));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
everything); Y in a hosted game's lobby: the host options (public, bots).
|
||||
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.host_panel_open)
|
||||
{
|
||||
if (!ui_join_code_screen_active("connected_pregame_screen") || !global_network_game_server_get())
|
||||
{
|
||||
ui_join_code_globals.host_panel_open = FALSE;
|
||||
return FALSE;
|
||||
}
|
||||
ui_join_code_host_panel_event(event);
|
||||
return TRUE;
|
||||
}
|
||||
if (ui_join_code_globals.panel_open)
|
||||
{
|
||||
if (!ui_join_code_screen_active("server_list_screen"))
|
||||
@@ -5809,7 +5884,8 @@ static boolean ui_join_code_take_event(
|
||||
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());
|
||||
ui_join_code_globals.host_panel_open = TRUE;
|
||||
ui_join_code_globals.host_selection = 0;
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
@@ -5910,7 +5986,7 @@ static void ui_join_code_render_panel(
|
||||
ui_join_code_globals.game_count =
|
||||
(short)p2p_public_games(ui_join_code_globals.games, MAXIMUM_SHOWN_PUBLIC_GAMES);
|
||||
ui_join_code_globals.selection =
|
||||
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS - 1);
|
||||
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count);
|
||||
ui_join_code_draw_quad(40, 72, 600, 410, 0xF0050A18);
|
||||
ui_join_code_draw_quad(40, 72, 600, 74, 0xFF3A6EA5);
|
||||
ui_join_code_draw_quad(40, 408, 600, 410, 0xFF3A6EA5);
|
||||
@@ -5925,7 +6001,7 @@ static void ui_join_code_render_panel(
|
||||
short index = (short)(ui_join_code_globals.scroll + row);
|
||||
short y = (short)(128 + row * 22);
|
||||
|
||||
if (index >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
|
||||
if (index > ui_join_code_globals.game_count)
|
||||
break;
|
||||
if (index == ui_join_code_globals.selection)
|
||||
ui_join_code_draw_quad(48, y, 592, (short)(y + 21), 0x803A6EA5);
|
||||
@@ -5933,20 +6009,9 @@ static void ui_join_code_render_panel(
|
||||
{
|
||||
ui_join_code_draw_text(56, 584, (short)(y + 1), "Enter a join code...", FALSE, 0, &text_color);
|
||||
}
|
||||
else if (index == 1)
|
||||
{
|
||||
ui_join_code_draw_text(56, 584, (short)(y + 1),
|
||||
config_boolean("voice.enabled") ? "Voice chat: On" : "Voice chat: Off", FALSE, 0, &text_color);
|
||||
}
|
||||
else if (index == 2)
|
||||
{
|
||||
ui_join_code_draw_text(56, 584, (short)(y + 1),
|
||||
config_boolean("voice.microphone") ? "Microphone: On (sent while you speak)" :
|
||||
"Microphone: Off (listening only)", FALSE, 0, &text_color);
|
||||
}
|
||||
else
|
||||
{
|
||||
struct ui_public_game const *game = &ui_join_code_globals.games[index - ONLINE_PANEL_FIXED_ROWS];
|
||||
struct ui_public_game const *game = &ui_join_code_globals.games[index - 1];
|
||||
|
||||
ui_join_code_draw_text(56, 212, (short)(y + 1), game->name, FALSE, 0, &text_color);
|
||||
ui_join_code_draw_text(216, 372, (short)(y + 1), ui_join_code_map_name(game->map), FALSE, 0,
|
||||
@@ -5957,9 +6022,9 @@ static void ui_join_code_render_panel(
|
||||
}
|
||||
}
|
||||
if (!ui_join_code_globals.game_count)
|
||||
ui_join_code_draw_text(56, 584, 196, "No public games right now", FALSE, 0, &dim_color);
|
||||
ui_join_code_draw_text(56, 584, 384, "%a-button Join or switch %x-button Enter a code %b-button Back", TRUE,
|
||||
0, &text_color);
|
||||
ui_join_code_draw_text(56, 584, 152, "No public games right now", FALSE, 0, &dim_color);
|
||||
ui_join_code_draw_text(56, 584, 384, "%a-button Join %x-button Enter a code %b-button Back", TRUE, 0,
|
||||
&text_color);
|
||||
}
|
||||
|
||||
/* over the screens, as they are drawn */
|
||||
@@ -6002,15 +6067,64 @@ static void ui_join_code_render(
|
||||
if (status)
|
||||
ui_join_code_draw_line(392, status, FALSE, FALSE);
|
||||
}
|
||||
else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get() &&
|
||||
p2p_join_code(code, sizeof(code)))
|
||||
else if (ui_join_code_screen_active("connected_pregame_screen") && global_network_game_server_get())
|
||||
{
|
||||
char line[96];
|
||||
char line[128];
|
||||
long bots = config_integer("bots.count");
|
||||
char bots_text[24];
|
||||
|
||||
snprintf(line, sizeof(line), "Online join code: %s %%y-button Public game: %s", code,
|
||||
p2p_public_game() ? "Yes" : "No");
|
||||
if (bots > 0)
|
||||
snprintf(bots_text, sizeof(bots_text), "%ld bot%s", bots, bots == 1 ? "" : "s");
|
||||
else
|
||||
snprintf(bots_text, sizeof(bots_text), "no bots");
|
||||
if (p2p_join_code(code, sizeof(code)))
|
||||
{
|
||||
snprintf(line, sizeof(line), "Online join code: %s %%y-button Host options (%s, %s)", code,
|
||||
p2p_public_game() ? "public" : "private", bots_text);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(line, sizeof(line), "%%y-button Host options (%s)", bots_text);
|
||||
}
|
||||
/* (under the buttons' key) */
|
||||
ui_join_code_draw_line(448, line, TRUE, TRUE);
|
||||
if (ui_join_code_globals.host_panel_open)
|
||||
{
|
||||
static char const *const skills[] = { "Easy", "Normal", "Hard" };
|
||||
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 };
|
||||
short row;
|
||||
|
||||
ui_join_code_draw_quad(140, 150, 500, 330, 0xF0050A18);
|
||||
ui_join_code_draw_quad(140, 150, 500, 152, 0xFF3A6EA5);
|
||||
ui_join_code_draw_quad(140, 328, 500, 330, 0xFF3A6EA5);
|
||||
ui_join_code_draw_text(156, 484, 158, "HOST OPTIONS", FALSE, 0, &title_color);
|
||||
for (row = 0; row < HOST_PANEL_ROWS; row++)
|
||||
{
|
||||
short y = (short)(190 + row * 30);
|
||||
|
||||
if (row == ui_join_code_globals.host_selection)
|
||||
ui_join_code_draw_quad(148, (short)(y - 3), 492, (short)(y + 21), 0x803A6EA5);
|
||||
switch (row)
|
||||
{
|
||||
case 0:
|
||||
snprintf(line, sizeof(line), "Public game: %s", p2p_public_game() ? "Yes (listed online)" : "No");
|
||||
break;
|
||||
case 1:
|
||||
snprintf(line, sizeof(line), "Bots: %s", bots > 0 ? bots_text : "None");
|
||||
break;
|
||||
default:
|
||||
{
|
||||
long skill = PIN(config_integer("bots.skill"), 0, 2);
|
||||
|
||||
snprintf(line, sizeof(line), "Bot skill: %s", skills[skill]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ui_join_code_draw_text(160, 484, y, line, FALSE, 0, &text_color);
|
||||
}
|
||||
ui_join_code_draw_text(156, 484, 300, "%a-button Change %b-button Done", TRUE, 0, &text_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -696,6 +696,12 @@ typedef char network_game_server_countdown_state_offset_assert[
|
||||
offsetof(struct network_game_server, countdown_state) ==
|
||||
8 + HALO_PORT_NETWORK_GAME_SIZE + MAXIMUM_NETWORK_MACHINE_COUNT * 0x10 + 0xC ? 1 : -1];
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* bots' machines (below: the VR port's bots) */
|
||||
static boolean network_game_server_bot_machines[MAXIMUM_NETWORK_MACHINE_COUNT];
|
||||
static short network_game_server_bot_machine_count(struct network_game_server *server);
|
||||
#endif
|
||||
|
||||
/* ---------- prototypes */
|
||||
|
||||
void countdown_timer_increment(
|
||||
@@ -2895,6 +2901,10 @@ boolean server_has_enough_machines(
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* (bots' machines count: the VR port's) */
|
||||
machine_count += network_game_server_bot_machine_count(server);
|
||||
#endif
|
||||
has_enough_machines = machine_count >= minimum_machine_count;
|
||||
|
||||
return has_enough_machines;
|
||||
@@ -3548,7 +3558,11 @@ void network_game_server_update_countdown(
|
||||
else
|
||||
{
|
||||
if (network_game_should_accept_remote_connections() == FALSE ||
|
||||
network_game_server_get_client_machine_count(server) > 1)
|
||||
network_game_server_get_client_machine_count(server)
|
||||
#ifdef HALO_ANDROID
|
||||
+ network_game_server_bot_machine_count(server)
|
||||
#endif
|
||||
> 1)
|
||||
{
|
||||
unsigned long countdown;
|
||||
|
||||
@@ -3815,6 +3829,11 @@ static boolean network_game_server_add_new_client(
|
||||
{
|
||||
for (i = 0; i < MAXIMUM_NETWORK_MACHINE_COUNT; i++)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
/* (not a bot machine's slot) */
|
||||
if (network_game_server_bot_machines[i] && server->game.machines[i].machine_index == i)
|
||||
continue;
|
||||
#endif
|
||||
if (server->client_machines[i].machine_index == NONE)
|
||||
{
|
||||
struct transport_address client_address = { 0 };
|
||||
@@ -4184,6 +4203,221 @@ static boolean network_game_server_have_all_machines_have_precached(
|
||||
return all_machines_have_precached;
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* ---------- bots (the VR port's: port/linux/game/bots.c)
|
||||
|
||||
Bots are the players of machines of the host's own that nothing connects
|
||||
to: from the top slot down, four players each (a machine's most). The host
|
||||
drives them (bots_tick); every other machine sees them as any other
|
||||
machine's players. A new connection never takes a bot's slot, and in the
|
||||
lobby they count as machines, so that a game of the host and bots starts. */
|
||||
|
||||
long config_integer(char const *name);
|
||||
|
||||
static void network_game_server_bot_copy_name(
|
||||
wchar_t *destination,
|
||||
wchar_t const *source,
|
||||
short size)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < size - 1 && source[index]; index++)
|
||||
destination[index] = source[index];
|
||||
destination[index] = 0;
|
||||
}
|
||||
|
||||
boolean network_game_server_is_bot_machine(
|
||||
long machine_index)
|
||||
{
|
||||
struct network_game_server *server = global_network_game_server_get();
|
||||
|
||||
return server && VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
|
||||
network_game_server_bot_machines[machine_index] &&
|
||||
server->game.machines[machine_index].machine_index == machine_index;
|
||||
}
|
||||
|
||||
static short network_game_server_bot_machine_count(
|
||||
struct network_game_server *server)
|
||||
{
|
||||
short count = 0;
|
||||
long machine_index;
|
||||
|
||||
for (machine_index = 0; machine_index < MAXIMUM_NETWORK_MACHINE_COUNT; machine_index++)
|
||||
{
|
||||
if (network_game_server_bot_machines[machine_index] &&
|
||||
server->game.machines[machine_index].machine_index == machine_index &&
|
||||
network_game_server_machine_has_players(server, (short)machine_index))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static short network_game_server_bot_count(
|
||||
struct network_game_server *server,
|
||||
struct network_player **last)
|
||||
{
|
||||
short count = 0;
|
||||
long player_index;
|
||||
|
||||
*last = NULL;
|
||||
for (player_index = 0; player_index < MAXIMUM_NETWORK_PLAYER_COUNT; player_index++)
|
||||
{
|
||||
struct network_player *player = &server->game.players[player_index];
|
||||
|
||||
if (network_player_is_valid(player) && VALID_INDEX(player->machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
|
||||
network_game_server_bot_machines[(long)player->machine_index])
|
||||
{
|
||||
count++;
|
||||
*last = player;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static boolean network_game_server_add_bot(
|
||||
struct network_game_server *server,
|
||||
short number)
|
||||
{
|
||||
struct network_player bot;
|
||||
long machine_index;
|
||||
short controller_index;
|
||||
|
||||
if (!network_game_has_free_player_slot(&server->game))
|
||||
return FALSE;
|
||||
/* (a bot machine with room, else a new one from the top) */
|
||||
for (machine_index = MAXIMUM_NETWORK_MACHINE_COUNT - 1; machine_index >= 0; machine_index--)
|
||||
{
|
||||
if (network_game_server_bot_machines[machine_index] &&
|
||||
server->game.machines[machine_index].machine_index == machine_index)
|
||||
{
|
||||
for (controller_index = 0; controller_index < MAXIMUM_LOCAL_PLAYERS; controller_index++)
|
||||
{
|
||||
long player_index;
|
||||
boolean taken = FALSE;
|
||||
|
||||
for (player_index = 0; player_index < MAXIMUM_NETWORK_PLAYER_COUNT && !taken; player_index++)
|
||||
{
|
||||
struct network_player *player = &server->game.players[player_index];
|
||||
|
||||
taken = network_player_is_valid(player) && player->machine_index == machine_index &&
|
||||
player->controller_index == controller_index;
|
||||
}
|
||||
if (!taken)
|
||||
goto found;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (machine_index = MAXIMUM_NETWORK_MACHINE_COUNT - 1; machine_index >= 0; machine_index--)
|
||||
{
|
||||
if (!server->client_machines[machine_index].connection &&
|
||||
server->client_machines[machine_index].machine_index == NONE &&
|
||||
server->game.machines[machine_index].machine_index == NONE)
|
||||
{
|
||||
struct network_machine machine;
|
||||
|
||||
csmemset(&machine, 0, sizeof(machine));
|
||||
network_game_server_bot_copy_name(machine.name, L"Bots", NUMBEROF(machine.name));
|
||||
machine.machine_index = (char)machine_index;
|
||||
if (!network_game_add_machine(&server->game, &machine))
|
||||
return FALSE;
|
||||
network_game_server_bot_machines[machine_index] = TRUE;
|
||||
controller_index = 0;
|
||||
goto found;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
|
||||
found:
|
||||
csmemset(&bot, 0, sizeof(bot));
|
||||
{
|
||||
wchar_t name[NETWORK_PLAYER_NAME_LENGTH];
|
||||
short length = 0;
|
||||
short digits = number < 10 ? 1 : number < 100 ? 2 : 3;
|
||||
short digit;
|
||||
|
||||
network_game_server_bot_copy_name(name, L"Bot ", NUMBEROF(name));
|
||||
while (name[length])
|
||||
length++;
|
||||
for (digit = digits - 1; digit >= 0 && length + digit < NETWORK_PLAYER_NAME_LENGTH - 1; digit--)
|
||||
{
|
||||
name[length + digit] = (wchar_t)(L'0' + number % 10);
|
||||
number /= 10;
|
||||
}
|
||||
name[length + digits] = 0;
|
||||
network_game_server_bot_copy_name(bot.name, name, NUMBEROF(bot.name));
|
||||
}
|
||||
bot.machine_index = (char)machine_index;
|
||||
bot.controller_index = (char)controller_index;
|
||||
bot.primary_color_index = NONE;
|
||||
bot.icon_index = 0;
|
||||
{
|
||||
long player_count_by_team[NUMBER_OF_MULTIPLAYER_TEAMS] = { 0, 0 };
|
||||
long player_index;
|
||||
|
||||
for (player_index = 0; player_index < MAXIMUM_NETWORK_PLAYER_COUNT; player_index++)
|
||||
{
|
||||
struct network_player *other_player = &server->game.players[player_index];
|
||||
|
||||
if (network_player_is_valid(other_player) && VALID_INDEX(other_player->team_index, NUMBER_OF_MULTIPLAYER_TEAMS))
|
||||
player_count_by_team[other_player->team_index]++;
|
||||
}
|
||||
bot.team_index = player_count_by_team[1] < player_count_by_team[0] ? 1 : 0;
|
||||
}
|
||||
if (!player_name_is_unique(server, bot.name))
|
||||
get_unique_random_name(server, &bot);
|
||||
get_unique_random_color(server, &bot);
|
||||
bot.player_list_index = NONE;
|
||||
if (!network_game_add_player(&server->game, &bot))
|
||||
return FALSE;
|
||||
network_event("added a bot on machine %ld", machine_index);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* (the lobby) as many bots as bots.count asks for */
|
||||
static void network_game_server_update_bots(
|
||||
struct network_game_server *server)
|
||||
{
|
||||
long wanted = config_integer("bots.count");
|
||||
struct network_player *last;
|
||||
short count = network_game_server_bot_count(server, &last);
|
||||
boolean changed = FALSE;
|
||||
long machine_index;
|
||||
|
||||
/* (bot machines a new game no longer has) */
|
||||
for (machine_index = 0; machine_index < MAXIMUM_NETWORK_MACHINE_COUNT; machine_index++)
|
||||
{
|
||||
if (network_game_server_bot_machines[machine_index] &&
|
||||
server->game.machines[machine_index].machine_index != machine_index)
|
||||
{
|
||||
network_game_server_bot_machines[machine_index] = FALSE;
|
||||
}
|
||||
}
|
||||
wanted = PIN(wanted, 0, 15);
|
||||
while (count < wanted && network_game_server_add_bot(server, (short)(count + 1)))
|
||||
{
|
||||
count++;
|
||||
changed = TRUE;
|
||||
}
|
||||
while (count > wanted && network_game_server_bot_count(server, &last) > 0 && last)
|
||||
{
|
||||
long bot_machine = last->machine_index;
|
||||
|
||||
network_game_remove_player(&server->game, last);
|
||||
if (!network_game_server_machine_has_players(server, (short)bot_machine))
|
||||
{
|
||||
network_game_remove_machine(&server->game, &server->game.machines[bot_machine]);
|
||||
network_game_server_bot_machines[bot_machine] = FALSE;
|
||||
}
|
||||
count--;
|
||||
changed = TRUE;
|
||||
}
|
||||
if (changed)
|
||||
network_game_server_send_game_data_pregame(server);
|
||||
}
|
||||
#endif
|
||||
|
||||
static boolean network_game_server_idle_pregame_tasks(
|
||||
struct network_game_server *server)
|
||||
{
|
||||
@@ -4194,6 +4428,9 @@ static boolean network_game_server_idle_pregame_tasks(
|
||||
{
|
||||
long itr;
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
network_game_server_update_bots(server);
|
||||
#endif
|
||||
/* send the lobby changes collected since the last settings update */
|
||||
network_game_server_flush_game_data_pregame(server);
|
||||
|
||||
|
||||
+3
-28
@@ -44,10 +44,6 @@ MUSL_URL = f"https://musl.libc.org/releases/musl-{MUSL_VERSION}.tar.gz"
|
||||
SDL_TAG = "release-3.4.16"
|
||||
SDL_DIR = THIRD_PARTY / "SDL3"
|
||||
SDL_URL = "https://github.com/libsdl-org/SDL.git"
|
||||
# Opus, the voice chat's codec (host_voice.c), built into the host library
|
||||
OPUS_TAG = "v1.5.2"
|
||||
OPUS_DIR = THIRD_PARTY / "opus"
|
||||
OPUS_URL = "https://github.com/xiph/opus.git"
|
||||
# the Khronos OpenXR loader for Android (the VR build, host_xr.c): its
|
||||
# headers and library are linked from here; the app packages the same AAR
|
||||
# through Gradle (port/android/app/build.gradle), which also merges the
|
||||
@@ -182,10 +178,6 @@ def fetch_third_party() -> None:
|
||||
print(f"Cloning SDL3 {SDL_TAG}")
|
||||
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(SDL_DIR)],
|
||||
check=True)
|
||||
if not OPUS_DIR.is_dir():
|
||||
print(f"Cloning Opus {OPUS_TAG}")
|
||||
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", OPUS_TAG, OPUS_URL, str(OPUS_DIR)],
|
||||
check=True)
|
||||
if not OPENXR_DIR.is_dir():
|
||||
print(f"Downloading {OPENXR_URL}")
|
||||
import zipfile
|
||||
@@ -517,23 +509,6 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
||||
)
|
||||
n.build(outputs=libsdl, rule="android_sdl3", implicit=[SDL_DIR / "CMakeLists.txt"])
|
||||
|
||||
# ---------- Opus (the voice chat's codec, host_voice.c)
|
||||
|
||||
opus_build = BUILD / "opus-build"
|
||||
libopus = opus_build / "libopus.a"
|
||||
n.rule(
|
||||
name="android_opus",
|
||||
command=(f"cmake -S {OPUS_DIR} -B {opus_build} -G Ninja "
|
||||
f"-DCMAKE_TOOLCHAIN_FILE={ndk}/build/cmake/android.toolchain.cmake "
|
||||
f"-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-{ANDROID_API} -DCMAKE_BUILD_TYPE=Release "
|
||||
f"-DCMAKE_POSITION_INDEPENDENT_CODE=ON -DOPUS_BUILD_SHARED_LIBRARY=OFF -DOPUS_BUILD_TESTING=OFF "
|
||||
f"-DOPUS_BUILD_PROGRAMS=OFF -DOPUS_INSTALL_PKG_CONFIG_MODULE=OFF -DOPUS_INSTALL_CMAKE_CONFIG_MODULE=OFF "
|
||||
f"> {BUILD}/opus-configure.log && ninja -C {opus_build} > {BUILD}/opus-build.log"),
|
||||
description="ANDROID OPUS",
|
||||
pool="console",
|
||||
)
|
||||
n.build(outputs=libopus, rule="android_opus", implicit=[OPUS_DIR / "CMakeLists.txt"])
|
||||
|
||||
# ---------- the host library
|
||||
|
||||
host_objects: List[Path] = []
|
||||
@@ -548,7 +523,7 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
||||
host_cflags = " ".join([
|
||||
"-O2", "-g", "-fPIC", "-Wall", "-Wno-unused-function", "-D_GNU_SOURCE",
|
||||
f"-I{PORT_DIR}/include", f"-I{PORT_DIR}/host", f"-I{SDL_DIR}/include", f"-I{LINUX_DIR}/src",
|
||||
f"-I{TOML_DIR}", f"-I{OPENXR_DIR}/include", f"-I{OPUS_DIR}/include",
|
||||
f"-I{TOML_DIR}", f"-I{OPENXR_DIR}/include",
|
||||
])
|
||||
host_sources = sorted((PORT_DIR / "host").glob("*.c")) + [
|
||||
LINUX_DIR / "src" / "posix_files.c", LINUX_DIR / "src" / "posix_net.c",
|
||||
@@ -574,12 +549,12 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
||||
host_objects.append(table_obj)
|
||||
n.rule(
|
||||
name="android_host_link",
|
||||
command=(f"$android_host_cc -shared -o $out $in {libopus} -L{sdl_build} -L{OPENXR_DIR}/lib -lopenxr_loader "
|
||||
command=(f"$android_host_cc -shared -o $out $in -L{sdl_build} -L{OPENXR_DIR}/lib -lopenxr_loader "
|
||||
+ " ".join(f"-l{lib}" for lib in HOST_LIBRARIES)
|
||||
+ " -Wl,-z,max-page-size=16384 -Wl,--no-undefined"),
|
||||
description="ANDROID HOST LINK $out",
|
||||
)
|
||||
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl, libopus])
|
||||
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl])
|
||||
|
||||
# ---------- staging for Gradle
|
||||
|
||||
|
||||
Reference in new issue
Block a user