Compare commits

..
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 f6c2322df9 Host options in the lobby: public, bots and their skill
Y in a hosted game's lobby opens a panel to make the game public, choose
how many bots join (none by default, up to 15) and how well they play
(bots.skill: easy, normal, hard; hard is the first bots' play, normal a
little slower to notice, turn and aim). Integer settings can now be
written to config.toml as booleans could.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:05:34 +02:00
Stefan MaierandClaude Opus 5.5 39fbaefaac Keep the new settings out of the network table
The settings file is written in the table's order, so settings of another
section between network.* entries wrote [network] twice, and the whole
file was then refused (defaults used). They now follow the network
settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:52:14 +02:00
Stefan MaierandClaude Opus 5.5 c6397cb515 Bots find their way with the game's pathfinding (phase 1)
A bot walks to its goal along a path from ai/path.c (the maps' pathfinding
surfaces, as the campaign's actors use them), found again every half
second: in a fight towards its target, weaving; out of sight hunting the
nearest enemy, or roaming near someone; looking where it goes. With no
path it walks straight at the goal, and when stuck picks another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:05:12 +02:00
Stefan MaierandClaude Opus 5.5 5ba6c13097 Bots as players of the host (phase 0)
bots.count bots are added in the lobby as players of machines of the
host's own that nothing connects to (four a machine, from the top slot
down): new connections skip their slots, and they count as machines for
the countdown, so a game of the host and bots starts. Every machine sees
them as players; each tick the host thinks for them (bots.c) and takes
the result as their input through update_server_handle_distributed_input.
The first brain finds the nearest visible enemy, turns at a player's rate
with some aim error, walks at it weaving and fires on target, and else
wanders, jumping and turning when stuck. Every multiplayer map has
pathfinding surfaces for the next phase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:08:09 +02:00
16 changed files with 1025 additions and 1122 deletions

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();
}
-15
View File
@@ -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 */
-494
View File
@@ -1,494 +0,0 @@
/*
HOST_VOICE.C
The voice chat's sound (the VR port's; port/linux/game/network_voice.c
sends and receives it): the microphone, Opus, and the voices played. It
runs here, natively, where SDL's audio threads may call it.
- The microphone: SDL's default recording device, mono at 16 kHz, with
Android's voice communication preset (its echo cancelling and noise
suppression: SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET). Every 20 ms of it
that is loud enough to be speech (or follows speech closely, VAD_HANGOVER)
is encoded (Opus, VOIP, 16 kbit/s) for the guest to send.
- The voices: each speaker's packets, decoded as they come (a gap in their
numbers concealed by Opus's packet loss concealment), wait in the
speaker's buffer until JITTER_START of them is there, and are mixed, each
at the gains its latest packet was given (left and right: where the
speaker is), into a stream of their own.
The devices are opened and closed on a thread of their own (voice_worker):
opening a microphone can take a long while (an emulator's took a minute),
which the game, whose clock the other machines follow, must not wait for.
*/
#include "host.h"
#include <SDL3/SDL.h>
#include <opus.h>
#include <pthread.h>
#include <string.h>
enum
{
VOICE_RATE = 16000,
FRAME_SAMPLES = VOICE_RATE / 50,
MAXIMUM_PACKET = 200,
MAXIMUM_SPEAKERS = 32,
/* a speaker's buffer: two seconds */
SPEAKER_BUFFER = VOICE_RATE * 2,
/* played from once this much is there, after silence */
JITTER_START = FRAME_SAMPLES * 3,
/* more than this behind: the oldest dropped */
JITTER_MAXIMUM = FRAME_SAMPLES * 15,
/* frames sent on after the voice drops below the threshold */
VAD_HANGOVER = 15,
BITRATE = 16000,
};
struct speaker
{
int used;
int id;
OpusDecoder *decoder;
int has_sequence;
unsigned short sequence;
short buffer[SPEAKER_BUFFER];
int read;
int count;
int playing;
float gain_left;
float gain_right;
unsigned long long heard_ticks;
};
static struct
{
SDL_Mutex *lock;
/* the devices wanted, which the worker opens and closes to match
(under worker_lock); the recording stream is read under it too */
pthread_mutex_t worker_lock;
pthread_cond_t worker_wake;
int worker_started;
int want_playback;
int want_recording;
/* the microphone */
SDL_AudioStream *recording;
OpusEncoder *encoder;
short frame[FRAME_SAMPLES];
int frame_count;
int hangover;
float threshold;
int level;
/* the voices */
SDL_AudioStream *playback;
struct speaker speakers[MAXIMUM_SPEAKERS];
float volume;
} voice;
static void voice_lock(void)
{
if (!voice.lock)
voice.lock = SDL_CreateMutex();
SDL_LockMutex(voice.lock);
}
static void voice_unlock(void)
{
SDL_UnlockMutex(voice.lock);
}
/* the voices mixed, as SDL asks for them (on its audio thread) */
static void SDLCALL voice_playback_callback(void *userdata, SDL_AudioStream *stream, int additional, int total)
{
short mixed[2 * 512];
(void)userdata;
(void)total;
while (additional > 0)
{
int frames = additional / 4;
int frame;
int index;
if (frames > 512)
frames = 512;
if (frames <= 0)
frames = 1;
memset(mixed, 0, sizeof(mixed));
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
{
struct speaker *speaker = &voice.speakers[index];
if (!speaker->used)
continue;
if (!speaker->playing && speaker->count >= JITTER_START)
speaker->playing = 1;
if (!speaker->playing)
continue;
for (frame = 0; frame < frames && speaker->count > 0; frame++)
{
float sample = (float)speaker->buffer[speaker->read] * voice.volume;
int left = mixed[2 * frame] + (int)(sample * speaker->gain_left);
int right = mixed[2 * frame + 1] + (int)(sample * speaker->gain_right);
mixed[2 * frame] = (short)(left > 32767 ? 32767 : left < -32768 ? -32768 : left);
mixed[2 * frame + 1] = (short)(right > 32767 ? 32767 : right < -32768 ? -32768 : right);
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
speaker->count--;
}
/* (run dry: wait for enough again) */
if (!speaker->count)
speaker->playing = 0;
}
voice_unlock();
SDL_PutAudioStreamData(stream, mixed, frames * 4);
additional -= frames * 4;
}
}
static int voice_open_playback(void)
{
SDL_AudioSpec spec;
if (voice.playback)
return 1;
spec.format = SDL_AUDIO_S16;
spec.channels = 2;
spec.freq = VOICE_RATE;
voice.playback = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, voice_playback_callback,
NULL);
if (!voice.playback)
{
host_logf(HOST_LOG_WARN, "voice: cannot open the playback stream: %s", SDL_GetError());
return 0;
}
SDL_ResumeAudioStreamDevice(voice.playback);
return 1;
}
static SDL_AudioStream *voice_open_recording(void)
{
SDL_AudioSpec spec;
SDL_AudioStream *recording;
int error;
if (!voice.encoder)
{
voice.encoder = opus_encoder_create(VOICE_RATE, 1, OPUS_APPLICATION_VOIP, &error);
if (!voice.encoder)
{
host_logf(HOST_LOG_WARN, "voice: cannot make the encoder (%d)", error);
return NULL;
}
opus_encoder_ctl(voice.encoder, OPUS_SET_BITRATE(BITRATE));
opus_encoder_ctl(voice.encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
opus_encoder_ctl(voice.encoder, OPUS_SET_INBAND_FEC(1));
opus_encoder_ctl(voice.encoder, OPUS_SET_PACKET_LOSS_PERC(10));
}
/* Android's voice communication preset: echo cancelling, noise
suppression (AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION) */
SDL_SetHint(SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET, "7");
spec.format = SDL_AUDIO_S16;
spec.channels = 1;
spec.freq = VOICE_RATE;
recording = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, NULL);
if (!recording)
{
host_logf(HOST_LOG_WARN, "voice: cannot open the microphone: %s", SDL_GetError());
return NULL;
}
SDL_ResumeAudioStreamDevice(recording);
host_logf(HOST_LOG_INFO, "voice: the microphone is open");
return recording;
}
static void voice_close_recording(void)
{
SDL_AudioStream *recording;
pthread_mutex_lock(&voice.worker_lock);
recording = voice.recording;
voice.recording = NULL;
voice.level = 0;
pthread_mutex_unlock(&voice.worker_lock);
if (!recording)
return;
SDL_DestroyAudioStream(recording);
host_logf(HOST_LOG_INFO, "voice: the microphone is closed");
}
static void voice_close_playback(void)
{
SDL_AudioStream *playback = voice.playback;
voice.playback = NULL;
if (playback)
SDL_DestroyAudioStream(playback);
}
/* opens and closes the devices as wanted, for as long as the app runs */
static void *voice_worker(void *unused)
{
(void)unused;
pthread_mutex_lock(&voice.worker_lock);
for (;;)
{
int want_playback = voice.want_playback;
int want_recording = voice.want_recording;
int have_playback = voice.playback != NULL;
int have_recording = voice.recording != NULL;
if (want_playback == have_playback && want_recording == have_recording)
{
pthread_cond_wait(&voice.worker_wake, &voice.worker_lock);
continue;
}
pthread_mutex_unlock(&voice.worker_lock);
if (want_recording && !have_recording)
{
/* (opened outside the lock, which the game reads it under) */
SDL_AudioStream *recording = voice_open_recording();
pthread_mutex_lock(&voice.worker_lock);
voice.recording = recording;
pthread_mutex_unlock(&voice.worker_lock);
/* (a microphone that will not open is not tried again until
wanted anew) */
if (!recording)
{
pthread_mutex_lock(&voice.worker_lock);
voice.want_recording = 0;
pthread_mutex_unlock(&voice.worker_lock);
}
}
else if (!want_recording && have_recording)
{
voice_close_recording();
}
if (want_playback && !have_playback)
{
if (!voice_open_playback())
{
pthread_mutex_lock(&voice.worker_lock);
voice.want_playback = 0;
pthread_mutex_unlock(&voice.worker_lock);
}
}
else if (!want_playback && have_playback)
{
voice_close_playback();
}
pthread_mutex_lock(&voice.worker_lock);
}
return NULL;
}
static void voice_want(int playback, int recording)
{
if (!voice.worker_started)
{
voice.worker_started = 1;
pthread_mutex_init(&voice.worker_lock, NULL);
pthread_cond_init(&voice.worker_wake, NULL);
if (host_native_thread_create(voice_worker, NULL, 256 * 1024) != 0)
{
host_logf(HOST_LOG_WARN, "voice: cannot start the device thread");
return;
}
}
pthread_mutex_lock(&voice.worker_lock);
voice.want_playback = playback;
voice.want_recording = recording;
pthread_cond_signal(&voice.worker_wake);
pthread_mutex_unlock(&voice.worker_lock);
}
/* ---------- the guest's side */
int host_voice_start(int microphone, float volume, float threshold)
{
voice_lock();
voice.volume = volume < 0.0f ? 0.0f : volume > 4.0f ? 4.0f : volume;
voice.threshold = threshold;
voice_unlock();
voice_want(1, microphone);
return 1;
}
void host_voice_stop(void)
{
int index;
voice_want(0, 0);
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
{
if (voice.speakers[index].decoder)
opus_decoder_destroy(voice.speakers[index].decoder);
}
memset(voice.speakers, 0, sizeof(voice.speakers));
voice_unlock();
}
int host_voice_take_packets(void *buffer, int size)
{
unsigned char *out = buffer;
int used = 0;
if (!voice.worker_started)
return 0;
pthread_mutex_lock(&voice.worker_lock);
if (!voice.recording)
{
voice.frame_count = 0;
voice.hangover = 0;
pthread_mutex_unlock(&voice.worker_lock);
return 0;
}
for (;;)
{
int wanted = (FRAME_SAMPLES - voice.frame_count) * (int)sizeof(short);
int got = SDL_GetAudioStreamData(voice.recording, voice.frame + voice.frame_count, wanted);
double energy = 0.0;
int index;
if (got <= 0)
break;
voice.frame_count += got / (int)sizeof(short);
if (voice.frame_count < FRAME_SAMPLES)
break;
voice.frame_count = 0;
for (index = 0; index < FRAME_SAMPLES; index++)
energy += (double)voice.frame[index] * voice.frame[index];
energy = SDL_sqrt(energy / FRAME_SAMPLES) / 32768.0;
voice.level = (int)(energy * 1000.0);
if (energy >= voice.threshold)
voice.hangover = VAD_HANGOVER;
else if (voice.hangover > 0)
voice.hangover--;
else
continue;
if (used + 1 + MAXIMUM_PACKET > size)
continue;
{
int length = opus_encode(voice.encoder, voice.frame, FRAME_SAMPLES, out + used + 1, MAXIMUM_PACKET);
if (length > 0 && length < 256)
{
out[used] = (unsigned char)length;
used += 1 + length;
}
}
}
pthread_mutex_unlock(&voice.worker_lock);
return used;
}
int host_voice_level(void)
{
return voice.level;
}
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right)
{
struct speaker *speaker = NULL;
struct speaker *oldest = NULL;
short decoded[FRAME_SAMPLES * 6];
int frames[4];
int frame_count = 0;
int index;
if (length <= 0 || length > MAXIMUM_PACKET)
return;
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
{
if (voice.speakers[index].used && voice.speakers[index].id == id)
speaker = &voice.speakers[index];
}
if (!speaker)
{
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
{
if (!voice.speakers[index].used)
speaker = &voice.speakers[index];
else if (!oldest || voice.speakers[index].heard_ticks < oldest->heard_ticks)
oldest = &voice.speakers[index];
}
if (!speaker)
speaker = oldest;
if (!speaker->decoder)
{
int error;
speaker->decoder = opus_decoder_create(VOICE_RATE, 1, &error);
if (!speaker->decoder)
{
voice_unlock();
return;
}
}
else
{
opus_decoder_ctl(speaker->decoder, OPUS_RESET_STATE);
}
speaker->used = 1;
speaker->id = id;
speaker->has_sequence = 0;
speaker->read = speaker->count = 0;
speaker->playing = 0;
}
speaker->heard_ticks = SDL_GetTicks();
speaker->gain_left = gain_left;
speaker->gain_right = gain_right;
{
unsigned short number = (unsigned short)sequence;
if (speaker->has_sequence)
{
short gap = (short)(number - speaker->sequence);
/* (late, or again: dropped) */
if (gap <= 0)
{
voice_unlock();
return;
}
/* (lost ones: concealed, a few) */
if (gap > 1 && gap <= 4 && speaker->count > 0)
{
int missing;
for (missing = 1; missing < gap && frame_count < 3; missing++)
{
frames[frame_count] = opus_decode(speaker->decoder, NULL, 0, decoded + frame_count * FRAME_SAMPLES,
FRAME_SAMPLES, 0);
frame_count++;
}
}
}
speaker->has_sequence = 1;
speaker->sequence = number;
}
frames[frame_count] = opus_decode(speaker->decoder, data, length, decoded + frame_count * FRAME_SAMPLES,
FRAME_SAMPLES * 2, 0);
frame_count++;
for (index = 0; index < frame_count; index++)
{
int sample;
for (sample = 0; sample < frames[index] && sample < FRAME_SAMPLES * 2; sample++)
{
if (speaker->count >= JITTER_MAXIMUM)
{
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
speaker->count--;
}
speaker->buffer[(speaker->read + speaker->count) % SPEAKER_BUFFER] =
decoded[index * FRAME_SAMPLES + sample];
speaker->count++;
}
}
voice_unlock();
}
-5
View File
@@ -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
+586
View File
@@ -0,0 +1,586 @@
/*
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.
*/
#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 "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 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;
};
/* ---------- 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;
/* 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;
}
/* 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;
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;
/* 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);
}
else
{
real_point3d point;
/* hunts, or roams: along its way, looking where it goes */
if (!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;
}
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", (int)count, bots.paths_found, bots.paths_failed);
bots.paths_found = bots.paths_failed = 0;
}
}
+2 -14
View File
@@ -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 :
+3 -9
View File
@@ -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
-326
View File
@@ -1,326 +0,0 @@
/*
NETWORK_VOICE.C
The voice chat (the VR port's): an extension of the distributed netcode
(port/linux/NETCODE.md, network_distributed.c), as the VR players' hands
are (network_vr.c).
- Every tick, a machine sends what its microphone heard of its player's
speech (halo_voice.h: Opus packets of 20 ms, only while they speak) in
_distributed_message_voice (unreliable): a client to the host, which
plays it and passes it on to its other clients; the host its own to
every client.
- A machine plays each speaker's voice from where they are: a teammate
(in a game of teams) over the radio, as loud anywhere and only a little
to the side they are on; anyone else as near as they are (no further off
than VOICE_RANGE), from their side. A speaker with no unit (dead) is
heard over the radio by their team, and not by the rest.
A machine that does not know the message (a build without this, of the same
network version) drops it, as it drops any type past its own.
*/
#include "cseries.h"
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
#include "units/units.h"
#include "network_distributed.h"
#include "halo_voice.h"
#include "halo_vr.h"
#include <math.h>
void platform_log(char const *format, ...);
/* ---------- constants */
enum
{
/* an entry: the speaker, its sequence number and the packet's length,
and the packet */
VOICE_ENTRY_HEADER = 4,
VOICE_MAXIMUM_PACKET = 200,
VOICE_MAXIMUM_TAKEN = 1024,
};
/* how far off someone not on the radio is heard (world units: 60 metres) */
#define VOICE_RANGE 20.0f
/* how far to the side a voice is put: on the radio, and near */
#define VOICE_RADIO_PAN 0.3f
#define VOICE_NEAR_PAN 0.8f
/* ---------- globals */
static struct
{
boolean active;
word sequence;
long sent;
long played;
long log_time;
} network_voice;
/* ---------- private code */
static boolean network_voice_in_network_game(
void)
{
short connection = game_connection();
return (connection == _game_connection_network_server || connection == _game_connection_network_client) &&
game_in_progress();
}
/* where a player's head is, and whether they have a unit */
static boolean network_voice_player_head(
struct player_datum const *player,
real_point3d *head)
{
long unit_index = distributed_living_unit(player);
if (unit_index == NONE)
return FALSE;
unit_get_camera_position(unit_index, head);
return TRUE;
}
/* the gains a speaker is heard at here (FALSE: not heard) */
static boolean network_voice_gains(
struct player_datum const *speaker,
real *left,
real *right)
{
long listener_index = local_player_get_player_index(0);
struct player_datum *listener = listener_index != NONE ? player_try_and_get(listener_index) : NULL;
boolean teammate = listener && game_engine_has_teams() && listener->team_index == speaker->team_index;
real_point3d speaker_head;
real_point3d listener_head;
real gain = 1.0f;
real pan = 0.0f;
if (!listener)
return FALSE;
if (!network_voice_player_head(speaker, &speaker_head))
{
/* (dead: on the radio, to the team) */
if (!teammate)
return FALSE;
*left = *right = 1.0f;
return TRUE;
}
if (network_voice_player_head(listener, &listener_head))
{
real dx = speaker_head.x - listener_head.x;
real dy = speaker_head.y - listener_head.y;
real dz = speaker_head.z - listener_head.z;
real distance = (real)sqrt(dx * dx + dy * dy + dz * dz);
real flat = (real)sqrt(dx * dx + dy * dy);
float look[3];
real forward_x;
real forward_y;
real length;
struct unit_datum *unit = unit_try_and_get(distributed_living_unit(listener));
/* (where the listener looks: the head, in VR) */
if (halo_vr_look_vector(0, look))
{
forward_x = look[0];
forward_y = look[1];
}
else
{
forward_x = unit ? unit->unit.looking_vector.i : 1.0f;
forward_y = unit ? unit->unit.looking_vector.j : 0.0f;
}
length = (real)sqrt(forward_x * forward_x + forward_y * forward_y);
if (length > 0.0001f && flat > 0.0001f)
{
/* (the right: forward turned clockwise) */
pan = (dx * forward_y - dy * forward_x) / (flat * length);
}
if (!teammate)
{
gain = 1.0f - distance / VOICE_RANGE;
if (gain <= 0.0f)
return FALSE;
gain *= gain;
}
pan *= teammate ? VOICE_RADIO_PAN : VOICE_NEAR_PAN;
}
else if (!teammate)
{
/* (the listener dead: the others as from where the body is not:
not at all) */
return FALSE;
}
*left = gain * (pan > 0.0f ? 1.0f - pan : 1.0f);
*right = gain * (pan < 0.0f ? 1.0f + pan : 1.0f);
return TRUE;
}
static void network_voice_play_entries(
byte const *entries,
byte const *end,
short count,
long machine_index)
{
short index;
for (index = 0; index < count && entries + VOICE_ENTRY_HEADER <= end; index++)
{
byte player_byte = entries[0];
word sequence = (word)(entries[1] | entries[2] << 8);
short length = entries[3];
long player_index;
struct player_datum *player;
real left;
real right;
if (length <= 0 || entries + VOICE_ENTRY_HEADER + length > end)
break;
player_index = distributed_player_from_byte(player_byte);
player = player_index != NONE ? player_try_and_get(player_index) : NULL;
/* (the host: only a client's own players') */
if (player && (machine_index == NONE || distributed_machine_has_player(machine_index, player_byte)) &&
player->local_player_index == NONE && network_voice_gains(player, &left, &right))
{
halo_voice_play(player_byte, sequence, entries + VOICE_ENTRY_HEADER, length, left, right);
network_voice.played++;
}
entries += VOICE_ENTRY_HEADER + length;
}
}
/* ---------- public code */
void network_voice_new_game(
void)
{
network_voice.sequence = 0;
}
word network_voice_entry_size(
void)
{
return VOICE_ENTRY_HEADER;
}
/* after each tick (network_distributed_tick): this machine's speech sent */
void network_voice_tick(
void)
{
struct
{
struct distributed_message_header header;
byte entries[VOICE_MAXIMUM_TAKEN + 64];
} message;
byte taken[VOICE_MAXIMUM_TAKEN];
long player_index = local_player_get_player_index(0);
long size;
long offset = 0;
long used = 0;
short count = 0;
if (!network_voice_in_network_game() || !halo_voice_enabled())
{
if (network_voice.active)
{
network_voice.active = FALSE;
halo_voice_end();
}
return;
}
if (!network_voice.active)
{
network_voice.active = TRUE;
halo_voice_begin();
}
if (game_time_get() - network_voice.log_time >= 150 && (network_voice.sent || network_voice.played))
{
platform_log("voice: sent %ld, played %ld packets (in 150 ticks)", network_voice.sent, network_voice.played);
network_voice.sent = network_voice.played = 0;
network_voice.log_time = game_time_get();
}
size = halo_voice_take(taken, sizeof(taken));
if (player_index == NONE)
return;
while (offset < size)
{
short length = taken[offset];
if (length <= 0 || offset + 1 + length > size || length > VOICE_MAXIMUM_PACKET)
break;
if (used + VOICE_ENTRY_HEADER + length > (long)sizeof(message.entries))
break;
message.entries[used] = distributed_player_to_byte(player_index);
message.entries[used + 1] = (byte)(network_voice.sequence & 0xFF);
message.entries[used + 2] = (byte)(network_voice.sequence >> 8);
message.entries[used + 3] = (byte)length;
csmemcpy(message.entries + used + VOICE_ENTRY_HEADER, taken + offset + 1, length);
used += VOICE_ENTRY_HEADER + length;
offset += 1 + length;
network_voice.sequence++;
count++;
}
if (!count)
return;
network_voice.sent += count;
distributed_send(&message, _distributed_message_voice, count, (word)(sizeof(message.header) + used),
game_connection() == _game_connection_network_server ? _distributed_to_clients : _distributed_to_host);
}
/* a machine's speech: on the host a client's, played and passed on to the
other clients; on a client the host's of any other */
void network_voice_handle(
long machine_index,
void const *message,
word size)
{
struct distributed_message_header header;
byte const *entries = (byte const *)message + sizeof(header);
byte const *end = (byte const *)message + size;
if (!halo_voice_enabled() || !network_voice.active)
return;
csmemcpy(&header, message, sizeof(header));
network_voice_play_entries(entries, end, header.count, machine_index);
if (machine_index != NONE)
{
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
short number;
struct
{
struct distributed_message_header header;
byte entries[VOICE_MAXIMUM_TAKEN + 64];
} relayed;
word entries_size = (word)(end - entries);
byte const *entry = entries;
short index;
/* (only the sender's own players' speech is passed on, whole) */
if (entries_size < VOICE_ENTRY_HEADER || entries_size > sizeof(relayed.entries))
return;
for (index = 0; index < header.count; index++)
{
if (entry + VOICE_ENTRY_HEADER > end || entry[3] == 0 || entry + VOICE_ENTRY_HEADER + entry[3] > end ||
!distributed_machine_has_player(machine_index, entry[0]))
{
return;
}
entry += VOICE_ENTRY_HEADER + entry[3];
}
if (entry != end)
return;
for (number = 0; number < machine_count; number++)
{
if (machine_indices[number] == machine_index)
continue;
csmemcpy(relayed.entries, entries, entries_size);
distributed_send_to_machine(machine_indices[number], &relayed, _distributed_message_voice, header.count,
(word)(sizeof(relayed.header) + entries_size));
}
}
}
-31
View File
@@ -1,31 +0,0 @@
/*
HALO_VOICE.H
The voice chat's sound for the game (port/linux/src/voice.c: on Android the
host's microphone, Opus and mixer, port/android/host/host_voice.c; elsewhere
nothing). port/linux/game/network_voice.c sends and receives it.
*/
#ifndef __HALO_VOICE_H
#define __HALO_VOICE_H
#ifdef __cplusplus
extern "C" {
#endif
/* whether the voice chat is on (voice.enabled) */
int halo_voice_enabled(void);
/* starts playing voices, and the microphone if voice.microphone; stops */
int halo_voice_begin(void);
void halo_voice_end(void);
/* the microphone's speech since asked: Opus packets, each after a byte of
its length; returns the bytes */
int halo_voice_take(void *buffer, int size);
/* a speaker's packet, played at these gains (left, right) */
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right);
#ifdef __cplusplus
}
#endif
#endif
+32 -16
View File
@@ -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)
+1
View File
@@ -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
-87
View File
@@ -1,87 +0,0 @@
/*
VOICE.C
The voice chat's sound (halo_voice.h): on Android, the host's
(port/android/host/host_voice.c); elsewhere none.
*/
#include "halo_voice.h"
#include "platform.h"
#include "port_config.h"
#ifdef HALO_ANDROID
#include "guest_host.h"
#include <stdio.h>
static int voice_started;
/* whether the app may use the microphone, as the launcher found
(LauncherActivity.java): opened without it, Android would ask in the middle
of the game */
static int voice_microphone_allowed(void)
{
char path[512];
FILE *file;
int allowed = 0;
snprintf(path, sizeof(path), "%s/microphone_permission.txt", platform_data_root());
file = fopen(path, "rb");
if (file)
{
allowed = fgetc(file) == '1';
fclose(file);
}
return allowed;
}
int halo_voice_enabled(void)
{
return config_boolean("voice.enabled");
}
int halo_voice_begin(void)
{
int microphone;
if (voice_started)
return 1;
voice_started = 1;
microphone = config_boolean("voice.microphone") && voice_microphone_allowed();
platform_log("voice: on, %s", microphone ? "with the microphone" :
config_boolean("voice.microphone") ? "without the microphone (not allowed)" : "listening only");
return host_voice_start(microphone, (float)config_real("voice.volume"), (float)config_real("voice.threshold"));
}
void halo_voice_end(void)
{
if (!voice_started)
return;
voice_started = 0;
host_voice_stop();
platform_log("voice: off");
}
int halo_voice_take(void *buffer, int size)
{
return voice_started ? host_voice_take_packets(buffer, size) : 0;
}
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
{
if (voice_started)
host_voice_play(speaker, sequence, data, length, left, right);
}
#else
int halo_voice_enabled(void) { return 0; }
int halo_voice_begin(void) { return 0; }
void halo_voice_end(void) {}
int halo_voice_take(void *buffer, int size) { (void)buffer; (void)size; return 0; }
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
{
(void)speaker; (void)sequence; (void)data; (void)length; (void)left; (void)right;
}
#endif
+159 -45
View File
@@ -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);
}
}
}
+238 -1
View File
@@ -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
View File
@@ -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