Compare commits

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

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

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

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

No files matched your search

+4 -4
View File
@@ -19,9 +19,9 @@ builds of the latest release:
| Platform | Release | Debug |
| --- | --- | --- |
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
| Linux | [halo-linux-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-debug.zip) |
Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
@@ -32,7 +32,7 @@ if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates).
Each build of the `main` branch that passes on all three platforms is a new
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
release. The [Releases](https://github.com/cybersecurity/halo-ce-universal/releases)
page keeps the last five releases. If the latest build has a problem, get
an older build from that page.
+1 -31
View File
@@ -39,37 +39,7 @@ What differs is found at run time, not by a build of its own.
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
| 7 | Multiplayer, voice, cross-play with the Quest | |
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
## Starting it on the Frame
A button in the Steam library ("Halo VR", a non-Steam game) runs
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
anew whenever the file is newer. Updating the game is replacing that APK.
What the script does that Steam would for an Android title:
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
install Lepton links the app's data to it, a dangling link without it;
and the shader path, which Lepton reads Steam's shader dump path against;
- a container id of its own: with the shortcut's id, SteamVR's client in the
container stalled in xrCreateInstance;
- a few starts: a new APK's first start only installs it, and a start that
ends within 30 s is tried again (up to three times), unless the game was
quit (it writes `quit_requested.txt`).
Steam's "Exit game" does not reach the game (its process is the container's,
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
pause menu, twice). A container ended within 30 s of its start makes
Lepton drop the app's install, so the next start installs it again.
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
it starts; appid 3151454348 for this name and path), its pictures in
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
beside Lepton's `<frame-ip>:5555`.
| 8 | Installing it, for others | |
## The game's data
@@ -19,6 +19,9 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- the voice chat in multiplayer (host_voice.c) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- installing a new version of the game (Updater) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -1,7 +1,10 @@
package com.halo.decomp;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
@@ -35,6 +38,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final int ASK_MICROPHONE = 2;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
private File dataRoot;
@@ -60,12 +64,37 @@ public class LauncherActivity extends Activity {
passOnHardwareId();
passOnInvite(getIntent());
if (haveData()) {
startGame();
if (!askForMicrophone())
startGame();
return;
}
buildInterface();
}
/**
* The voice chat's microphone (port/android/host/host_voice.c), asked
* for here, once, on this flat panel, before the game starts: in the
* game, the question would stop it. Returns true if it asked (the game
* starts on the answer).
*/
private boolean askForMicrophone() {
SharedPreferences preferences = getSharedPreferences("launcher", MODE_PRIVATE);
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|| preferences.getBoolean("asked_microphone", false))
return false;
preferences.edit().putBoolean("asked_microphone", true).apply();
requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, ASK_MICROPHONE);
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
super.onRequestPermissionsResult(requestCode, permissions, results);
if (requestCode == ASK_MICROPHONE)
startGame();
}
/**
* An internet play invite link the app was opened with: the game
* (port/linux/src/p2p.c) picks it up from join_link.txt, whether it is
@@ -118,6 +147,23 @@ public class LauncherActivity extends Activity {
partial.delete();
}
/**
* Whether the microphone may be used, for the game (port/linux/src/
* voice.c), which opens it only then: opening it otherwise would have
* Android ask in the middle of the game.
*/
private void passOnMicrophonePermission() {
if (dataRoot == null)
return;
boolean granted =
checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
try (OutputStream out = new FileOutputStream(new File(dataRoot, "microphone_permission.txt"))) {
out.write((granted ? "1" : "0").getBytes("UTF-8"));
} catch (java.io.IOException e) {
// the game then does without the microphone
}
}
/** The game data, or the marker file of the VR self-test (host_xr.c). */
private boolean haveData() {
return dataRoot != null && (new File(dataRoot, "maps/ui.map").isFile() ||
@@ -125,6 +171,7 @@ public class LauncherActivity extends Activity {
}
private void startGame() {
passOnMicrophonePermission();
startActivity(new Intent(this, HaloActivity.class));
finish();
}
+15
View File
@@ -134,6 +134,21 @@ int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
/* darkens the eye images from their edges, strength 0..1 (0: off) */
void host_xr_set_vignette(float strength);
/* ---------- the voice chat (host_voice.c) */
/* opens the voices' playback, and the microphone if asked (else closes
it): their volume, and the level (0..1 of full scale) the microphone's
sound must reach to be sent */
int host_voice_start(int microphone, float volume, float threshold);
void host_voice_stop(void);
/* the microphone's speech since asked: Opus packets, each after a byte of
its length; returns the bytes */
int host_voice_take_packets(void *buffer, int size);
/* the microphone's level (0..1000) */
int host_voice_level(void);
/* a speaker's packet (sequence numbered), played at these gains */
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right);
/* ---------- Android */
/* the storage directories the port uses, copied into buffer */
-24
View File
@@ -301,19 +301,6 @@ static void *game_main(void *unused)
environment_set(&environment, "HALO_SAVE_ROOT", save_root);
time_zone(zone, sizeof(zone));
environment_set(&environment, "TZ", zone);
/* internet play's MQTT brokers (network.brokers_file): the APK's list,
written beside config.toml at each start, as a desktop update replaces
the file beside its game */
{
size_t brokers_size = 0;
void *brokers = SDL_LoadFile("brokers.txt", &brokers_size);
snprintf(path, sizeof(path), "%s/brokers.txt", data_root);
if (!brokers || !SDL_SaveFile(path, brokers, brokers_size))
host_logf(HOST_LOG_ERROR, "cannot write %s: %s", path, SDL_GetError());
SDL_free(brokers);
}
snprintf(path, sizeof(path), "%s/config.toml", data_root);
image = SDL_LoadFile("halo_guest.elf", &image_size);
if (!image)
@@ -326,17 +313,6 @@ static void *game_main(void *unused)
ranges: the headset's runtime, loaded by the probe, maps its libraries
wherever there is room, the Xbox window at 0x80000000 included */
snprintf(path, sizeof(path), "%s/config.toml", data_root);
{
/* (debug.gl_thread: Mesa's GL thread on or off, as the driver's
mesa_glthread, before the GL context is made; -1 leaves it be) */
double gl_thread = config_number(path, "debug.gl_thread", -1.0);
if (gl_thread >= 0.0)
{
setenv("mesa_glthread", gl_thread != 0.0 ? "true" : "false", 1);
host_logf(HOST_LOG_INFO, "Mesa's GL thread %s (debug.gl_thread)", gl_thread != 0.0 ? "on" : "off");
}
}
{
int eye_width, eye_height;
+494
View File
@@ -0,0 +1,494 @@
/*
HOST_VOICE.C
The voice chat's sound (the VR port's; port/linux/game/network_voice.c
sends and receives it): the microphone, Opus, and the voices played. It
runs here, natively, where SDL's audio threads may call it.
- The microphone: SDL's default recording device, mono at 16 kHz, with
Android's voice communication preset (its echo cancelling and noise
suppression: SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET). Every 20 ms of it
that is loud enough to be speech (or follows speech closely, VAD_HANGOVER)
is encoded (Opus, VOIP, 16 kbit/s) for the guest to send.
- The voices: each speaker's packets, decoded as they come (a gap in their
numbers concealed by Opus's packet loss concealment), wait in the
speaker's buffer until JITTER_START of them is there, and are mixed, each
at the gains its latest packet was given (left and right: where the
speaker is), into a stream of their own.
The devices are opened and closed on a thread of their own (voice_worker):
opening a microphone can take a long while (an emulator's took a minute),
which the game, whose clock the other machines follow, must not wait for.
*/
#include "host.h"
#include <SDL3/SDL.h>
#include <opus.h>
#include <pthread.h>
#include <string.h>
enum
{
VOICE_RATE = 16000,
FRAME_SAMPLES = VOICE_RATE / 50,
MAXIMUM_PACKET = 200,
MAXIMUM_SPEAKERS = 32,
/* a speaker's buffer: two seconds */
SPEAKER_BUFFER = VOICE_RATE * 2,
/* played from once this much is there, after silence */
JITTER_START = FRAME_SAMPLES * 3,
/* more than this behind: the oldest dropped */
JITTER_MAXIMUM = FRAME_SAMPLES * 15,
/* frames sent on after the voice drops below the threshold */
VAD_HANGOVER = 15,
BITRATE = 16000,
};
struct speaker
{
int used;
int id;
OpusDecoder *decoder;
int has_sequence;
unsigned short sequence;
short buffer[SPEAKER_BUFFER];
int read;
int count;
int playing;
float gain_left;
float gain_right;
unsigned long long heard_ticks;
};
static struct
{
SDL_Mutex *lock;
/* the devices wanted, which the worker opens and closes to match
(under worker_lock); the recording stream is read under it too */
pthread_mutex_t worker_lock;
pthread_cond_t worker_wake;
int worker_started;
int want_playback;
int want_recording;
/* the microphone */
SDL_AudioStream *recording;
OpusEncoder *encoder;
short frame[FRAME_SAMPLES];
int frame_count;
int hangover;
float threshold;
int level;
/* the voices */
SDL_AudioStream *playback;
struct speaker speakers[MAXIMUM_SPEAKERS];
float volume;
} voice;
static void voice_lock(void)
{
if (!voice.lock)
voice.lock = SDL_CreateMutex();
SDL_LockMutex(voice.lock);
}
static void voice_unlock(void)
{
SDL_UnlockMutex(voice.lock);
}
/* the voices mixed, as SDL asks for them (on its audio thread) */
static void SDLCALL voice_playback_callback(void *userdata, SDL_AudioStream *stream, int additional, int total)
{
short mixed[2 * 512];
(void)userdata;
(void)total;
while (additional > 0)
{
int frames = additional / 4;
int frame;
int index;
if (frames > 512)
frames = 512;
if (frames <= 0)
frames = 1;
memset(mixed, 0, sizeof(mixed));
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
{
struct speaker *speaker = &voice.speakers[index];
if (!speaker->used)
continue;
if (!speaker->playing && speaker->count >= JITTER_START)
speaker->playing = 1;
if (!speaker->playing)
continue;
for (frame = 0; frame < frames && speaker->count > 0; frame++)
{
float sample = (float)speaker->buffer[speaker->read] * voice.volume;
int left = mixed[2 * frame] + (int)(sample * speaker->gain_left);
int right = mixed[2 * frame + 1] + (int)(sample * speaker->gain_right);
mixed[2 * frame] = (short)(left > 32767 ? 32767 : left < -32768 ? -32768 : left);
mixed[2 * frame + 1] = (short)(right > 32767 ? 32767 : right < -32768 ? -32768 : right);
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
speaker->count--;
}
/* (run dry: wait for enough again) */
if (!speaker->count)
speaker->playing = 0;
}
voice_unlock();
SDL_PutAudioStreamData(stream, mixed, frames * 4);
additional -= frames * 4;
}
}
static int voice_open_playback(void)
{
SDL_AudioSpec spec;
if (voice.playback)
return 1;
spec.format = SDL_AUDIO_S16;
spec.channels = 2;
spec.freq = VOICE_RATE;
voice.playback = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, voice_playback_callback,
NULL);
if (!voice.playback)
{
host_logf(HOST_LOG_WARN, "voice: cannot open the playback stream: %s", SDL_GetError());
return 0;
}
SDL_ResumeAudioStreamDevice(voice.playback);
return 1;
}
static SDL_AudioStream *voice_open_recording(void)
{
SDL_AudioSpec spec;
SDL_AudioStream *recording;
int error;
if (!voice.encoder)
{
voice.encoder = opus_encoder_create(VOICE_RATE, 1, OPUS_APPLICATION_VOIP, &error);
if (!voice.encoder)
{
host_logf(HOST_LOG_WARN, "voice: cannot make the encoder (%d)", error);
return NULL;
}
opus_encoder_ctl(voice.encoder, OPUS_SET_BITRATE(BITRATE));
opus_encoder_ctl(voice.encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
opus_encoder_ctl(voice.encoder, OPUS_SET_INBAND_FEC(1));
opus_encoder_ctl(voice.encoder, OPUS_SET_PACKET_LOSS_PERC(10));
}
/* Android's voice communication preset: echo cancelling, noise
suppression (AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION) */
SDL_SetHint(SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET, "7");
spec.format = SDL_AUDIO_S16;
spec.channels = 1;
spec.freq = VOICE_RATE;
recording = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, NULL);
if (!recording)
{
host_logf(HOST_LOG_WARN, "voice: cannot open the microphone: %s", SDL_GetError());
return NULL;
}
SDL_ResumeAudioStreamDevice(recording);
host_logf(HOST_LOG_INFO, "voice: the microphone is open");
return recording;
}
static void voice_close_recording(void)
{
SDL_AudioStream *recording;
pthread_mutex_lock(&voice.worker_lock);
recording = voice.recording;
voice.recording = NULL;
voice.level = 0;
pthread_mutex_unlock(&voice.worker_lock);
if (!recording)
return;
SDL_DestroyAudioStream(recording);
host_logf(HOST_LOG_INFO, "voice: the microphone is closed");
}
static void voice_close_playback(void)
{
SDL_AudioStream *playback = voice.playback;
voice.playback = NULL;
if (playback)
SDL_DestroyAudioStream(playback);
}
/* opens and closes the devices as wanted, for as long as the app runs */
static void *voice_worker(void *unused)
{
(void)unused;
pthread_mutex_lock(&voice.worker_lock);
for (;;)
{
int want_playback = voice.want_playback;
int want_recording = voice.want_recording;
int have_playback = voice.playback != NULL;
int have_recording = voice.recording != NULL;
if (want_playback == have_playback && want_recording == have_recording)
{
pthread_cond_wait(&voice.worker_wake, &voice.worker_lock);
continue;
}
pthread_mutex_unlock(&voice.worker_lock);
if (want_recording && !have_recording)
{
/* (opened outside the lock, which the game reads it under) */
SDL_AudioStream *recording = voice_open_recording();
pthread_mutex_lock(&voice.worker_lock);
voice.recording = recording;
pthread_mutex_unlock(&voice.worker_lock);
/* (a microphone that will not open is not tried again until
wanted anew) */
if (!recording)
{
pthread_mutex_lock(&voice.worker_lock);
voice.want_recording = 0;
pthread_mutex_unlock(&voice.worker_lock);
}
}
else if (!want_recording && have_recording)
{
voice_close_recording();
}
if (want_playback && !have_playback)
{
if (!voice_open_playback())
{
pthread_mutex_lock(&voice.worker_lock);
voice.want_playback = 0;
pthread_mutex_unlock(&voice.worker_lock);
}
}
else if (!want_playback && have_playback)
{
voice_close_playback();
}
pthread_mutex_lock(&voice.worker_lock);
}
return NULL;
}
static void voice_want(int playback, int recording)
{
if (!voice.worker_started)
{
voice.worker_started = 1;
pthread_mutex_init(&voice.worker_lock, NULL);
pthread_cond_init(&voice.worker_wake, NULL);
if (host_native_thread_create(voice_worker, NULL, 256 * 1024) != 0)
{
host_logf(HOST_LOG_WARN, "voice: cannot start the device thread");
return;
}
}
pthread_mutex_lock(&voice.worker_lock);
voice.want_playback = playback;
voice.want_recording = recording;
pthread_cond_signal(&voice.worker_wake);
pthread_mutex_unlock(&voice.worker_lock);
}
/* ---------- the guest's side */
int host_voice_start(int microphone, float volume, float threshold)
{
voice_lock();
voice.volume = volume < 0.0f ? 0.0f : volume > 4.0f ? 4.0f : volume;
voice.threshold = threshold;
voice_unlock();
voice_want(1, microphone);
return 1;
}
void host_voice_stop(void)
{
int index;
voice_want(0, 0);
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
{
if (voice.speakers[index].decoder)
opus_decoder_destroy(voice.speakers[index].decoder);
}
memset(voice.speakers, 0, sizeof(voice.speakers));
voice_unlock();
}
int host_voice_take_packets(void *buffer, int size)
{
unsigned char *out = buffer;
int used = 0;
if (!voice.worker_started)
return 0;
pthread_mutex_lock(&voice.worker_lock);
if (!voice.recording)
{
voice.frame_count = 0;
voice.hangover = 0;
pthread_mutex_unlock(&voice.worker_lock);
return 0;
}
for (;;)
{
int wanted = (FRAME_SAMPLES - voice.frame_count) * (int)sizeof(short);
int got = SDL_GetAudioStreamData(voice.recording, voice.frame + voice.frame_count, wanted);
double energy = 0.0;
int index;
if (got <= 0)
break;
voice.frame_count += got / (int)sizeof(short);
if (voice.frame_count < FRAME_SAMPLES)
break;
voice.frame_count = 0;
for (index = 0; index < FRAME_SAMPLES; index++)
energy += (double)voice.frame[index] * voice.frame[index];
energy = SDL_sqrt(energy / FRAME_SAMPLES) / 32768.0;
voice.level = (int)(energy * 1000.0);
if (energy >= voice.threshold)
voice.hangover = VAD_HANGOVER;
else if (voice.hangover > 0)
voice.hangover--;
else
continue;
if (used + 1 + MAXIMUM_PACKET > size)
continue;
{
int length = opus_encode(voice.encoder, voice.frame, FRAME_SAMPLES, out + used + 1, MAXIMUM_PACKET);
if (length > 0 && length < 256)
{
out[used] = (unsigned char)length;
used += 1 + length;
}
}
}
pthread_mutex_unlock(&voice.worker_lock);
return used;
}
int host_voice_level(void)
{
return voice.level;
}
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right)
{
struct speaker *speaker = NULL;
struct speaker *oldest = NULL;
short decoded[FRAME_SAMPLES * 6];
int frames[4];
int frame_count = 0;
int index;
if (length <= 0 || length > MAXIMUM_PACKET)
return;
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
{
if (voice.speakers[index].used && voice.speakers[index].id == id)
speaker = &voice.speakers[index];
}
if (!speaker)
{
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
{
if (!voice.speakers[index].used)
speaker = &voice.speakers[index];
else if (!oldest || voice.speakers[index].heard_ticks < oldest->heard_ticks)
oldest = &voice.speakers[index];
}
if (!speaker)
speaker = oldest;
if (!speaker->decoder)
{
int error;
speaker->decoder = opus_decoder_create(VOICE_RATE, 1, &error);
if (!speaker->decoder)
{
voice_unlock();
return;
}
}
else
{
opus_decoder_ctl(speaker->decoder, OPUS_RESET_STATE);
}
speaker->used = 1;
speaker->id = id;
speaker->has_sequence = 0;
speaker->read = speaker->count = 0;
speaker->playing = 0;
}
speaker->heard_ticks = SDL_GetTicks();
speaker->gain_left = gain_left;
speaker->gain_right = gain_right;
{
unsigned short number = (unsigned short)sequence;
if (speaker->has_sequence)
{
short gap = (short)(number - speaker->sequence);
/* (late, or again: dropped) */
if (gap <= 0)
{
voice_unlock();
return;
}
/* (lost ones: concealed, a few) */
if (gap > 1 && gap <= 4 && speaker->count > 0)
{
int missing;
for (missing = 1; missing < gap && frame_count < 3; missing++)
{
frames[frame_count] = opus_decode(speaker->decoder, NULL, 0, decoded + frame_count * FRAME_SAMPLES,
FRAME_SAMPLES, 0);
frame_count++;
}
}
}
speaker->has_sequence = 1;
speaker->sequence = number;
}
frames[frame_count] = opus_decode(speaker->decoder, data, length, decoded + frame_count * FRAME_SAMPLES,
FRAME_SAMPLES * 2, 0);
frame_count++;
for (index = 0; index < frame_count; index++)
{
int sample;
for (sample = 0; sample < frames[index] && sample < FRAME_SAMPLES * 2; sample++)
{
if (speaker->count >= JITTER_MAXIMUM)
{
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
speaker->count--;
}
speaker->buffer[(speaker->read + speaker->count) % SPEAKER_BUFFER] =
decoded[index * FRAME_SAMPLES + sample];
speaker->count++;
}
}
voice_unlock();
}
+3 -195
View File
@@ -38,8 +38,6 @@ picture; the copy flips them to GL's bottom-up images.
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sched.h>
#include <stdio.h>
#define XR_USE_PLATFORM_ANDROID
#define XR_USE_GRAPHICS_API_OPENGL_ES
@@ -121,22 +119,6 @@ static struct
int left_grip_valid;
int has_performance_settings;
/* (XR_META_performance_metrics: the compositor's counts, in the log,
for telling frames shown late from the game's own pace) */
int has_metrics;
PFN_xrQueryPerformanceMetricsCounterMETA query_metric;
XrPath metric_paths[16];
uint32_t metric_count;
struct timespec metrics_logged;
/* (SteamVR: the frames' thread kept on the fast cores, xr_fast_cores) */
int steamvr;
int cores_pinned;
/* (with the metrics: the time the copies into the layers' images and
xrEndFrame took, since the last line, and the frames) */
double copy_seconds[HALO_XR_LAYER_COUNT];
double end_seconds;
double acquire_seconds;
int timed_frames;
int has_refresh_rate;
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate;
PFN_xrGetDisplayRefreshRateFB get_refresh_rate;
@@ -1098,9 +1080,6 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
xr.has_refresh_rate = has_extension(extensions, extension_count, XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
if (xr.has_refresh_rate)
enabled[enabled_count++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
xr.has_metrics = has_extension(extensions, extension_count, XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
if (xr.has_metrics)
enabled[enabled_count++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
android.applicationVM = machine;
android.applicationActivity = activity;
@@ -1134,7 +1113,6 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
if (XR_SUCCEEDED(xrGetInstanceProperties(xr.instance, &instance)))
{
xr.quad_alpha_from_colour = strstr(instance.runtimeName, "SteamVR") != NULL;
xr.steamvr = xr.quad_alpha_from_colour;
host_logf(HOST_LOG_INFO, "xr: runtime %s", instance.runtimeName);
}
if (choice)
@@ -1248,25 +1226,6 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
xr.get_refresh_rate(xr.session, &info->refresh_rate);
}
if (xr.has_metrics)
{
PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumerate = NULL;
PFN_xrSetPerformanceMetricsStateMETA set_state = NULL;
XrPerformanceMetricsStateMETA state = { XR_TYPE_PERFORMANCE_METRICS_STATE_META };
xrGetInstanceProcAddr(xr.instance, "xrEnumeratePerformanceMetricsCounterPathsMETA",
(PFN_xrVoidFunction *)&enumerate);
xrGetInstanceProcAddr(xr.instance, "xrSetPerformanceMetricsStateMETA", (PFN_xrVoidFunction *)&set_state);
xrGetInstanceProcAddr(xr.instance, "xrQueryPerformanceMetricsCounterMETA",
(PFN_xrVoidFunction *)&xr.query_metric);
state.enabled = XR_TRUE;
xr.has_metrics = enumerate && set_state && xr.query_metric &&
XR_SUCCEEDED(enumerate(xr.instance, 16, &xr.metric_count, xr.metric_paths)) &&
XR_SUCCEEDED(set_state(xr.session, &state));
host_logf(HOST_LOG_INFO, "xr: performance metrics %s (%u counters)", xr.has_metrics ? "on" : "off",
xr.has_metrics ? xr.metric_count : 0);
}
if (xr.has_performance_settings)
{
/* the game's one busy thread and its GPU work both need the clocks
@@ -1446,30 +1405,8 @@ int host_xr_copy_layer_array(int layer, uint32_t texture, int array_layer)
/* copies part of a render target (left, top, right, bottom, 0..1 from its
top left) into a layer's image */
static double seconds_now(void)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (double)now.tv_sec + (double)now.tv_nsec * 1e-9;
}
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom);
int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom)
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int copied = copy_layer_region(layer, texture, clear_alpha, left, top, right, bottom);
if (xr.has_metrics && layer >= 0 && layer < HALO_XR_LAYER_COUNT)
xr.copy_seconds[layer] += seconds_now() - start;
return copied;
}
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom)
{
struct xr_swapchain *swapchain;
XrSwapchainImageAcquireInfo acquire = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
@@ -1488,14 +1425,8 @@ static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float
return 0;
}
wait.timeout = XR_INFINITE_DURATION;
{
double start = xr.has_metrics ? seconds_now() : 0.0;
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
return 0;
if (xr.has_metrics)
xr.acquire_seconds += seconds_now() - start;
}
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
return 0;
gl_save(&saved);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, swapchain->framebuffers[swapchain->acquired_index]);
@@ -1589,59 +1520,6 @@ static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float
return 1;
}
/* The Steam Frame: Android in a container, without XR_EXT_performance_settings
to ask for the clocks, and the scheduler free to put the game's one busy
thread, which simulates and draws each frame, on a slow core (its two
2.3 GHz ones) now and then: frames late to the compositor, shown twice,
the picture doubled as the head turns. The thread that ends the frames is
kept on the cores no slower than nine tenths of the fastest. */
static void xr_fast_cores(void)
{
cpu_set_t set;
long fastest = 0, speeds[CPU_SETSIZE];
int cpu, count = 0;
char line[128] = "";
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
{
char path[96];
FILE *file;
speeds[cpu] = 0;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu);
file = fopen(path, "r");
if (!file)
continue;
if (fscanf(file, "%ld", &speeds[cpu]) != 1)
speeds[cpu] = 0;
fclose(file);
if (speeds[cpu] > fastest)
fastest = speeds[cpu];
}
if (!fastest)
{
host_logf(HOST_LOG_INFO, "xr: the cores' speeds unknown: the frames' thread left where it runs");
return;
}
CPU_ZERO(&set);
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
{
if (speeds[cpu] && speeds[cpu] * 10 >= fastest * 9)
{
char item[8];
CPU_SET(cpu, &set);
count++;
snprintf(item, sizeof(item), " %d", cpu);
strcat(line, item);
}
}
if (count && sched_setaffinity(0, sizeof(set), &set) == 0)
host_logf(HOST_LOG_INFO, "xr: the frames' thread on the fast cores:%s (fastest %ld MHz)", line, fastest / 1000);
else
host_logf(HOST_LOG_INFO, "xr: the frames' thread could not be kept on the fast cores");
}
int host_xr_end_frame(const struct halo_xr_submit *submit)
{
XrFrameEndInfo end = { XR_TYPE_FRAME_END_INFO };
@@ -1784,81 +1662,11 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
}
}
if (xr.steamvr && !xr.cores_pinned)
{
xr.cores_pinned = 1;
if (!getenv("HALO_XR_ANY_CORE"))
xr_fast_cores();
}
/* (every five seconds, the compositor's counters, and the core the
frames' thread is on) */
if (xr.has_metrics)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec - xr.metrics_logged.tv_sec >= 5)
{
char line[768] = "";
uint32_t counter;
xr.metrics_logged = now;
for (counter = 0; counter < xr.metric_count && counter < 16; counter++)
{
XrPerformanceMetricsCounterMETA value = { XR_TYPE_PERFORMANCE_METRICS_COUNTER_META };
char name[XR_MAX_PATH_LENGTH];
uint32_t length = 0;
char item[96];
if (!XR_SUCCEEDED(xr.query_metric(xr.session, xr.metric_paths[counter], &value)) ||
!XR_SUCCEEDED(xrPathToString(xr.instance, xr.metric_paths[counter], sizeof(name), &length, name)))
{
continue;
}
snprintf(item, sizeof(item), " %s=%g", strrchr(name, '/') ? strrchr(name, '/') + 1 : name,
(value.counterFlags & XR_PERFORMANCE_METRICS_COUNTER_FLOAT_VALUE_VALID_BIT_META) ?
(double)value.floatValue : (double)value.uintValue);
if (strlen(line) + strlen(item) < sizeof(line))
strcat(line, item);
}
host_logf(HOST_LOG_INFO, "xr: metrics%s, on core %d", line, sched_getcpu());
if (xr.timed_frames)
{
double frames = (double)xr.timed_frames;
char copies[256] = "";
int timed;
for (timed = 0; timed < HALO_XR_LAYER_COUNT; timed++)
{
char item[24];
snprintf(item, sizeof(item), " %d:%.2f", timed, xr.copy_seconds[timed] * 1e3 / frames);
strcat(copies, item);
xr.copy_seconds[timed] = 0.0;
}
host_logf(HOST_LOG_INFO, "xr: per frame ms: copies (layer:ms)%s; of them waiting for images %.2f; "
"xrEndFrame %.2f", copies, xr.acquire_seconds * 1e3 / frames, xr.end_seconds * 1e3 / frames);
xr.acquire_seconds = xr.end_seconds = 0.0;
xr.timed_frames = 0;
}
}
}
end.displayTime = xr.frame_state.predictedDisplayTime;
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
end.layerCount = layer_count;
end.layers = layers;
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int ended = xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
if (xr.has_metrics)
{
xr.end_seconds += seconds_now() - start;
xr.timed_frames++;
}
return ended;
}
return xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
}
void host_xr_set_vignette(float strength)
+5
View File
@@ -60,6 +60,11 @@ host_xr_copy_layer_region
host_xr_end_frame
host_xr_haptic
host_xr_set_vignette
host_voice_start
host_voice_stop
host_voice_take_packets
host_voice_level
host_voice_play
host_xr_copy_layer_array
host_android_path
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

-85
View File
@@ -1,85 +0,0 @@
#!/bin/bash
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
# and installs it anew whenever the APK is newer than the installed one
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
# (a container of its own, not the shortcut's id: with that, SteamVR's
# client in the container waited on xrCreateInstance)
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
# (what Steam gives an Android title of its own, and a non-Steam game not:
# the app's data in the container links to the compat data path's
# internal/, a dangling link without it; and the shader paths: Lepton reads
# the dump path against the other)
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
APK="$HOME/Documents/HaloCE/HaloVR.apk"
LOG="$HOME/Documents/HaloCE/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
rm -f "$QUIT"
CONTEXT="steamlaunch-$SteamAppId"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
log "start (pid $$, parent $PPID)"
# Steam's "Exit game" ends the game's processes as Steam knows them (this
# script, under Steam's reaper), not the container, which podman runs: a
# signal here is passed on to Lepton, and a watcher of its own takes the
# container down once this script is gone (killed)
stopping=0
child=0
stop()
{
log "signal $1"
stopping=1
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
}
trap 'stop TERM' TERM
trap 'stop INT' INT
trap 'stop HUP' HUP
script=$$
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
' </dev/null >/dev/null 2>&1 &
# the game's log while it runs (Lepton's logcat of its container), for
# looking into problems: logcat.txt, anew each start
LOGCAT="$HOME/Documents/HaloCE/logcat.txt"
: > "$LOGCAT"
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do
"'"$LEPTON"'" logcat "'"$CONTEXT"'" 2>/dev/null | grep -v "^ERROR: \|^steamlaunch-\|^$" >> "'"$LOGCAT"'"
sleep 3
done
' </dev/null >/dev/null 2>&1 &
run()
{
"$LEPTON" waitforexitandrun -- "$APK" &
child=$!
# (wait returns early when a signal comes: until Lepton is gone)
while kill -0 "$child" 2>/dev/null; do
wait "$child"
done
child=0
}
# (a new APK is only installed on its first start, not started; and a start
# right after a game that was ended hard can fail in SteamVR, which then
# needs a moment: again, up to three times, while it ends within 30 s)
for attempt in 1 2 3; do
started=$SECONDS
run
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
break
fi
log "ended within $((SECONDS - started)) s: again"
sleep 5
done
rm -f "$QUIT"
log "end"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

@@ -2,7 +2,7 @@
<defs>
<radialGradient id="cone" gradientUnits="userSpaceOnUse" cx="167.994" cy="346.437" r="165.922"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#000"/></radialGradient><radialGradient id="coned" gradientUnits="userSpaceOnUse" cx="167.994" cy="346.437" r="165.922"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#878787"/></radialGradient>
<g id="art">
<path d="M167.994,346.437 L54.604,180.515 L281.384,180.515 Z" fill="url(#cone)"/>
<path d="M167.994,346.437 L69.695,202.598 L266.292,202.598 Z" fill="url(#cone)"/>
<circle cx="167.994" cy="345.987" r="53.201" fill="#878787"/>
<path d="M167.994,346.437 L137.767,302.207 A53.201,53.201 0 0 1 198.220,302.207 Z" fill="url(#coned)"/>
<!-- "15m" pixel label -->

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -2,7 +2,7 @@
<defs>
<radialGradient id="cone" gradientUnits="userSpaceOnUse" cx="126.000" cy="130.342" r="128.840"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#000"/></radialGradient><radialGradient id="coned" gradientUnits="userSpaceOnUse" cx="126.000" cy="130.342" r="128.840"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#878787"/></radialGradient>
<g id="art">
<path d="M126.000,130.342 L37.886,1.502 L214.114,1.502 Z" fill="url(#cone)"/>
<path d="M126.000,130.342 L49.632,18.677 L202.368,18.677 Z" fill="url(#cone)"/>
<circle cx="126.000" cy="129.985" r="41.776" fill="#878787"/>
<path d="M126.000,130.342 L102.251,95.616 A41.776,41.776 0 0 1 149.749,95.616 Z" fill="url(#coned)"/>
<!-- "15m" pixel label -->

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

+1 -1
View File
@@ -51,7 +51,7 @@ until it is redrawn.
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` |
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` |
13 of 156 frames are placeholders.
13 of 155 frames are placeholders.
The PC version's `ui\gamespy` and `ui\ticker` fonts are drawn with `ui\small_ui`, which
the Xbox's map has.
-3
View File
@@ -318,9 +318,6 @@
<bitmap name="main_menu/multiplayer_type_select/join_game/header_server_browser">
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/lobby/header_add_player">
<frame png="ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/lobby/header_lobby">
<frame png="ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png" width="512" height="64"/>
</bitmap>
@@ -1,93 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_screen" width="640" height="480" flags="pass_unhandled_to_focused_child main_menu_if_no_history" bitmap="bitmaps/gradient">
<on event="created" run="port lobby open"/>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<on event="created" run="net server accept conx"/>
<on event="created" run="net server allow start"/>
<on event="b" run="port lobby leave" back="true"/>
<on event="back" run="port lobby leave" back="true"/>
<on event="start" run="port lobby join" open="main_menu/multiplayer_type_select/lobby/player_profile_screen" branch="true"/>
<on event="b" run="net game unjoin player" back="true"/>
<on event="back" run="net game unjoin player" back="true"/>
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_list"/>
<child widget="main_menu/multiplayer_type_select/lobby/header_lobby"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_join_help"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/header_lobby" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/lobby/header_lobby">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_join_help" type="text" controller="1" left="355" top="446" width="275" height="24" font="ui\small_ui" color="#FF2896FF" align="right" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
<data input="net splitscreen prejoin players"/>
<data input="port lobby update"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_0" x="20" y="73"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_1" x="20" y="103"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_2" x="20" y="133"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_3" x="20" y="163"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_4" x="20" y="193"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_5" x="20" y="223"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_6" x="20" y="253"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_7" x="20" y="283"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_8" x="20" y="313"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_9" x="20" y="343"/>
<child widget="main_menu/multiplayer_type_select/lobby/list_item_10" x="20" y="373"/>
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
<child widget="main_menu/new_select/list_item_4" x="20" y="193"/>
<child widget="main_menu/new_select/list_item_5" x="20" y="223"/>
<child widget="main_menu/new_select/list_item_6" x="20" y="253"/>
<child widget="main_menu/new_select/list_item_7" x="20" y="283"/>
<child widget="main_menu/new_select/list_item_8" x="20" y="313"/>
<child widget="main_menu/new_select/list_item_9" x="20" y="343"/>
<child widget="main_menu/new_select/list_item_10" x="20" y="373"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_0" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd_top" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_1" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_2" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_3" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_4" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_5" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_6" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_7" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_8" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_9" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/list_item_10" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="left right" run="swap player team"/>
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_desc" width="640" height="480">
<child widget="main_menu/current_profile_name"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_right_item" x="22" y="2"/>
@@ -95,8 +36,6 @@
<widget name="main_menu/multiplayer_type_select/lobby/lobby_right_item" controller="1" left="406" top="75" width="162" height="326" bitmap="bitmaps/spinner_list_3_wide_item_background">
<child widget="main_menu/multiplayer_type_select/lobby/lobby_map_pic"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_map_name"/>
<child widget="main_menu/solo_level_select/replay_level_right_pic"/>
<child widget="main_menu/solo_level_select/replay_level_right_name"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_game_data"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_map_pic" controller="1" left="419" top="87" width="140" height="114" bitmap="ui\shell\bitmaps\mp_map_grafix">
@@ -106,16 +45,10 @@
<widget name="main_menu/multiplayer_type_select/lobby/lobby_game_data" type="text" controller="1" left="417" top="250" width="146" height="144" font="ui\small_ui" color="#FF2896FF">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_team" x="120" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_start" x="250" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_add" x="380" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_team" x="250" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_start" x="380" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_leave" x="510" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_button_add" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="ADD PLAYER" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port lobby add player"/>
<on event="start" run="port lobby add player"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/lobby_button_team" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="SWITCH TEAM" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="swap player team"/>
<on event="start" run="swap player team"/>
@@ -131,46 +64,14 @@
<on event="start" run="mouse emit back event"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<on event="created" run="net server defer start"/>
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
<child widget="main_menu/multiplayer_type_select/lobby/player_profile_list"/>
<child widget="main_menu/multiplayer_type_select/lobby/header_add_player"/>
<child widget="main_menu/multiplayer_type_select/lobby/player_profile_help" x="20" y="416"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/header_add_player" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/lobby/header_add_player">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/profile_manager/player_profile_extended_desc">
<data input="3wide player profile list update"/>
<on event="created" run="port lobby player list initialize"/>
<on event="deleted" run="player profile list dispose"/>
<on event="custom_activation" run="port lobby player choose" back="true" branch="true"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_0" x="20" y="73"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_1" x="20" y="103"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_2" x="20" y="133"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_3" x="20" y="163"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_4" x="20" y="193"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_5" x="20" y="223"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_6" x="20" y="253"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_7" x="20" y="283"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_8" x="20" y="313"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_9" x="20" y="343"/>
<child widget="main_menu/multiplayer_type_select/coop/list_item_10" x="20" y="373"/>
<child widget="main_menu/multiplayer_type_select/coop/player_2_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_help" type="text" width="350" height="24" text="The new player's profile." font="ui\small_ui" color="#FF2896FF" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<on event="created" run="port lobby open"/>
<on event="b" run="port lobby preview leave" back="true"/>
<on event="back" run="port lobby preview leave" back="true"/>
<on event="start" run="port lobby preview add" open="main_menu/multiplayer_type_select/lobby/player_profile_screen" branch="true"/>
<on event="b" back="true"/>
<on event="back" back="true"/>
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_list"/>
<child widget="main_menu/multiplayer_type_select/lobby/header_lobby"/>
<child widget="main_menu/multiplayer_type_select/lobby/lobby_join_help"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
<widget name="main_menu/multiplayer_type_select/lobby/preview_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child left_right_tabs_items" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
<data input="port lobby preview update"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_status" x="30" y="75"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_bar" y="414"/>
@@ -178,8 +79,7 @@
<widget name="main_menu/multiplayer_type_select/lobby/preview_status" type="text" controller="1" width="360" height="300" font="ui\large_ui" color="#FF2896FF">
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_join" x="250" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_add" x="380" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_join" x="380" y="1"/>
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_back" x="510" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_join" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="JOIN GAME" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
@@ -187,11 +87,6 @@
<on event="start" run="port lobby preview join"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_add" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="ADD PLAYER" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port lobby add player"/>
<on event="start" run="port lobby add player"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_back" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="BACK" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="mouse emit back event"/>
<on event="start" run="mouse emit back event"/>
@@ -13,9 +13,6 @@
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_pic"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_data"/>
<child widget="main_menu/solo_level_select/replay_level_right_name"/>
<child widget="main_menu/solo_level_select/replay_level_right_pic"/>
<child widget="main_menu/solo_level_select/replay_level_right_data"/>
</widget>
<widget name="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name" type="text" controller="1" left="417" top="204" width="146" height="43" string_list="main_menu/mp_map_list" string_index="2" font="ui\large_ui" color="#FF2896FF">
</widget>
@@ -36,9 +33,7 @@
<on event="created" run="mp level list initialize"/>
<on event="deleted" run="mp level list dispose"/>
<on event="custom_activation" run="mp level select" open="main_menu/multiplayer_type_select/connected/gametype_select_screen_wrapper"/>
<on event="b" run="port map list back"/>
<on event="back" run="port map list back"/>
<child widget="main_menu/new_select/list_item_0_map_kind" x="82" y="73"/>
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
@@ -18,10 +18,6 @@
<child widget="main_menu/multiplayer_type_select/server_settings/op_max_players" x="54" y="101"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="129"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_listing" x="54" y="157"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" x="54" y="185"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" x="54" y="213"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" x="54" y="241"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" x="54" y="241"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="185"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="213"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="241"/>
@@ -78,50 +74,6 @@
<widget name="main_menu/multiplayer_type_select/server_settings/listing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="PUBLIC|PRIVATE" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/friendly_fire_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/friendly_fire_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/friendly_fire_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="10" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/friendly_fire_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|ON|SHIELD ONLY|EXPLOSIVES ONLY" setting="network.coop_friendly_fire" values="off|on|shields_only|explosives_only" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/extra_enemies_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/extra_enemies_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/extra_enemies_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="11" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/extra_enemies_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NONE|PER PLAYER|STATIC MULTIPLIER" setting="network.coop_enemies_mode" values="none|per_player|multiplier" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_per_player_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_per_player_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_per_player_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="12" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_per_player_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="25%|50%|100%|150%|200%" setting="network.coop_enemies" values="25|50|100|150|200" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="13" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="2X|4X|8X|16X|32X" setting="network.coop_enemies_multiplier" values="2|4|8|16|32" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_game_type" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
<on event="start" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
@@ -207,10 +159,6 @@
<string text="INDICATOR OPTIONS:"/>
<string text="TEAMPLAY OPTIONS:"/>
<string text="LISTING:"/>
<string text="FRIENDLY FIRE:"/>
<string text="EXTRA ENEMIES:"/>
<string text="PER PLAYER:"/>
<string text="MULTIPLIER:"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/help_strings">
<string text=""/>
@@ -225,14 +173,5 @@
<string text="Friendly fire and team balance, for this game."/>
<string text="Anyone can see and join your game: it is listed\nin everyone's Server Browser."/>
<string text="Only players with your invite link can join."/>
<string text="Players can not be hurt by weapons and explosives\nfired by the other players."/>
<string text="Players can be hurt by weapons or explosives\nfired by the other players."/>
<string text="Damage from the other players will only reduce\nshields. Health will be unaffected."/>
<string text="Players can be hurt by damage from explosives\nfired by the other players."/>
<string text="Enemy squads are as the campaign has them."/>
<string text="Enemy squads grow with the players: by the amount\nbelow for each player past the first."/>
<string text="Enemy squads are the amount below times as large,\nhowever many players there are."/>
<string text="For each player past the first, enemy squads get\nthis much more of themselves (100%: as many again)."/>
<string text="Each enemy squad is this many times as large."/>
</strings>
</menus>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/new_select/list_item_0_map_kind" width="256" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds_sm" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<child widget="main_menu/new_select/list_item_0_map_kind_spinner" x="15" y="2"/>
</widget>
<widget name="main_menu/new_select/list_item_0_map_kind_spinner" type="spinner" width="226" height="22" flags="left_right_tabs_items" string_list="main_menu/new_select/var_map_kinds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 226 19 232">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/new_select/var_map_kinds">
<string text="SINGLEPLAYER"/>
<string text="MULTIPLAYER"/>
</strings>
</menus>
@@ -14,15 +14,13 @@
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help">
<data input="port settings help"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_mode" x="54" y="73" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" x="54" y="99" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" x="54" y="99" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" x="54" y="125" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="151"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="177" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="203"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="229" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="255"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="281"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_scale" x="54" y="99" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="125"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="151" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="177"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="203" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="229"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="255"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -39,38 +37,20 @@
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/mode_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="FULLSCREEN|BORDERLESS|WINDOWED" setting="display.mode" values="fullscreen|borderless|windowed" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_spinner" x="320" y="1"/>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_scale" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NATIVE" setting="display.resolution" values="native" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="1280 x 960" setting="display.window_size" values="1280x960" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NATIVE|ORIGINAL" setting="display.resolution_scaling" values="native|original" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="640 x 480|1280 x 960|1920 x 1440|2560 x 1920" setting="display.window_scale" values="1|2|3|4" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.vsync" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -79,7 +59,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="AUTO|30|60|120|144|165|240|NONE" setting="display.max_fps" values="0|30|60|120|144|165|240|-1" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -88,7 +68,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.interpolation" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -97,7 +77,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.direct_camera" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -106,7 +86,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_hud" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -115,7 +95,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="9" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_text" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -132,9 +112,7 @@
</widget>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels">
<string text="DISPLAY MODE:"/>
<string text="RESOLUTION:"/>
<string text="WINDOW SIZE:"/>
<string text="RESOLUTION SCALING:"/>
<string text="V-SYNC:"/>
<string text="FRAME RATE LIMIT:"/>
<string text="SMOOTH MOTION:"/>
@@ -144,10 +122,8 @@
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings">
<string text=""/>
<string text="Fullscreen takes the display; borderless covers it\nwith a window. F11 switches to the window."/>
<string text="What fullscreen and borderless draw at. Fullscreen\nsets the display to it; borderless scales it."/>
<string text="The window's size: 4:3, then 16:10, 16:9 and 21:9\n(its edges can also be dragged)."/>
<string text="Native draws at the resolution; Original draws\nthe Xbox's 640x480 and scales it up."/>
<string text="Fullscreen and borderless draw at the display's\nresolution; windowed, 640x480 scaled. F11: window."/>
<string text="The window's size when windowed (its edges can\nalso be dragged)."/>
<string text="Wait for the display between frames, so that the\npicture never tears."/>
<string text="With V-Sync off, the most frames a second. Auto:\ntwice the display's refresh rate."/>
<string text="Draw a frame for every display refresh, blending\nbetween the game's 30 ticks a second."/>
@@ -14,9 +14,6 @@
<child widget="main_menu/solo_level_select/difficulty_indicator" x="416" y="343"/>
<child widget="main_menu/solo_level_select/difficulty_indicator" x="466" y="343"/>
<child widget="main_menu/solo_level_select/difficulty_indicator" x="516" y="343"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_pic"/>
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_data"/>
</widget>
<widget name="main_menu/solo_level_select/replay_level_right_name" type="text" controller="1" left="417" top="204" width="146" height="43" string_list="main_menu/map_list" string_index="2" font="ui\large_ui" color="#FF2896FF">
</widget>
@@ -41,7 +38,7 @@
<on event="created" run="initialize sp level list solo"/>
<on event="deleted" run="dispose sp level list"/>
<on event="custom_activation" run="solo level set map" open="main_menu/difficulty_select/difficulty_select_list_screen"/>
<child widget="main_menu/new_select/list_item_0_map_kind" x="82" y="73"/>
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

-2
View File
@@ -17,7 +17,6 @@
"ce/main_menu.multiplayer_type_select.mp_map_select.xml",
"ce/main_menu.multiplayer_type_select.server_settings.xml",
"ce/main_menu.multiplayer_type_select.xml",
"ce/main_menu.new_select.port.xml",
"ce/main_menu.new_select.xml",
"ce/main_menu.player_profiles_select.xml",
"ce/main_menu.profile_manager.xml",
@@ -55,7 +54,6 @@
"ce/port/main_menu/multiplayer_type_select/coop/header_player_2.png",
"ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png",
"ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png",
"ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png",
"ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png",
"ce/port/pause/pausebox_center__0.png",
"ce/port/pause/pausebox_center__1.png",
@@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
<defs>
<filter id="soft" filterUnits="userSpaceOnUse" x="-60" y="-60" width="632" height="184">
<feGaussianBlur stdDeviation="14.23 13.06"/>
</filter>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="64">
<rect width="512" height="59" fill="#fff"/>
</mask>
</defs>
<g mask="url(#cut)">
<rect x="29.08" y="28.05" width="226.48" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 563 B

-10
View File
@@ -1,10 +0,0 @@
# The public MQTT brokers of internet play: the machines of an invite find
# each other through them, and the server browser's listings go through
# them. The game uses every one at once (up to 4), so any one working is
# enough. One host:port on each line; "#" starts a comment.
# (The game reads this file beside config.toml: network.brokers_file. An
# update replaces it; keep a list of your own under another name.)
opence.milenko.org:1883
broker.emqx.io:1883
broker.hivemq.com:1883
test.mosquitto.org:1883
+4 -74
View File
@@ -21,64 +21,6 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
vehicle it drives) from its local input at once. Remote players are
driven by the inputs the host relays every tick, the latest one held
until a newer arrives.
- **Actors driven.** Only the host runs the AI. Each tick it sends its
clients the control its actors gave their units (how they move, where
they face, aim and look, their trigger and buttons, their animation
impulses) with the units' state, those near a client's players every
tick and others less often; a client drives each unit with the latest
it has, as it drives a remote player's, until it hears nothing of it for
two seconds (`port/linux/game/network_actors.c`).
- **Co-op.** A network game on a campaign level with no game engine
(Create Game's Map screen, its SINGLEPLAYER maps, over LAN and the
internet) is co-op. Only the host runs the
level's scripts and spawns players. `network_coop.c` sends the clients
everything the scripts do that they would otherwise miss:
- every tick: the cinematic, camera, screen fade, the HUD settings the
scripts control (what is shown, the mission timer), the skip vote, and
which teams are allies and friends (the scripts' allegiances, so the
marines are the players' allies on every machine);
- once each, numbered so nothing is applied twice: script sounds,
chapter titles, help and objective text, "Checkpoint" messages, screen
shake, nav points, custom animations on units and scenery, and units
opening and closing (dropships' doors);
- device groups (doors, elevators, switches; a client sets none itself,
and its player's use of one is relayed to the host), and each device's
position and power as it changes: a client puts its device where the
host's is once that stops, or if they drift apart while it moves;
- which named objects exist, so scripted creates and deletes match.
Every machine follows the host's structure BSP: a switch is sent at once
and reliably, and each client's input says which BSP it has loaded.
Until a client has the host's, the host takes none of its players'
movement and none of its loading zones, and after any switch no loading
zone switches again until every machine has the new BSP (ten seconds at
most). A loading zone into a
BSP the team hasn't been in brings every player to whoever crossed it,
as split screen does; one back into a BSP it has been in switches only
with two thirds of the living players at it (in the trigger, or within
4 world units of the player in it), so one player can't drag the team
back through the level. A player outside the loaded BSP and falling for
two seconds is brought back beside a teammate. A dead player watches a
living teammate (`coop_spectate.c`) and comes back beside one once it is
safe. With everyone dead they come back where they were at the last
checkpoint, since only the host could revert. A level won ends the round
as in multiplayer, and the next round is the campaign's next level.
Cutscenes are skipped by vote: more than half the machines must press
skip. The host then reverts as single player does, but keeps its clock
moving forward (the netcode depends on that) and moves the script
threads' wake times along with it. The object, device and name syncs
bring the clients up to date.
The host's EXTRA ENEMIES (`coop_enemies.c`, `network.coop_enemies_mode`)
give each squad of enemies a level places more of itself: PER PLAYER, a
percentage of itself for each player past the first; STATIC MULTIPLIER,
that many times itself for any number of players. They stand in rings
about its starting locations where the ground is open, and never take the
actors a level needs for its own (the actor pool, `halo_port_capacity.h`,
holds 1024). Riders a dropship has no seats for are kept, and placed
beside its riders once they get out. Only the host runs the AI, so the
clients see them as the host's other actors.
- **Host authoritative.** The host alone decides damage, deaths, spawns,
pickups, scores and the game's objects; clients do not decide them but
apply what the host sends.
@@ -151,15 +93,7 @@ is dead; version 8 is the first whose clients play by the host's rules
(below), so a build without them joins no host of it; version 9 tells
every machine of a player the host dropped for cheating, each client
tells the host its Discord user, and a machine's join request carries its
hardware id; version 10 sends every player's ping for the scoreboard;
version 11 sends with the game's settings its gametype's PC options;
version 12 plays the campaign together (co-op, above), drives the host's
actors on its clients and sends the flinches and deaths the host picked;
version 13 drives up to 1056 of the host's AI units on its clients (co-op's
extra enemies), where 12 drove 288; version 14 sends co-op's device positions
and its units opening and closing; version 15 sends co-op's allegiances with
its presentation, a message of another size; version 16 has a client's input
say which structure BSP it has loaded (co-op).
hardware id; version 10 sends every player's ping for the scoreboard.
A client plays by its host's rules: in another's game (searching for it,
in its lobby, or playing it) the developer console, the telnet console
@@ -204,9 +138,7 @@ added to `bans.txt` beside `debug.txt` (a line each, as in `cheaters.txt`,
with `ip=` and `hwid=`): the host refuses a machine joining whose address or
hardware id is in it (a line taken out unbans). Both are as the player's
machine tells them: anyone with administrator or root access can change
them, and players behind one address share it. The console's `kick <player
name>` drops a player as `ban` does (every machine told), but adds no line
and keeps no address out: the player may join again at once.
them, and players behind one address share it.
A speed hack of less than a tenth is let be: the host's bounds on how far
and how fast a client's player moves and fires hold it to the host's time
anyway.
@@ -350,10 +282,8 @@ a pregame keep-alive every five seconds from the host
host's latest tick it had heard of when it made the report.
- The host deals a report once it has checked it: from that machine's
player; damage one of their weapons (a vehicle's a driver's or
gunner's; now or in the last ten seconds), a grenade the host's own game
saw them throw in the last ten seconds that has not gone off (each
throw's explosion is taken once, its other hits that tick with it:
holding grenades deals nothing) or the vehicle they drove (in the
gunner's; now or in the last ten seconds), their grenades (while they
have them, and for a while after) or the vehicle they drove (in the
last ten seconds: its collisions) can deal (its projectiles' impacts
and detonations, followed through the tags), no harder than it can be
(all of it, but an airborne melee blow's half again); of the shape the
+19 -51
View File
@@ -83,8 +83,7 @@ These files are in the data root:
| `init.txt` | Console commands that the game does at start-up. For example, `map_name levels\a10\a10` starts the first campaign level. |
The settings are in `config.toml` next to the executable. Refer to
"Settings". Internet play's MQTT brokers are in `brokers.txt` next to it
(`network.brokers_file`).
"Settings".
If the game stops because of a fatal signal, it writes the address and a
backtrace to the standard error. To find the function at the address, enter
@@ -96,11 +95,9 @@ The keyboard and the mouse are a control scheme of their own for the player
of controller 1: each action has up to two keys or mouse buttons, which
Settings > Controls Setup (or `[controls]` in `config.toml`) changes. The
game adds the input of the first gamepad to controller 1. The other
gamepads operate controllers 2 to 4. With two or more players on this
computer (co-op, or split screen in a network game) and only one gamepad,
that gamepad is controller 2 (player 2) and the keyboard and mouse stay
controller 1. The gamepad changes controller only when none of its buttons
is held. The profile's button layout (Settings > Gamepads) is the
gamepads operate controllers 2 to 4. In co-op with only one gamepad, that
gamepad is controller 2 (player 2) and the keyboard and mouse stay
controller 1. The profile's button layout (Settings > Gamepads) is the
gamepads' only.
| Action | Keys and buttons (default) |
@@ -166,22 +163,9 @@ Multiplayer > CO-OP CAMPAIGN is the Xbox's cooperative play, which the PC
version does not have: two players on this computer play the campaign in
split screen. Player 1 is the player who chose it, on the current profile.
Player 2 then chooses a profile with their own controller (a gamepad), and
New Game's levels are those either profile has reached. Either player's
controller chooses the level and the difficulty. A co-op game does
New Game's levels are those either profile has reached. A co-op game does
not continue a saved game of one player.
Network games have split screen too: up to 4 players on each computer. In
the game lobby, another controller presses START to join, and the new
player's profile is chosen on the ADD PLAYER screen that opens (with any
controller). With one gamepad, choose the lobby's ADD PLAYER button first:
until then that gamepad shares controller 1 with the keyboard. Two players on one profile get different names from the host. A
player's B in the lobby leaves the game alone, and the last player of the
computer leaves it for all of them. In the game, each player's pause menu
opens on their part of the screen, and its LEAVE GAME is theirs: their part
of the screen stays until the game ends. A game under way shows its own
screen before JOIN GAME: players join there the same way, START or ADD
PLAYER then START, and JOIN GAME brings them all into the game.
In a multiplayer game, the pause menu (escape) has SETTINGS, which opens
the profile's settings while the game goes on, and for the host END GAME.
END GAME ends the game as its time limit does: the players stay, and after
@@ -220,19 +204,16 @@ the setting for one start of the game. It has priority over the file.
| Setting | Default | Environment variable | Function |
| --- | --- | --- | --- |
| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at the mode of `display.resolution` (the nearest the display has), or at its desktop mode. `"borderless"`: a window over the whole desktop, whose mode does not change. `"windowed"`: a window of `display.window_size`. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. |
| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: borderless, as `display.mode = "borderless"`. `false`: a window, as `display.mode = "windowed"`. Used when `display.mode` is empty. |
| `display.resolution` | `"native"` | `HALO_RESOLUTION` | What fullscreen and borderless draw at: `"native"`, the display's own resolution, or `"<width>x<height>"`, such as `"1920x1080"`, 640x480 or more. Fullscreen sets the display to it. Borderless draws at it and scales the picture to the display, where the display has room for it. The picture has 480 lines of the game and the width of the resolution's shape. A window draws at its own size instead. Video Setup's Resolution sets it, from the display's modes; it shows with Fullscreen and Borderless. |
| `display.resolution_scaling` | `"native"` | `HALO_RESOLUTION_SCALING` | `"native"`: the game draws at the resolution of the window, or of the display (or `display.resolution`) fullscreen. `"original"`: the game draws the 640x480 picture of the Xbox and scales it up to the window or the display, whatever `display.mode` is. Video Setup sets it. |
| `display.window_size` | `""` | `HALO_WINDOW_SIZE` | The size of the window, as `"<width>x<height>"`, such as `"1920x1080"`, 640x480 or more. You can change the size of the window; the game's picture takes its shape. Empty: `display.window_scale` decides. Video Setup's Window Size sets it, from sizes of each shape (4:3, 16:10, 16:9 and 21:9) that fit the desktop; it shows with Windowed. |
| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | Used when `display.window_size` is empty: the size of the window, as a multiple of 640x480. |
| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at its desktop resolution. `"borderless"`: a window over the whole desktop. Both draw at the resolution of the display, the picture 480 lines of the game and the width of the display. `"windowed"`: a window with the 640x480 picture of the Xbox, scaled. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. |
| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: fullscreen at the resolution of the display. The picture has 480 lines of the game and the width of the display. `false`: a window with the 640x480 picture of the Xbox. Used when `display.mode` is empty. |
| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | The size of the window, as a multiple of 640x480. You can change the size of the window. |
| `display.vsync` | `true` | `HALO_NO_VSYNC=1` sets `false` | `true`: each frame waits for the display. |
| `display.max_fps` | `0` | `HALO_MAX_FPS` | With vsync off, the most frames each second. `0`: twice the display's refresh rate. `-1`: no limit, which can hang some Intel graphics (Raptor Lake), resetting the desktop's graphics too. |
| `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. |
| `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". |
| `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". |
| `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. |
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. |
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the display's resolution, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. |
| `display.menus` | `"pc"` | `HALO_MENUS` | `"pc"`: the PC version's menus, from the files in `port/assets/menus` and a `menus` folder next to `config.toml`. Refer to "Menus". `"xbox"`: the Xbox's menus. |
| `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only within the motion sensor's reach, while the enemy is in sight and not camouflaged, so it never shows where an enemy hides. The gametype's motion tracker setting also applies: no names if it shows no players, only allies' if it shows only friends. |
| `display.player_name_scale` | `1.0` | `HALO_PLAYER_NAME_SCALE` | How large the players' names are drawn: `1.0` is three quarters of the size of the HUD's text, from `0.25` to `4`. With high-res text, larger names are rasterized at their size, so they stay sharp. |
@@ -260,12 +241,7 @@ the setting for one start of the game. It has priority over the file.
| `network.allow_upnp` | `true` | `HALO_NET_ALLOW_UPNP` | `true`: internet play can ask the router to forward its port (UPnP). `false`: the game does not ask. Refer to "Internet play". |
| `network.public_lobby` | `true` | `HALO_NET_PUBLIC_LOBBY` | `true`: the server browser. Public games are listed, and Join Game > Server Browser shows them. `false`: no games are listed or shown. Refer to "Server browser". |
| `network.host_public` | `true` | `HALO_NET_HOST_PUBLIC` | `true`: a new game of Create Game > Internet starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in Server Setup changes it for each game. Refer to "Server browser". |
| `network.coop_friendly_fire` | `"on"` | `HALO_NET_COOP_FRIENDLY_FIRE` | Whether the players of an online co-op game hurt each other: `"off"`, `"on"`, `"shields_only"` or `"explosives_only"`. FRIENDLY FIRE in co-op's Server Setup writes its choice here. Their AI allies they always can, as in the campaign. |
| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. |
| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad). PER PLAYER in co-op's Server Setup writes its choice here. |
| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. |
| `network.coop_public` | `false` | `HALO_NET_COOP_PUBLIC` | `true`: an online co-op game (Create Game > Internet, a SINGLEPLAYER map) starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in co-op's Server Setup writes its choice here. Refer to "Server browser". |
| `network.brokers_file` | `"brokers.txt"` | `HALO_NET_BROKERS_FILE` | The file of the public MQTT brokers that let the machines of an invite find each other, and that carry the listings of the server browser: next to `config.toml`, unless a full path. One `host:port` on each line, up to 4; `#` starts a comment. |
| `network.signalling_brokers` | three public brokers | `HALO_NET_BROKERS` | The public MQTT brokers (`host:port`, with commas between them) that let the machines of an invite find each other, and that carry the listings of the server browser. |
| `network.stun_servers` | Google and Cloudflare | `HALO_NET_STUN` | The public STUN servers (`host:port`, with commas between them) that give the internet address of a machine. |
| `discord.application_id` | the application of the project | `HALO_DISCORD_APPLICATION` | The Discord application for invites. Empty: no Discord. |
| `update.auto` | `true` | `HALO_UPDATE_AUTO` | `true`: at start-up, the game looks for a new version. Refer to "Updates". `false`: the game does not look. |
@@ -457,8 +433,7 @@ Server Setup in Create Game > Internet has a LISTING row:
- PRIVATE: only players with the invite link can join.
Each new game starts as PUBLIC (`network.host_public = false` makes new
games start as PRIVATE). An online co-op game starts as PRIVATE, and keeps
the last choice of its LISTING (`network.coop_public`). A LAN game is never listed. `network.public_lobby = false` turns the server browser off.
games start as PRIVATE). A LAN game is never listed. `network.public_lobby = false` turns the server browser off.
In the Server Browser, select a game to join it. The game joins the invite
of the game, as for a link. When it reaches the host, it opens the lobby.
@@ -469,7 +444,7 @@ How it operates (`src/p2p_lobby.c`):
- The host of a public game publishes a listing: the invite, and the name,
map, gametype and player counts of the game. The listing goes to the
same MQTT brokers as the invites (`network.brokers_file`), retained,
same MQTT brokers as the invites (`network.signalling_brokers`), retained,
to a topic of the host (`hceu/3/lobby/s/<hash of its key>`).
- The key of the host signs the listing (Ed25519). The key is the key of the
invite, so no other machine can list the invite of the host, change its
@@ -491,13 +466,8 @@ with the invite can ask the host to connect, and the host then sends its
addresses. Thus anyone can learn the address of the host of a public game,
as for any public server.
The brokers are in `brokers.txt` next to the executable (from
`port/assets/network/brokers.txt`; on Android, the app writes it next to
`config.toml` at each start), one `host:port` on each line. The game uses
all of them at once (up to 4), so one that works is enough. An update
replaces `brokers.txt`: to use brokers of your own, put them in another
file and name it in `network.brokers_file`. All the players must use the
same broker to see each other's games. The game uses
To use a broker of your own, add it to `network.signalling_brokers`. All the
players must use the same broker to see each other's games. The game uses
MQTT 5 if the broker has it, else MQTT 3.1.1. A broker that does not keep
retained messages, or does not let clients subscribe with wildcards, carries
only invites, not listings.
@@ -514,7 +484,7 @@ Only machines with the invite can find the game:
random 16-byte token. The identifier of the host is from the first 6
bytes of the hash.
- The machines exchange their public keys and addresses through public MQTT
brokers (`network.brokers_file`). The topics are HMACs of the token.
brokers (`network.signalling_brokers`). The topics are HMACs of the token.
A key from the token encrypts and authenticates the messages
(`src/p2p_signal.c`, `src/p2p_crypto.c`). The host authenticates its answer
with a key that only it and the player can calculate. Its public key must
@@ -554,9 +524,7 @@ Only machines with the invite can find the game:
`bans.txt`, and refuses a machine whose address or hardware id is in it.
- The host can ban a player with `ban <player name>` in the developer
console (Tab completes the name). Remove a line from `bans.txt` to unban.
Refer to `NETCODE.md`. `kick <player name>` drops the player the same
way, but keeps nothing: no line in `bans.txt`, and the player can join
again at once. So that every player can be named, the host trims
Refer to `NETCODE.md`. So that every player can be named, the host trims
the spaces around a name and removes characters that draw as nothing. A
letter with a mark is typed as the plain letter (`ban jose` for "José").
A name with nothing left to type becomes "Player", and a name that another
@@ -626,7 +594,7 @@ The game sends the activity only to a Discord client of the same user.
| Game code | All 466 C files of the game. The changes are in "Game source changes". |
| Graphics | Direct3D 8 on OpenGL 4.5 core through SDL3 (`src/d3d8_gl.c`). The port translates the NV2A vertex shaders and register combiners to GLSL. It decodes all the Xbox texture formats. The vertex and index buffers come from a GL copy of the Xbox memory. |
| High-res HUD | The HUD is drawn from high-res assets: redraws at 8x the size of the maps' bitmaps (4x for the largest), in `port/assets/hud`. They cover the meters, counters, panels and their outlines, the motion sensor, reticles, waypoints and scopes, but no bitmap with English text. `tools/hud_assets.py` makes them from the SVG redraws, and the build embeds them in the executable. When the game uploads one of those bitmaps, `src/hud_hires.c` gives the high-res texture in its place, if the bitmap's pixels are those of the English maps: another language's maps keep their own. The game sizes and places the HUD from its tags as before. `display.high_res_hud = false` turns this off. |
| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the resolution the game draws at, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. |
| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the display's resolution, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. |
| Sound | Xbox DirectSound on SDL3 audio (`src/dsound_sdl.c`): PCM and Xbox ADPCM, mixed at 48 kHz, with volume, pitch, mix bins, distance, stereo pan, occlusion and obstruction. There is no Doppler effect, no cones and no reverb. |
| Input | XInput on SDL3 (`src/xinput_sdl.c`): keyboard, mouse, gamepads with rumble, and the debug keyboard for the console. |
| Files | The Win32 file functions and the MSVC file functions on POSIX, with the translation of Xbox paths. |
@@ -712,11 +680,11 @@ Other changes:
| `networking/`, `game/`, `interface/`, `bungie_net/network/` and the pools of objects, effects and sounds | The system link limits and the memory for them. |
| `game/`, `objects/`, `units/`, `networking/` | The distributed netcode. Refer to `NETCODE.md`. |
| `cache/cache_files.c` | When a map's tags load and unload, the port finds the bitmaps that the high-res HUD replaces (`game/hud_hires_tags.c`), and adds the tags of the menus to the menus' map (`game/menu_tags.c`). |
| `interface/ui_widget.c`, `interface/ui_widget_event_handler_functions.c`, `interface/ui_widget_game_data_input_functions.c` | The main menu is the PC version's from `port/assets/menus` (`display.menus`); the menus' widgets can call the port's functions (`game/menu_functions.c`) and send the PC version's custom activation event; the widgets' memory is 256 KB, not 16 KB; the main menu and Multiplayer clear co-op's controllers, so that a gamepad going back to controller 1 is not a controller unplugged; the lobby's split screen players leave alone, and one who quits in game is not joined to the next game (`interface/player_ui.c`). |
| `interface/ui_widget.c`, `interface/ui_widget_event_handler_functions.c`, `interface/ui_widget_game_data_input_functions.c` | The main menu is the PC version's from `port/assets/menus` (`display.menus`); the menus' widgets can call the port's functions (`game/menu_functions.c`) and send the PC version's custom activation event; the widgets' memory is 256 KB, not 16 KB; the main menu and Multiplayer clear co-op's controllers, so that a gamepad going back to controller 1 is not a controller unplugged. |
| `input/input_abstraction.c`, `game/player_control.c`, `game/players.c`, `game/player_queues_new.c`, `units/units.h` | The keyboard and mouse's actions (`src/xinput_sdl.c`, `include/halo_keyboard.h`) join controller 1's game controls; their reload key reloads on its own, and their action key only acts (a control flag of the port's, sent with the player's action, stops the reload the controller's X falls back to). |
| `sound/sound_manager.c`, `interface/hud.c`, `game/game_engine.c` | The music's and the other sounds' volumes; the HUD's and the scoreboard's settings are read again when Settings changes them. |
| `interface/hud.c` | In multiplayer, players' names are drawn above their heads (`display.player_names`, `display.player_name_scale`). |
| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the resolution the game draws at (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. |
| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the display's resolution (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. |
The x86 inline assembly of the game is replaced by C. Thus the compiler
can optimize that code for each processor:
+32 -231
View File
@@ -117,10 +117,6 @@ enum
/* ---------- structures */
/* (how many things to pick up it gives up at once, and for how long) */
#define BOT_IGNORED_ITEMS 4
#define BOT_IGNORE_ITEM_TICKS 600
struct bot
{
long player_index;
@@ -145,20 +141,8 @@ struct bot
while on that account: following a person, a thing to pick up */
short path_failures;
long follow_again_time;
/* things to pick up given up for a while (no way to them, or there and
not taken: on a ledge, out of reach), and how long it has stood by the
one it goes for */
long ignored_items[BOT_IGNORED_ITEMS];
long ignored_item_times[BOT_IGNORED_ITEMS];
short ignored_item_next;
/* (the nearest it has come to the thing it goes for, and when) */
long item_progress_index;
real item_best_distance;
long item_best_time;
/* stuck again and again about the same place (as at the foot of a ramp
or behind its rail): how often since when, to give the goal up */
short stuck_count;
long stuck_count_time;
long ignored_item_index;
long ignored_item_time;
real wander_yaw;
short weave_phase;
/* where it is going, and the way there */
@@ -247,7 +231,6 @@ static struct
long switches;
long stucks;
long long_stucks;
long gave_up;
long fail_start;
long fail_goal;
long fail_search;
@@ -557,40 +540,18 @@ the level between them at its knees */
static boolean bot_clear_walk(
long unit_index,
real_point3d const *from,
real_point3d const *to,
boolean wide)
real_point3d const *to)
{
struct collision_result collision;
real_point3d start = *from;
real_vector3d vector;
real length;
short line;
start.z += 0.3f;
vector.i = to->x - from->x;
vector.j = to->y - from->y;
vector.k = to->z - from->z;
length = (real)sqrt(vector.i * vector.i + vector.j * vector.j);
/* (at its knees and at its chest: a rail with room under it, a slit in
a wall, are not ways through; wide, along its shoulders too: a door it
would pass the middle of but catch its frame) */
for (line = 0; line < (wide && length > 0.001f ? 4 : 2); line++)
{
real_point3d start = *from;
start.z += line == 1 ? 1.0f : line ? 0.6f : 0.3f;
if (line >= 2)
{
real side = line == 2 ? 0.35f : -0.35f;
start.x += -vector.j / length * side;
start.y += vector.i / length * side;
}
if (collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
&start, &vector, unit_index, &collision))
{
return FALSE;
}
}
return TRUE;
return !collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
&start, &vector, unit_index, &collision);
}
static boolean bot_next_point_now(
@@ -631,7 +592,7 @@ static boolean bot_next_point_now(
real dy = bot->goal.y - position->y;
if (dx * dx + dy * dy < 3.0f * 3.0f && fabsf(bot->goal.z - position->z) < 0.6f &&
bot_clear_walk(unit_index, position, &bot->goal, FALSE))
bot_clear_walk(unit_index, position, &bot->goal))
{
*point = bot->goal;
return TRUE;
@@ -659,7 +620,7 @@ static boolean bot_next_point_now(
/* (on its own level only: one above may be in view over a
ledge that a ramp leads round to) */
if (fabsf(next->z - position->z) < 0.6f && bot_clear_walk(unit_index, position, next, TRUE))
if (fabsf(next->z - position->z) < 0.6f && bot_clear_walk(unit_index, position, next))
{
bot->path.step_index++;
if (bot->path.step_index >= bot->path.step_count)
@@ -681,20 +642,17 @@ static boolean bot_next_point_now(
return TRUE;
}
/* a goal when none is in sight: an enemy, or somewhere near someone at
random. Not all the nearest: each bot one of the three nearest (by its
number), and while it is far, a few metres beside them, so that a team
spreads out over the ways there instead of going as one crowd */
/* 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[3] = { 1000000.0f, 1000000.0f, 1000000.0f };
real_point3d best_origin[3];
short found = 0;
real best_distance = 1000000.0f;
short index;
boolean found = FALSE;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
@@ -702,7 +660,6 @@ static void bot_choose_goal(
long unit_index;
real_point3d origin;
real distance;
short rank;
if (!other || other == player || (teams && other->team_index == player->team_index))
continue;
@@ -711,38 +668,14 @@ static void bot_choose_goal(
continue;
object_get_origin(unit_index, &origin);
distance = distance3d(position, &origin);
/* (kept in order, nearest first) */
for (rank = 0; rank < 3 && distance >= best_distance[rank]; rank++)
;
if (rank == 3)
continue;
found = (short)MIN(found + 1, 3);
if (distance < best_distance)
{
short move;
for (move = 2; move > rank; move--)
{
best_distance[move] = best_distance[move - 1];
best_origin[move] = best_origin[move - 1];
}
}
best_distance[rank] = distance;
best_origin[rank] = origin;
}
if (found)
{
short pick = (short)(DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index) % found);
bot->goal = best_origin[pick];
if (best_distance[pick] > 25.0f)
{
real angle = (real)DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index) * 2.4f;
bot->goal.x += (real)cos(angle) * 6.0f;
bot->goal.y += (real)sin(angle) * 6.0f;
best_distance = distance;
bot->goal = origin;
found = TRUE;
}
}
else
if (!found)
{
/* (no enemy alive: a few metres off, somewhere) */
real angle = bot_random() * 2.0f * _pi;
@@ -884,59 +817,7 @@ static real bot_item_worth(
}
/* the thing most worth going for near the bot, or NONE */
static boolean bot_item_ignored(
struct bot const *bot,
long item_index)
{
short index;
for (index = 0; index < BOT_IGNORED_ITEMS; index++)
{
if (bot->ignored_items[index] == item_index &&
game_time_get() - bot->ignored_item_times[index] < BOT_IGNORE_ITEM_TICKS &&
game_time_get() >= bot->ignored_item_times[index])
{
return TRUE;
}
}
return FALSE;
}
static void bot_ignore_item(
struct bot *bot,
long item_index)
{
bot->ignored_items[bot->ignored_item_next] = item_index;
bot->ignored_item_times[bot->ignored_item_next] = game_time_get();
bot->ignored_item_next = (short)((bot->ignored_item_next + 1) % BOT_IGNORED_ITEMS);
bot->item_progress_index = NONE;
}
/* going for a thing to pick up: whether to give it up, coming no nearer
(half a metre) for two and a half seconds: out of reach, past a rail, up
where no way it finds goes, or in a crowd */
static boolean bot_item_out_of_reach(
struct bot *bot,
real distance)
{
if (bot->item_progress_index != bot->item_index || game_time_get() < bot->item_best_time)
{
bot->item_progress_index = bot->item_index;
bot->item_best_distance = distance;
bot->item_best_time = game_time_get();
return FALSE;
}
if (distance < bot->item_best_distance - 0.5f)
{
bot->item_best_distance = distance;
bot->item_best_time = game_time_get();
return FALSE;
}
return game_time_get() - bot->item_best_time > 75;
}
static long bot_find_item(
struct bot const *bot,
struct unit_datum const *unit,
real_point3d const *position,
real range,
@@ -959,7 +840,7 @@ static long bot_find_item(
if (distance > range)
continue;
worth = bot_item_worth(unit, iterator.index);
if (worth <= 0.0f || worth < least_worth || bot_item_ignored(bot, iterator.index))
if (worth <= 0.0f || worth < least_worth)
continue;
/* (the strong ones worth a longer way) */
score = worth * worth / (1.0f + distance / 15.0f);
@@ -2512,7 +2393,7 @@ static void bot_think(
game_time_get() < bot->item_look_time))
{
bot->item_look_time = game_time_get();
bot->item_index = bot_find_item(bot, unit, &position, 10.0f, 4.0f);
bot->item_index = bot_find_item(unit, &position, 10.0f, 4.0f);
}
if (bot->item_index != NONE && !bot->carrying && !bot->has_objective && object_try_and_get(bot->item_index) &&
bot_item_worth(unit, bot->item_index) > 0.0f)
@@ -2527,11 +2408,6 @@ static void bot_think(
bot_walk_to(action, bot->yaw, &position, &point);
if (distance3d(&position, &item_position) < 2.0f)
bot_take_item(bot, player, unit, action);
if (bot_item_out_of_reach(bot, distance3d(&position, &item_position)))
{
bot_ignore_item(bot, bot->item_index);
bot->item_index = NONE;
}
}
/* closes on it along its way, weaving, until near */
else if (bot->has_objective && bot->objective_unit_index != bot->target_unit_index)
@@ -2621,9 +2497,11 @@ static void bot_think(
/* something worth having near: there first */
else if (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS || game_time_get() < bot->item_look_time)
{
long item = bot_find_item(bot, unit, &position, BOT_ITEM_RANGE, 0.0f);
long item = bot_find_item(unit, &position, BOT_ITEM_RANGE, 0.0f);
bot->item_look_time = game_time_get();
if (item == bot->ignored_item_index && game_time_get() - bot->ignored_item_time < 600)
item = NONE;
if (item != bot->item_index)
{
bot->item_index = item;
@@ -2641,7 +2519,8 @@ static void bot_think(
/* (taken or gone, or no way to it: on) */
if (bot->path_failures >= 3)
{
bot_ignore_item(bot, bot->item_index);
bot->ignored_item_index = bot->item_index;
bot->ignored_item_time = game_time_get();
bot->path_failures = 0;
bot->item_index = NONE;
bot->has_goal = FALSE;
@@ -2657,14 +2536,6 @@ static void bot_think(
bot->goal_time = game_time_get();
if (distance3d(&position, &bot->goal) < 2.0f)
bot_take_item(bot, player, unit, action);
/* (no nearer a while: out of reach, as on a ledge, or not
offered: given up for a while) */
if (bot_item_out_of_reach(bot, distance3d(&position, &bot->goal)))
{
bot_ignore_item(bot, bot->item_index);
bot->item_index = NONE;
bot->has_goal = FALSE;
}
}
}
/* (in a team, every third bot with nothing to do keeps near the
@@ -2719,15 +2590,7 @@ static void bot_think(
bot->path_failures = 0;
}
wanted_yaw = bot->wander_yaw;
/* (holding a spot and near it: there, not pressing on into what may
stand on it, a pillar, a wall) */
if (bot->holding && bot->has_objective &&
(bot->goal.x - position.x) * (bot->goal.x - position.x) +
(bot->goal.y - position.y) * (bot->goal.y - position.y) < 1.5f * 1.5f)
{
bot->walking = FALSE;
}
else if (bot_next_point(bot, player->unit_index, &position, &point))
if (bot_next_point(bot, player->unit_index, &position, &point))
{
real dx = point.x - position.x;
real dy = point.y - position.y;
@@ -2817,8 +2680,7 @@ static void bot_think(
boolean progress = distance < bot->walk_distance - 0.015f || moved > 0.06f;
bot->walk_distance = MIN(bot->walk_distance, distance);
/* (at the point, as on a thing to pick up: there, not stuck) */
if ((progress && moved > 0.02f) || distance < 0.5f)
if (progress && moved > 0.02f)
{
bot->stuck_ticks = 0;
}
@@ -2827,27 +2689,8 @@ static void bot_think(
bot->stuck_ticks++;
if (bot->stuck_ticks == 6)
{
/* (to the side that is open, as round a pillar; either, at
random: left, the strafe's positive, bot_walk_to) */
real_point3d left = position, right = position;
boolean left_open, right_open;
left.x += -(real)sin(bot->yaw) * 1.2f;
left.y += (real)cos(bot->yaw) * 1.2f;
right.x -= -(real)sin(bot->yaw) * 1.2f;
right.y -= (real)cos(bot->yaw) * 1.2f;
left_open = bot_clear_walk(player->unit_index, &position, &left, FALSE);
right_open = bot_clear_walk(player->unit_index, &position, &right, FALSE);
bot->sidestep = left_open != right_open ? (left_open ? 1.0f : -1.0f) :
bot_random() < 0.5f ? 1.0f : -1.0f;
bot->sidestep = bot_random() < 0.5f ? 1.0f : -1.0f;
bot->sidestep_ticks = 10;
/* (the point a little above and near, up a step or the lip of
a ramp: a jump on towards it, not a step aside) */
if (bot->walk_point.z - position.z > 0.3f && distance < 3.0f)
{
bot->sidestep_ticks = 0;
SET_FLAG(action->control_flags, _unit_control_jump_bit, TRUE);
}
/* (near a point of its path it cannot reach, at a corner: the
next one) */
if (distance < 1.5f && bot->path.valid && bot->path.step_index < bot->path.step_count)
@@ -2856,38 +2699,6 @@ static void bot_think(
bot->sidestep_ticks = 4;
}
bots.stucks++;
/* (the third time in five seconds: the goal given up, the
thing to pick up for a while, and somewhere else, away from
where it could not get) */
if (game_time_get() - bot->stuck_count_time > 150 || game_time_get() < bot->stuck_count_time)
{
bot->stuck_count = 0;
bot->stuck_count_time = game_time_get();
}
if (++bot->stuck_count >= 3)
{
real away = (real)atan2(position.y - bot->walk_point.y, position.x - bot->walk_point.x) +
(bot_random() - 0.5f) * 1.5f;
bot->stuck_count = 0;
if (bot->item_index != NONE)
{
bot_ignore_item(bot, bot->item_index);
bot->item_index = NONE;
}
if (bot->holding)
bot->path_failures = 3;
else if (!bot->has_objective)
{
bot->goal = position;
bot->goal.x += (real)cos(away) * 10.0f;
bot->goal.y += (real)sin(away) * 10.0f;
bot->has_goal = TRUE;
bot->goal_time = game_time_get();
bot->follow_again_time = game_time_get() + 150;
}
bots.gave_up++;
}
bot->path_time = game_time_get() - PATH_REFRESH_TICKS;
bot->walk_distance = 1000000.0f;
@@ -2896,10 +2707,6 @@ static void bot_think(
SET_FLAG(action->control_flags, _unit_control_jump_bit, TRUE);
if (bot->stuck_ticks == 24)
bots.long_stucks++;
/* (a spot to hold it cannot get to: the place's centre instead,
bot_hold_spot) */
if (bot->stuck_ticks == 18 && bot->holding)
bot->path_failures = 3;
}
}
else
@@ -2928,13 +2735,7 @@ static struct bot *bot_get(
free_bot->player_index = player_index;
free_bot->target_unit_index = NONE;
free_bot->item_index = NONE;
free_bot->item_progress_index = NONE;
{
short ignored;
for (ignored = 0; ignored < BOT_IGNORED_ITEMS; ignored++)
free_bot->ignored_items[ignored] = NONE;
}
free_bot->ignored_item_index = NONE;
free_bot->vehicle_index = NONE;
free_bot->ignored_vehicle_index = NONE;
free_bot->step_radius = BOT_STEP_RADIUS;
@@ -3022,12 +2823,12 @@ void bots_tick(
}
}
bots.logged_time = game_time_get();
platform_log("bots: driving %d; paths found %ld, not %ld; weapon swaps asked %ld; grenades %ld; changes %ld; stuck %ld, long %ld, gave up %ld (no start %ld, no goal %ld, no way %ld); vehicles: in %ld, out %ld",
(int)count, bots.paths_found, bots.paths_failed, bots.pickups, bots.grenades, bots.switches, bots.stucks, bots.long_stucks, bots.gave_up, bots.fail_start, bots.fail_goal, bots.fail_search,
platform_log("bots: driving %d; paths found %ld, not %ld; weapon swaps asked %ld; grenades %ld; changes %ld; stuck %ld, long %ld (no start %ld, no goal %ld, no way %ld); vehicles: in %ld, out %ld",
(int)count, bots.paths_found, bots.paths_failed, bots.pickups, bots.grenades, bots.switches, bots.stucks, bots.long_stucks, bots.fail_start, bots.fail_goal, bots.fail_search,
bots.vehicle_entries, bots.vehicle_exits);
bots.vehicle_entries = bots.vehicle_exits = 0;
bots.fail_start = bots.fail_goal = bots.fail_search = 0;
bots.paths_found = bots.paths_failed = bots.pickups = bots.grenades = bots.switches = bots.stucks = bots.long_stucks = bots.gave_up = 0;
bots.paths_found = bots.paths_failed = bots.pickups = bots.grenades = bots.switches = bots.stucks = bots.long_stucks = 0;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *player = distributed_player(index);
-523
View File
@@ -1,523 +0,0 @@
/*
COOP_ENEMIES.C
Network co-op's extra enemies: Server Setup's EXTRA ENEMIES
(network.coop_enemies_mode). PER PLAYER (network.coop_enemies, a
percentage): for each player past the first, each squad of the players'
enemies that a level places (encounters.c's encounter_create) gets that
much of its count more; at 100%, two players meet twice the squad, four
players four times it. STATIC MULTIPLIER (network.coop_enemies_multiplier):
each squad is that many times as large, for any number of players. The
extra enemies stand in rings about the squad's starting locations, on
open ground, so they do not start inside each other.
A squad a script loads into a dropship (vehicle_load_magic) can be larger
than the dropship's seats. The riders left without a seat are erased, and
kept: once the dropship's own riders get out (vehicle_unload, or any other
way), they are placed beside those riders, a few ticks apart, and drop and
fight as they do. A dropship gone first takes them with it.
Only the host runs the AI, so all of this is the host's.
*/
#include "cseries.h"
#include "ai/actor_placement.h"
#include "ai/actors.h"
#include "ai/ai_scenario_definitions.h"
#include "ai/encounters.h"
#include "game/game.h"
#include "game/game_allegiance.h"
#include "game/players.h"
#include "networking/network_game_globals.h"
#include "networking/network_game_manager.h"
#include "objects/objects.h"
#include "physics/collisions.h"
#include "scenario/scenario.h"
#include "scenario/scenario_definitions.h"
#include "units/units.h"
#include "units/unit_definitions.h"
#include "coop_enemies.h"
#include "network_coop.h"
#include <math.h>
#include <string.h>
/* port_config.c's */
long config_integer(char const *name);
char const *config_string(char const *name);
/* ---------- constants */
enum
{
/* the actors a level keeps for its own placements, which the extra
enemies never take: as many as the Xbox's whole pool */
COOP_ENEMIES_LEVEL_ACTORS = 256,
/* EXTRA ENEMIES' amounts, as Server Setup has them: 25 to 200% per
player, or 2 to 32 times (none is its NONE) */
COOP_ENEMIES_MINIMUM_PERCENT = 25,
COOP_ENEMIES_MAXIMUM_PERCENT = 200,
COOP_ENEMIES_MINIMUM_MULTIPLIER = 2,
COOP_ENEMIES_MAXIMUM_MULTIPLIER = 32,
/* the places tried for an extra enemy, a ring further out each */
SPREAD_ATTEMPTS = 8,
SPREAD_PLACES_PER_RING = 6,
MAXIMUM_RIDING_VEHICLES = 32,
MAXIMUM_SEATED_RIDERS = 16,
MAXIMUM_KEPT_RIDERS = 512,
/* a kept rider placed every this many ticks, as the seated ones
jumped out */
RIDER_RELEASE_TICKS = 4,
};
/* the rings' spacing, and the height over the ground the way to a place is
looked along, and the ground looked for below it */
#define SPREAD_SPACING 0.8f
#define SPREAD_STEP_HEIGHT 0.6f
#define SPREAD_GROUND_DEPTH 2.0f
/* a kept rider's place beside a rider who got out, each a little apart */
#define RIDER_SPACING 0.5f
/* EXTRA ENEMIES' choices (network.coop_enemies_mode's values) */
enum
{
_coop_enemies_none,
_coop_enemies_per_player,
_coop_enemies_multiplier,
};
/* ---------- structures */
/* a vehicle a script loaded riders into: its seated ones, and whether they
have begun to get out */
struct coop_riding_vehicle
{
long vehicle_index;
long seated_unit_indices[MAXIMUM_SEATED_RIDERS];
short seat_indices[MAXIMUM_SEATED_RIDERS];
short seated_count;
boolean releasing;
short released_count;
long next_release_time;
};
/* a rider left without a seat, to place once its vehicle's get out */
struct coop_kept_rider
{
long vehicle_index;
long variant_definition_index;
long encounter_index;
short squad_index;
};
/* ---------- globals */
static struct
{
/* EXTRA ENEMIES, as the game began with it: its choice, and the amount
of PER PLAYER (a percentage) and of STATIC MULTIPLIER */
short mode;
short percent;
short multiplier;
struct coop_riding_vehicle vehicles[MAXIMUM_RIDING_VEHICLES];
short vehicle_count;
struct coop_kept_rider riders[MAXIMUM_KEPT_RIDERS];
short rider_count;
} coop_enemies;
/* ---------- private code */
static boolean coop_enemies_host(
void)
{
return game_connection() == _game_connection_network_server && network_coop_active();
}
/* whether this game has extra enemies (it is a co-op host's, with some
chosen) */
static boolean coop_enemies_on(
void)
{
return coop_enemies.mode != _coop_enemies_none && coop_enemies_host();
}
/* the players in the game, every machine's: the network game's (a level
places its first enemies before the players' units are made, as it loads:
game_initialize_for_new_map) */
static short coop_enemies_player_count(
void)
{
struct network_game *game = network_game_get_game();
short count = 0;
short index;
for (index = 0; game && index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; index++)
{
if (network_player_is_valid(&game->players[index]))
count++;
}
return count;
}
/* whether the way from a to b is open (no structure between them) */
static boolean coop_enemies_open(
real_point3d const *a,
real_point3d const *b)
{
struct collision_result collision;
real_vector3d vector;
vector.i = b->x - a->x;
vector.j = b->y - a->y;
vector.k = b->z - a->z;
return !collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit) |
FLAG(_collision_test_back_facing_surfaces_bit), a, &vector, NONE, &collision);
}
/* the ground below a point, within SPREAD_GROUND_DEPTH of it */
static boolean coop_enemies_ground(
real_point3d const *point,
real_point3d *ground)
{
struct collision_result collision;
real_vector3d vector = { 0.0f, 0.0f, -(SPREAD_STEP_HEIGHT + SPREAD_GROUND_DEPTH) };
if (!collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
point, &vector, NONE, &collision))
{
return FALSE;
}
*ground = collision.point;
return TRUE;
}
static struct coop_riding_vehicle *coop_enemies_riding_vehicle(
long vehicle_index,
boolean create)
{
short index;
for (index = 0; index < coop_enemies.vehicle_count; index++)
{
if (coop_enemies.vehicles[index].vehicle_index == vehicle_index)
return &coop_enemies.vehicles[index];
}
if (!create || coop_enemies.vehicle_count >= MAXIMUM_RIDING_VEHICLES)
return NULL;
csmemset(&coop_enemies.vehicles[coop_enemies.vehicle_count], 0, sizeof(coop_enemies.vehicles[0]));
coop_enemies.vehicles[coop_enemies.vehicle_count].vehicle_index = vehicle_index;
return &coop_enemies.vehicles[coop_enemies.vehicle_count++];
}
/* the riders kept for a vehicle forgotten, and the vehicle with them */
static void coop_enemies_forget_vehicle(
short vehicle_number)
{
long vehicle_index = coop_enemies.vehicles[vehicle_number].vehicle_index;
short index, kept = 0;
for (index = 0; index < coop_enemies.rider_count; index++)
{
if (coop_enemies.riders[index].vehicle_index != vehicle_index)
coop_enemies.riders[kept++] = coop_enemies.riders[index];
}
coop_enemies.rider_count = kept;
coop_enemies.vehicles[vehicle_number] = coop_enemies.vehicles[--coop_enemies.vehicle_count];
}
/* whether a vehicle's seated riders have begun to get out: one alive and
out of it (one killed in its seat is not getting out) */
static boolean coop_enemies_riders_getting_out(
struct coop_riding_vehicle const *vehicle)
{
short index;
for (index = 0; index < vehicle->seated_count; index++)
{
struct object_datum *unit = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
_object_mask_unit);
if (unit && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) &&
unit->object.parent_object_index != vehicle->vehicle_index)
{
return TRUE;
}
}
return FALSE;
}
/* where a vehicle's `number`th kept rider is placed: beside one of the
riders who got out of it (where it is now: out of the vehicle, in the
open), a ring further about them each round; else at a seat's marker, else
the vehicle's place */
static void coop_enemies_rider_place(
struct coop_riding_vehicle const *vehicle,
short number,
real_point3d *position,
real *facing)
{
struct object_datum *object = object_get(vehicle->vehicle_index);
struct unit_definition *definition = unit_definition_get(object->definition_index);
long out_unit_indices[MAXIMUM_SEATED_RIDERS];
short out_count = 0;
short index, round;
real angle;
*position = object->object.position;
*facing = (real)atan2(object->object.forward.j, object->object.forward.i);
for (index = 0; index < vehicle->seated_count; index++)
{
struct object_datum *unit = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
_object_mask_unit);
if (unit && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) &&
unit->object.parent_object_index != vehicle->vehicle_index)
{
out_unit_indices[out_count++] = vehicle->seated_unit_indices[index];
}
}
if (out_count > 0)
{
*position = object_get(out_unit_indices[number % out_count])->object.position;
round = (short)(number / out_count);
}
else
{
struct object_marker marker;
round = number;
if (vehicle->seated_count > 0 && VALID_INDEX(vehicle->seat_indices[0], definition->unit.seats.count))
{
struct unit_seat *seat = TAG_BLOCK_GET_ELEMENT(&definition->unit.seats, vehicle->seat_indices[0],
struct unit_seat);
if (seat->marker_name[0] &&
object_get_marker_by_name(vehicle->vehicle_index, seat->marker_name, &marker, 1) > 0)
{
*position = marker.matrix.position;
}
}
}
/* (never on the rider's own place: a ring about it, further each round) */
angle = (real)number * (_pi * 2.0f / SPREAD_PLACES_PER_RING);
position->x += (real)cos(angle) * RIDER_SPACING * (real)(round / SPREAD_PLACES_PER_RING + 1);
position->y += (real)sin(angle) * RIDER_SPACING * (real)(round / SPREAD_PLACES_PER_RING + 1);
}
/* ---------- public code */
void coop_enemies_new_game(
void)
{
char const *mode = config_string("network.coop_enemies_mode");
csmemset(&coop_enemies, 0, sizeof(coop_enemies));
/* (an amount past Server Setup's, as it shows it: its nearest choice's
end) */
coop_enemies.percent = (short)PIN(config_integer("network.coop_enemies"), COOP_ENEMIES_MINIMUM_PERCENT,
COOP_ENEMIES_MAXIMUM_PERCENT);
coop_enemies.multiplier = (short)PIN(config_integer("network.coop_enemies_multiplier"),
COOP_ENEMIES_MINIMUM_MULTIPLIER, COOP_ENEMIES_MAXIMUM_MULTIPLIER);
coop_enemies.mode = !strcmp(mode, "per_player") ? _coop_enemies_per_player :
!strcmp(mode, "multiplier") ? _coop_enemies_multiplier : _coop_enemies_none;
}
void coop_enemies_reset(
void)
{
coop_enemies.vehicle_count = 0;
coop_enemies.rider_count = 0;
}
short coop_enemies_extra_count(
long encounter_index,
short count)
{
struct encounter_datum *encounter;
long extra, room;
short players;
if (count <= 0 || !coop_enemies_on())
return 0;
encounter = encounter_get(encounter_index);
if (!game_team_is_enemy(_game_team_player, encounter->team_index))
return 0;
if (coop_enemies.mode == _coop_enemies_multiplier)
extra = (long)count * (coop_enemies.multiplier - 1);
else
{
players = coop_enemies_player_count();
if (players <= 1)
return 0;
extra = ((long)count * coop_enemies.percent * (players - 1) + 50) / 100;
}
room = MAXIMUM_ACTORS - COOP_ENEMIES_LEVEL_ACTORS - actor_data->actual_count;
return (short)PIN(extra, 0, MAX(room, 0));
}
boolean coop_enemies_spread_position(
real_point3d const *origin,
short number,
real_point3d *position)
{
real_point3d from = *origin;
short place = (short)((number - 1) % SPREAD_PLACES_PER_RING);
short ring = (short)((number - 1) / SPREAD_PLACES_PER_RING);
short attempt;
from.z += SPREAD_STEP_HEIGHT;
for (attempt = 0; attempt < SPREAD_ATTEMPTS; attempt++, ring++)
{
/* (each ring turned half a place from the last, so its places fall
between the last's) */
real angle = ((real)place + 0.5f * (real)(ring % 2)) * (_pi * 2.0f / SPREAD_PLACES_PER_RING);
real radius = SPREAD_SPACING * (real)(ring + 1);
real_point3d to = from;
to.x += (real)cos(angle) * radius;
to.y += (real)sin(angle) * radius;
if (coop_enemies_open(&from, &to) && coop_enemies_ground(&to, position))
return TRUE;
}
return FALSE;
}
void coop_enemies_rider_seated(
long vehicle_index,
long unit_index)
{
struct coop_riding_vehicle *vehicle;
struct unit_datum *unit;
if (!coop_enemies_on())
return;
vehicle = coop_enemies_riding_vehicle(vehicle_index, TRUE);
unit = unit_get(unit_index);
if (!vehicle)
return;
/* (a vehicle loaded again: the riders that got out of it before are not
this load's) */
{
short index, kept = 0;
for (index = 0; index < vehicle->seated_count; index++)
{
struct object_datum *seated = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
_object_mask_unit);
if (seated && seated->object.parent_object_index == vehicle_index)
{
vehicle->seated_unit_indices[kept] = vehicle->seated_unit_indices[index];
vehicle->seat_indices[kept++] = vehicle->seat_indices[index];
}
}
vehicle->seated_count = kept;
}
if (vehicle->seated_count >= MAXIMUM_SEATED_RIDERS)
return;
vehicle->seated_unit_indices[vehicle->seated_count] = unit_index;
vehicle->seat_indices[vehicle->seated_count] = unit->unit.parent_seat_index;
vehicle->seated_count++;
/* (a vehicle loaded again, after its riders got out: they get out again) */
vehicle->releasing = FALSE;
}
boolean coop_enemies_rider_unseated(
long vehicle_index,
long unit_index)
{
struct unit_datum *unit;
struct actor_datum *actor;
struct coop_kept_rider *rider;
struct coop_riding_vehicle *vehicle;
if (!coop_enemies_on() || coop_enemies.rider_count >= MAXIMUM_KEPT_RIDERS)
return FALSE;
unit = unit_get(unit_index);
vehicle = coop_enemies_riding_vehicle(vehicle_index, FALSE);
/* (kept only for a vehicle with riders seated, whose getting out places
it: one of a load that seated nobody is left where it is) */
if (unit->object.type != _object_type_biped || unit->unit.actor_index == NONE || !vehicle ||
vehicle->seated_count == 0)
{
return FALSE;
}
actor = actor_get(unit->unit.actor_index);
if (actor->meta.swarm || actor->meta.encounter_index == NONE ||
!game_team_is_enemy(_game_team_player, encounter_get(actor->meta.encounter_index)->team_index))
{
return FALSE;
}
rider = &coop_enemies.riders[coop_enemies.rider_count++];
rider->vehicle_index = vehicle_index;
rider->variant_definition_index = actor->meta.variant_definition_index;
rider->encounter_index = actor->meta.encounter_index;
rider->squad_index = actor->meta.squad_index;
return TRUE;
}
void coop_enemies_update(
void)
{
long now = game_time_get();
short vehicle_number;
if (!coop_enemies_host())
return;
for (vehicle_number = coop_enemies.vehicle_count - 1; vehicle_number >= 0; vehicle_number--)
{
struct coop_riding_vehicle *vehicle = &coop_enemies.vehicles[vehicle_number];
struct object_datum *object = object_try_and_get_and_verify_type(vehicle->vehicle_index, _object_mask_unit);
short index;
/* (the vehicle's next kept rider) */
for (index = 0; index < coop_enemies.rider_count; index++)
{
if (coop_enemies.riders[index].vehicle_index == vehicle->vehicle_index)
break;
}
/* (a vehicle gone, or destroyed, takes its kept riders with it; one
with none kept, every one loaded seated or all placed, is done with) */
if (!object || TEST_FLAG(object->object.damage_flags, _object_dead_bit) || index >= coop_enemies.rider_count)
{
coop_enemies_forget_vehicle(vehicle_number);
continue;
}
if (!vehicle->releasing)
{
if (!coop_enemies_riders_getting_out(vehicle))
continue;
vehicle->releasing = TRUE;
vehicle->released_count = 0;
vehicle->next_release_time = now;
}
if (now < vehicle->next_release_time)
continue;
{
struct coop_kept_rider rider = coop_enemies.riders[index];
struct encounter_definition *encounter_definition;
struct squad_definition *squad_definition;
struct actor_starting_location location;
coop_enemies.riders[index] = coop_enemies.riders[--coop_enemies.rider_count];
encounter_definition = TAG_BLOCK_GET_ELEMENT(&global_scenario_get()->ai_encounters,
DATUM_INDEX_TO_ABSOLUTE_INDEX(rider.encounter_index), struct encounter_definition);
squad_definition = TAG_BLOCK_GET_ELEMENT(&encounter_definition->squads, rider.squad_index,
struct squad_definition);
/* (as the squad's: its states, no command list of a starting
location's) */
csmemset(&location, 0, sizeof(location));
coop_enemies_rider_place(vehicle, vehicle->released_count++, &location.position, &location.facing);
location.cluster_index = NONE;
location.noncombat_sequence_id = NONE;
location.default_state = squad_definition->default_state;
location.initial_state = squad_definition->initial_state;
location.actor_variant_index = NONE;
location.command_list_index = NONE;
actor_place(rider.variant_definition_index, rider.encounter_index, rider.squad_index, &location, FALSE, 0);
encounter_update_status(rider.encounter_index);
}
vehicle->next_release_time = now + RIDER_RELEASE_TICKS;
}
}
-48
View File
@@ -1,48 +0,0 @@
/*
COOP_ENEMIES.H
Network co-op's extra enemies, for its players (coop_enemies.c).
*/
#ifndef __COOP_ENEMIES_H
#define __COOP_ENEMIES_H
/* network_coop.c: a game begins (its EXTRA ENEMIES read), or the host's
game state went back (a cutscene skipped): the dropships' riders kept are
forgotten */
void coop_enemies_new_game(
void);
void coop_enemies_reset(
void);
/* encounters.c: how many enemies more a squad placed with `count` gets
(none but on a co-op host, for an encounter of the players' enemies) */
short coop_enemies_extra_count(
long encounter_index,
short count);
/* encounters.c: the place of a squad's `number`th extra enemy (from 1) at
its starting location `origin`: rings about it, on open ground; FALSE if
none is (the starting location itself then) */
boolean coop_enemies_spread_position(
real_point3d const *origin,
short number,
real_point3d *position);
/* units.c's vehicle_scripting_load_magic: a rider seated, and one left
without a seat (the vehicle's seats taken: an extra enemy), which is kept,
off the map, until the vehicle's riders get out (TRUE: kept, the caller
erases it) */
void coop_enemies_rider_seated(
long vehicle_index,
long unit_index);
boolean coop_enemies_rider_unseated(
long vehicle_index,
long unit_index);
/* network_coop.c, after each of the host's ticks: the riders kept placed
at their vehicle's seats once its own riders get out */
void coop_enemies_update(
void);
#endif
-285
View File
@@ -1,285 +0,0 @@
/*
COOP_SPECTATE.C
Spectating in network co-op. A dead player watches a living teammate
until they respawn (players.c respawns them beside a teammate once it is
safe). The campaign's dead camera would watch the body; instead this
follows a teammate from behind, and A (jump on the keyboard) switches to
the next one. director.c and hud.c call in here.
It also draws the line telling players about the vote to skip a cutscene
(network_coop.c counts the votes).
*/
#include "cseries.h"
#include "camera/dead_camera.h"
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
#include "input/input.h"
#include "interface/hud_messaging.h"
#include "objects/objects.h"
#include "rasterizer/rasterizer.h"
#include "render/render.h"
#include "text/draw_string.h"
#include "text/font_group.h"
#include "text/unicode.h"
#include "units/units.h"
#include "coop_spectate.h"
#include "network_coop.h"
#include <math.h>
/* ---------- constants */
/* camera distance behind the unit, pitch (radians), and how much of the
turn toward the unit's facing it makes each frame */
#define SPECTATE_DISTANCE 3.5f
#define SPECTATE_PITCH -0.3f
#define SPECTATE_TURN 0.15f
/* a passenger (riding a Pelican) is watched from in front of them, above
and looking down, so the camera stays inside the vehicle; a wide view (the
observer allows up to 90 degrees) takes in all of them from that close */
#define SPECTATE_PASSENGER_DISTANCE 1.0f
#define SPECTATE_PASSENGER_PITCH -0.5f
#define SPECTATE_PASSENGER_FIELD_OF_VIEW DEGREES_TO_RADIANS(85.0f)
/* field of view for every other spectate view (dead_camera.c's default) */
#define SPECTATE_FIELD_OF_VIEW DEGREES_TO_RADIANS(70.0f)
/* a teammate the scripts hold (Pillar of Autumn's cryo tube) is watched from
in front, far enough back to see all of what holds them */
#define SPECTATE_HELD_DISTANCE 2.0f
#define SPECTATE_HELD_PITCH -0.1f
/* ---------- globals */
/* the player each local player is watching, or NONE */
static long coop_spectate_watched[MAXIMUM_LOCAL_PLAYERS] = { NONE, NONE, NONE, NONE };
/* input_abstraction.c: how many ticks the keyboard's jump key has been held */
byte input_abstraction_port_accept(short controller_index);
/* ---------- private code */
/* The next living player after `after` (or the first, if `after` is NONE),
skipping `self` and wrapping around. NONE if nobody else is alive. */
static long next_living_player(long self, long after)
{
struct data_iterator iterator;
struct player_datum *player;
long first = NONE;
boolean passed = after == NONE;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
if (iterator.datum_index == after)
{
passed = TRUE;
continue;
}
if (iterator.datum_index == self || player->unit_index == NONE)
continue;
if (passed)
return iterator.datum_index;
if (first == NONE)
first = iterator.datum_index;
}
return first;
}
/* Returns TRUE once per press of A (or jump on the keyboard), however long
it is held. The buttons count ticks, and one tick can span several frames,
so testing "held" alone would switch more than once. */
static boolean next_pressed(short controller_index)
{
static boolean held[MAXIMUM_GAMEPADS];
struct gamepad_state const *gamepad;
boolean down;
boolean pressed;
if (controller_index < 0 || controller_index >= MAXIMUM_GAMEPADS)
return FALSE;
gamepad = input_get_gamepad_state(controller_index);
down = (gamepad && gamepad->buttons[FIRST_GAMEPAD_ANALOG_BUTTON + _gamepad_analog_button_a] > 0) ||
input_abstraction_port_accept(controller_index) > 0;
pressed = down && !held[controller_index];
held[controller_index] = down;
return pressed;
}
/* whether the unit rides in a vehicle it isn't driving */
static boolean unit_is_passenger(
long unit_index)
{
long vehicle_index = object_get(unit_index)->object.parent_object_index;
return vehicle_index != NONE && object_try_and_get_and_verify_type(vehicle_index, _object_mask_unit) &&
unit_get(vehicle_index)->unit.driver_object_index != unit_index;
}
/* ---------- public code */
boolean coop_spectating(
void)
{
return network_coop_active();
}
long coop_spectate_unit(
short local_player_index)
{
long self = local_player_get_player_index(local_player_index);
long *watched = &coop_spectate_watched[local_player_index];
struct player_datum *player;
boolean next;
if (self == NONE)
return NONE;
player = player_get(self);
/* checked every frame so a press is never missed or counted twice */
next = next_pressed(player->network_player_data.controller_index);
/* alive: keep `watched`, so the next death starts on the same teammate */
if (player->unit_index != NONE)
return NONE;
if (*watched == NONE || !player_try_and_get(*watched) || player_get(*watched)->unit_index == NONE || next)
{
*watched = next_living_player(self, *watched);
}
return *watched != NONE ? player_get(*watched)->unit_index : NONE;
}
boolean coop_spectate_watching_rider(
short local_player_index)
{
long self = local_player_get_player_index(local_player_index);
long watched = coop_spectate_watched[local_player_index];
long unit_index;
long driver_index;
if (self == NONE || player_get(self)->unit_index != NONE)
return FALSE;
/* the scripted camera may have been running since before this player
died, so nobody has been picked to watch yet */
if (watched == NONE || !player_try_and_get(watched) || player_get(watched)->unit_index == NONE)
watched = next_living_player(self, NONE);
unit_index = watched != NONE ? player_get(watched)->unit_index : NONE;
if (unit_index == NONE || !unit_is_passenger(unit_index))
return FALSE;
driver_index = unit_get(object_get(unit_index)->object.parent_object_index)->unit.driver_object_index;
return driver_index == NONE || unit_get(driver_index)->unit.player_index == NONE;
}
boolean coop_spectate_nothing_to_watch(
short local_player_index)
{
long self = local_player_get_player_index(local_player_index);
return self != NONE && player_get(self)->unit_index == NONE && next_living_player(self, NONE) == NONE;
}
void coop_spectate_camera(
struct dead_camera *camera)
{
struct object_datum *unit = camera->unit_index != NONE ? object_try_and_get(camera->unit_index) : NULL;
real yaw, turn;
if (!unit)
return;
yaw = (real)atan2(unit->object.forward.j, unit->object.forward.i);
if (unit_is_passenger(camera->unit_index))
{
/* fixed in front of the seat, turning with the vehicle */
camera->facing.yaw = yaw + _pi;
camera->facing.pitch = SPECTATE_PASSENGER_PITCH;
camera->distance = SPECTATE_PASSENGER_DISTANCE;
camera->field_of_view = SPECTATE_PASSENGER_FIELD_OF_VIEW;
return;
}
camera->field_of_view = SPECTATE_FIELD_OF_VIEW;
if (!player_input_enabled())
{
camera->facing.yaw = yaw + _pi;
camera->facing.pitch = SPECTATE_HELD_PITCH;
camera->distance = SPECTATE_HELD_DISTANCE;
return;
}
/* ease the camera round behind the unit */
turn = yaw - camera->facing.yaw;
while (turn > _pi)
turn -= 2.0f * _pi;
while (turn < -_pi)
turn += 2.0f * _pi;
camera->facing.yaw += turn * SPECTATE_TURN;
camera->facing.pitch = SPECTATE_PITCH;
camera->distance = SPECTATE_DISTANCE;
}
/* centred text near the bottom of the screen, in the HUD's font */
static void draw_bottom_text(
wchar_t const *text)
{
long font_index = hud_get_font_index();
struct font_header *font;
real_argb_color color = *global_real_argb_white;
rectangle2d bounds;
short height;
if (font_index == NONE)
return;
font = font_definition_get(font_index);
height = (short)(font->ascending_height + font->descending_height);
bounds = render.camera.window_bounds;
bounds.y0 = (short)(bounds.y1 - 3 * height - 24);
bounds.y1 = (short)(bounds.y1 - 24);
/* draw mode 2: centered */
draw_string_set_draw_mode(font_index, NONE, 2, 0, &color);
rasterizer_draw_unicode_string(&bounds, NULL, NULL, 0, text);
}
void coop_spectate_draw(
short local_player_index)
{
long watched = coop_spectate_watched[local_player_index];
wchar_t text[96];
wchar_t const *hint;
/* nobody to watch yet (still joining): network_coop.c shows the host's
view from behind instead */
if (watched == NONE || !player_try_and_get(watched))
{
if (coop_spectate_nothing_to_watch(local_player_index))
draw_bottom_text(L"JOINING THE GAME\r\nYou spawn beside a teammate once it is safe");
return;
}
if (!players_coop_waiting_to_start(local_player_get_player_index(local_player_index)))
hint = L"You come back beside them once it is safe";
else
hint = L"You join them when they're on foot";
usnprintf(text, NUMBEROF(text) - 1, L"SPECTATING %.12s (A: NEXT)\r\n%s", player_get(watched)->name, hint);
text[NUMBEROF(text) - 1] = 0;
draw_bottom_text(text);
}
void coop_skip_vote_draw(
short local_player_index)
{
short votes, voters;
boolean voted;
wchar_t text[96];
/* once, for the first local player */
if (local_player_index != local_player_get_next(NONE) || !network_coop_skip_vote_status(&votes, &voters, &voted))
return;
if (voted)
usnprintf(text, NUMBEROF(text) - 1, L"VOTED TO SKIP %d OF %d", votes, voters);
else
usnprintf(text, NUMBEROF(text) - 1, L"PRESS SPACE OR A TO VOTE TO SKIP %d OF %d", votes, voters);
text[NUMBEROF(text) - 1] = 0;
draw_bottom_text(text);
}
-45
View File
@@ -1,45 +0,0 @@
/*
COOP_SPECTATE.H
Spectating in network co-op, and the cutscene skip vote's line
(coop_spectate.c).
*/
#ifndef __COOP_SPECTATE_H
#define __COOP_SPECTATE_H
struct dead_camera;
/* whether this is network co-op: a network game with no game engine (a campaign map) */
boolean coop_spectating(
void);
/* the unit a dead local player watches (A switches teammate), or NONE if
the player is alive or nobody else is */
long coop_spectate_unit(
short local_player_index);
/* TRUE if the teammate this local player is spectating (while dead or not
yet spawned) rides in an AI-driven vehicle, such as an insertion Pelican.
network_coop.c then shows that seat instead of the scripted camera. */
boolean coop_spectate_watching_rider(
short local_player_index);
/* whether a local player has nothing to look at: no unit of its own and
no living teammate to watch (network_coop.c shows it the host's view) */
boolean coop_spectate_nothing_to_watch(
short local_player_index);
/* keeps the dead camera behind the watched unit as it turns */
void coop_spectate_camera(
struct dead_camera *camera);
/* the HUD line saying who a dead local player is watching */
void coop_spectate_draw(
short local_player_index);
/* during a skippable cutscene in network co-op, the vote count and how to vote */
void coop_skip_vote_draw(
short local_player_index);
#endif
File diff suppressed because it is too large. Load diff
+5 -45
View File
@@ -45,8 +45,6 @@ no screen pauses the game (a network game's clock runs on).
/* the platform layer's (port/linux/src) */
void platform_log(char const *format, ...);
char const *config_string(char const *name);
int platform_display_resolutions(long *widths, long *heights, int maximum);
int platform_window_sizes(long *widths, long *heights, int maximum);
/* the game's network server (this machine hosts: network_game_globals.c) */
void *global_network_game_server_get(void);
@@ -343,12 +341,8 @@ static char const *const port_function_names[] =
"mp level select", "mp profiles list initialize", "mp profiles list dispose", "mp profile set for game",
"port lobby preview join",
"port setup edit",
"port map list back",
"port pause end game",
"port coop begin", "port coop player 2 list initialize", "port coop player 2",
"port lobby open", "port lobby add player", "port lobby join", "port lobby leave",
"port lobby player list initialize", "port lobby player choose",
"port lobby preview add", "port lobby preview leave",
/* (the gametype editor's: the Xbox's walk their rows by place, which the
PC version's screens changed) */
"mp profile begin editing", "mp profile save changes", "request del playlist profile", "final del playlist profile",
@@ -704,41 +698,6 @@ static long split(char const *text, char const **pieces)
return count;
}
/* a spinner's strings, or its values, split: its own, but Video Setup's
sizes are the display's (sdl_platform.c), shown "1920 x 1080" and set
"1920x1080": its resolutions after Resolution's own (NATIVE), and the
window sizes that fit it in place of Window Size's own */
static long spinner_split(struct halo_menu_widget const *widget, boolean values, char const **pieces)
{
long count = split(values ? widget->values : widget->strings, pieces);
long widths[MAXIMUM_STRINGS], heights[MAXIMUM_STRINGS];
long added, index;
if (widget->setting && !strcmp(widget->setting, "display.resolution"))
added = platform_display_resolutions(widths, heights, (int)(MAXIMUM_STRINGS - count));
else if (widget->setting && !strcmp(widget->setting, "display.window_size"))
{
added = platform_window_sizes(widths, heights, MAXIMUM_STRINGS);
if (added > 0)
count = 0;
}
else
return count;
for (index = 0; index < added; index++)
{
char text[32];
char *piece;
snprintf(text, sizeof(text), values ? "%ldx%ld" : "%ld x %ld", widths[index], heights[index]);
piece = allocate((long)strlen(text) + 1);
if (!piece)
break;
strcpy(piece, text);
pieces[count++] = piece;
}
return count;
}
static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
{
struct bitmap_group *group = allocate(sizeof(struct bitmap_group));
@@ -953,7 +912,7 @@ static void setting_add(struct halo_menu_widget const *source, long definition_i
setting->definition_index = definition_index;
setting->setting = source->setting;
setting->loaded_index = NONE;
setting->value_count = spinner_split(source, TRUE, setting->values);
setting->value_count = split(source->values, setting->values);
}
/* a font: large, small, terminal, or the map's by its path */
@@ -1616,8 +1575,9 @@ void menu_tags_loaded(
{
return;
}
/* (in VR, the Xbox's unless vr.menus asks for the PC version's) */
if (halo_vr_active() && strcmp(config_string("vr.menus"), "pc"))
/* (in VR, the Xbox's: the VR port's join codes, game list, host options
and keyboard pointer are built into its screens) */
if (halo_vr_active())
return;
menus = halo_menus_load();
if (!menus)
@@ -1681,7 +1641,7 @@ void menu_tags_loaded(
if (widget->strings)
{
char const *pieces[MAXIMUM_STRINGS];
long count = spinner_split(widget, FALSE, pieces);
long count = split(widget->strings, pieces);
instance_set(instances, UNICODE_STRING_LIST_TAG, build.spinner_tags[index], widget->name, " strings",
string_list_build(pieces, count));
-901
View File
@@ -1,901 +0,0 @@
/*
NETWORK_ACTORS.C
AI sync (port/linux/NETCODE.md).
Only the host runs the AI (game.c skips ai_update on clients). An actor
drives its unit the way a player does, through unit_control. The clients
have the units (network_objects.c); this file makes them move like the
host's.
Each tick the host records the control every actor gave its unit (a hook
in unit_control, and the bursts it holds the trigger for) and sends it to
each client with the unit's state: position, seat, shields and health,
camouflage, and whether it flees in a panic. Units near a client's players
are sent every tick, others less often, but a unit starting a one-tick
action (a jump, a grenade) goes to everyone that tick. Each tick a client
feeds the latest control into unit_control, the same path that drives a
remote player's unit, so the unit walks, aims, fires and throws as it does
on the host. The state corrects any drift.
What the AI starts once is numbered, so a client plays each once however
many times it receives it: animation impulses (dives, vaults), melee
attacks and leaps (which the AI starts directly, not through the control),
speech, and custom animations (outside co-op, whose events carry them).
Pain and death sounds aren't sent: a client plays those itself as it
replays the host's damage.
A client creates no actors of its own (actors.c): the host's are the only
ones. Deaths need nothing here: network_damage.c kills a client's copy of
an AI unit the same way it kills a player's.
*/
/* ---------- headers */
#include "cseries.h"
#include "game/game.h"
#include "networking/network_game_globals.h"
#include "objects/objects.h"
#include "objects/damage.h"
#include "sound/game_sound.h"
#include "sound/sound_definitions.h"
#include "units/units.h"
#include "units/unit_control_data.h"
#include "network_coop.h"
#include "network_distributed.h"
#include <math.h>
/* ---------- constants */
enum
{
/* AI units tracked at once: every actor (halo_port_capacity.h's
HALO_PORT_MAXIMUM_ACTORS) and the units the cutscenes' recorded
animations drive */
MAXIMUM_NETWORK_ACTORS = HALO_PORT_MAXIMUM_ACTORS + 32,
MAXIMUM_ENTRIES_PER_MESSAGE = 64,
/* A client keeps applying a control for this long after the host last
sent one. After that the unit is left alone: the actor let go of it, or
the host went quiet. */
CONTROL_HELD_TICKS = 2 * TICKS_PER_SECOND,
/* an impulse is repeated in this many entries, in case one is lost */
IMPULSE_REPEAT_TICKS = 3,
/* the number of unit_start_animation_impulse impulses (private to units.c) */
NUMBER_OF_UNIT_ANIMATION_IMPULSES = 14,
NO_IMPULSE = 0xFF,
NO_TEAM = 0xFF,
/* An entry's impulse is one of the animation impulses or, past them, an
action the AI starts directly: a melee attack (unit_melee_attack_begin)
or a leap (unit_leap_begin). */
_actor_action_melee = NUMBER_OF_UNIT_ANIMATION_IMPULSES,
_actor_action_leap,
NUMBER_OF_ACTOR_ACTIONS,
/* control flags a unit acts on in one tick: a unit that sets one goes to
every client that tick, however far away, so the action isn't missed */
ONE_SHOT_CONTROL_FLAGS = FLAG(_unit_control_jump_bit) | FLAG(_unit_control_action_bit) |
FLAG(_unit_control_weapon_reload_bit) | FLAG(_unit_control_throw_grenade_bit) |
FLAG(_unit_control_swap_weapons_bit),
};
/* struct distributed_actor_state flags */
enum
{
_distributed_actor_rides_bit = 0,
_distributed_actor_shield_depleted_bit,
_distributed_actor_shield_charging_bit,
_distributed_actor_shield_over_charging_bit,
_distributed_actor_camouflaged_bit,
_distributed_actor_super_camouflaged_bit,
/* the impulse turns the unit to impulse_alignment */
_distributed_actor_impulse_aligned_bit,
/* fleeing in a panic, toward run_blindly_angle (unit_start_running_blindly) */
_distributed_actor_running_blindly_bit,
};
/* facing, aiming and looking vectors are sent as yaw and pitch */
enum
{
_angle_yaw,
_angle_pitch,
NUMBER_OF_ANGLES
};
/* ---------- structures */
/* an AI unit's control and state at the end of the host's tick (like
struct distributed_unit_state for players) */
struct distributed_actor_state
{
long unit_index;
long vehicle_index;
short seat_index;
byte flags;
/* the impulse's sequence number; impulse is NO_IMPULSE when there is none */
byte impulse_number;
byte impulse;
byte animation_state;
byte aiming_speed;
byte primary_trigger;
word control_flags;
signed char throttle[3];
byte active_camouflage;
/* the speech's sequence number; 0 when there is none */
byte speech_number;
/* the unit's team (NO_TEAM for none): the AI sets it after the unit is
made, so a client's copy would keep the team it was made with */
byte team;
short facing[NUMBER_OF_ANGLES];
short aiming[NUMBER_OF_ANGLES];
short looking[NUMBER_OF_ANGLES];
short impulse_alignment;
short run_blindly_angle;
word body_vitality;
word shield_vitality;
/* the sound the unit started saying (a sound tag) */
long speech_sound;
/* the custom animation it started (unit_start_user_animation), and its
sequence number; the graph is NONE when there is none */
long user_animation_graph;
short user_animation;
byte user_animation_number;
byte user_animation_interpolate;
real_point3d position;
struct distributed_vector velocity;
struct distributed_vector forward;
struct distributed_vector up;
};
/* the damage an AI unit is taking: its shield's flare and its body's wounds
are drawn by it */
struct distributed_actor_damage
{
long unit_index;
word current_shield_damage;
word recent_shield_damage;
word current_body_damage;
word recent_body_damage;
};
struct distributed_actor_damage_message
{
struct distributed_message_header header;
struct distributed_actor_damage units[MAXIMUM_ENTRIES_PER_MESSAGE];
};
struct distributed_actor_state_message
{
struct distributed_message_header header;
struct distributed_actor_state states[MAXIMUM_ENTRIES_PER_MESSAGE];
};
/* host: what an actor gave its unit this tick */
struct host_actor
{
long unit_index;
/* got a control since the last send (noted during game_tick, sent after it) */
boolean noted;
/* its damage was sent last tick (and once more when it ends) */
boolean damaged;
struct unit_control_data control;
/* the control flags of the last tick's entry */
word sent_control_flags;
/* the latest impulse, how many more entries carry it, and its number */
short impulse;
short impulse_sends;
byte impulse_number;
boolean impulse_aligned;
real_vector2d impulse_alignment;
/* the latest speech, how many more entries carry it, and its number */
long speech_sound;
short speech_sends;
byte speech_number;
/* the latest custom animation, the same way */
long user_animation_graph;
short user_animation;
boolean user_animation_interpolate;
short user_animation_sends;
byte user_animation_number;
};
/* client: the latest the host sent about an AI unit */
struct client_actor
{
long unit_index;
long received_time;
struct unit_control_data control;
/* the impulse to play, and the number of the last one played */
short impulse;
byte impulse_number;
byte played_impulse_number;
boolean impulse_aligned;
real_vector2d impulse_alignment;
/* the speech to play (NONE when there is none), and the number of the last played */
long speech_sound;
byte speech_number;
byte played_speech_number;
/* the custom animation to play (graph NONE when there is none), and the
number of the last played */
long user_animation_graph;
short user_animation;
boolean user_animation_interpolate;
byte user_animation_number;
byte played_user_animation_number;
boolean driven;
};
/* ---------- globals */
static struct host_actor host_actors[MAXIMUM_NETWORK_ACTORS];
static short host_actor_count;
/* host: the numbers of every unit's impulses, speech and animations, from
one counter, so a unit that drops out of the table and comes back can't
repeat the number a client last played */
static byte host_event_number;
static struct client_actor client_actors[MAXIMUM_NETWORK_ACTORS];
static short client_actor_count;
/* ---------- private code */
static void angles_pack(
real_vector3d const *vector,
short *angles)
{
angles[_angle_yaw] = distributed_angle_pack((real)atan2(vector->j, vector->i));
angles[_angle_pitch] = distributed_angle_pack((real)asin(PIN(vector->k, -1.0f, 1.0f)));
}
static void angles_unpack(
short const *angles,
real_vector3d *vector)
{
real yaw = distributed_angle_unpack(angles[_angle_yaw], FALSE);
real pitch = distributed_angle_unpack(angles[_angle_pitch], TRUE);
vector->i = (real)(cos(pitch) * cos(yaw));
vector->j = (real)(cos(pitch) * sin(yaw));
vector->k = (real)sin(pitch);
}
/* host: the next event's number (never 0, which a client's new entry has
as played) */
static byte next_event_number(
void)
{
if (++host_event_number == 0)
host_event_number = 1;
return host_event_number;
}
/* the unit's entry, adding one if needed; NULL if the table is full */
static struct host_actor *host_actor_for(
long unit_index)
{
short index;
for (index = 0; index < host_actor_count; index++)
{
if (host_actors[index].unit_index == unit_index)
return &host_actors[index];
}
if (host_actor_count >= MAXIMUM_NETWORK_ACTORS)
return NULL;
csmemset(&host_actors[host_actor_count], 0, sizeof(host_actors[0]));
host_actors[host_actor_count].unit_index = unit_index;
host_actors[host_actor_count].impulse = NONE;
host_actors[host_actor_count].speech_sound = NONE;
host_actors[host_actor_count].user_animation_graph = NONE;
return &host_actors[host_actor_count++];
}
static struct client_actor *client_actor_for(
long unit_index)
{
short index;
for (index = 0; index < client_actor_count; index++)
{
if (client_actors[index].unit_index == unit_index)
return &client_actors[index];
}
if (client_actor_count >= MAXIMUM_NETWORK_ACTORS)
return NULL;
csmemset(&client_actors[client_actor_count], 0, sizeof(client_actors[0]));
client_actors[client_actor_count].unit_index = unit_index;
client_actors[client_actor_count].impulse = NONE;
client_actors[client_actor_count].speech_sound = NONE;
client_actors[client_actor_count].user_animation_graph = NONE;
return &client_actors[client_actor_count++];
}
/* whether the unit exists, is alive, and isn't a player's */
static boolean actor_unit_valid(
long unit_index)
{
struct unit_datum *unit;
if (!object_try_and_get_and_verify_type(unit_index, _object_mask_unit))
return FALSE;
unit = unit_get(unit_index);
return !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) && unit->unit.player_index == NONE;
}
/* host: builds the entry to send for an AI unit */
static void actor_state_from_unit(
struct host_actor const *actor,
struct distributed_actor_state *state)
{
struct unit_datum *unit = unit_get(actor->unit_index);
struct damage_network_state damage;
real camouflage = PIN(unit->unit.active_camouflage, 0.0f, 1.0f);
csmemset(state, 0, sizeof(*state));
state->unit_index = actor->unit_index;
state->vehicle_index = NONE;
state->seat_index = NONE;
if (unit->object.parent_object_index != NONE && unit->unit.parent_seat_index != NONE)
{
SET_FLAG(state->flags, _distributed_actor_rides_bit, TRUE);
state->vehicle_index = unit->object.parent_object_index;
state->seat_index = unit->unit.parent_seat_index;
}
state->animation_state = (byte)actor->control.animation_state;
state->aiming_speed = (byte)actor->control.aiming_speed;
state->primary_trigger = (byte)(long)floor(PIN(actor->control.primary_trigger, 0.0f, 1.0f) * 255.0f + 0.5f);
state->control_flags = actor->control.control_flags;
/* for a burst the AI holds the trigger for (unit_persistent_control),
send the flags and trigger the unit's update produced this tick */
if (unit->unit.persistent_control_timer > 0)
{
state->control_flags = (word)(unit->unit.control_flags & (FLAG(NUMBER_OF_UNIT_CONTROL_FLAGS) - 1));
state->primary_trigger = (byte)(long)floor(PIN(unit->unit.primary_trigger, 0.0f, 1.0f) * 255.0f + 0.5f);
}
state->throttle[0] = (signed char)(long)floor(PIN(actor->control.throttle.i, -1.0f, 1.0f) * 127.0f + 0.5f);
state->throttle[1] = (signed char)(long)floor(PIN(actor->control.throttle.j, -1.0f, 1.0f) * 127.0f + 0.5f);
state->throttle[2] = (signed char)(long)floor(PIN(actor->control.throttle.k, -1.0f, 1.0f) * 127.0f + 0.5f);
angles_pack(&actor->control.facing_vector, state->facing);
angles_pack(&actor->control.aiming_vector, state->aiming);
angles_pack(&actor->control.looking_vector, state->looking);
state->impulse = NO_IMPULSE;
if (actor->impulse != NONE && actor->impulse_sends > 0)
{
state->impulse = (byte)actor->impulse;
state->impulse_number = actor->impulse_number;
SET_FLAG(state->flags, _distributed_actor_impulse_aligned_bit, actor->impulse_aligned);
if (actor->impulse_aligned)
{
state->impulse_alignment = distributed_angle_pack(
(real)atan2(actor->impulse_alignment.j, actor->impulse_alignment.i));
}
}
state->speech_sound = NONE;
if (actor->speech_sound != NONE && actor->speech_sends > 0)
{
state->speech_sound = actor->speech_sound;
state->speech_number = actor->speech_number;
}
state->user_animation_graph = NONE;
if (actor->user_animation_graph != NONE && actor->user_animation_sends > 0)
{
state->user_animation_graph = actor->user_animation_graph;
state->user_animation = actor->user_animation;
state->user_animation_interpolate = (byte)actor->user_animation_interpolate;
state->user_animation_number = actor->user_animation_number;
}
if (TEST_FLAG(unit->unit.flags, _unit_running_blindly_bit))
{
SET_FLAG(state->flags, _distributed_actor_running_blindly_bit, TRUE);
state->run_blindly_angle = distributed_angle_pack(unit->unit.run_blindly_angle);
}
damage_get_network_state(actor->unit_index, &damage);
SET_FLAG(state->flags, _distributed_actor_shield_depleted_bit, damage.shield_depleted);
SET_FLAG(state->flags, _distributed_actor_shield_charging_bit, damage.shield_charging);
SET_FLAG(state->flags, _distributed_actor_shield_over_charging_bit, damage.shield_over_charging);
state->body_vitality = distributed_vitality_pack(damage.body_vitality);
state->shield_vitality = distributed_vitality_pack(damage.shield_vitality);
SET_FLAG(state->flags, _distributed_actor_camouflaged_bit,
TEST_FLAG(unit->unit.flags, _unit_active_camouflaged_bit));
SET_FLAG(state->flags, _distributed_actor_super_camouflaged_bit,
TEST_FLAG(unit->unit.flags, _unit_super_camouflaged_bit));
state->active_camouflage = (byte)(long)floor(camouflage * 255.0f + 0.5f);
state->team = unit->object.owner_team_index >= 0 && unit->object.owner_team_index < NO_TEAM ?
(byte)unit->object.owner_team_index : NO_TEAM;
state->position = unit->object.position;
distributed_vector_pack(&unit->object.translational_velocity, DISTRIBUTED_VELOCITY_SCALE, &state->velocity);
distributed_vector_pack(&unit->object.forward, DISTRIBUTED_UNIT_SCALE, &state->forward);
distributed_vector_pack(&unit->object.up, DISTRIBUTED_UNIT_SCALE, &state->up);
}
/* Client: stores the entry's control and applies its state to the unit.
Returns FALSE for an entry with invalid values. */
static boolean actor_state_apply(
struct distributed_actor_state const *state,
long now)
{
struct client_actor *actor;
struct unit_datum *unit;
struct damage_network_state damage;
real_vector3d velocity;
real_vector3d forward;
real_vector3d up;
if (!distributed_object_index_valid(state->unit_index) || !network_objects_client_has(state->unit_index))
return FALSE;
/* (a body the host's unit got back up from: a Flood combat form that
feigned death, which it does only once its body is at rest. Left dead,
the ragdoll was dragged about by the host's positions. The host sends
only living units.) */
if (object_try_and_get_and_verify_type(state->unit_index, _object_mask_unit) &&
TEST_FLAG(unit_get(state->unit_index)->object.damage_flags, _object_dead_bit) &&
TEST_FLAG(unit_get(state->unit_index)->object.flags, _object_at_rest_bit) &&
unit_get(state->unit_index)->unit.player_index == NONE && state->body_vitality > 0)
{
unit_port_resurrect(state->unit_index);
}
if (!actor_unit_valid(state->unit_index) ||
state->animation_state >= NUMBER_OF_UNIT_ANIMATION_STATES ||
state->aiming_speed >= NUMBER_OF_UNIT_AIMING_SPEEDS ||
!VALID_FLAGS(state->control_flags, NUMBER_OF_UNIT_CONTROL_FLAGS) ||
(state->impulse != NO_IMPULSE && state->impulse >= NUMBER_OF_ACTOR_ACTIONS) ||
(state->speech_sound != NONE && !distributed_tag_of_group(state->speech_sound, SOUND_DEFINITION_TAG)) ||
(state->user_animation_graph != NONE &&
!distributed_graph_animation(state->user_animation_graph, state->user_animation)) ||
!distributed_point_valid(&state->position, UNIT_WORLD_BOUND))
{
return FALSE;
}
distributed_vector_unpack(&state->velocity, DISTRIBUTED_VELOCITY_SCALE, &velocity);
distributed_vector_unpack(&state->forward, DISTRIBUTED_UNIT_SCALE, &forward);
distributed_vector_unpack(&state->up, DISTRIBUTED_UNIT_SCALE, &up);
if (!distributed_axes_make_valid(&forward, &up))
return FALSE;
actor = client_actor_for(state->unit_index);
if (!actor)
return TRUE;
/* the control, applied every tick until the next entry */
actor->received_time = now;
actor->control.animation_state = (char)state->animation_state;
actor->control.aiming_speed = (char)state->aiming_speed;
actor->control.control_flags = state->control_flags;
actor->control.weapon_index = NONE;
actor->control.grenade_index = NONE;
actor->control.zoom_level = NONE;
actor->control.throttle.i = (real)state->throttle[0] / 127.0f;
actor->control.throttle.j = (real)state->throttle[1] / 127.0f;
actor->control.throttle.k = (real)state->throttle[2] / 127.0f;
actor->control.primary_trigger = (real)state->primary_trigger / 255.0f;
angles_unpack(state->facing, &actor->control.facing_vector);
angles_unpack(state->aiming, &actor->control.aiming_vector);
angles_unpack(state->looking, &actor->control.looking_vector);
if (state->impulse != NO_IMPULSE && state->impulse_number != actor->played_impulse_number)
{
real yaw = distributed_angle_unpack(state->impulse_alignment, FALSE);
actor->impulse = state->impulse;
actor->impulse_number = state->impulse_number;
actor->impulse_aligned = TEST_FLAG(state->flags, _distributed_actor_impulse_aligned_bit);
actor->impulse_alignment.i = (real)cos(yaw);
actor->impulse_alignment.j = (real)sin(yaw);
}
if (state->speech_sound != NONE && state->speech_number != actor->played_speech_number)
{
actor->speech_sound = state->speech_sound;
actor->speech_number = state->speech_number;
}
if (state->user_animation_graph != NONE && state->user_animation_number != actor->played_user_animation_number)
{
actor->user_animation_graph = state->user_animation_graph;
actor->user_animation = state->user_animation;
actor->user_animation_interpolate = state->user_animation_interpolate != 0;
actor->user_animation_number = state->user_animation_number;
}
/* the state: seat, shields and health, camouflage, running blindly, and
position (a unit in a vehicle goes where the vehicle goes) */
unit = unit_get(state->unit_index);
if (TEST_FLAG(state->flags, _distributed_actor_rides_bit))
{
if (unit->object.parent_object_index != state->vehicle_index ||
unit->unit.parent_seat_index != state->seat_index)
{
network_objects_set_seat(state->unit_index, state->vehicle_index, state->seat_index);
}
}
else if (unit->object.parent_object_index != NONE)
{
network_objects_set_seat(state->unit_index, NONE, NONE);
}
damage_get_network_state(state->unit_index, &damage);
damage.shield_depleted = TEST_FLAG(state->flags, _distributed_actor_shield_depleted_bit);
damage.shield_charging = TEST_FLAG(state->flags, _distributed_actor_shield_charging_bit);
damage.shield_over_charging = TEST_FLAG(state->flags, _distributed_actor_shield_over_charging_bit);
damage.body_vitality = distributed_vitality_unpack(state->body_vitality);
damage.shield_vitality = distributed_vitality_unpack(state->shield_vitality);
damage_set_network_state(state->unit_index, &damage);
SET_FLAG(unit->unit.flags, _unit_active_camouflaged_bit,
TEST_FLAG(state->flags, _distributed_actor_camouflaged_bit));
SET_FLAG(unit->unit.flags, _unit_super_camouflaged_bit,
TEST_FLAG(state->flags, _distributed_actor_super_camouflaged_bit));
unit->unit.active_camouflage = (real)state->active_camouflage / 255.0f;
if (state->team != NO_TEAM)
unit->object.owner_team_index = state->team;
SET_FLAG(unit->unit.flags, _unit_running_blindly_bit,
TEST_FLAG(state->flags, _distributed_actor_running_blindly_bit));
if (TEST_FLAG(state->flags, _distributed_actor_running_blindly_bit))
unit->unit.run_blindly_angle = distributed_angle_unpack(state->run_blindly_angle, FALSE);
/* vehicles are placed only by the host's object states (network_objects.c) */
if (unit->object.parent_object_index == NONE && unit->object.type != _object_type_vehicle)
{
real dx = state->position.x - unit->object.position.x;
real dy = state->position.y - unit->object.position.y;
real dz = state->position.z - unit->object.position.z;
if (dx * dx + dy * dy + dz * dz > REMOTE_CORRECTION_TOLERANCE * REMOTE_CORRECTION_TOLERANCE &&
network_objects_reconcile(state->unit_index, &state->position, &forward, &up, &velocity, NULL,
REMOTE_BLEND_DISTANCE))
{
distributed_count_correction();
}
}
return TRUE;
}
/* Client: plays a sound at the unit's head, as unit_dialogue_update starts
a unit's speech. */
static void speech_play(
long unit_index,
long sound_definition_index)
{
struct object_marker marker;
real_point3d position = *global_origin3d;
real_vector3d forward = *global_forward3d;
short node_index = 0;
if (object_get_marker_by_name(unit_index, "head", &marker, 1))
{
node_index = marker.node_index;
position = marker.node_matrix.position;
forward = marker.node_matrix.forward;
}
object_impulse_sound_new(unit_index, sound_definition_index, node_index, &position, &forward, 1.0f);
}
/* ---------- public code */
void network_actors_new_game(
void)
{
host_actor_count = 0;
client_actor_count = 0;
}
/* host: unit_control calls this when an actor drives its unit */
void network_actors_note_control(
long unit_index,
struct unit_control_data const *control_data)
{
struct host_actor *actor;
if (game_connection() != _game_connection_network_server)
return;
actor = host_actor_for(unit_index);
if (!actor)
return;
actor->noted = TRUE;
actor->control = *control_data;
}
/* host: an impulse or action for an AI unit, sent in the next few entries */
static void note_action(
long unit_index,
short action,
real_vector2d const *alignment_vector)
{
struct host_actor *actor;
if (game_connection() != _game_connection_network_server)
return;
actor = host_actor_for(unit_index);
if (!actor)
return;
actor->impulse = action;
actor->impulse_sends = IMPULSE_REPEAT_TICKS;
actor->impulse_number = next_event_number();
actor->impulse_aligned = alignment_vector != NULL;
if (alignment_vector)
actor->impulse_alignment = *alignment_vector;
}
/* host: unit_start_animation_impulse calls this for an AI unit */
void network_actors_note_impulse(
long unit_index,
short animation_impulse,
real_vector2d const *alignment_vector)
{
note_action(unit_index, animation_impulse, alignment_vector);
}
/* host: unit_melee_attack_begin calls this when an AI unit starts a melee attack */
void network_actors_note_melee(
long unit_index,
real_vector2d const *alignment_vector)
{
note_action(unit_index, _actor_action_melee, alignment_vector);
}
/* host: unit_leap_begin calls this when an AI unit leaps */
void network_actors_note_leap(
long unit_index,
real_vector2d const *alignment_vector)
{
note_action(unit_index, _actor_action_leap, alignment_vector);
}
/* host: unit_dialogue_update calls this when an AI unit starts saying
something (not a pain or death sound, which clients play themselves) */
void network_actors_note_speech(
long unit_index,
long sound_definition_index)
{
struct host_actor *actor;
if (game_connection() != _game_connection_network_server)
return;
actor = host_actor_for(unit_index);
if (!actor)
return;
actor->speech_sound = sound_definition_index;
actor->speech_sends = IMPULSE_REPEAT_TICKS;
actor->speech_number = next_event_number();
}
/* host: unit_start_user_animation calls this when an AI unit starts a
custom animation (co-op's events carry them in co-op) */
void network_actors_note_user_animation(
long unit_index,
long animation_graph_index,
short animation_index,
boolean interpolate)
{
struct host_actor *actor;
if (game_connection() != _game_connection_network_server || network_coop_active())
return;
actor = host_actor_for(unit_index);
if (!actor)
return;
actor->user_animation_graph = animation_graph_index;
actor->user_animation = animation_index;
actor->user_animation_interpolate = interpolate;
actor->user_animation_sends = IMPULSE_REPEAT_TICKS;
actor->user_animation_number = next_event_number();
}
/* host: the damage of the AI units taking some, to every client (and once
more when it ends, so a flare goes) */
static void host_send_damage(
void)
{
struct distributed_actor_damage_message message;
short count = 0;
short index;
for (index = 0; index < host_actor_count; index++)
{
struct host_actor *actor = &host_actors[index];
struct damage_network_state damage;
boolean damaged;
if (!actor_unit_valid(actor->unit_index))
continue;
damage_get_network_state(actor->unit_index, &damage);
damaged = damage.current_shield_damage > 0.0f || damage.recent_shield_damage > 0.0f ||
damage.current_body_damage > 0.0f || damage.recent_body_damage > 0.0f;
if (!damaged && !actor->damaged)
continue;
actor->damaged = damaged;
message.units[count].unit_index = actor->unit_index;
message.units[count].current_shield_damage = distributed_vitality_pack(damage.current_shield_damage);
message.units[count].recent_shield_damage = distributed_vitality_pack(damage.recent_shield_damage);
message.units[count].current_body_damage = distributed_vitality_pack(damage.current_body_damage);
message.units[count].recent_body_damage = distributed_vitality_pack(damage.recent_body_damage);
if (++count == MAXIMUM_ENTRIES_PER_MESSAGE)
{
distributed_send(&message, _distributed_message_actor_damage, count,
(word)(sizeof(message.header) + count * sizeof(message.units[0])), _distributed_to_clients);
count = 0;
}
}
if (count)
{
distributed_send(&message, _distributed_message_actor_damage, count,
(word)(sizeof(message.header) + count * sizeof(message.units[0])), _distributed_to_clients);
}
}
/* Host, after each tick: sends each client the AI units driven this tick.
Units near the client's players go every tick, others less often. A unit
with a fresh impulse, action, speech or animation goes to everyone in each
of the next IMPULSE_REPEAT_TICKS ticks, and one starting a one-tick action
goes that tick. A unit no actor drove this tick is dropped from the table,
unless it is fleeing: a unit running blindly moves by itself, without its
actor's control, and dropped it stood running in place on the clients. */
void network_actors_host_tick(
void)
{
static struct distributed_actor_state states[MAXIMUM_NETWORK_ACTORS];
/* sent to every client this tick, whatever its period */
static boolean urgent[MAXIMUM_NETWORK_ACTORS];
struct distributed_actor_state_message message;
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
long now = game_time_get();
short state_count = 0;
short index;
short machine_number;
for (index = 0; index < host_actor_count; index++)
{
struct host_actor *actor = &host_actors[index];
if (!actor_unit_valid(actor->unit_index) ||
(!actor->noted && !TEST_FLAG(unit_get(actor->unit_index)->unit.flags, _unit_running_blindly_bit)))
{
host_actors[index--] = host_actors[--host_actor_count];
continue;
}
actor_state_from_unit(actor, &states[state_count]);
urgent[state_count] = states[state_count].impulse != NO_IMPULSE || states[state_count].speech_sound != NONE ||
states[state_count].user_animation_graph != NONE ||
(states[state_count].control_flags & ~actor->sent_control_flags & ONE_SHOT_CONTROL_FLAGS) != 0;
actor->sent_control_flags = states[state_count].control_flags;
state_count++;
actor->noted = FALSE;
if (actor->impulse_sends > 0)
actor->impulse_sends--;
if (actor->speech_sends > 0)
actor->speech_sends--;
if (actor->user_animation_sends > 0)
actor->user_animation_sends--;
}
host_send_damage();
for (machine_number = 0; machine_number < machine_count; machine_number++)
{
long machine_index = machine_indices[machine_number];
short count = 0;
for (index = 0; index < state_count; index++)
{
struct distributed_actor_state const *state = &states[index];
short period = network_objects_send_period(machine_index, &state->position);
if (!urgent[index] && (now + DATUM_INDEX_TO_ABSOLUTE_INDEX(state->unit_index)) % period != 0)
continue;
message.states[count++] = *state;
if (count == MAXIMUM_ENTRIES_PER_MESSAGE || count == DATAGRAM_ENTRIES(struct distributed_actor_state))
{
distributed_send_to_machine(machine_index, &message, _distributed_message_actor_states, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_actor_state)));
count = 0;
}
}
if (count)
{
distributed_send_to_machine(machine_index, &message, _distributed_message_actor_states, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_actor_state)));
}
}
}
word network_actors_damage_entry_size(
void)
{
return sizeof(struct distributed_actor_damage);
}
/* client: the damage the host's AI units are taking */
void network_actors_handle_damage(
void const *entries,
short count)
{
struct distributed_actor_damage const *units = entries;
short index;
for (index = 0; index < count; index++)
{
struct damage_network_state damage;
if (!distributed_object_index_valid(units[index].unit_index) ||
!network_objects_client_has(units[index].unit_index) || !actor_unit_valid(units[index].unit_index))
{
continue;
}
damage_get_network_state(units[index].unit_index, &damage);
damage.current_shield_damage = distributed_vitality_unpack(units[index].current_shield_damage);
damage.recent_shield_damage = distributed_vitality_unpack(units[index].recent_shield_damage);
damage.current_body_damage = distributed_vitality_unpack(units[index].current_body_damage);
damage.recent_body_damage = distributed_vitality_unpack(units[index].recent_body_damage);
damage_set_network_state(units[index].unit_index, &damage);
}
}
word network_actors_entry_size(
void)
{
return sizeof(struct distributed_actor_state);
}
/* client: entries from the host */
void network_actors_handle_states(
void const *entries,
short count)
{
struct distributed_actor_state const *states = entries;
long now = game_time_get();
short index;
for (index = 0; index < count; index++)
actor_state_apply(&states[index], now);
}
/* Client: called at the point in the tick where the host runs the AI.
Drives each AI unit with the last control the host sent. */
void network_actors_drive(
void)
{
long now = game_time_get();
short index;
for (index = 0; index < client_actor_count; index++)
{
struct client_actor *actor = &client_actors[index];
if (!actor_unit_valid(actor->unit_index) || now - actor->received_time > CONTROL_HELD_TICKS)
{
if (actor->driven && object_try_and_get_and_verify_type(actor->unit_index, _object_mask_unit))
unit_set_actively_controlled(actor->unit_index, FALSE);
client_actors[index--] = client_actors[--client_actor_count];
continue;
}
/* vehicles follow the host's transform instead (network_objects.c) */
if (object_get(actor->unit_index)->object.type == _object_type_vehicle)
continue;
if (!actor->driven)
{
unit_set_actively_controlled(actor->unit_index, TRUE);
actor->driven = TRUE;
}
unit_control(actor->unit_index, &actor->control);
if (actor->impulse != NONE && actor->impulse_number != actor->played_impulse_number)
{
real_vector2d *alignment = actor->impulse_aligned ? &actor->impulse_alignment : NULL;
if (actor->impulse == _actor_action_melee)
unit_melee_attack_begin(actor->unit_index, FALSE, alignment);
else if (actor->impulse == _actor_action_leap)
unit_leap_begin(actor->unit_index, alignment);
else
unit_start_animation_impulse(actor->unit_index, actor->impulse, alignment);
actor->played_impulse_number = actor->impulse_number;
actor->impulse = NONE;
}
if (actor->speech_sound != NONE)
{
speech_play(actor->unit_index, actor->speech_sound);
actor->played_speech_number = actor->speech_number;
actor->speech_sound = NONE;
}
if (actor->user_animation_graph != NONE)
{
unit_port_play_user_animation(actor->unit_index, actor->user_animation_graph, actor->user_animation,
actor->user_animation_interpolate, 0);
actor->played_user_animation_number = actor->user_animation_number;
actor->user_animation_graph = NONE;
}
}
}
File diff suppressed because it is too large. Load diff
-105
View File
@@ -1,105 +0,0 @@
/*
NETWORK_COOP.H
The calls Halo's own code (source/) makes into network_coop.c. On a co-op
host the note_ functions send what the scripts just did to the clients; on
any other machine they do nothing, so the clients can call the same engine
functions to apply what they receive.
*/
#ifndef __NETWORK_COOP_H
#define __NETWORK_COOP_H
/* ---------- constants */
/* network_coop_note_hud */
enum
{
_coop_hud_help_text,
_coop_hud_objective,
_coop_hud_help_flash,
_coop_hud_messages_clear,
_coop_hud_checkpoint,
};
/* network_coop_note_player_effect */
enum
{
_coop_player_effect_translation,
_coop_player_effect_rotation,
_coop_player_effect_start,
_coop_player_effect_stop,
};
/* network_coop_note_nav_point */
enum
{
_coop_nav_point_team_flag,
_coop_nav_point_team_object,
_coop_nav_point_unit_flag,
_coop_nav_point_unit_object,
};
/* ---------- prototypes/NETWORK_COOP.C */
/* whether this is a network co-op game (not its lobby, whose menu scene also
has no game engine) */
boolean network_coop_active(void);
/* whether this machine is a co-op client, whose devices only the host moves */
boolean network_coop_devices_remote(void);
/* devices.c: a group's devices were set straight to its value */
void network_coop_note_device_snap(short group_index);
/* unit_scripting_commands.c: a script set a unit's maximum or current
vitality; TRUE if it was a co-op player's, now set on every player's */
boolean network_coop_set_players_vitality(long unit_index, boolean maximum, real body, real shield);
/* host: whether the player's machine has the host's structure BSP loaded
(always outside co-op, and for the host's own players). Until a client has,
the host takes none of its players' movement, which falls there with no
floor, and lets them trigger no BSP switch (players.c). */
boolean network_coop_player_has_structure_bsp(long player_index);
/* network_distributed.c: the structure BSP a client's input says it has */
void network_coop_note_player_structure_bsp(short player_index, short structure_bsp_index);
/* units.c: a vehicle left the vehicle carrying it (a Pelican's Warthog drop) */
void network_coop_vehicle_dropped(long vehicle_index, long carrier_index);
/* cinematics.c: a chapter title */
void network_coop_note_title(short title_index, real delay);
/* hud_messaging.c, hud.c: value is a message index, or TRUE/FALSE */
void network_coop_note_hud(short kind, short value);
/* player_effects.c: the script screen shake (a, b, c as the function takes them) */
void network_coop_note_player_effect(short kind, real a, real b, real c);
/* hud_nav_points.c: activate (or, with nav_index NONE, deactivate) a nav point.
target is the team or the unit; marker is the cutscene flag or the object. */
void network_coop_note_nav_point(short kind, short nav_index, long target, long marker, real vertical_offset);
/* units.c: a unit started a custom animation (animation_index NONE: stopped) */
void network_coop_note_unit_animation(long unit_index, long animation_graph_index, short animation_index,
boolean interpolate);
/* units.c: a unit (a dropship) opened or closed (unit_open, unit_close) */
void network_coop_note_unit_open(long unit_index, boolean open);
/* units.c: unit_custom_animation_at_frame moved it to a frame */
void network_coop_note_unit_animation_frame(long unit_index, short frame_index);
/* scenery.c: a scenery animation started */
/* the scripts' effects (hs_library_external.c): at a cutscene flag, or on an
object's marker */
void network_coop_note_effect(long effect_definition_index, short cutscene_flag_index);
/* the scripts' objects_attach and objects_detach (objects.c) */
void network_coop_note_attach(long parent_index, char const *parent_marker_name, long child_index,
char const *child_marker_name);
void network_coop_note_detach(long parent_index, long child_index);
void network_coop_note_object_effect(long effect_definition_index, long object_index, char const *marker_name);
void network_coop_note_scenery_animation(long object_index, long animation_graph_index, short animation_index,
short frame_index);
/* player_control.c: whether the skip key should do anything, and the press.
network_coop_vote_skip returns FALSE outside network co-op, where the
cinematic is skipped as usual. */
boolean network_coop_skip_offered(void);
boolean network_coop_vote_skip(void);
/* main.c: after a co-op host reverts to skip a cinematic; now is the game
time before the revert */
void network_coop_skip_reverted(long now);
/* the vote count to show, if a skippable cinematic is playing */
boolean network_coop_skip_vote_status(short *votes, short *voters, boolean *voted);
#endif
+39 -177
View File
@@ -16,9 +16,8 @@ being hit looks and feels like on the clients).
- A client deals no damage itself. What its own players' shots, grenades,
melee and vehicles hit it reports to the host, which deals it once it has
checked it: the report is of that machine's player, the damage one that
player's weapons (now or lately), a grenade the host saw them throw
(each throw's explosion once) or the vehicle they drove deal, of the
shape the game gives it (at a point, a melee blow from the
player's weapons (now or lately), grenades or the vehicle they drove
deal, of the shape the game gives it (at a point, a melee blow from the
striker, a collision from the vehicle), the target about where the host
had it when the shooter saw it (the host keeps a second of where
players' units and vehicles were, and looks back as far as the report
@@ -37,7 +36,6 @@ being hit looks and feels like on the clients).
*/
#include "cseries.h"
#include "cseries/errors.h"
#include "game/game.h"
#include "game/game_globals.h"
#include "game/players.h"
@@ -83,13 +81,6 @@ enum
died or dropped the launcher) */
MAXIMUM_RECENT_WEAPONS = 8,
RECENT_WEAPON_TICKS = 10 * TICKS_PER_SECOND,
/* the grenades a player has thrown lately, as the host's own game threw
them, whose damage the host takes from them: each goes off once, within
RECENT_WEAPON_TICKS of its throw. Each player has their own (all of
MAXIMUM_TRACKED_PLAYERS); one that hits nothing is kept the whole time,
so this is more than a player throws in it, infinite grenades and all
(a throw takes most of a second); past it the oldest gives way */
MAXIMUM_GRENADE_THROWS = 32,
/* a player's hits: a bucket of this many seconds of fire, filling at a
second a second, a hit taking what one of its weapon's projectiles
takes to fire (at twice the weapon's fastest rate: a shotgun's
@@ -350,9 +341,7 @@ static struct
{
long definition_indices[MAXIMUM_RECENT_WEAPONS];
long times[MAXIMUM_RECENT_WEAPONS];
/* (the grenades thrown, not yet gone off: NONE for none) */
long throw_times[MAXIMUM_GRENADE_THROWS];
short throw_types[MAXIMUM_GRENADE_THROWS];
long grenade_times[NUMBER_OF_UNIT_GRENADE_TYPES];
long driven_time;
/* (the blow of the player's unit with no weapon: units.c's
unit_unarmed_melee_damage) */
@@ -1089,74 +1078,21 @@ static real distributed_source_deals(
return distributed_source_deals_uncached(source_index, damage_index, grenade, kinds, reach);
}
/* the oldest grenade of the type the host saw the player throw within
RECENT_WEAPON_TICKS that has not gone off yet (network_damage_note_grenade),
NONE for none */
static short distributed_grenade_throw(
short player_index,
short grenade_type)
{
short index;
short oldest = NONE;
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
{
long time = damage_players[player_index].throw_times[index];
if (time != NONE && damage_players[player_index].throw_types[index] == grenade_type &&
game_time_get() - time <= RECENT_WEAPON_TICKS &&
(oldest == NONE || time < damage_players[player_index].throw_times[oldest]))
{
oldest = index;
}
}
return oldest;
}
/* whether an explosion of the damage is among the player's this tick
(distributed_report_paid): at the epicenter, or (NULL) anywhere */
static boolean distributed_explosion_this_tick(
short player_index,
long damage_index,
real_point3d const *epicenter)
{
short index;
if (damage_explosions[player_index].time != game_time_get())
return FALSE;
for (index = 0; index < damage_explosions[player_index].count; index++)
{
if (damage_explosions[player_index].explosions[index].definition_index == damage_index &&
(!epicenter || (damage_explosions[player_index].explosions[index].epicenter.x == epicenter->x &&
damage_explosions[player_index].explosions[index].epicenter.y == epicenter->y &&
damage_explosions[player_index].explosions[index].epicenter.z == epicenter->z)))
{
return TRUE;
}
}
return FALSE;
}
/* how many hits a second the player could deal of the damage: their
weapons, lately, a grenade the host saw them throw that has not gone off
(or one going off this tick, hitting more than one object), the vehicle
they drove lately; 0 when none of them deals it; how they deal it (the
_damage_source flags); how far from them what they fire deals it; and the
type of grenade when nothing but a grenade of theirs deals it, else NONE */
weapons, lately, their grenades, the vehicle they drove lately; 0 when none
of them deals it; how they deal it (the _damage_source flags); and how far
from them what they fire deals it */
static real distributed_player_deals(
short player_index,
long damage_index,
byte *kinds,
struct damage_reach *reach,
short *grenade_type)
struct damage_reach *reach)
{
struct game_globals *globals = scenario_get_game_globals();
short index;
real rate = 0.0f;
real grenade_rate = 0.0f;
*kinds = 0;
*grenade_type = NONE;
reach->distance = 0.0f;
reach->ticks = 0.0f;
reach->carried_ticks = 0.0f;
@@ -1176,6 +1112,25 @@ static real distributed_player_deals(
*kinds |= weapon_kinds;
}
}
for (index = 0; index < globals->grenades.count && index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
{
struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, index,
struct game_globals_grenade);
if (game_time_get() - damage_players[player_index].grenade_times[index] <= RECENT_WEAPON_TICKS &&
grenade->projectile.index != NONE)
{
byte grenade_kinds;
struct damage_reach grenade_reach;
real grenade_rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &grenade_kinds,
&grenade_reach);
if (grenade_rate > 0.0f)
distributed_reach_combine(reach, &grenade_reach);
rate = MAX(rate, grenade_rate);
*kinds |= grenade_kinds;
}
}
/* (a blow with no weapon: the unit's own, units.c) */
if (damage_players[player_index].unarmed_melee_damage_index == damage_index && damage_index != NONE &&
game_time_get() - damage_players[player_index].unarmed_time <= RECENT_WEAPON_TICKS)
@@ -1197,36 +1152,6 @@ static real distributed_player_deals(
SET_FLAG(*kinds, _damage_source_collision_bit, TRUE);
}
}
/* (a grenade's damage only from one thrown: holding grenades deals
nothing) */
for (index = 0; index < globals->grenades.count && index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
{
struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, index,
struct game_globals_grenade);
boolean thrown = distributed_grenade_throw(player_index, index) != NONE;
if ((thrown || distributed_explosion_this_tick(player_index, damage_index, NULL)) &&
grenade->projectile.index != NONE)
{
byte this_kinds;
struct damage_reach this_reach;
real this_rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &this_kinds,
&this_reach);
if (this_rate > 0.0f)
{
distributed_reach_combine(reach, &this_reach);
/* (one with a throw left before one only going off now) */
if (*grenade_type == NONE || thrown)
*grenade_type = index;
}
grenade_rate = MAX(grenade_rate, this_rate);
*kinds |= this_kinds;
}
}
if (rate > 0.0f)
*grenade_type = NONE;
rate = MAX(rate, grenade_rate);
return rate * HIT_REPORT_RATE_MARGIN;
}
@@ -1507,6 +1432,16 @@ static void distributed_note_weapons(
if (vehicle && vehicle->unit.driver_object_index == unit_index)
damage_players[player_index].driven_time = game_time_get();
}
{
struct unit_datum *unit = unit_get(unit_index);
short grenade_index;
for (grenade_index = 0; grenade_index < NUMBER_OF_UNIT_GRENADE_TYPES; grenade_index++)
{
if (unit->unit.grenade_counts[grenade_index] > 0)
damage_players[player_index].grenade_times[grenade_index] = game_time_get();
}
}
for (unit_number = 0; unit_number < 2; unit_number++)
{
struct unit_datum *unit = units[unit_number] != NONE ?
@@ -1704,8 +1639,6 @@ static boolean distributed_report_valid(
real reach;
struct damage_reach fired_reach;
short seen;
short grenade_type;
short throw_index = NONE;
/* that machine's player */
if (player_index == NO_PLAYER || player_index >= MAXIMUM_TRACKED_PLAYERS ||
@@ -1738,8 +1671,7 @@ static boolean distributed_report_valid(
return FALSE;
ticks = MIN(now - report->host_time + TARGET_HISTORY_SLACK_TICKS, TARGET_HISTORY_TICKS - 1);
/* damage that player could deal, and how they deal it */
rate = distributed_player_deals(player_index, report->damage.definition_index, &kinds, &fired_reach,
&grenade_type);
rate = distributed_player_deals(player_index, report->damage.definition_index, &kinds, &fired_reach);
if (rate <= 0.0f)
return FALSE;
/* ... of its shape: at a point, else a melee blow, else a collision; and
@@ -1781,26 +1713,12 @@ static boolean distributed_report_valid(
reach += definition->cutoff_radius;
if (!(distributed_distance_squared(&report->damage.origin, &report->target_position) <= reach * reach))
return FALSE;
/* what only a grenade of theirs deals, from one the host saw them throw
that has not gone off: its explosion once (the explosion's other objects
that tick come free, distributed_report_paid), what it hits at a point
while it is about */
if (grenade_type != NONE &&
!distributed_explosion_this_tick(player_index, report->damage.definition_index, &report->damage.epicenter))
{
throw_index = distributed_grenade_throw(player_index, grenade_type);
if (throw_index == NONE)
return FALSE;
}
/* no more than the weapon fires */
if (!distributed_report_paid(player_index, report, rate, area && !melee && !collision,
definition->cutoff_radius > 0.0f))
{
return FALSE;
}
/* (the grenade gone off: its throw spent) */
if (throw_index != NONE && area && !melee && !collision)
damage_players[player_index].throw_times[throw_index] = NONE;
/* a melee blow from where the host had the player (its origin their
head, its epicenter their body); a collision from the vehicle they
drove */
@@ -2021,26 +1939,6 @@ static boolean distributed_event_goes_to(
return FALSE;
}
/* How often the host logs how many client hits it accepted and refused
(only when it refused some). Tells "my shots don't register" apart from
"I missed". */
#define REPORT_LOG_TICKS (30 * TICKS_PER_SECOND)
static void distributed_log_reports(
void)
{
static long logged_dealt;
static long logged_rejected;
if (game_time_get() % REPORT_LOG_TICKS != 0 || damage_rejected_reports == logged_rejected)
return;
error(_error_silent, "damage: clients' hits in the last %d seconds: %ld dealt, %ld refused",
REPORT_LOG_TICKS / TICKS_PER_SECOND, damage_dealt_reports - logged_dealt,
damage_rejected_reports - logged_rejected);
logged_dealt = damage_dealt_reports;
logged_rejected = damage_rejected_reports;
}
void network_damage_host_tick(
void)
{
@@ -2051,7 +1949,6 @@ void network_damage_host_tick(
distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES) : 0;
short machine_number;
distributed_log_reports();
distributed_note_weapons();
distributed_note_targets();
distributed_note_event_destinations();
@@ -2205,41 +2102,6 @@ void network_damage_statistics(
*replayed_events = damage_replayed_events;
}
/* the host: a grenade a player's unit threw (units.c), as the host's own
game threw it, which that player's machine reports the damage of
(distributed_report_valid) */
void network_damage_note_grenade(
long unit_index,
short grenade_type)
{
struct unit_datum *unit = unit_get(unit_index);
long player_index;
short index;
short slot = 0;
if (game_connection() != _game_connection_network_server || unit->unit.player_index == NONE ||
grenade_type < 0 || grenade_type >= NUMBER_OF_UNIT_GRENADE_TYPES)
{
return;
}
player_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index);
if (player_index >= MAXIMUM_TRACKED_PLAYERS)
return;
/* (in a free place, else the oldest's) */
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
{
if (damage_players[player_index].throw_times[index] == NONE)
{
slot = index;
break;
}
if (damage_players[player_index].throw_times[index] < damage_players[player_index].throw_times[slot])
slot = index;
}
damage_players[player_index].throw_times[slot] = game_time_get();
damage_players[player_index].throw_types[slot] = grenade_type;
}
void network_damage_new_game(
void)
{
@@ -2259,8 +2121,8 @@ void network_damage_new_game(
for (index = 0; index < MAXIMUM_RECENT_WEAPONS; index++)
damage_players[player_index].definition_indices[index] = NONE;
damage_players[player_index].unarmed_melee_damage_index = NONE;
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
damage_players[player_index].throw_times[index] = NONE;
for (index = 0; index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
damage_players[player_index].grenade_times[index] = -RECENT_WEAPON_TICKS - 1;
damage_players[player_index].driven_time = -RECENT_WEAPON_TICKS - 1;
damage_players[player_index].hit_seconds = HIT_REPORT_BURST_SECONDS;
damage_explosions[player_index].time = NONE;
+27 -250
View File
@@ -53,24 +53,18 @@ machine (their datum identifiers need not be).
#include "cseries.h"
#include "cseries/errors.h"
#include "cache/cache_files.h"
#include "models/model_animation_definitions.h"
#include "game/game.h"
#include "game/game_globals.h"
#include "game/players.h"
#include "game/game_engine.h"
#include "main/main.h"
#include "game/player_queues_new.h"
#include "networking/network_game_globals.h"
#include "objects/objects.h"
#include "objects/damage.h"
#include "scenario/scenario.h"
#include "scenario/scenario_definitions.h"
#include "structures/structure_bsp_definitions.h"
#include "units/units.h"
#include "units/biped_definitions.h"
#include "units/bipeds.h"
#include "network_coop.h"
#include "network_distributed.h"
#include <limits.h>
@@ -244,8 +238,11 @@ before the host takes it no longer, and before the client is put where the
host has it (no further than that: between the two they would disagree for
good, the host's player somewhere its own is not) */
#define HOST_ACCEPT_TOLERANCE 3.5f
#define REMOTE_CORRECTION_TOLERANCE 0.05f
#define LOCAL_CORRECTION_TOLERANCE 3.0f
/* how fast a client's own player's unit moves at most (world units a tick) */
/* how far from the origin a unit is (world units), and how fast a client's
own player's unit moves at most (world units a tick) */
#define UNIT_WORLD_BOUND 32768.0f
#define MAXIMUM_PREDICTED_SPEED 2.0f
/* ... on foot: this many times as fast as a player runs and jumps, or as
fast as the host's ticks threw its copy lately, whichever is more; and how
@@ -320,9 +317,7 @@ ticks before it */
struct distributed_player_input
{
byte player_index;
/* co-op: the structure BSP the client has loaded */
byte structure_bsp_index;
byte pad[2];
byte pad[3];
/* the client's tick */
long tick;
/* the latest host tick the client has had a message of (the host
@@ -671,40 +666,6 @@ boolean distributed_point_valid(
return fabsf(point->x) <= bound && fabsf(point->y) <= bound && fabsf(point->z) <= bound;
}
/* whether a tag index from the host really is a tag of that group */
boolean distributed_tag_of_group(
long tag_index,
unsigned long group_tag)
{
struct tag_iterator iterator;
long index;
tag_iterator_new(&iterator, group_tag);
while ((index = tag_iterator_next(&iterator)) != NONE)
{
if (index == tag_index)
return TRUE;
}
return FALSE;
}
/* the graph's animation, if the graph tag and index are valid */
struct animation *distributed_graph_animation(
long animation_graph_index,
short animation_index)
{
struct animation_graph *graph;
if (!distributed_tag_of_group(animation_graph_index, ANIMATION_GRAPH_TAG))
return NULL;
graph = animation_graph_definition_get(animation_graph_index);
if (animation_index < 0 || animation_index >= graph->animations.count)
return NULL;
return TAG_BLOCK_GET_ELEMENT(&graph->animations, animation_index, struct animation);
}
boolean distributed_object_index_valid(
long object_index)
{
@@ -788,7 +749,7 @@ void distributed_unit_vector_unpack(
}
}
word distributed_vitality_pack(
static word distributed_vitality_pack(
real value)
{
value *= VITALITY_SCALE;
@@ -796,13 +757,15 @@ word distributed_vitality_pack(
return (word)(long)floor(value + 0.5f);
}
real distributed_vitality_unpack(
static real distributed_vitality_unpack(
word value)
{
return (real)value / VITALITY_SCALE;
}
short distributed_angle_pack(
/* an angle as a 16-bit fraction of a turn, and back (yaw from 0 to 2 pi,
pitch from -pi to pi) */
static short distributed_angle_pack(
real angle)
{
real turns = angle / (2.0f * _pi);
@@ -811,7 +774,7 @@ short distributed_angle_pack(
return (short)(word)((long)floor(turns * 65536.0f + 0.5f) & 0xFFFF);
}
real distributed_angle_unpack(
static real distributed_angle_unpack(
short value,
boolean signed_angle)
{
@@ -2085,14 +2048,9 @@ static void distributed_apply_predictions(
{
distributed_predictions[player_index].valid = FALSE;
distributed_predictions[player_index].taken_host_time = NONE;
/* (of the unit it has now: not one of a life before; and not
from a co-op client still loading the host's BSP, whose player
falls there with no floor under it) */
if (unit_index != NONE && unit_index == state->unit_index &&
network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index))
{
/* (of the unit it has now: not one of a life before) */
if (unit_index != NONE && unit_index == state->unit_index)
distributed_take_prediction(player_index, unit_index, &bound);
}
}
/* (what the host's next tick starts from) */
if (unit_index != NONE)
@@ -2264,7 +2222,6 @@ static void distributed_client_send_inputs(
if (!update_client_distributed_input(local_player_index, &input->tick, &input->action, input->control_flags))
continue;
input->player_index = distributed_player_to_byte(player_index);
input->structure_bsp_index = (byte)global_structure_bsp_index_get();
input->host_time = distributed_host_time;
count++;
}
@@ -2275,37 +2232,6 @@ static void distributed_client_send_inputs(
}
}
/* (the host, in co-op) where each client player's input last faced, by
absolute index: how far it turns between inputs is its looking around */
static struct
{
boolean valid;
real_euler_angles2d facing;
} distributed_input_facings[MAXIMUM_TRACKED_PLAYERS];
/* (the host, in co-op) a client player's input counted in the scripts'
action tests, as a local player's is (player_control_action_test_note) */
static void distributed_note_input_actions(
short player_index,
struct player_action const *action)
{
real_euler_angles2d turn = { 0.0f, 0.0f };
if (distributed_input_facings[player_index].valid)
{
turn.yaw = action->desired_facing.yaw - distributed_input_facings[player_index].facing.yaw;
turn.pitch = action->desired_facing.pitch - distributed_input_facings[player_index].facing.pitch;
/* (the short way round) */
if (turn.yaw > _pi)
turn.yaw -= 2.0f * _pi;
else if (turn.yaw < -_pi)
turn.yaw += 2.0f * _pi;
}
distributed_input_facings[player_index].valid = TRUE;
distributed_input_facings[player_index].facing = action->desired_facing;
player_control_action_test_note(action->control_flags, &turn, &action->throttle, action->primary_trigger);
}
/* (the host) a client's players' input, and from it (once a message) how
long a message takes that client and back */
static void distributed_handle_inputs(
@@ -2335,10 +2261,6 @@ static void distributed_handle_inputs(
action.throttle.i = PIN(action.throttle.i, -1.0f, 1.0f);
action.throttle.j = PIN(action.throttle.j, -1.0f, 1.0f);
action.primary_trigger = PIN(action.primary_trigger, 0.0f, 1.0f);
/* (a spectator's buttons pick whom it watches: not counted) */
if (network_coop_active() && player->unit_index != NONE)
distributed_note_input_actions(input->player_index, &action);
network_coop_note_player_structure_bsp(input->player_index, input->structure_bsp_index);
update_server_handle_distributed_input(DATUM_INDEX_NEW(input->player_index, player->identifier), input->tick,
&action, input->control_flags, DISTRIBUTED_INPUT_HISTORY);
if (input->host_time != NONE && (host_time == NONE || input->host_time > host_time))
@@ -3009,52 +2931,6 @@ static void distributed_send_pings(
}
}
/* Co-op: the host's structure BSP. Clients switch BSP only when told
(their own trigger volumes don't, players.c). A switch is sent at once and
reliably; it is also resent regularly so a client that joined late catches
up. */
#define STRUCTURE_BSP_INTERVAL_TICKS (TICKS_PER_SECOND / 2)
static short distributed_sent_structure_bsp_index = NONE;
struct distributed_structure_bsp
{
short structure_bsp_index;
short pad;
};
struct distributed_structure_bsp_message
{
struct distributed_message_header header;
struct distributed_structure_bsp structure_bsp;
};
static void distributed_send_structure_bsp(
void)
{
struct distributed_structure_bsp_message message;
short index = global_structure_bsp_index_get();
boolean switched = index != distributed_sent_structure_bsp_index;
message.structure_bsp.structure_bsp_index = index;
message.structure_bsp.pad = 0;
distributed_send(&message, _distributed_message_structure_bsp, 1, (word)sizeof(message),
switched ? _distributed_to_clients_reliably : _distributed_to_clients);
distributed_sent_structure_bsp_index = index;
}
static void distributed_handle_structure_bsp(
struct distributed_structure_bsp const *structure_bsp)
{
short index = structure_bsp->structure_bsp_index;
if (network_coop_active() && index >= 0 && index < global_scenario_get()->structure_bsp_references.count &&
index != global_structure_bsp_index_get())
{
main_switch_structure_bsp(index);
}
}
/* the players' statistics that changed since they were last sent, and when
refreshing, STATISTICS_REFRESH_PLAYERS more whatever they are, round them
all (a client that lost a change has it again within eight seconds) */
@@ -3200,7 +3076,6 @@ void network_distributed_new_game(
csmemset(distributed_accepted, 0, sizeof(distributed_accepted));
csmemset(distributed_host_speeds, 0, sizeof(distributed_host_speeds));
csmemset(distributed_round_trips, 0, sizeof(distributed_round_trips));
csmemset(distributed_input_facings, 0, sizeof(distributed_input_facings));
csmemset(distributed_client_clocks, 0, sizeof(distributed_client_clocks));
csmemset(distributed_client_identities, 0, sizeof(distributed_client_identities));
distributed_identity_sent = FALSE;
@@ -3261,8 +3136,7 @@ void network_distributed_new_game(
network_damage_new_game();
network_vr_new_game();
bots_new_game();
network_actors_new_game();
network_coop_new_game();
network_voice_new_game();
}
/* after each tick (game_time.c) */
@@ -3305,18 +3179,10 @@ void network_distributed_tick(
distributed_statistics_due = FALSE;
if (game_time_get() % PING_INTERVAL_TICKS == 0)
distributed_send_pings();
/* (before the players, so clients load a new BSP before they hear
where the host moved everyone into it) */
if (network_coop_active() && (global_structure_bsp_index_get() != distributed_sent_structure_bsp_index ||
game_time_get() % STRUCTURE_BSP_INTERVAL_TICKS == 0))
{
distributed_send_structure_bsp();
}
distributed_host_send_players();
network_vr_tick();
bots_tick();
network_actors_host_tick();
network_coop_host_tick();
network_voice_tick();
distributed_send_pickups();
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
distributed_send_game_state(NONE);
@@ -3326,10 +3192,10 @@ 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();
network_coop_client_tick();
}
distributed_batches_flush();
distributed_machines.in_tick = FALSE;
@@ -3358,19 +3224,6 @@ static boolean distributed_message_stale(
case _distributed_message_damage_events:
case _distributed_message_vr_poses:
case _distributed_message_pings:
case _distributed_message_actor_states:
case _distributed_message_structure_bsp:
case _distributed_message_coop_presentation:
case _distributed_message_coop_device_groups:
case _distributed_message_coop_object_names:
case _distributed_message_coop_skip_vote:
case _distributed_message_coop_events:
case _distributed_message_coop_object_transforms:
case _distributed_message_actor_damage:
case _distributed_message_coop_object_looks:
case _distributed_message_damage_animations:
case _distributed_message_coop_screen_effect:
case _distributed_message_coop_device_states:
break;
default:
return FALSE;
@@ -3673,27 +3526,6 @@ void network_distributed_ban(
}
}
/* (the host: network_server_manager.c, its kick command) players kicked by
the host, which may join again: every machine told, nothing kept (no line
in BANS_FILE) */
void network_distributed_kick(
char const *names)
{
char kept_names[64];
char notice[MAXIMUM_NOTICE_LENGTH];
distributed_printable(kept_names, sizeof(kept_names), names);
snprintf(notice, sizeof(notice), "%s kicked by the host", kept_names);
/* (to every client in the game: in the lobby, the host's own) */
if (game_in_progress())
distributed_send_notice(notice);
else
{
console_warning("%s", notice);
error(_error_log, "%s", notice);
}
}
/* (the host) a client machine's tick, which one of its messages is
stamped with: its clock measured, each window, against the host's; one
whose game runs fast (distributed_client_clock) has its players'
@@ -3829,19 +3661,6 @@ void network_distributed_handle_message(
case _distributed_message_unit_states: entry_size = DISTRIBUTED_UNIT_STATE_MINIMUM_SIZE; break;
case _distributed_message_player_statistics: entry_size = sizeof(struct distributed_player_statistics); break;
case _distributed_message_pings: entry_size = sizeof(struct distributed_player_ping); break;
case _distributed_message_actor_states: entry_size = network_actors_entry_size(); break;
case _distributed_message_structure_bsp: entry_size = sizeof(struct distributed_structure_bsp); break;
case _distributed_message_coop_presentation: entry_size = network_coop_presentation_entry_size(); break;
case _distributed_message_coop_events: entry_size = network_coop_event_entry_size(); break;
case _distributed_message_coop_skip_vote: entry_size = network_coop_skip_vote_entry_size(); break;
case _distributed_message_coop_device_groups: entry_size = network_coop_device_group_entry_size(); break;
case _distributed_message_coop_object_names: entry_size = network_coop_object_names_entry_size(); break;
case _distributed_message_coop_object_transforms: entry_size = network_coop_object_transform_entry_size(); break;
case _distributed_message_actor_damage: entry_size = network_actors_damage_entry_size(); break;
case _distributed_message_coop_object_looks: entry_size = network_coop_object_look_entry_size(); break;
case _distributed_message_damage_animations: entry_size = network_objects_damage_animation_entry_size(); break;
case _distributed_message_coop_screen_effect: entry_size = network_coop_screen_effect_entry_size(); break;
case _distributed_message_coop_device_states: entry_size = network_coop_device_state_entry_size(); break;
case _distributed_message_pickups: entry_size = sizeof(struct distributed_pickup); break;
case _distributed_message_player_inputs: entry_size = sizeof(struct distributed_player_input); break;
case _distributed_message_relayed_actions: entry_size = DISTRIBUTED_RELAYED_ACTION_MINIMUM_SIZE; break;
@@ -3851,6 +3670,7 @@ void network_distributed_handle_message(
case _distributed_message_client_ready: entry_size = 0; break;
case _distributed_message_client_identity: entry_size = sizeof(struct distributed_client_identity); break;
case _distributed_message_vr_poses: entry_size = network_vr_entry_size(); break;
case _distributed_message_voice: entry_size = network_voice_entry_size(); break;
case _distributed_message_damage_events:
case _distributed_message_hit_reports: entry_size = network_damage_entry_size(header.type); break;
default: entry_size = network_objects_entry_size(header.type); break;
@@ -3860,23 +3680,20 @@ void network_distributed_handle_message(
{
return;
}
/* (the kinds whose handlers read one entry: not without it) */
switch (header.type)
{
case _distributed_message_structure_bsp:
case _distributed_message_coop_presentation:
case _distributed_message_coop_object_names:
case _distributed_message_coop_skip_vote:
if (header.count < 1)
return;
break;
default:
break;
}
distributed_statistics.received++;
/* (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 :
@@ -3896,7 +3713,6 @@ void network_distributed_handle_message(
case _distributed_message_hit_reports:
case _distributed_message_vehicle_prediction:
case _distributed_message_player_inputs:
case _distributed_message_coop_skip_vote:
if (machine_index == NONE || game_connection() != _game_connection_network_server)
return;
break;
@@ -3933,45 +3749,6 @@ void network_distributed_handle_message(
case _distributed_message_unit_states:
distributed_handle_unit_states((byte const *)entries, (byte const *)message + size, header.count);
break;
case _distributed_message_actor_states:
network_actors_handle_states(entries, header.count);
break;
case _distributed_message_structure_bsp:
distributed_handle_structure_bsp((struct distributed_structure_bsp const *)entries);
break;
case _distributed_message_coop_presentation:
network_coop_handle_presentation(entries, header.game_time);
break;
case _distributed_message_coop_events:
network_coop_handle_events(entries, header.count);
break;
case _distributed_message_coop_skip_vote:
network_coop_handle_skip_vote(machine_index, entries);
break;
case _distributed_message_coop_device_groups:
network_coop_handle_device_groups(entries, header.count);
break;
case _distributed_message_coop_object_names:
network_coop_handle_object_names(entries);
break;
case _distributed_message_coop_object_transforms:
network_coop_handle_object_transforms(entries, header.count);
break;
case _distributed_message_actor_damage:
network_actors_handle_damage(entries, header.count);
break;
case _distributed_message_coop_object_looks:
network_coop_handle_object_looks(entries, header.count);
break;
case _distributed_message_damage_animations:
network_objects_handle_damage_animations(entries, header.count);
break;
case _distributed_message_coop_screen_effect:
network_coop_handle_screen_effect(entries, header.count);
break;
case _distributed_message_coop_device_states:
network_coop_handle_device_states(entries, header.count);
break;
case _distributed_message_player_statistics:
{
/* the host's count of kills, deaths, ... */
+10 -113
View File
@@ -65,47 +65,6 @@ enum
/* every player's ping as the host measures it, every two seconds, for
the scoreboard (unreliable) */
_distributed_message_pings,
/* the host's actors' units: their control and state (network_actors.c).
A number of its own, clear of the kinds upstream adds: a build without
it drops the message as a kind it does not know, so the network version
stays upstream's and its players join as before */
_distributed_message_actor_states = 64,
/* co-op (a campaign map, no game engine): the host's structure BSP,
twice a second, for a client to switch to; numbered as actor_states */
_distributed_message_structure_bsp,
/* co-op: the host's cinematic, camera and screen fade, every tick
(network_coop.c) */
_distributed_message_coop_presentation,
/* (0.2.0's co-op script sounds; now part of coop_events. The number
stays taken so a 0.2.0 build never misreads a newer message.) */
_distributed_message_coop_sounds_retired,
/* co-op: device group state (network_coop.c) */
_distributed_message_coop_device_groups,
/* co-op: which named objects exist on the host (network_coop.c) */
_distributed_message_coop_object_names,
/* co-op: a client's vote to skip the cutscene, to the host (network_coop.c) */
_distributed_message_coop_skip_vote,
/* co-op: what the host's scripts did once: sounds, titles, HUD text,
animations (network_coop.c) */
_distributed_message_coop_events,
/* co-op: where the host's scenery and machines are, when they move
(network_coop.c) */
_distributed_message_coop_object_transforms,
/* the damage the host's AI units are taking, which their shields'
flares are drawn by (network_actors.c); 73 in upstream's AI sync too */
_distributed_message_actor_damage = 73,
/* co-op: how the host's objects look (their permutations, scale), when
that changes (network_coop.c) */
_distributed_message_coop_object_looks,
/* the flinch and death animations the host picked for its units, which a
client plays rather than its own picks (network_objects.c) */
_distributed_message_damage_animations = 75,
/* co-op: the host's cinematic screen effect (blur, filters, video), when
it changes and every two seconds (network_coop.c) */
_distributed_message_coop_screen_effect = 76,
/* co-op: where the host's devices are (doors, elevators), when they move
(network_coop.c) */
_distributed_message_coop_device_states = 77,
/* The VR port's own messages, numbered far past the game's (240 on),
so that messages upstream adds never take their numbers: a build
@@ -113,6 +72,10 @@ enum
/* a VR player's hands, every tick: a client's own to the host, the
host's of the others to each client (unreliable; network_vr.c) */
_distributed_message_vr_poses = 240,
/* a player's speech, every tick while they speak: a client's own to
the host, the host's and the other clients' to each client (unreliable;
network_voice.c) */
_distributed_message_voice,
NUMBER_OF_DISTRIBUTED_MESSAGES
};
@@ -134,10 +97,6 @@ tick, drawn as it goes; further, it is put there, drawn gliding
#define HOST_BLEND_DISTANCE 0.25f
#define HOST_VEHICLE_BLEND_DISTANCE 0.5f
#define REMOTE_BLEND_DISTANCE 1.0f
/* world units: how far a remote unit may be from where the host says
before it is put there, and how far from the origin a unit can be */
#define REMOTE_CORRECTION_TOLERANCE 0.05f
#define UNIT_WORLD_BOUND 32768.0f
#define REMOTE_VEHICLE_BLEND_DISTANCE 2.0f
enum
@@ -235,10 +194,6 @@ never 0, which any object at the index matches) */
boolean distributed_real_valid(real value);
boolean distributed_point_valid(real_point3d const *point, real bound);
boolean distributed_object_index_valid(long object_index);
/* whether a tag index from the host really is a tag of that group */
boolean distributed_tag_of_group(long tag_index, unsigned long group_tag);
/* the graph's animation, if a graph tag and animation index from the host are valid */
struct animation *distributed_graph_animation(long animation_graph_index, short animation_index);
/* ... an orientation's two axes (unpacked): TRUE when they are one long
and about square, then made exactly so */
boolean distributed_axes_make_valid(real_vector3d *forward, real_vector3d *up);
@@ -249,70 +204,12 @@ void distributed_unit_vector_unpack(struct distributed_vector const *vector, rea
#define DISTRIBUTED_UNIT_SCALE 32767.0f
#define DISTRIBUTED_VELOCITY_SCALE 1024.0f
#define DISTRIBUTED_ANGULAR_VELOCITY_SCALE 4096.0f
/* an angle as a 16-bit fraction of a turn, and back (yaw from 0 to 2 pi,
pitch from -pi to pi) */
short distributed_angle_pack(real angle);
real distributed_angle_unpack(short value, boolean signed_angle);
/* shields and health in 16 bits */
word distributed_vitality_pack(real value);
real distributed_vitality_unpack(word value);
/* ---------- prototypes/NETWORK_ACTORS.C */
void network_actors_new_game(void);
/* (the host, after each tick) the units its actors drove this tick, to
each client */
void network_actors_host_tick(void);
/* (a client) the host's word on its actors' units */
void network_actors_handle_states(void const *entries, short count);
word network_actors_entry_size(void);
/* (a client, in its tick where the host runs its actors) each actor's unit
given the control the host last sent for it */
void network_actors_drive(void);
/* ---------- prototypes/NETWORK_COOP.C */
void network_coop_new_game(void);
/* after each tick, on the host and on a client */
void network_coop_host_tick(void);
void network_coop_client_tick(void);
/* the message handlers, and their entry sizes */
word network_coop_presentation_entry_size(void);
void network_coop_handle_presentation(void const *entries, long host_time);
word network_coop_event_entry_size(void);
void network_coop_handle_events(void const *entries, short count);
word network_coop_device_group_entry_size(void);
void network_coop_handle_device_groups(void const *entries, short count);
word network_coop_object_names_entry_size(void);
void network_coop_handle_object_names(void const *entries);
word network_coop_object_transform_entry_size(void);
word network_coop_object_look_entry_size(void);
void network_coop_handle_object_looks(void const *entries, short count);
word network_coop_screen_effect_entry_size(void);
word network_coop_device_state_entry_size(void);
void network_coop_handle_device_states(void const *entries, short count);
void network_coop_handle_screen_effect(void const *entries, short count);
word network_actors_damage_entry_size(void);
void network_actors_handle_damage(void const *entries, short count);
void network_coop_handle_object_transforms(void const *entries, short count);
word network_coop_skip_vote_entry_size(void);
void network_coop_handle_skip_vote(long machine_index, void const *entries);
/* game_sound.c: a sound the host's scripts played */
enum
{
_coop_sound_impulse,
_coop_sound_looping_start,
_coop_sound_looping_stop,
};
void network_coop_note_sound(short kind, long definition_index, long object_index, real scale);
/* ---------- prototypes/NETWORK_OBJECTS.C */
void network_objects_new_game(void);
/* after each tick */
void network_objects_host_tick(void);
word network_objects_damage_animation_entry_size(void);
void network_objects_handle_damage_animations(void const *entries, short count);
void network_objects_client_tick(void);
/* (the host) a client has loaded the game and asks for the host's objects:
again, having failed to make one of them */
@@ -339,16 +236,10 @@ boolean network_objects_reconcile(long object_index, real_point3d const *positio
real blend_distance);
/* a unit in the vehicle's seat as the host has it (NONE: in none) */
void network_objects_set_seat(long unit_index, long vehicle_index, short seat_index);
/* (the host) the ticks between sends to a machine of a unit at position, by
how near that machine's players are (network_actors.c) */
short network_objects_send_period(long machine_index, real_point3d const *position);
/* ---------- prototypes/NETWORK_DAMAGE.C */
void network_damage_new_game(void);
/* the host: a grenade a player's unit threw (units.c), whose damage that
player's machine reports */
void network_damage_note_grenade(long unit_index, short grenade_type);
void network_damage_host_tick(void);
void network_damage_client_tick(void);
void network_damage_handle_events(void const *entries, short count);
@@ -365,4 +256,10 @@ void network_vr_handle_poses(long machine_index, void const *entries, short coun
void bots_new_game(void);
void bots_tick(void);
/* the voice chat (network_voice.c) */
void network_voice_new_game(void);
void network_voice_tick(void);
word network_voice_entry_size(void);
void network_voice_handle(long machine_index, void const *message, word size);
#endif // __NETWORK_DISTRIBUTED_H
+20 -407
View File
@@ -50,7 +50,6 @@ same datum index (identifier and all), so that any message can name one:
#include "objects/objects.h"
#include "objects/damage.h"
#include "objects/object_definitions.h"
#include "models/model_animation_definitions.h"
#include "models/model_definitions.h"
#include "units/units.h"
#include "units/biped_definitions.h"
@@ -60,8 +59,6 @@ same datum index (identifier and all), so that any message can name one:
#include "items/weapons.h"
#include "items/weapon_definitions.h"
#include "items/equipment_definitions.h"
#include "cutscene/cinematics.h"
#include "network_coop.h"
#include "network_distributed.h"
#include <math.h>
@@ -332,14 +329,11 @@ static struct
} objects_host_inventories[MAXIMUM_TRACKED_OBJECTS];
/* ... each client machine, found once a tick: where its players' living
units are, those units, and the vehicles they drive (with the player's
absolute index); and where every player's living unit is, for a machine
with none (dead or joining, it watches one of them) */
absolute index) */
static struct
{
short count;
long indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short player_unit_count;
real_point3d player_unit_origins[MAXIMUM_TRACKED_PLAYERS];
struct
{
short unit_count;
@@ -643,32 +637,6 @@ static void distributed_vector_clamp(
}
}
/* Whether the object is a vehicle no player drives in co-op (a Pelican on
its flight path): nothing on a client steers it, so its copy is kept at rest
there (no physics of its own) and moved only to where the host has it. In
multiplayer an empty vehicle (one bailed out of, or tumbling) keeps its own
physics between updates. */
static boolean distributed_vehicle_unsteered(
long object_index)
{
struct unit_datum *vehicle;
if (!network_coop_active() || object_get(object_index)->object.type != _object_type_vehicle)
return FALSE;
vehicle = unit_get(object_index);
return vehicle->unit.driver_object_index == NONE ||
unit_get(vehicle->unit.driver_object_index)->unit.player_index == NONE;
}
/* Whether a client puts its copy exactly where the host has it, rather than
closing on it: an unsteered vehicle, and anything in a co-op cutscene
(where it keeps up with the host's camera). */
static boolean distributed_object_follows_host(
long object_index)
{
return (network_coop_active() && cinematic_in_progress()) || distributed_vehicle_unsteered(object_index);
}
/* (the transform checked) */
static void distributed_object_move(
long object_index,
@@ -690,10 +658,7 @@ static void distributed_object_move(
object->object.translational_velocity = *velocity;
if (angular_velocity)
object->object.angular_velocity = *angular_velocity;
if (!distributed_object_follows_host(object_index))
render_interpolation_correct_object(object_index, &offset);
if (distributed_vehicle_unsteered(object_index))
SET_FLAG(object->object.flags, _object_at_rest_bit, TRUE);
render_interpolation_correct_object(object_index, &offset);
}
void network_objects_correct(
@@ -738,33 +703,15 @@ boolean network_objects_reconcile(
distributed_object_move(object_index, position, &valid_forward, &valid_up, velocity, angular_velocity);
return TRUE;
}
/* Move halfway, and draw the move as a glide over the next few frames,
as larger corrections are. Drawn as one step, a unit corrected every tick
or two (a distant enemy) visibly hitched each time. */
if (distributed_object_follows_host(object_index))
blended = *position;
else
{
blended.x = object->object.position.x + dx * 0.5f;
blended.y = object->object.position.y + dy * 0.5f;
blended.z = object->object.position.z + dz * 0.5f;
}
{
real_vector3d offset;
offset.i = object->object.position.x - blended.x;
offset.j = object->object.position.y - blended.y;
offset.k = object->object.position.z - blended.z;
object_set_position(object_index, &blended, &valid_forward, &valid_up);
if (!distributed_object_follows_host(object_index))
render_interpolation_correct_object(object_index, &offset);
}
/* (half of the way: the tick's snapshots draw it moving, no jump) */
blended.x = object->object.position.x + dx * 0.5f;
blended.y = object->object.position.y + dy * 0.5f;
blended.z = object->object.position.z + dz * 0.5f;
object_set_position(object_index, &blended, &valid_forward, &valid_up);
if (velocity)
object->object.translational_velocity = *velocity;
if (angular_velocity)
object->object.angular_velocity = *angular_velocity;
if (distributed_vehicle_unsteered(object_index))
SET_FLAG(object->object.flags, _object_at_rest_bit, TRUE);
return FALSE;
}
@@ -819,175 +766,6 @@ static long distributed_driven_vehicle(
object_get(unit_index)->object.parent_object_index : NONE;
}
/* ---------- damage animations
A unit's flinches and deaths are animations picked by where the damage came
from and at random among their permutations, so a client replaying the
host's damage could pick others. The host sends its picks (each in three
ticks' messages, in case one is lost). A client plays the host's pick when
it gets there first, or switches to it if its own has only just begun
(unit_port_correct_damage_animation). */
enum
{
DAMAGE_ANIMATION_SENDS = 3,
MAXIMUM_QUEUED_DAMAGE_ANIMATIONS = 32,
/* how long a client keeps the host's pick for a hit it hasn't replayed yet */
DAMAGE_ANIMATION_KEPT_TICKS = 2 * TICKS_PER_SECOND,
};
/* type: units.h's _unit_damage_animation_soft_ping and the rest; number:
the pick's, so a client tells a pick sent again from a new one */
struct distributed_damage_animation
{
long unit_index;
short animation_index;
byte type;
byte number;
};
struct distributed_damage_animations_message
{
struct distributed_message_header header;
struct distributed_damage_animation animations[MAXIMUM_QUEUED_DAMAGE_ANIMATIONS];
};
/* host: the picks still to be sent */
static struct
{
struct distributed_damage_animation animation;
short sends;
} host_damage_animations[MAXIMUM_QUEUED_DAMAGE_ANIMATIONS];
static short host_damage_animation_count;
static byte host_damage_animation_number;
/* client: the host's latest pick of each type for each unit, by absolute
index: its number, and when it came (NONE once played) */
static struct
{
long unit_index;
short animation_index[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS];
byte number[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS];
long time[NUMBER_OF_UNIT_DAMAGE_ANIMATIONS];
} client_damage_animations[MAXIMUM_TRACKED_OBJECTS];
/* whether the animation is one of the unit's graph */
static boolean distributed_unit_has_animation(
long unit_index,
short animation_index)
{
long graph_index = unit_definition_get(unit_get(unit_index)->definition_index)->object.animation_graph.index;
return graph_index != NONE && animation_index >= 0 &&
animation_index < animation_graph_definition_get(graph_index)->animations.count;
}
/* units.c, as a unit flinches or dies: the host notes the animation it
picked; a client takes the host's pick when it has one */
short network_objects_damage_animation(
long unit_index,
short type,
short animation_index)
{
short connection = game_connection();
long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_index);
if (type < 0 || type >= NUMBER_OF_UNIT_DAMAGE_ANIMATIONS)
return animation_index;
if (connection == _game_connection_network_server && animation_index != NONE &&
host_damage_animation_count < MAXIMUM_QUEUED_DAMAGE_ANIMATIONS)
{
struct distributed_damage_animation *animation =
&host_damage_animations[host_damage_animation_count].animation;
animation->unit_index = unit_index;
animation->animation_index = animation_index;
animation->type = (byte)type;
animation->number = ++host_damage_animation_number;
host_damage_animations[host_damage_animation_count++].sends = DAMAGE_ANIMATION_SENDS;
}
else if (connection == _game_connection_network_client && absolute_index >= 0 &&
absolute_index < MAXIMUM_TRACKED_OBJECTS && client_damage_animations[absolute_index].unit_index == unit_index)
{
long *time = &client_damage_animations[absolute_index].time[type];
if (*time != NONE && game_time_get() - *time < DAMAGE_ANIMATION_KEPT_TICKS)
{
*time = NONE;
return client_damage_animations[absolute_index].animation_index[type];
}
}
return animation_index;
}
/* host, each tick: the picks still to be sent, to every client */
static void distributed_host_send_damage_animations(
void)
{
struct distributed_damage_animations_message message;
short count = 0;
short index;
for (index = 0; index < host_damage_animation_count; index++)
{
message.animations[count++] = host_damage_animations[index].animation;
if (--host_damage_animations[index].sends <= 0)
host_damage_animations[index--] = host_damage_animations[--host_damage_animation_count];
}
if (count > 0)
{
distributed_send(&message, _distributed_message_damage_animations, count,
(word)(sizeof(message.header) + count * sizeof(message.animations[0])), _distributed_to_clients);
}
}
word network_objects_damage_animation_entry_size(
void)
{
return sizeof(struct distributed_damage_animation);
}
void network_objects_handle_damage_animations(
void const *entries,
short count)
{
struct distributed_damage_animation const *animations = entries;
short index;
for (index = 0; index < count; index++)
{
struct distributed_damage_animation const *animation = &animations[index];
long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(animation->unit_index);
short type;
if (!distributed_object_index_valid(animation->unit_index) || absolute_index >= MAXIMUM_TRACKED_OBJECTS ||
animation->type >= NUMBER_OF_UNIT_DAMAGE_ANIMATIONS || !network_objects_client_has(animation->unit_index) ||
!object_try_and_get_and_verify_type(animation->unit_index, _object_mask_unit) ||
!distributed_unit_has_animation(animation->unit_index, animation->animation_index))
{
continue;
}
if (client_damage_animations[absolute_index].unit_index != animation->unit_index)
{
csmemset(&client_damage_animations[absolute_index], 0, sizeof(client_damage_animations[absolute_index]));
client_damage_animations[absolute_index].unit_index = animation->unit_index;
for (type = 0; type < NUMBER_OF_UNIT_DAMAGE_ANIMATIONS; type++)
client_damage_animations[absolute_index].time[type] = NONE;
}
/* (a pick sent again) */
else if (client_damage_animations[absolute_index].number[animation->type] == animation->number)
{
continue;
}
client_damage_animations[absolute_index].number[animation->type] = animation->number;
client_damage_animations[absolute_index].animation_index[animation->type] = animation->animation_index;
/* (replayed here already: switched to the host's if it has only just begun) */
client_damage_animations[absolute_index].time[animation->type] =
unit_port_correct_damage_animation(animation->unit_index, animation->type, animation->animation_index) ?
NONE : game_time_get();
}
}
/* ---------- the host */
static void distributed_change_from_object(
@@ -1099,30 +877,6 @@ static void distributed_host_update_objects(
}
}
/* The order a newly loaded client receives the host's objects in, most
needed first: players' units (it may spectate them), the vehicles players
and AI ride, other units, then everything else. */
enum
{
_send_rank_player_unit,
_send_rank_vehicle,
_send_rank_unit,
_send_rank_other,
NUMBER_OF_SEND_RANKS
};
static short distributed_object_send_rank(
long object_index)
{
struct object_datum *object = object_get(object_index);
if (object->object.type == _object_type_vehicle)
return _send_rank_vehicle;
if (object_try_and_get_and_verify_type(object_index, _object_mask_unit))
return unit_get(object_index)->unit.player_index != NONE ? _send_rank_player_unit : _send_rank_unit;
return _send_rank_other;
}
/* a client has loaded the game: every object the host has, to it alone,
and word that that is all of them; asked again, only the word (they are on
their way ahead of it), unless it failed to make one since it last asked
@@ -1135,7 +889,6 @@ void network_objects_client_asked(
short count = 0;
short limit = MIN(MAXIMUM_ENTRIES_PER_MESSAGE, RELIABLE_ENTRIES(struct distributed_object_change));
long absolute_index;
short rank;
long *player_list;
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES)
@@ -1163,22 +916,14 @@ void network_objects_client_asked(
/* (what every unit carries, with the next of them) */
objects_host_inventories[absolute_index].checksum = 0;
objects_host_inventories[absolute_index].weapons_checksum = 0;
}
for (rank = 0; rank < NUMBER_OF_SEND_RANKS; rank++)
{
for (absolute_index = 0; absolute_index < objects_host_told_count; absolute_index++)
if (objects_host_told[absolute_index] == NONE)
continue;
distributed_change_from_object(objects_host_told[absolute_index], &message.changes[count]);
if (++count == limit)
{
long object_index = objects_host_told[absolute_index];
if (object_index == NONE || distributed_object_send_rank(object_index) != rank)
continue;
distributed_change_from_object(object_index, &message.changes[count]);
if (++count == limit)
{
distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_object_changes, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_object_change)));
count = 0;
}
distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_object_changes, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_object_change)));
count = 0;
}
}
if (count)
@@ -1223,24 +968,8 @@ static void distributed_host_find_viewers(
{
short machine_number;
struct data_iterator iterator;
struct player_datum *player;
objects_host_viewers.count = distributed_client_machines(objects_host_viewers.indices,
HALO_PORT_MAXIMUM_NETWORK_MACHINES);
objects_host_viewers.player_unit_count = 0;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL &&
objects_host_viewers.player_unit_count < MAXIMUM_TRACKED_PLAYERS)
{
long unit_index = distributed_living_unit(player);
if (unit_index != NONE)
{
object_get_origin(unit_index,
&objects_host_viewers.player_unit_origins[objects_host_viewers.player_unit_count++]);
}
}
for (machine_number = 0; machine_number < objects_host_viewers.count; machine_number++)
{
long *player_list = machine_get_player_list(objects_host_viewers.indices[machine_number]);
@@ -1272,32 +1001,19 @@ static void distributed_host_find_viewers(
}
}
/* How often, in ticks, the host sends a moving object's position to a
client machine, based on its distance from that machine's nearest player.
A machine with no player in the world (dead or joining) spectates someone,
so the distance is measured from every player instead. Sending such a
machine every moving object every tick flooded it on campaign levels and
threw everything it saw out of sync. */
/* how often (ticks) the host sends that client machine an object where it
is, moving: by its nearest player; every tick to one with none in the
world (dead, it watches anyone) */
static short distributed_host_object_period(
short machine_number,
real_point3d const *position)
{
short count = objects_host_viewers.machines[machine_number].unit_count;
real_point3d const *origins = objects_host_viewers.machines[machine_number].origins;
real nearest = -1.0f;
short index;
if (!count)
for (index = 0; index < objects_host_viewers.machines[machine_number].unit_count; index++)
{
count = objects_host_viewers.player_unit_count;
origins = objects_host_viewers.player_unit_origins;
}
/* nobody is alive anywhere, so nothing needs to be sent often */
if (!count)
return MAXIMUM_OBJECT_PERIOD_TICKS;
for (index = 0; index < count; index++)
{
real_point3d const *origin = &origins[index];
real_point3d const *origin = &objects_host_viewers.machines[machine_number].origins[index];
real dx = position->x - origin->x;
real dy = position->y - origin->y;
real dz = position->z - origin->z;
@@ -1311,21 +1027,6 @@ static short distributed_host_object_period(
nearest < FAR_OBJECT_DISTANCE * FAR_OBJECT_DISTANCE ? 3 : MAXIMUM_OBJECT_PERIOD_TICKS;
}
/* ... for other units sent by a machine's index (network_actors.c) */
short network_objects_send_period(
long machine_index,
real_point3d const *position)
{
short machine_number;
for (machine_number = 0; machine_number < objects_host_viewers.count; machine_number++)
{
if (objects_host_viewers.indices[machine_number] == machine_index)
return distributed_host_object_period(machine_number, position);
}
return 1;
}
/* the kinds of states sent this tick */
enum
{
@@ -1795,10 +1496,6 @@ void network_objects_apply_vehicle_predictions(
{
continue;
}
/* (not from a co-op client still loading the host's BSP: the
vehicle falls there with no ground under it) */
if (!network_coop_player_has_structure_bsp(driver->unit.player_index))
continue;
dx = state->position.x - vehicle->object.position.x;
dy = state->position.y - vehicle->object.position.y;
dz = state->position.z - vehicle->object.position.z;
@@ -1947,7 +1644,6 @@ void network_objects_host_tick(
distributed_host_send_states();
if (game_time_get() % INVENTORY_INTERVAL_TICKS == 0)
distributed_host_send_inventories();
distributed_host_send_damage_animations();
}
/* ---------- a client */
@@ -2142,19 +1838,6 @@ static void distributed_client_apply_change(
struct object_datum *object = object_get(object_index);
csmemcpy(object->object.base_change_colors, change->change_colors, sizeof(object->object.base_change_colors));
/* (and the colors drawn, which object_new chose from the tag: an AI unit's
are its variant's, set after it was made, actors.c) */
{
short color_index;
for (color_index = 0; color_index < NUMBER_OF_OBJECT_CHANGE_COLORS; color_index++)
{
object->object.outgoing_change_colors[color_index].red = PIN(change->change_colors[color_index].red, 0.0f, 1.0f);
object->object.outgoing_change_colors[color_index].green =
PIN(change->change_colors[color_index].green, 0.0f, 1.0f);
object->object.outgoing_change_colors[color_index].blue = PIN(change->change_colors[color_index].blue, 0.0f, 1.0f);
}
}
/* (a permutation the model has, or none: the model's renderer takes it
as it is) */
{
@@ -2409,20 +2092,6 @@ static void distributed_client_dead_biped(
}
}
/* client: a vehicle the host says is dead is destroyed here too, with its
explosion. No killing blow comes for vehicles, only for bipeds. */
static void distributed_client_dead_vehicle(
long object_index)
{
struct unit_datum *vehicle = (struct unit_datum *)object_try_and_get_and_verify_type(object_index,
_object_mask_vehicle);
if (!vehicle || TEST_FLAG(vehicle->object.damage_flags, _object_dead_bit))
return;
unit_kill_no_statistics(object_index);
object_damage_update(object_index);
}
void network_objects_handle_states(
void const *entries,
short count)
@@ -2446,7 +2115,6 @@ void network_objects_handle_states(
if (TEST_FLAG(state->flags, _distributed_object_dead_bit))
{
distributed_client_dead_biped(state->object_index);
distributed_client_dead_vehicle(state->object_index);
/* (a body its tag destroys at once is gone) */
if (!object_try_and_get_and_verify_type(state->object_index, _object_mask_all))
continue;
@@ -2479,12 +2147,6 @@ void network_objects_handle_states(
if (blend_distance > 0.0f)
blend_distance = REMOTE_VEHICLE_BLEND_DISTANCE;
/* (one no player drives follows every move the host sends, however small) */
if (distributed_vehicle_unsteered(state->object_index))
{
tolerance = 0.0f;
angle_tolerance = 2.0f;
}
if (vehicle->unit.driver_object_index != NONE &&
distributed_player_is_local(unit_get(vehicle->unit.driver_object_index)->unit.player_index))
{
@@ -2515,8 +2177,7 @@ void network_objects_handle_states(
{
distributed_count_correction();
}
SET_FLAG(object->object.flags, _object_at_rest_bit, TEST_FLAG(state->flags, _distributed_object_at_rest_bit) ||
distributed_vehicle_unsteered(state->object_index));
SET_FLAG(object->object.flags, _object_at_rest_bit, TEST_FLAG(state->flags, _distributed_object_at_rest_bit));
}
}
@@ -2937,51 +2598,6 @@ static void distributed_client_remove_own_objects(
objects_client_new_object_count = 0;
}
/* Client: keeps each AI-driven vehicle (such as a Pelican on its flight
path) moving at the velocity the host last sent. These vehicles are held at
rest here, and a distant one is only updated every few ticks, so otherwise
it would stop between updates and jump on each one. */
static void distributed_client_carry_unsteered_vehicles(
void)
{
struct object_iterator iterator;
struct object_datum *object;
object_iterator_new(&iterator, _object_mask_vehicle, 0);
while ((object = object_iterator_next(&iterator)) != NULL)
{
real_vector3d const *velocity = &object->object.translational_velocity;
real_vector3d axis = object->object.angular_velocity;
real_vector3d forward = object->object.forward;
real_vector3d up = object->object.up;
real_point3d position;
real angle;
if (object->object.parent_object_index != NONE || unit_get(iterator.index)->unit.driver_object_index == NONE ||
!distributed_vehicle_unsteered(iterator.index))
{
continue;
}
angle = normalize3d(&axis);
if (velocity->i == 0.0f && velocity->j == 0.0f && velocity->k == 0.0f && angle == 0.0f)
continue;
position.x = object->object.position.x + velocity->i;
position.y = object->object.position.y + velocity->j;
position.z = object->object.position.z + velocity->k;
/* rotate the same way physics.c applies angular velocity */
if (angle != 0.0f)
{
real_matrix4x3 rotation;
matrix4x3_rotation_from_axis_and_angle(&rotation, &axis, (real)sin(angle), (real)cos(angle));
matrix4x3_transform_vector(&rotation, &object->object.forward, &forward);
matrix4x3_transform_vector(&rotation, &object->object.up, &up);
}
if (distributed_transform_valid(&position, &forward, &up, NULL, NULL, &forward, &up))
object_set_position(iterator.index, &position, &forward, &up);
}
}
void network_objects_client_tick(
void)
{
@@ -3009,7 +2625,6 @@ void network_objects_client_tick(
distributed_client_ready_picked_up_weapons();
distributed_client_note_own_inventories();
distributed_client_send_vehicles();
distributed_client_carry_unsteered_vehicles();
/* (who it is, as its Discord told it: once its ready went, which makes
it a machine the host takes messages of) */
if (objects_client_ready_time != NONE)
@@ -3027,8 +2642,6 @@ void network_objects_new_game(
short index;
csmemset(objects_host_inventories, 0, sizeof(objects_host_inventories));
host_damage_animation_count = 0;
csmemset(client_damage_animations, 0, sizeof(client_damage_animations));
for (absolute_index = 0; absolute_index < MAXIMUM_TRACKED_OBJECTS; absolute_index++)
{
objects_host_told[absolute_index] = NONE;
+326
View File
@@ -0,0 +1,326 @@
/*
NETWORK_VOICE.C
The voice chat (the VR port's): an extension of the distributed netcode
(port/linux/NETCODE.md, network_distributed.c), as the VR players' hands
are (network_vr.c).
- Every tick, a machine sends what its microphone heard of its player's
speech (halo_voice.h: Opus packets of 20 ms, only while they speak) in
_distributed_message_voice (unreliable): a client to the host, which
plays it and passes it on to its other clients; the host its own to
every client.
- A machine plays each speaker's voice from where they are: a teammate
(in a game of teams) over the radio, as loud anywhere and only a little
to the side they are on; anyone else as near as they are (no further off
than VOICE_RANGE), from their side. A speaker with no unit (dead) is
heard over the radio by their team, and not by the rest.
A machine that does not know the message (a build without this, of the same
network version) drops it, as it drops any type past its own.
*/
#include "cseries.h"
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
#include "units/units.h"
#include "network_distributed.h"
#include "halo_voice.h"
#include "halo_vr.h"
#include <math.h>
void platform_log(char const *format, ...);
/* ---------- constants */
enum
{
/* an entry: the speaker, its sequence number and the packet's length,
and the packet */
VOICE_ENTRY_HEADER = 4,
VOICE_MAXIMUM_PACKET = 200,
VOICE_MAXIMUM_TAKEN = 1024,
};
/* how far off someone not on the radio is heard (world units: 60 metres) */
#define VOICE_RANGE 20.0f
/* how far to the side a voice is put: on the radio, and near */
#define VOICE_RADIO_PAN 0.3f
#define VOICE_NEAR_PAN 0.8f
/* ---------- globals */
static struct
{
boolean active;
word sequence;
long sent;
long played;
long log_time;
} network_voice;
/* ---------- private code */
static boolean network_voice_in_network_game(
void)
{
short connection = game_connection();
return (connection == _game_connection_network_server || connection == _game_connection_network_client) &&
game_in_progress();
}
/* where a player's head is, and whether they have a unit */
static boolean network_voice_player_head(
struct player_datum const *player,
real_point3d *head)
{
long unit_index = distributed_living_unit(player);
if (unit_index == NONE)
return FALSE;
unit_get_camera_position(unit_index, head);
return TRUE;
}
/* the gains a speaker is heard at here (FALSE: not heard) */
static boolean network_voice_gains(
struct player_datum const *speaker,
real *left,
real *right)
{
long listener_index = local_player_get_player_index(0);
struct player_datum *listener = listener_index != NONE ? player_try_and_get(listener_index) : NULL;
boolean teammate = listener && game_engine_has_teams() && listener->team_index == speaker->team_index;
real_point3d speaker_head;
real_point3d listener_head;
real gain = 1.0f;
real pan = 0.0f;
if (!listener)
return FALSE;
if (!network_voice_player_head(speaker, &speaker_head))
{
/* (dead: on the radio, to the team) */
if (!teammate)
return FALSE;
*left = *right = 1.0f;
return TRUE;
}
if (network_voice_player_head(listener, &listener_head))
{
real dx = speaker_head.x - listener_head.x;
real dy = speaker_head.y - listener_head.y;
real dz = speaker_head.z - listener_head.z;
real distance = (real)sqrt(dx * dx + dy * dy + dz * dz);
real flat = (real)sqrt(dx * dx + dy * dy);
float look[3];
real forward_x;
real forward_y;
real length;
struct unit_datum *unit = unit_try_and_get(distributed_living_unit(listener));
/* (where the listener looks: the head, in VR) */
if (halo_vr_look_vector(0, look))
{
forward_x = look[0];
forward_y = look[1];
}
else
{
forward_x = unit ? unit->unit.looking_vector.i : 1.0f;
forward_y = unit ? unit->unit.looking_vector.j : 0.0f;
}
length = (real)sqrt(forward_x * forward_x + forward_y * forward_y);
if (length > 0.0001f && flat > 0.0001f)
{
/* (the right: forward turned clockwise) */
pan = (dx * forward_y - dy * forward_x) / (flat * length);
}
if (!teammate)
{
gain = 1.0f - distance / VOICE_RANGE;
if (gain <= 0.0f)
return FALSE;
gain *= gain;
}
pan *= teammate ? VOICE_RADIO_PAN : VOICE_NEAR_PAN;
}
else if (!teammate)
{
/* (the listener dead: the others as from where the body is not:
not at all) */
return FALSE;
}
*left = gain * (pan > 0.0f ? 1.0f - pan : 1.0f);
*right = gain * (pan < 0.0f ? 1.0f + pan : 1.0f);
return TRUE;
}
static void network_voice_play_entries(
byte const *entries,
byte const *end,
short count,
long machine_index)
{
short index;
for (index = 0; index < count && entries + VOICE_ENTRY_HEADER <= end; index++)
{
byte player_byte = entries[0];
word sequence = (word)(entries[1] | entries[2] << 8);
short length = entries[3];
long player_index;
struct player_datum *player;
real left;
real right;
if (length <= 0 || entries + VOICE_ENTRY_HEADER + length > end)
break;
player_index = distributed_player_from_byte(player_byte);
player = player_index != NONE ? player_try_and_get(player_index) : NULL;
/* (the host: only a client's own players') */
if (player && (machine_index == NONE || distributed_machine_has_player(machine_index, player_byte)) &&
player->local_player_index == NONE && network_voice_gains(player, &left, &right))
{
halo_voice_play(player_byte, sequence, entries + VOICE_ENTRY_HEADER, length, left, right);
network_voice.played++;
}
entries += VOICE_ENTRY_HEADER + length;
}
}
/* ---------- public code */
void network_voice_new_game(
void)
{
network_voice.sequence = 0;
}
word network_voice_entry_size(
void)
{
return VOICE_ENTRY_HEADER;
}
/* after each tick (network_distributed_tick): this machine's speech sent */
void network_voice_tick(
void)
{
struct
{
struct distributed_message_header header;
byte entries[VOICE_MAXIMUM_TAKEN + 64];
} message;
byte taken[VOICE_MAXIMUM_TAKEN];
long player_index = local_player_get_player_index(0);
long size;
long offset = 0;
long used = 0;
short count = 0;
if (!network_voice_in_network_game() || !halo_voice_enabled())
{
if (network_voice.active)
{
network_voice.active = FALSE;
halo_voice_end();
}
return;
}
if (!network_voice.active)
{
network_voice.active = TRUE;
halo_voice_begin();
}
if (game_time_get() - network_voice.log_time >= 150 && (network_voice.sent || network_voice.played))
{
platform_log("voice: sent %ld, played %ld packets (in 150 ticks)", network_voice.sent, network_voice.played);
network_voice.sent = network_voice.played = 0;
network_voice.log_time = game_time_get();
}
size = halo_voice_take(taken, sizeof(taken));
if (player_index == NONE)
return;
while (offset < size)
{
short length = taken[offset];
if (length <= 0 || offset + 1 + length > size || length > VOICE_MAXIMUM_PACKET)
break;
if (used + VOICE_ENTRY_HEADER + length > (long)sizeof(message.entries))
break;
message.entries[used] = distributed_player_to_byte(player_index);
message.entries[used + 1] = (byte)(network_voice.sequence & 0xFF);
message.entries[used + 2] = (byte)(network_voice.sequence >> 8);
message.entries[used + 3] = (byte)length;
csmemcpy(message.entries + used + VOICE_ENTRY_HEADER, taken + offset + 1, length);
used += VOICE_ENTRY_HEADER + length;
offset += 1 + length;
network_voice.sequence++;
count++;
}
if (!count)
return;
network_voice.sent += count;
distributed_send(&message, _distributed_message_voice, count, (word)(sizeof(message.header) + used),
game_connection() == _game_connection_network_server ? _distributed_to_clients : _distributed_to_host);
}
/* a machine's speech: on the host a client's, played and passed on to the
other clients; on a client the host's of any other */
void network_voice_handle(
long machine_index,
void const *message,
word size)
{
struct distributed_message_header header;
byte const *entries = (byte const *)message + sizeof(header);
byte const *end = (byte const *)message + size;
if (!halo_voice_enabled() || !network_voice.active)
return;
csmemcpy(&header, message, sizeof(header));
network_voice_play_entries(entries, end, header.count, machine_index);
if (machine_index != NONE)
{
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
short number;
struct
{
struct distributed_message_header header;
byte entries[VOICE_MAXIMUM_TAKEN + 64];
} relayed;
word entries_size = (word)(end - entries);
byte const *entry = entries;
short index;
/* (only the sender's own players' speech is passed on, whole) */
if (entries_size < VOICE_ENTRY_HEADER || entries_size > sizeof(relayed.entries))
return;
for (index = 0; index < header.count; index++)
{
if (entry + VOICE_ENTRY_HEADER > end || entry[3] == 0 || entry + VOICE_ENTRY_HEADER + entry[3] > end ||
!distributed_machine_has_player(machine_index, entry[0]))
{
return;
}
entry += VOICE_ENTRY_HEADER + entry[3];
}
if (entry != end)
return;
for (number = 0; number < machine_count; number++)
{
if (machine_indices[number] == machine_index)
continue;
csmemcpy(relayed.entries, entries, entries_size);
distributed_send_to_machine(machine_indices[number], &relayed, _distributed_message_voice, header.count,
(word)(sizeof(relayed.header) + entries_size));
}
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
/* the game includes <StdDef.h>; Linux file names are case sensitive. On a
case-insensitive file system (macOS) a plain <stddef.h> would find this file
again; include_next goes to the real one, as the other shims do. */
/* the game includes <StdDef.h>; Linux file names are case sensitive.
#include_next, since on a case-insensitive file system (macOS) this file
is also what <stddef.h> finds */
#include_next <stddef.h>
+3 -15
View File
@@ -24,27 +24,15 @@ state above the tag cache (which ends at 0x819A6000), inside the Xbox memory
window (0x80000000-0x88000000, port/linux/src/platform.h) and below everything
the window hands out top-down (texture and sound caches, Direct3D resources).
The CPU part holds about 17.2 MB of pools at the sizes below (the Xbox pools
The CPU part holds about 13.6 MB of pools at the sizes below (the Xbox pools
fill 3,165,260 of its 0x305000 bytes); the GPU part holds only the decal
vertices, as on the Xbox. A change to a pool's size changes the game state's
layout: saved games of builds before it no longer load. */
vertices, as on the Xbox. */
#define HALO_PORT_GAME_STATE_BASE_ADDRESS 0x81A00000 /* (0x80061000) */
#define HALO_PORT_GAME_STATE_CPU_SIZE 0x13C0000 /* (0x305000) */
#define HALO_PORT_GAME_STATE_CPU_SIZE 0xFC0000 /* (0x305000) */
#define HALO_PORT_GAME_STATE_GPU_SIZE 0x40000 /* (0x40000) */
#define HALO_PORT_GAME_STATE_SIZE (HALO_PORT_GAME_STATE_CPU_SIZE+HALO_PORT_GAME_STATE_GPU_SIZE)
/* ---------- AI
Network co-op adds enemies for its players (port/linux/game/coop_enemies.c):
the actors, and their knowledge of the units about them (props: with many
players, more each), have room for several times a level's own. */
#define HALO_PORT_MAXIMUM_ACTORS 1024 /* (256) */
#define HALO_PORT_MAXIMUM_PROPS 8192 /* (768) */
#define HALO_PORT_MAXIMUM_SWARMS 128 /* (32) */
#define HALO_PORT_MAXIMUM_SWARM_COMPONENTS 1024 /* (256) */
/* ---------- objects */
#define HALO_PORT_MAXIMUM_OBJECTS_PER_MAP 8192 /* (2048) */
+1 -1
View File
@@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode,
in its game's advertisement's reserved bytes (network_server_message_handler.c),
which hosts built before there was a version send as zeros: version 0.
Raise it with any change to what the machines send each other. */
#define HALO_PORT_NETWORK_VERSION 16
#define HALO_PORT_NETWORK_VERSION 11
/* ... the advertisement's reserved bytes: the version (a little-endian word),
then flags */
#define HALO_PORT_ADVERTISED_VERSION_OFFSET 0
+31
View File
@@ -0,0 +1,31 @@
/*
HALO_VOICE.H
The voice chat's sound for the game (port/linux/src/voice.c: on Android the
host's microphone, Opus and mixer, port/android/host/host_voice.c; elsewhere
nothing). port/linux/game/network_voice.c sends and receives it.
*/
#ifndef __HALO_VOICE_H
#define __HALO_VOICE_H
#ifdef __cplusplus
extern "C" {
#endif
/* whether the voice chat is on (voice.enabled) */
int halo_voice_enabled(void);
/* starts playing voices, and the microphone if voice.microphone; stops */
int halo_voice_begin(void);
void halo_voice_end(void);
/* the microphone's speech since asked: Opus packets, each after a byte of
its length; returns the bytes */
int halo_voice_take(void *buffer, int size);
/* a speaker's packet, played at these gains (left, right) */
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right);
#ifdef __cplusplus
}
#endif
#endif
-12
View File
@@ -247,18 +247,6 @@ void halo_vr_network_grenade_thrown(short local_player_index, long unit_index, c
/* the platform layer's side (vr.c) */
void halo_vr_initialize(void);
void halo_vr_frame_begin(void);
/* vr.log_performance: the end of a part of the frame, for the report: the
input and network, the game's ticks (how many), the camera and director,
the drawing; the rest is presenting */
enum
{
HALO_VR_PHASE_INPUT,
HALO_VR_PHASE_TICKS,
HALO_VR_PHASE_CAMERA,
HALO_VR_PHASE_DRAW,
HALO_VR_PHASE_COUNT
};
void halo_vr_phase(int phase, int ticks);
void halo_vr_present(unsigned int back_buffer_texture);
/* the eye image size: the back buffer's size in pixels in VR */
int halo_vr_eye_size(int *width, int *height);
+29 -61
View File
@@ -61,19 +61,17 @@ struct xgpu_capabilities xgpu_capabilities;
The Xbox screen is 640x480. The native ports can draw a wider one: 480
lines, and as many columns as the display's shape gives. On Android that is
display.screen_width (port_config.c; 640 keeps 4:3); on the desktop, the
shape of the window (of the display while the game is fullscreen, or of
display.resolution), and 640 where display.resolution_scaling is "original".
The game's camera derives its horizontal field of view from the viewport, so
the 3D view simply widens. The menus and full-screen overlays are laid out
for 640 columns; while they draw (halo_screen_ui_offset), everything shifts
right to center them.
display's shape while the game is fullscreen, and 640 in a window. The
game's camera derives its horizontal field of view from the viewport, so the
3D view simply widens. The menus and full-screen overlays are laid out for
640 columns; while they draw (halo_screen_ui_offset), everything shifts right
to center them.
The desktop also draws at that resolution (platform_screen_mode): render
Fullscreen on the desktop also draws at the display's resolution: render
targets the size of the screen get that many pixels (screen_scale), and
viewports, clears and visibility counts are scaled to match, so the game
still works in its 480 lines; "original" draws 640x480, scaled up at
presentation. The width and the scale change only between frames, after one
is presented (halo_screen_commit). */
still works in its 480 lines. The width and the scale change only between
frames, after one is presented (halo_screen_commit). */
#define SCREEN_HEIGHT 480
#define SCREEN_MAXIMUM_WIDTH 1920
@@ -1366,6 +1364,25 @@ HRESULT WINAPI Direct3D_CreateDevice(UINT adapter, D3DDEVTYPE device_type, void
/* ---------- the menus' pointer */
#ifdef HALO_ANDROID
/* the controller pointing at the menus' screen in VR (halo_vr_menu_pointer) */
int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer)
{
int x, y, click_x, click_y, moved, clicks;
platform_menus_set_active(menus_active != 0);
if (!menus_active || !halo_vr_menu_pointer(&x, &y, &click_x, &click_y, &moved, &clicks))
return 0;
memset(pointer, 0, sizeof(*pointer));
pointer->x = (short)(x - (halo_screen_width() - 640) / 2);
pointer->y = (short)y;
pointer->click_x = (short)(click_x - (halo_screen_width() - 640) / 2);
pointer->click_y = (short)click_y;
pointer->moved = (unsigned char)(moved != 0);
pointer->left_clicks = (unsigned char)(clicks < 255 ? clicks : 255);
return 1;
}
#else
/* a point in the window, as SDL reports it, in the menus' coordinates: the
inverse of the letterboxed display blit at presentation, the screen's
width and the menus' centering (halo_screen_ui_offset) */
@@ -1397,47 +1414,6 @@ static void ui_point_from_window(float window_x, float window_y, short *x, short
*y = (short)floorf(screen_y);
}
#ifdef HALO_ANDROID
BOOL platform_flat_mouse(float *x, float *y);
/* the controller pointing at the menus' screen in VR (halo_vr_menu_pointer);
flat (the emulator), the mouse */
int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer)
{
int x, y, click_x, click_y, moved, clicks;
platform_menus_set_active(menus_active != 0);
if (menus_active && !halo_vr_active() && device.gl_ready)
{
static BOOL was_down;
static short last_x, last_y;
float window_x, window_y;
BOOL down = platform_flat_mouse(&window_x, &window_y);
memset(pointer, 0, sizeof(*pointer));
ui_point_from_window(window_x, window_y, &pointer->x, &pointer->y);
pointer->click_x = pointer->x;
pointer->click_y = pointer->y;
pointer->moved = pointer->x != last_x || pointer->y != last_y;
pointer->left_clicks = (unsigned char)(down && !was_down);
last_x = pointer->x;
last_y = pointer->y;
was_down = down;
return 1;
}
if (!menus_active || !halo_vr_menu_pointer(&x, &y, &click_x, &click_y, &moved, &clicks))
return 0;
memset(pointer, 0, sizeof(*pointer));
pointer->x = (short)(x - (halo_screen_width() - 640) / 2);
pointer->y = (short)y;
pointer->click_x = (short)(click_x - (halo_screen_width() - 640) / 2);
pointer->click_y = (short)click_y;
pointer->moved = (unsigned char)(moved != 0);
pointer->left_clicks = (unsigned char)(clicks < 255 ? clicks : 255);
return 1;
}
#else
int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer)
{
struct platform_ui_pointer state;
@@ -2487,10 +2463,6 @@ static GLuint mip_composite_get(const struct xgpu_texture_description *descripti
static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale[4][4])
{
/* Bind only after resolving every stage, since texture uploads can
overwrite the active unit's binding. */
GLenum gl_targets[D3DTSS_MAXSTAGES];
GLuint gl_textures[D3DTSS_MAXSTAGES];
int stage;
for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++)
@@ -2502,8 +2474,7 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale
texture_scale[stage][2] = texture_scale[stage][3] = 1.0f;
if (!texture || !texture->Data || mode == 0 || mode == 0x04 || mode == 0x05 || mode == 0x11)
{
gl_targets[stage] = GL_TEXTURE_2D;
gl_textures[stage] = 0;
state_texture(stage, GL_TEXTURE_2D, 0);
key->sampler_type[stage] = mode == 0x11 ? _xgpu_sampler_2d : _xgpu_sampler_none;
continue;
}
@@ -2559,8 +2530,7 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale
texture_scale[stage][1] = 1.0f / (float)description.height;
}
}
gl_targets[stage] = gl_target;
gl_textures[stage] = gl_texture;
state_texture(stage, gl_target, gl_texture);
state_sampler(stage, device.samplers[stage]);
configure_sampler(stage, description.levels > 1, description.hires);
if (stage == 0)
@@ -2569,8 +2539,6 @@ static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale
gl_target == GL_TEXTURE_3D ? _xgpu_sampler_3d : _xgpu_sampler_2d;
}
}
for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++)
state_texture(stage, gl_targets[stage], gl_textures[stage]);
}
static GLenum stencil_operation(DWORD operation)
+14 -22
View File
@@ -249,7 +249,9 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
"\tvec4 oFog = vec4(1.0), oPts = vec4(point_size), oUnused = vec4(0.0);\n"
"\tint a0 = 0;\n"
"\tvec4 A, B, C, mac, ilu;\n");
#ifdef HALO_ANDROID
xgpu_text_append(&text, "\tvec4 clip_position = vec4(0.0);\n\tbool clip_captured = false;\n");
#endif
for (index = 0; index < instruction_count; index++)
{
@@ -301,6 +303,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
case _ilu_lit: xgpu_text_append(&text, "\tilu = nv2a_lit(C);\n"); break;
default: xgpu_text_append(&text, "\tilu = vec4(0.0);\n"); break;
}
#ifdef HALO_ANDROID
/* the screen-space conversion takes the reciprocal of the clip-space
position's w (rcc of r12.w); keep the position it converts */
if (ilu == _ilu_rcc && field(instruction, 3, 28, 2) == _mux_temporary &&
@@ -308,6 +311,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
{
xgpu_text_append(&text, "\tclip_position = oPos;\n\tclip_captured = true;\n");
}
#endif
/* results are written only after both units have read their inputs */
if (mac == _mac_arl)
@@ -353,33 +357,21 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
/* Direct3D 8 puts pixel centres on integer screen coordinates (the
game offsets its screen-space quads by -0.5 to match), OpenGL on
half-integers */
#ifdef HALO_ANDROID
/* The conversion is screen = clip * c[-38] * rcc(w) + c[-37]; undoing
it by multiplying by w again is lossy near the camera plane, where
rcc clamps and 1/w rounds differently on each GPU (Mali put vertices
of the first-person weapon at the vanishing point). Where the clip
position was kept, the same result is computed without dividing. */
"\tvec4 position;\n"
"\tif (clip_captured)\n"
"\t\tposition = vec4((clip_position.xyz * c[%d].xyz + (c[%d].xyz + vec3(0.5 + screen_offset, 0.5, 0.0)\n"
"\t\tgl_Position = vec4((clip_position.xyz * c[%d].xyz + (c[%d].xyz + vec3(0.5 + screen_offset, 0.5, 0.0)\n"
"\t\t\t- viewport_offset.xyz) * clip_position.w) / scale, clip_position.w);\n"
"\telse\n"
"\t{\n"
"\t\tvec3 ndc = (vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale;\n"
"\t\tposition = vec4(ndc * oPos.w, oPos.w);\n"
"\t}\n"
/* A position whose w is zero, or is not a number, is the clip-space
origin: the screen conversion's reciprocal is clamped rather than
infinite, so a large position times a w of zero is exactly zero, and
the origin is inside the frustum. Nothing then clips the triangle
away and OpenGL divides zero by zero there: the vertex lands on the
middle of the screen and the triangle is drawn out to it from the
first-person weapon, whose pose follows the camera and so reaches the
camera plane. The divide on the Xbox sends such a vertex to infinity
and the clipper takes the triangle; put it behind the camera instead,
which the clipper also takes. */
"\tif (!(abs(position.w) > 0.0))\n"
"\t\tposition = vec4(0.0, 0.0, 0.0, -1.0);\n"
"\tgl_Position = position;\n"
"\t\tgl_Position = vec4((vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale * oPos.w, oPos.w);\n"
#else
"\tvec3 ndc = (vec3(oPos.xy + vec2(0.5 + screen_offset, 0.5), oPos.z) - viewport_offset.xyz) / scale;\n"
"\tgl_Position = vec4(ndc * oPos.w, oPos.w);\n"
#endif
#ifdef HALO_ANDROID
/* what glClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE) does on desktop
GL: rows from the top, depth 0..1 */
@@ -397,16 +389,16 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
"\txT2 = oT2;\n"
"\txT3 = oT3;\n"
"\txFog = oFog.x;\n"
"}\n",
XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37
"}\n"
#ifdef HALO_ANDROID
, XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37,
/* (what is drawn in screen space, which kept no clip position, is
taken to each view as though infinitely far: a lens flare's sun
lands where the sky's does) */
/* (what the game projects itself, a sprite or a lens flare's
test, comes as a screen position with w 1 and its depth in z: its
eye's offset is by that depth, where it is depth tested) */
, multiview ?
multiview ?
"\t{\n"
"\t\tint row = int(gl_ViewID_OVR) * 2;\n"
"\t\tfloat near = 0.0;\n"
+5 -51
View File
@@ -3,10 +3,9 @@ P2P_SIGNAL.C
Internet play's signalling (p2p.c): how a joiner and the host of an invite
tell each other where they can be reached, through public MQTT brokers
(those in brokers.txt, network.brokers_file; MQTT 5 over TCP, or 3.1.1 with
a broker that refuses 5). Every broker is used at once, so any one of them
working is enough (an answer goes back through each broker a request came
through).
(network.signalling_brokers; MQTT 5 over TCP, or 3.1.1 with a broker that
refuses 5). Every broker is used at once, so any one of them working is
enough (an answer goes back through each broker a request came through).
They also carry the server browser's listings of public games (p2p_lobby.c):
retained in each host's slot, published at least once (QoS 1, sent again
until the broker acknowledges them), expiring at the broker after
@@ -1522,50 +1521,8 @@ static void broker_readable(struct broker *broker)
/* ---------- p2p.c's side */
/* the brokers in network.brokers_file (beside config.toml, unless a full
path: port/assets/network/brokers.txt, which the builds put there), one on
each line, "#" starting a comment, into text: host:port entries separated by
commas; empty if the file cannot be read */
static void brokers_list(char *text, size_t size)
{
const char *name = config_string("network.brokers_file");
char path[1024];
char *file;
size_t file_size = 0, index, length = 0;
int comment = 0;
text[0] = 0;
if (name[0] == '/' || name[0] == '\\' || (name[0] && name[1] == ':'))
snprintf(path, sizeof(path), "%s", name);
else
{
config_folder(path, sizeof(path));
snprintf(path + strlen(path), sizeof(path) - strlen(path), "%s", name);
}
file = config_file_read(path, &file_size);
if (!file)
{
platform_log("Internet play: the brokers' file %s cannot be read (network.brokers_file)", path);
return;
}
for (index = 0; index < file_size && length + 1 < size; index++)
{
char character = file[index];
if (character == '\n' || character == '\r')
comment = 0;
else if (character == '#')
comment = 1;
if (!comment)
text[length++] = character == '\n' || character == '\r' || character == '\t' ? ',' : character;
}
text[length] = 0;
free(file);
}
void p2p_signal_start(void)
{
char list[1024];
const char *text;
unsigned char random[8];
char hex[17];
@@ -1582,8 +1539,7 @@ void p2p_signal_start(void)
p2p_key_hash(p2p_public_key(), hash);
p2p_lobby_slot_topic(hash, signalling.own_slot, sizeof(signalling.own_slot));
}
brokers_list(list, sizeof(list));
text = list;
text = config_string("network.signalling_brokers");
while (*text && signalling.broker_count < MAXIMUM_BROKERS)
{
const char *end = text + strcspn(text, ",");
@@ -1615,9 +1571,7 @@ void p2p_signal_start(void)
text = *end ? end + 1 : end;
}
if (!signalling.broker_count)
platform_log("Internet play: no signalling brokers (network.brokers_file), so invites cannot work");
else if (text[strspn(text, ", ")])
platform_log("Internet play: only the first %d signalling brokers are used", MAXIMUM_BROKERS);
platform_log("Internet play: no signalling brokers (network.signalling_brokers), so invites cannot work");
}
void p2p_signal_select_sets(int *read, int *read_count, int *write, int *write_count, int maximum_count)
+25 -66
View File
@@ -66,28 +66,15 @@ struct config_setting
static const struct config_setting config_settings[] =
{
{ "display.fullscreen", _config_boolean, "true", "HALO_FULLSCREEN", _environment_value, _platform_desktop,
"Where display.mode is empty: start borderless over the whole display;\n"
"false starts in a window. F11 switches." },
"Start fullscreen, drawing at the display's resolution and shape; false\n"
"starts in a window, which draws the Xbox's 640x480. F11 switches." },
{ "display.mode", _config_string, "\"\"", "HALO_DISPLAY_MODE", _environment_value, _platform_desktop,
"\"fullscreen\" takes the display (at display.resolution's mode),\n"
"\"borderless\" is a window over the whole desktop, \"windowed\" a window\n"
"(display.window_size). Empty: display.fullscreen's (true: borderless).\n"
"F11 switches to the window and back." },
{ "display.resolution", _config_string, "\"native\"", "HALO_RESOLUTION", _environment_value, _platform_desktop,
"What fullscreen and borderless draw at: \"native\", the display's own, or\n"
"\"<width>x<height>\" (\"1920x1080\"), 640x480 or more. Fullscreen sets the\n"
"display to it; borderless draws it scaled to the display." },
{ "display.resolution_scaling", _config_string, "\"native\"", "HALO_RESOLUTION_SCALING", _environment_value,
_platform_desktop,
"\"native\" draws at the window's resolution (fullscreen, the display's or\n"
"display.resolution); \"original\" draws the Xbox's 640x480 and scales it\n"
"up." },
{ "display.window_size", _config_string, "\"\"", "HALO_WINDOW_SIZE", _environment_value, _platform_desktop,
"The window's size, \"<width>x<height>\" (\"1920x1080\"), 640x480 or more (it\n"
"can be resized). Empty: display.window_scale's." },
"\"fullscreen\" takes the display at its desktop resolution, \"borderless\"\n"
"is a window over the whole desktop (both draw at the display's\n"
"resolution), \"windowed\" a window of the Xbox's 640x480, scaled. Empty:\n"
"display.fullscreen's (true: borderless). F11 switches to the window and back." },
{ "display.window_scale", _config_integer, "2", "HALO_WINDOW_SCALE", _environment_value, _platform_desktop,
"Where display.window_size is empty: the window's size as a multiple of\n"
"640x480." },
"The window's size as a multiple of 640x480 (it can be resized)." },
{ "display.screen_width", _config_integer, "0", "HALO_SCREEN_WIDTH", _environment_value, _platform_android,
"Columns of the 480-line picture: 0 for the display's shape, 640 for the\n"
"Xbox's 4:3." },
@@ -109,8 +96,8 @@ static const struct config_setting config_settings[] =
"false draws the maps' own bitmaps." },
{ "display.high_res_text", _config_boolean, "true", "HALO_HIGH_RES_TEXT", _environment_value, _platform_all,
"Draw the menus' and HUD's text with the fonts in port/assets/fonts\n"
"(Overpass) at the resolution the game draws at, and the menus' titles\n"
"from port/assets/titles; false draws the maps' bitmap fonts and titles." },
"(Overpass) at the display's resolution, and the menus' titles from\n"
"port/assets/titles; false draws the maps' bitmap fonts and titles." },
{ "display.menus", _config_string, "\"pc\"", "HALO_MENUS", _environment_value, _platform_all,
"The menus: \"pc\" for the PC version's main menu (port/assets/menus,\n"
"and a menus folder here for your own), \"xbox\" for the Xbox's." },
@@ -139,9 +126,6 @@ static const struct config_setting config_settings[] =
{ "vr.enabled", _config_boolean, "true", "HALO_VR_ENABLED", _environment_value, _platform_android,
"On a VR headset (Quest, Steam Frame), play in VR; false shows the game\n"
"on a flat panel instead. Read when the app starts." },
{ "vr.menus", _config_string, "\"pc\"", "HALO_VR_MENUS", _environment_value, _platform_android,
"The menus in VR: \"pc\" for the PC version's (as display.menus), \"xbox\"\n"
"for the Xbox's. Read when the menus' map loads." },
{ "vr.resolution_scale", _config_real, "1.0", "HALO_VR_RESOLUTION_SCALE", _environment_value, _platform_android,
"The eye images' size as a multiple of what the headset recommends;\n"
"lower is faster. Read when the app starts." },
@@ -415,39 +399,11 @@ static const struct config_setting config_settings[] =
"in everyone's server browser: anyone can see and join it) or, false,\n"
"PRIVATE (only players with its invite link can join). Server Setup's\n"
"LISTING changes it for each game." },
{ "network.coop_public", _config_boolean, "false", "HALO_NET_COOP_PUBLIC", _environment_value, _platform_all,
"Whether an online co-op game (Create Game > Internet, a SINGLEPLAYER\n"
"map) starts as PUBLIC or, false, PRIVATE: Server Setup's LISTING in\n"
"co-op, which writes its choice here." },
{ "network.coop_friendly_fire", _config_string, "\"on\"", "HALO_NET_COOP_FRIENDLY_FIRE", _environment_value,
_platform_all,
"Whether the players of an online co-op game hurt each other: \"off\",\n"
"\"on\", \"shields_only\" or \"explosives_only\" (Server Setup's FRIENDLY\n"
"FIRE in co-op, which writes its choice here). Their AI allies they\n"
"always can, as in the campaign." },
{ "network.coop_enemies_mode", _config_string, "\"per_player\"", "HALO_NET_COOP_ENEMIES_MODE", _environment_value,
_platform_all,
"Online co-op's extra enemies: \"none\", \"per_player\" (each squad of\n"
"enemies grows by coop_enemies for each player past the first) or\n"
"\"multiplier\" (each is coop_enemies_multiplier times as large, for any\n"
"number of players). Server Setup's EXTRA ENEMIES in co-op writes its\n"
"choice here." },
{ "network.coop_enemies", _config_integer, "50", "HALO_NET_COOP_ENEMIES", _environment_value, _platform_all,
"Online co-op's extra enemies per player, a percentage: for each player\n"
"past the first, each squad of enemies a level places gets this much of\n"
"itself more (100: as many again; 25 to 200). Server Setup's PER PLAYER\n"
"in co-op writes its choice here." },
{ "network.coop_enemies_multiplier", _config_integer, "2", "HALO_NET_COOP_ENEMIES_MULTIPLIER", _environment_value,
_platform_all,
"Online co-op's static multiplier of its enemies: each squad of enemies\n"
"a level places is this many times as large (2 to 32). Server Setup's\n"
"MULTIPLIER in co-op writes its choice here." },
{ "network.brokers_file", _config_string, "\"brokers.txt\"",
"HALO_NET_BROKERS_FILE", _environment_value, _platform_all,
"The file of the public MQTT brokers through which the machines of an\n"
"invite find each other (its messages are encrypted), beside this file\n"
"unless a full path: one host:port on each line, up to 4. Updates\n"
"replace brokers.txt: keep a list of your own under another name." },
{ "network.signalling_brokers", _config_string,
"\"broker.emqx.io:1883,broker.hivemq.com:1883,test.mosquitto.org:1883\"",
"HALO_NET_BROKERS", _environment_value, _platform_all,
"Public MQTT brokers through which the machines of an invite find each\n"
"other (its messages are encrypted); comma-separated host:port." },
{ "network.stun_servers", _config_string, "\"stun.l.google.com:19302,stun.cloudflare.com:3478\"",
"HALO_NET_STUN", _environment_value, _platform_all,
"Public STUN servers that tell this machine its internet address;\n"
@@ -457,6 +413,17 @@ static const struct config_setting config_settings[] =
"The Discord application internet play invites go through while the\n"
"Discord desktop client runs; empty for none." },
{ "voice.enabled", _config_boolean, "true", "HALO_VOICE_ENABLED", _environment_value, _platform_android,
"Voice chat in multiplayer: hear the other players (teammates over the\n"
"radio, the rest when near, from where they are)." },
{ "voice.microphone", _config_boolean, "true", "HALO_VOICE_MICROPHONE", _environment_value, _platform_android,
"Send your voice in multiplayer, whenever you speak (the headset's\n"
"microphone; Android asks for it once). False only listens." },
{ "voice.volume", _config_real, "1.0", "HALO_VOICE_VOLUME", _environment_value, _platform_android,
"The other players' voices' volume (1 as they come)." },
{ "voice.threshold", _config_real, "0.02", "HALO_VOICE_THRESHOLD", _environment_value, _platform_android,
"How loud the microphone must be to send (0..1 of full scale): higher\n"
"if it sends noise, lower if it cuts words off." },
{ "bots.count", _config_integer, "0", "HALO_BOTS_COUNT", _environment_value, _platform_android,
"Bots in the multiplayer games this machine hosts (0 to 15; 0: none):\n"
"players it drives itself, which every machine sees as players. Set in\n"
@@ -505,9 +472,6 @@ static const struct config_setting config_settings[] =
"machines of twice it), to test the netcode as over the internet; 0 none." },
{ "debug.network_loss", _config_real, "0.0", "HALO_NETWORK_LOSS", _environment_value, _platform_all,
"Percent of datagrams received that are dropped, for the same; 0 none." },
{ "debug.gl_thread", _config_integer, "-1", "HALO_GL_THREAD", _environment_value, _platform_android,
"Mesa's GL thread (its drivers: the Steam Frame's): 1 on, 0 off, -1 as the\n"
"driver chooses. Read when the app starts." },
{ "debug.test_input", _config_string, "\"\"", "HALO_TEST_INPUT", _environment_value, _platform_all,
"\"bot:<seed>\" plays controller 1 with a scripted pattern (automated\n"
"network tests); \"look:<seed>\" stands still, only turning and looking\n"
@@ -1297,11 +1261,6 @@ int config_write_integer(const char *name, long value)
/* ---------- public code */
char *config_file_read(const char *path, size_t *size)
{
return config_read_file(path, size);
}
unsigned long config_changes(void)
{
return config_change_count;
-3
View File
@@ -30,9 +30,6 @@ such setting */
int config_text(const char *name, char *text, size_t size);
/* the folder config.toml is in, with its separator */
void config_folder(char *path, size_t size);
/* a whole file (a UTF-8 path), NUL terminated, or NULL; free() it (the Android
guest has no SDL_LoadFile) */
char *config_file_read(const char *path, size_t *size);
/* a setting's default, as text; 0 if there is no such setting */
int config_default(const char *name, char *text, size_t size);
/* how many times config_write has changed a setting: what keeps one reads
+55 -359
View File
@@ -62,10 +62,6 @@ gone, ends: else the keyboard stays held from the game) */
static Uint64 binding_polled_ms;
#define BINDING_ABANDONED_MS 500
static unsigned mouse_buttons_down;
#ifdef HALO_ANDROID
/* the mouse's place, flat (platform_flat_mouse) */
static float flat_mouse_x, flat_mouse_y;
#endif
/* (an input taken that nothing asks for is let go after this) */
#define BINDING_UNCLAIMED_MS 2000
/* the multiplayer scoreboard is open (platform_scoreboard_scroll): the wheel
@@ -383,43 +379,38 @@ static BOOL platform_fullscreen_setting(void)
return !config_boolean("debug.hidden_window") && platform_display_mode() != _display_mode_windowed;
}
/* a size as a setting has it, "<width>x<height>": whether it is one, and
the Xbox's 640x480 or more */
static BOOL platform_size_parse(const char *text, long *width, long *height)
/* the window's fullscreen kind (display.mode): borderless, a window over
the whole desktop (SDL's fullscreen without a mode), or fullscreen, the
display taken at its desktop resolution. Either draws at the display's
resolution (platform_screen_mode); F11 switches to the kind set. */
static void platform_fullscreen_kind_apply(void)
{
char *end;
static int applied = -1;
int exclusive = platform_display_mode() == _display_mode_fullscreen ? 1 : 0;
SDL_DisplayID display;
*width = strtol(text, &end, 10);
*height = *end == 'x' || *end == 'X' ? strtol(end + 1, &end, 10) : 0;
return !*end && *width >= 640 && *height >= 480;
if (!platform_window || exclusive == applied)
return;
applied = exclusive;
display = SDL_GetDisplayForWindow(platform_window);
SDL_SetWindowFullscreenMode(platform_window,
exclusive && display ? SDL_GetDesktopDisplayMode(display) : NULL);
}
/* display.resolution in pixels, or 0x0 for the display's own ("native"),
as for one the game cannot draw at */
static void platform_resolution_setting(long *width, long *height)
/* whether the game is, or is to be, fullscreen, and if so the size in
pixels of the display it fills (d3d8_gl.c draws at that resolution) */
BOOL platform_screen_mode(long *width, long *height)
{
if (!platform_size_parse(config_string("display.resolution"), width, height))
*width = *height = 0;
}
SDL_DisplayID display;
const SDL_DisplayMode *mode;
/* the window's size (display.window_size), else the Xbox's 640x480 times
display.window_scale, as older versions set it */
static void platform_window_size_setting(long *width, long *height)
{
long scale = config_integer("display.window_scale");
if (!platform_size_parse(config_string("display.window_size"), width, height))
if (platform_window ? !(SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) :
!platform_fullscreen_setting() || !platform_sdl_initialize())
{
*width = 640 * (scale < 1 ? 1 : scale);
*height = 480 * (scale < 1 ? 1 : scale);
return FALSE;
}
}
/* a display's own size in pixels (its desktop mode) */
static BOOL platform_display_size(SDL_DisplayID display, long *width, long *height)
{
const SDL_DisplayMode *mode = display ? SDL_GetDesktopDisplayMode(display) : NULL;
display = platform_window ? SDL_GetDisplayForWindow(platform_window) : SDL_GetPrimaryDisplay();
mode = display ? SDL_GetDesktopDisplayMode(display) : NULL;
if (!mode)
return FALSE;
*width = (long)(mode->w * mode->pixel_density + 0.5f);
@@ -427,263 +418,23 @@ static BOOL platform_display_size(SDL_DisplayID display, long *width, long *heig
return TRUE;
}
/* the window's fullscreen kind (display.mode): borderless, a window over
the whole desktop (SDL's fullscreen without a mode), or fullscreen, the
display taken at display.resolution's mode (the one nearest it), else at its
desktop one. F11 switches to the kind set. */
static void platform_fullscreen_kind_apply(void)
{
static int applied = -1;
static long applied_width, applied_height;
int exclusive = platform_display_mode() == _display_mode_fullscreen ? 1 : 0;
long width = 0, height = 0;
SDL_DisplayID display;
SDL_DisplayMode closest;
const SDL_DisplayMode *mode = NULL;
if (!platform_window)
return;
if (exclusive)
platform_resolution_setting(&width, &height);
if (exclusive == applied && width == applied_width && height == applied_height)
return;
applied = exclusive;
applied_width = width;
applied_height = height;
display = SDL_GetDisplayForWindow(platform_window);
if (exclusive && display)
{
if (width && SDL_GetClosestFullscreenDisplayMode(display, (int)width, (int)height, 0.0f, false, &closest))
mode = &closest;
else
mode = SDL_GetDesktopDisplayMode(display);
}
SDL_SetWindowFullscreenMode(platform_window, mode);
}
/* whether the game last asked for the window to be fullscreen */
static BOOL platform_fullscreen_requested = FALSE;
static void platform_window_set_fullscreen(BOOL fullscreen)
{
platform_fullscreen_requested = fullscreen;
SDL_SetWindowFullscreen(platform_window, fullscreen ? true : false);
}
/* whether the window is fullscreen. SDL sets its flag when the window
manager confirms the request, and some never do: gamescope (the Steam
Deck's Game Mode) makes the window the size of the display but leaves the
flag unset, so the game drew 640x480 and gamescope stretched it. A window
that was asked to be fullscreen and covers its display counts too. */
static BOOL platform_window_fullscreen(void)
{
SDL_DisplayID display;
const SDL_DisplayMode *mode;
int width, height;
if (SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN)
return TRUE;
if (!platform_fullscreen_requested)
return FALSE;
display = SDL_GetDisplayForWindow(platform_window);
mode = display ? SDL_GetDesktopDisplayMode(display) : NULL;
if (!mode || !SDL_GetWindowSizeInPixels(platform_window, &width, &height))
return FALSE;
return width >= (int)(mode->w * mode->pixel_density + 0.5f) &&
height >= (int)(mode->h * mode->pixel_density + 0.5f);
}
/* the size in pixels the game draws its picture at (d3d8_gl.c): the
window's, the display's while fullscreen, or display.resolution's while
fullscreen (either kind) where the display has room for it; before the
window opens, what it will be. FALSE where display.resolution_scaling is
"original": the Xbox's 640x480, scaled to the window. */
BOOL platform_screen_mode(long *width, long *height)
{
/* (the size last given, for a window that has none: minimized) */
static long last_width, last_height;
long resolution_width, resolution_height;
BOOL fullscreen;
if (!strcmp(config_string("display.resolution_scaling"), "original"))
return FALSE;
if (platform_window)
{
int pixel_width = 0, pixel_height = 0;
SDL_GetWindowSizeInPixels(platform_window, &pixel_width, &pixel_height);
if (pixel_width <= 0 || pixel_height <= 0)
{
*width = last_width;
*height = last_height;
return last_width > 0;
}
*width = pixel_width;
*height = pixel_height;
fullscreen = platform_window_fullscreen();
}
else
{
if (!platform_sdl_initialize())
return FALSE;
fullscreen = platform_fullscreen_setting();
platform_window_size_setting(width, height);
if (fullscreen && !platform_display_size(SDL_GetPrimaryDisplay(), width, height))
return FALSE;
}
/* (fullscreen's display is at the resolution already, where it has that
mode: platform_fullscreen_kind_apply; borderless's is scaled to) */
platform_resolution_setting(&resolution_width, &resolution_height);
if (fullscreen && resolution_width && resolution_width <= *width && resolution_height <= *height)
{
*width = resolution_width;
*height = resolution_height;
}
last_width = *width;
last_height = *height;
return TRUE;
}
/* the size added to the list unless it has it already; the count */
static int platform_resolution_add(long *widths, long *heights, int count, int maximum, long width, long height)
{
int index;
for (index = 0; index < count; index++)
{
if (widths[index] == width && heights[index] == height)
return count;
}
if (count < maximum)
{
widths[count] = width;
heights[count] = height;
count++;
}
return count;
}
int platform_display_resolutions(long *widths, long *heights, int maximum)
{
SDL_DisplayID display;
SDL_DisplayMode **modes;
long display_width, display_height, width, height;
int mode_count = 0, count = 0, index;
if (maximum < 1 || !platform_sdl_initialize())
return 0;
display = platform_window ? SDL_GetDisplayForWindow(platform_window) : 0;
if (!display)
display = SDL_GetPrimaryDisplay();
if (!platform_display_size(display, &display_width, &display_height))
return 0;
modes = SDL_GetFullscreenDisplayModes(display, &mode_count);
for (index = 0; modes && index < mode_count; index++)
{
width = (long)(modes[index]->w * modes[index]->pixel_density + 0.5f);
height = (long)(modes[index]->h * modes[index]->pixel_density + 0.5f);
if (width >= 640 && height >= 480 && width <= display_width && height <= display_height &&
(width != display_width || height != display_height))
{
count = platform_resolution_add(widths, heights, count, maximum, width, height);
}
}
SDL_free(modes);
/* (the one set, though this display has no such mode, so that Video
Setup shows it) */
platform_resolution_setting(&width, &height);
if (width && (width != display_width || height != display_height))
count = platform_resolution_add(widths, heights, count, maximum, width, height);
/* largest first */
for (index = 1; index < count; index++)
{
int place;
width = widths[index];
height = heights[index];
for (place = index; place > 0 && (widths[place - 1] < width ||
(widths[place - 1] == width && heights[place - 1] < height)); place--)
{
widths[place] = widths[place - 1];
heights[place] = heights[place - 1];
}
widths[place] = width;
heights[place] = height;
}
return count;
}
/* Video Setup's window sizes, by shape (4:3, 16:10, 16:9, 21:9), each from
the smallest */
static const short platform_window_sizes_offered[][2] =
{
{ 640, 480 }, { 800, 600 }, { 1024, 768 }, { 1280, 960 }, { 1600, 1200 }, { 1920, 1440 }, { 2560, 1920 },
{ 1280, 800 }, { 1440, 900 }, { 1680, 1050 }, { 1920, 1200 }, { 2560, 1600 },
{ 1280, 720 }, { 1600, 900 }, { 1920, 1080 }, { 2560, 1440 }, { 3840, 2160 },
{ 2560, 1080 }, { 3440, 1440 }, { 3840, 1600 }, { 5120, 2160 },
};
int platform_window_sizes(long *widths, long *heights, int maximum)
{
SDL_DisplayID display;
SDL_Rect usable;
long width, height;
int count = 0, index;
if (maximum < 1 || !platform_sdl_initialize())
return 0;
display = platform_window ? SDL_GetDisplayForWindow(platform_window) : 0;
if (!display)
display = SDL_GetPrimaryDisplay();
if (!display || !SDL_GetDisplayUsableBounds(display, &usable))
usable.w = usable.h = 0;
for (index = 0; index < (int)(sizeof(platform_window_sizes_offered) / sizeof(*platform_window_sizes_offered));
index++)
{
width = platform_window_sizes_offered[index][0];
height = platform_window_sizes_offered[index][1];
/* (the Xbox's own whatever the desktop's size) */
if (!index || (width <= usable.w && height <= usable.h))
count = platform_resolution_add(widths, heights, count, maximum, width, height);
}
/* (the one set, though it is none of them or too big for this desktop,
so that Video Setup shows it) */
platform_window_size_setting(&width, &height);
return platform_resolution_add(widths, heights, count, maximum, width, height);
}
#else
int platform_display_resolutions(long *widths, long *heights, int maximum)
{
(void)widths;
(void)heights;
(void)maximum;
return 0;
}
int platform_window_sizes(long *widths, long *heights, int maximum)
{
(void)widths;
(void)heights;
(void)maximum;
return 0;
}
#endif
#ifndef HALO_ANDROID
/* the window's size (platform_window_size_setting), as the window was made
or last resized: platform_display_apply */
static long platform_window_width = -1, platform_window_height = -1;
#endif
/* the window's scale (display.window_scale, as the window was made or last
resized: platform_display_apply) */
static long platform_window_scale = -1;
BOOL platform_video_initialize(unsigned long width, unsigned long height)
{
int scale = (int)config_integer("display.window_scale");
int version;
if (platform_window)
return TRUE;
if (!platform_sdl_initialize())
return FALSE;
if (scale < 1)
scale = 1;
platform_window_scale = scale;
#ifdef HALO_ANDROID
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
@@ -708,23 +459,14 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height)
#endif
#ifdef HALO_ANDROID
{
int scale = (int)config_integer("display.window_scale");
if (scale < 1)
scale = 1;
platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale),
SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
}
platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale),
SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
#else
/* fullscreen (either kind) unless display.mode is the window, which F11
switches to and from: display.window_size, whatever shape the fullscreen
picture has. The game draws at the size platform_screen_mode gives
(d3d8_gl.c). */
(void)width;
(void)height;
platform_window_size_setting(&platform_window_width, &platform_window_height);
platform_window = SDL_CreateWindow("Halo", (int)platform_window_width, (int)platform_window_height,
/* fullscreen at the desktop's resolution unless display.fullscreen is
false, where the game draws the display's shape at its resolution
(d3d8_gl.c); the window size is the windowed mode F11 switches to and
from, where it draws 640x480 */
platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale),
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY |
(config_boolean("debug.hidden_window") ? SDL_WINDOW_HIDDEN : 0) |
(platform_fullscreen_setting() ? SDL_WINDOW_FULLSCREEN : 0));
@@ -735,7 +477,6 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height)
return FALSE;
}
#ifndef HALO_ANDROID
platform_fullscreen_requested = platform_fullscreen_setting();
platform_fullscreen_kind_apply();
#endif
platform_gl_context = SDL_GL_CreateContext(platform_window);
@@ -771,28 +512,23 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height)
return TRUE;
}
/* display.mode, display.resolution (fullscreen's display mode),
display.window_size (when it changes: the window can be resized) and
display.vsync, as Settings has written them; display.resolution_scaling
and borderless's resolution are taken up between frames
(halo_screen_commit) */
/* display.fullscreen, display.window_scale (when it changes: the window can
be resized) and display.vsync, as Settings has written them */
void platform_display_apply(void)
{
#ifndef HALO_ANDROID
BOOL fullscreen = platform_fullscreen_setting();
long width, height;
long scale = config_integer("display.window_scale");
if (!platform_window)
return;
platform_fullscreen_kind_apply();
if (platform_window_fullscreen() != (fullscreen != FALSE))
platform_window_set_fullscreen(fullscreen);
platform_window_size_setting(&width, &height);
if (width != platform_window_width || height != platform_window_height)
if (((SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) != 0) != (fullscreen != FALSE))
SDL_SetWindowFullscreen(platform_window, fullscreen ? true : false);
if (scale != platform_window_scale && scale >= 1)
{
platform_window_width = width;
platform_window_height = height;
SDL_SetWindowSize(platform_window, (int)width, (int)height);
platform_window_scale = scale;
SDL_SetWindowSize(platform_window, (int)(640 * scale), (int)(480 * scale));
}
#else
if (!platform_window)
@@ -1175,13 +911,13 @@ static void platform_show_pending_message(void)
#else
{
/* (a box cannot show above a fullscreen game) */
BOOL fullscreen = platform_window_fullscreen();
int fullscreen = (SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) != 0;
if (fullscreen)
platform_window_set_fullscreen(FALSE);
SDL_SetWindowFullscreen(platform_window, false);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_WARNING, title, text, platform_window);
if (fullscreen)
platform_window_set_fullscreen(TRUE);
SDL_SetWindowFullscreen(platform_window, true);
}
#endif
}
@@ -1189,10 +925,8 @@ static void platform_show_pending_message(void)
/* ---------- events */
/* quits as closing the window does, when the events are next read (the
menus' Quit: port/linux/game/menu_functions.c); on Android, where the
system closes its apps, the VR options' Quit (ui_widget.c) ends the process
at once: on the Steam Frame, Steam cannot end the game, which runs in
Lepton's container (port/android/STEAM_FRAME.md) */
menus' Quit: port/linux/game/menu_functions.c); Android's menus have none,
as the system closes its apps */
void platform_request_quit(void)
{
#ifndef HALO_ANDROID
@@ -1201,20 +935,6 @@ void platform_request_quit(void)
memset(&event, 0, sizeof(event));
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#else
{
/* (and says so beside the game's data: the Steam Frame's launcher
does not then take it for a start that failed and start it again) */
char path[512];
FILE *file;
snprintf(path, sizeof(path), "%s/quit_requested.txt", platform_data_root());
file = fopen(path, "wb");
if (file)
fclose(file);
}
platform_log("quit: the player asked");
exit(EXIT_SUCCESS);
#endif
}
@@ -1312,15 +1032,13 @@ void platform_pump_events(void)
window's size and place while fullscreen) */
if (event.key.down && !event.key.repeat && event.key.scancode == SDL_SCANCODE_F11)
{
platform_window_set_fullscreen(!platform_window_fullscreen());
SDL_SetWindowFullscreen(platform_window,
(SDL_GetWindowFlags(platform_window) & SDL_WINDOW_FULLSCREEN) ? false : true);
}
#endif
break;
case SDL_EVENT_MOUSE_MOTION:
#ifdef HALO_ANDROID
flat_mouse_x = event.motion.x;
flat_mouse_y = event.motion.y;
#else
#ifndef HALO_ANDROID
/* in the menus the mouse moves the pointer, not the view */
if (input_state.ui_pointer)
{
@@ -1480,23 +1198,6 @@ int platform_binding_capture_poll(int *input)
return result;
}
#ifdef HALO_ANDROID
/* flat, not in VR (the emulator, for testing the menus): the mouse's place
in the window and whether its left button is down, polled */
BOOL platform_flat_mouse(float *x, float *y)
{
BOOL down;
pthread_mutex_lock(&input_lock);
*x = flat_mouse_x;
*y = flat_mouse_y;
/* (an injected mouse's, adb shell input mouse: button 0) */
down = (mouse_buttons_down & ((1u << SDL_BUTTON_LEFT) | 1u)) != 0;
pthread_mutex_unlock(&input_lock);
return down;
}
#endif
#ifndef HALO_ANDROID
/* ---------- the menus' pointer */
@@ -1547,17 +1248,12 @@ BOOL platform_ui_pointer_read(struct platform_ui_pointer *pointer)
return active;
}
#endif
void platform_video_window_size(int *width, int *height)
{
#ifdef HALO_ANDROID
SDL_GetWindowSizeInPixels(platform_window, width, height);
#else
SDL_GetWindowSize(platform_window, width, height);
#endif
}
#endif
void platform_input_read(struct platform_input_state *state, BOOL consume_motion)
{
pthread_mutex_lock(&input_lock);
+1 -10
View File
@@ -62,15 +62,6 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height);
#ifndef HALO_ANDROID
BOOL platform_screen_mode(long *width, long *height);
#endif
/* Video Setup's resolutions (port/linux/game/menu_tags.c), in pixels: the
display's modes the game can draw at, largest first, each once, but the
display's own (Native), and display.resolution's where it is none of them;
how many. None on Android. */
int platform_display_resolutions(long *widths, long *heights, int maximum);
/* Video Setup's window sizes: those of each shape (4:3, 16:10, 16:9, 21:9)
that fit the desktop, and display.window_size's; how many. None on
Android. */
int platform_window_sizes(long *widths, long *heights, int maximum);
void platform_video_drawable_size(int *width, int *height);
/* the window's mode and size and V-Sync, from config.toml as Settings has
just written it (the main thread's) */
@@ -98,8 +89,8 @@ struct platform_ui_pointer
};
void platform_ui_pointer_set_active(BOOL active);
BOOL platform_ui_pointer_read(struct platform_ui_pointer *pointer);
#endif
void platform_video_window_size(int *width, int *height);
#endif
BOOL platform_next_keystroke(struct platform_keystroke *keystroke);
/* the multiplayer scoreboard (game_engine.c) open or not: while it is, the
mouse wheel and Page Up/Down scroll it instead of switching weapons; how
+87
View File
@@ -0,0 +1,87 @@
/*
VOICE.C
The voice chat's sound (halo_voice.h): on Android, the host's
(port/android/host/host_voice.c); elsewhere none.
*/
#include "halo_voice.h"
#include "platform.h"
#include "port_config.h"
#ifdef HALO_ANDROID
#include "guest_host.h"
#include <stdio.h>
static int voice_started;
/* whether the app may use the microphone, as the launcher found
(LauncherActivity.java): opened without it, Android would ask in the middle
of the game */
static int voice_microphone_allowed(void)
{
char path[512];
FILE *file;
int allowed = 0;
snprintf(path, sizeof(path), "%s/microphone_permission.txt", platform_data_root());
file = fopen(path, "rb");
if (file)
{
allowed = fgetc(file) == '1';
fclose(file);
}
return allowed;
}
int halo_voice_enabled(void)
{
return config_boolean("voice.enabled");
}
int halo_voice_begin(void)
{
int microphone;
if (voice_started)
return 1;
voice_started = 1;
microphone = config_boolean("voice.microphone") && voice_microphone_allowed();
platform_log("voice: on, %s", microphone ? "with the microphone" :
config_boolean("voice.microphone") ? "without the microphone (not allowed)" : "listening only");
return host_voice_start(microphone, (float)config_real("voice.volume"), (float)config_real("voice.threshold"));
}
void halo_voice_end(void)
{
if (!voice_started)
return;
voice_started = 0;
host_voice_stop();
platform_log("voice: off");
}
int halo_voice_take(void *buffer, int size)
{
return voice_started ? host_voice_take_packets(buffer, size) : 0;
}
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
{
if (voice_started)
host_voice_play(speaker, sequence, data, length, left, right);
}
#else
int halo_voice_enabled(void) { return 0; }
int halo_voice_begin(void) { return 0; }
void halo_voice_end(void) {}
int halo_voice_take(void *buffer, int size) { (void)buffer; (void)size; return 0; }
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
{
(void)speaker; (void)sequence; (void)data; (void)length; (void)left; (void)right;
}
#endif
-64
View File
@@ -69,14 +69,6 @@ static struct
unsigned long long wait_us;
int report_frames;
int report_stereo_frames;
/* (the frame's parts, halo_vr_phase: when each ended, and the game's
ticks in it; summed apart for frames with ticks and without, with how
many took longer than the headset's frame) */
unsigned long long phase_end_us[HALO_VR_PHASE_COUNT];
int frame_ticks;
unsigned long long phase_us[2][HALO_VR_PHASE_COUNT + 1];
int phase_frames[2];
int phase_late[2];
/* the view's turning, gathered for halo_vr_look_delta */
float look_yaw;
@@ -538,47 +530,10 @@ static unsigned long long now_us(void)
/* vr.log_performance: every five seconds, the frames the game gave the
headset, and where their time went: waiting for the headset (xrWaitFrame),
drawing the eyes, and the rest (the game's update, the overlay, the copies) */
void halo_vr_phase(int phase, int ticks)
{
if (!vr.active || !vr.log_performance || phase < 0 || phase >= HALO_VR_PHASE_COUNT)
return;
vr.phase_end_us[phase] = now_us();
if (phase == HALO_VR_PHASE_TICKS)
vr.frame_ticks = ticks;
}
/* the frame's parts, from the marks halo_vr_phase left (a part not marked
this frame: none) */
static void performance_phases(unsigned long long now)
{
int kind = vr.frame_ticks > 0;
unsigned long long last = vr.frame_start_us;
int phase;
for (phase = 0; phase < HALO_VR_PHASE_COUNT; phase++)
{
unsigned long long end = vr.phase_end_us[phase];
if (end >= last && end <= now)
{
vr.phase_us[kind][phase] += end - last;
last = end;
}
}
vr.phase_us[kind][HALO_VR_PHASE_COUNT] += now - last;
vr.phase_frames[kind]++;
/* (late: longer than the headset's frame, less a millisecond) */
if (vr.info.refresh_rate > 1.0f && (float)(now - vr.frame_start_us + 1000) > 1e6f / vr.info.refresh_rate)
vr.phase_late[kind]++;
memset(vr.phase_end_us, 0, sizeof(vr.phase_end_us));
vr.frame_ticks = 0;
}
static void performance_report(int stereo)
{
unsigned long long now = now_us();
performance_phases(now);
vr.work_us += now - vr.frame_start_us;
vr.report_frames++;
vr.report_stereo_frames += stereo != 0;
@@ -593,25 +548,6 @@ static void performance_report(int stereo)
platform_log("VR: %.1f fps (%d%% stereo); per frame %.1f ms working (eyes %.1f ms), %.1f ms waiting "
"for the headset", frames / seconds, (int)(100.0f * vr.report_stereo_frames / frames),
vr.work_us * 1e-3f / frames, vr.eyes_us * 1e-3f / frames, vr.wait_us * 1e-3f / frames);
{
int kind;
for (kind = 1; kind >= 0; kind--)
{
float count = (float)(vr.phase_frames[kind] ? vr.phase_frames[kind] : 1);
platform_log("VR: %s: %d frames, %d late; ms input %.1f, ticks %.1f, camera %.1f, draw %.1f, "
"present %.1f", kind ? "with ticks" : "no ticks", vr.phase_frames[kind], vr.phase_late[kind],
vr.phase_us[kind][HALO_VR_PHASE_INPUT] * 1e-3f / count,
vr.phase_us[kind][HALO_VR_PHASE_TICKS] * 1e-3f / count,
vr.phase_us[kind][HALO_VR_PHASE_CAMERA] * 1e-3f / count,
vr.phase_us[kind][HALO_VR_PHASE_DRAW] * 1e-3f / count,
vr.phase_us[kind][HALO_VR_PHASE_COUNT] * 1e-3f / count);
}
}
memset(vr.phase_us, 0, sizeof(vr.phase_us));
memset(vr.phase_frames, 0, sizeof(vr.phase_frames));
memset(vr.phase_late, 0, sizeof(vr.phase_late));
}
vr.report_start_us = now;
vr.report_frames = vr.report_stereo_frames = 0;
+7 -37
View File
@@ -55,9 +55,9 @@ drive the controller.
/* main/console.c */
extern unsigned char console_is_active(void);
/* port/linux/game/menu_functions.c: two or more players on this machine
(co-op, or split screen in a network game) */
extern unsigned char pc_menu_split_players(void);
/* port/linux/game/menu_functions.c: two players on this machine (the
campaign's co-op) */
extern unsigned char pc_menu_coop_players(void);
/* ---------- device tables */
@@ -638,43 +638,13 @@ static int sdl_gamepads(SDL_Gamepad *gamepads[PORT_COUNT])
return found;
}
/* whether one gamepad is port 1's (port_gamepad) */
static BOOL lone_gamepad_split;
/* no button of the gamepad held, its sticks and triggers at rest */
static BOOL gamepad_idle(SDL_Gamepad *gamepad)
{
int index;
for (index = 0; index < SDL_GAMEPAD_BUTTON_COUNT; index++)
{
if (SDL_GetGamepadButton(gamepad, (SDL_GamepadButton)index))
return FALSE;
}
for (index = 0; index < SDL_GAMEPAD_AXIS_COUNT; index++)
{
if (abs(SDL_GetGamepadAxis(gamepad, (SDL_GamepadAxis)index)) > 8000)
return FALSE;
}
return TRUE;
}
/* the gamepad of a port: the first shares port 0 with the keyboard, but for
two or more players with one gamepad (co-op, split screen), port 1 has it
(the keyboard's player is 1, the gamepad's 2). It changes port only at rest:
a button held across the change would be pressed again on the other port
(the B that leaves a profile screen leaving the game as player 1's) */
two players with one gamepad (co-op), port 1 has it (the keyboard's player
is 1, the gamepad's 2) */
static SDL_Gamepad *port_gamepad(SDL_Gamepad *gamepads[PORT_COUNT], int count, int port)
{
if (count == 1)
{
BOOL split = pc_menu_split_players() != 0;
if (split != lone_gamepad_split && gamepad_idle(gamepads[0]))
lone_gamepad_split = split;
if (lone_gamepad_split)
return port == 1 ? gamepads[0] : NULL;
}
if (count == 1 && pc_menu_coop_players())
return port == 1 ? gamepads[0] : NULL;
return port < count ? gamepads[port] : NULL;
}
-1
View File
@@ -52,7 +52,6 @@ The game finds the game data as on Linux. Refer to "Start the game" in
| Settings | `config.toml` next to `halo.exe` |
| Saved games | `%APPDATA%\halo`, or `paths.saves` in `config.toml` |
| Log | `debug.txt` in the data root (the folder that contains `maps\`) |
| Log of the port | The console. The release build has no console: `halo.log` next to `halo.exe` |
## How the port operates
-18
View File
@@ -37,24 +37,6 @@ static void windows_startup(void)
/* Sleep() waits in 1 ms steps rather than the default 15.6 ms: the
game's frame pacing sleeps for short intervals */
timeBeginPeriod(1);
/* a release build has no console (/SUBSYSTEM:WINDOWS), so the platform
layer's log (platform_log's stderr: invite links, the data root, crash
reports) goes to halo.log next to halo.exe, unless stderr already goes
somewhere (a pipe or a file the game was started with) */
if (_fileno(stderr) < 0)
{
char path[MAX_PATH];
DWORD length = GetModuleFileNameA(NULL, path, sizeof(path));
char *slash;
if (length && length < sizeof(path) && (slash = strrchr(path, '\\')) &&
(size_t)(slash + 1 - path) + sizeof("halo.log") <= sizeof(path))
{
strcpy(slash + 1, "halo.log");
if (freopen(path, "w", stderr))
setvbuf(stderr, NULL, _IONBF, 0);
}
}
}
static int errno_from_windows_error(DWORD error)
+1 -12
View File
@@ -317,7 +317,6 @@ symbols in this file:
#include "units/units.h"
#include "units/vehicle_definitions.h"
#include "units/vehicles.h"
#include "networking/network_game_globals.h"
/* ---------- constants */
@@ -2225,12 +2224,6 @@ long actor_new(
long actor_definition_index;
long actor_index = NONE;
/* port: a client of the distributed netcode runs no AI: it has the host's
actors' units, driven as the host sends them (port/linux/game/network_actors.c),
and its own (placed at the map's start or by its scripts) would be extras
the host doesn't have */
if (network_game_distributed_client())
return NONE;
if (actor_variant_definition_index != NONE)
{
actor_variant_definition = actor_variant_definition_get(actor_variant_definition_index);
@@ -2669,9 +2662,6 @@ long actor_place(
long unit_index;
match_assert("c:\\halo\\SOURCE\\ai\\actors.c", 603, starting_location);
/* port: none on a client of the distributed netcode (actor_new) */
if (network_game_distributed_client())
return NONE;
objects_garbage_collection();
actor_variant_definition =
@@ -2841,8 +2831,7 @@ short actors_spawn_from_unit(
{
short spawned_actor_count = 0;
/* port: none on a client of the distributed netcode (actor_new) */
if (actor_variant_definition_index != NONE && actor_count > 0 && !network_game_distributed_client())
if (actor_variant_definition_index != NONE && actor_count > 0)
{
struct unit_datum *source_unit = unit_get(unit_index);
long source_actor_index = source_unit->unit.swarm_actor_index != NONE ?
+4 -9
View File
@@ -25,10 +25,9 @@ enum
enum
{
/* port: the native builds' larger pools (halo_port_capacity.h) */
MAXIMUM_ACTORS = HALO_PORT_MAXIMUM_ACTORS,
MAXIMUM_SWARMS = HALO_PORT_MAXIMUM_SWARMS,
MAXIMUM_SWARM_COMPONENTS = HALO_PORT_MAXIMUM_SWARM_COMPONENTS,
MAXIMUM_ACTORS = 256,
MAXIMUM_SWARMS = 32,
MAXIMUM_SWARM_COMPONENTS = 256,
SWARM_COMPONENT_DATUM_SIZE = 0x40,
};
@@ -256,11 +255,7 @@ enum
NUMBER_OF_ACTOR_PERCEPTION_TYPES,
};
/* port: the debug information's actors (ai_debug.c's actor_debug_array,
by actor index): every actor of the native builds' larger pool
(halo_port_capacity.h), else one past the Xbox's 256 is written past its
end */
#define MAXIMUM_NUMBER_OF_ACTORS HALO_PORT_MAXIMUM_ACTORS
#define MAXIMUM_NUMBER_OF_ACTORS 256
#define MAXIMUM_NUMBER_OF_ACTOR_PATHS 32
#define MAXIMUM_UNIT_INDICES_PER_SWARM 16
+5 -17
View File
@@ -465,8 +465,7 @@ typedef char ai_prop_unopposable_enemy_offset_assert[
static void ai_place_pending_mounted_weapons(
void);
static boolean ai_enemies_endanger_player(
boolean must_be_attacking,
long unit_index);
boolean must_be_attacking);
static void ai_flush_spatial_effects(
void);
@@ -1972,11 +1971,8 @@ void ai_update(
return;
}
/* port: unit_index NONE is any player's unit (the game's own check); else
only enemies of that unit count (network co-op's respawn, players.c) */
static boolean ai_enemies_endanger_player(
boolean must_be_attacking,
long unit_index)
boolean must_be_attacking)
{
long current_time = game_time_get();
struct data_iterator iterator;
@@ -1989,8 +1985,7 @@ static boolean ai_enemies_endanger_player(
{
if (prop->player &&
prop->enemy &&
unit_get(prop->unit_index)->unit.player_index != NONE &&
(unit_index == NONE || prop->unit_index == unit_index))
unit_get(prop->unit_index)->unit.player_index != NONE)
{
struct actor_datum *actor = actor_get(prop->owner_actor_index);
long object_index = actor->meta.swarm
@@ -2073,20 +2068,13 @@ static boolean ai_enemies_endanger_player(
boolean ai_enemies_can_see_player(
void)
{
return ai_enemies_endanger_player(FALSE, NONE);
return ai_enemies_endanger_player(FALSE);
}
boolean ai_enemies_attacking_player(
void)
{
return ai_enemies_endanger_player(TRUE, NONE);
}
/* port: whether enemies are attacking this player's unit */
boolean ai_port_enemies_attacking_unit(
long unit_index)
{
return ai_enemies_endanger_player(TRUE, unit_index);
return ai_enemies_endanger_player(TRUE);
}
long ai_get_race_from_team_index(
-3
View File
@@ -142,9 +142,6 @@ boolean ai_consider_major_upgrade(
short squad_index,
real chance);
/* port: network co-op's respawn (players.c) */
boolean ai_port_enemies_attacking_unit(
long unit_index);
boolean ai_enemies_attacking_player(
void);
boolean ai_enemies_can_see_player(
+2 -2
View File
@@ -298,7 +298,7 @@ void ai_profile_display(
ai_profile.meters[_ai_meter_units_updated].current_value,
ai_profile.meters[_ai_meter_units].current_value,
ai_profile.meters[_ai_meter_props].current_value,
HALO_PORT_MAXIMUM_PROPS);
768);
return;
}
@@ -450,7 +450,7 @@ static void ai_profile_render_encounters(
ai_profile.meters[_ai_meter_encounters_updated].current_value,
ai_profile.meters[_ai_meter_encounters].current_value,
ai_profile.meters[_ai_meter_props].current_value,
HALO_PORT_MAXIMUM_PROPS);
768);
ai_profile_draw_string(profilestring, NUMBEROF(tab_stops), tab_stops, global_real_argb_white);
return;
}
+5 -34
View File
@@ -311,7 +311,6 @@ symbols in this file:
#include "scenario/scenario_definitions.h"
#include "structures/structure_bsp_definitions.h"
#include "units/units.h"
#include "coop_enemies.h" /* port: port/linux/game/coop_enemies.c */
#include <stddef.h>
@@ -541,8 +540,7 @@ static boolean encounter_place_actor(
long encounter_index,
short squad_index,
short initial_variant,
boolean spawning,
short extra_number);
boolean spawning);
static void encounterless_deactivate(
long actor_index);
static void encounters_test_activation(
@@ -1838,7 +1836,7 @@ boolean encounter_spawn_actor(
{
if (ai_globals->ai_initialized_for_map)
{
if (encounter_place_actor(encounter_index, squad_index, 0, TRUE, 0))
if (encounter_place_actor(encounter_index, squad_index, 0, TRUE))
{
struct encounter_datum *encounter = encounter_get(encounter_index);
struct encounter_definition *encounter_definition = TAG_BLOCK_GET_ELEMENT(
@@ -2247,17 +2245,9 @@ void encounter_create(
for (i = 0; i < count; ++i)
{
encounter_place_actor(encounter_index, squad_index, initial_variant, FALSE, 0);
encounter_place_actor(encounter_index, squad_index, initial_variant, FALSE);
initial_variant = 0;
}
/* port: network co-op's extra enemies, for its players
(coop_enemies.c) */
{
short extra_count = coop_enemies_extra_count(encounter_index, count);
for (i = 0; i < extra_count; ++i)
encounter_place_actor(encounter_index, squad_index, 0, FALSE, (short)(i + 1));
}
}
encounter_update_status(encounter_index);
@@ -3006,25 +2996,18 @@ static void encounter_post_combat(
return;
}
/* port: extra_number, from 1, places one of network co-op's extra enemies
(coop_enemies.c): at the squad's starting locations in turn, each the
place a ring about its own (0: the squad's own actor, as the Xbox's) */
static boolean encounter_place_actor(
long encounter_index,
short squad_index,
short initial_variant,
boolean spawning,
short extra_number)
boolean spawning)
{
boolean placed = FALSE;
struct encounter_definition *encounter_definition = TAG_BLOCK_GET_ELEMENT(
&global_scenario_get()->ai_encounters, DATUM_INDEX_TO_ABSOLUTE_INDEX(encounter_index), struct encounter_definition);
struct squad_definition *squad_definition = TAG_BLOCK_GET_ELEMENT(
&encounter_definition->squads, squad_index, struct squad_definition);
short starting_location_index = extra_number > 0 && squad_definition->starting_locations.count > 0 ?
(short)((extra_number - 1) % squad_definition->starting_locations.count) :
encounter_get_actor_starting_location(encounter_index, squad_index, spawning);
struct actor_starting_location spread_location;
short starting_location_index = encounter_get_actor_starting_location(encounter_index, squad_index, spawning);
if (starting_location_index != NONE)
{
@@ -3033,18 +3016,6 @@ static boolean encounter_place_actor(
short actor_palette_index = squad_definition->actor_palette_index;
struct scenario *scenario = global_scenario_get();
/* port: an extra enemy a ring about the starting location */
if (extra_number > 0)
{
spread_location = *starting_location;
if (coop_enemies_spread_position(&starting_location->position,
(short)((extra_number - 1) / squad_definition->starting_locations.count + 1),
&spread_location.position))
{
starting_location = &spread_location;
}
}
if (starting_location->actor_variant_index != NONE)
actor_palette_index = starting_location->actor_variant_index;
+2 -3
View File
@@ -95,8 +95,7 @@ static long last_prop_data_full_warn_time = NONE;
void props_initialize(
void)
{
/* port: the native builds' larger pool (halo_port_capacity.h) */
prop_data = game_state_data_new("prop", HALO_PORT_MAXIMUM_PROPS, sizeof(struct prop_datum));
prop_data = game_state_data_new("prop", 768, sizeof(struct prop_datum));
match_assert("c:\\halo\\SOURCE\\ai\\props.c", 0x24, prop_data);
@@ -142,7 +141,7 @@ static void prop_add(
error(
_error_silent,
"AI knowledge database (%d entries) is full (warns once every 30 sec)",
HALO_PORT_MAXIMUM_PROPS);
768);
last_prop_data_full_warn_time = game_time;
}
}
-2
View File
@@ -53,8 +53,6 @@ void scripted_camera_set_camera_point_relative(
long relative_object_index);
short scripted_camera_time(
void);
long scripted_camera_object_relative_to(
void);
void scripted_camera_update(
struct dead_camera *camera,
struct camera_control const *controls,
+4 -2
View File
@@ -190,9 +190,11 @@ void dead_camera_update(
}
else
{
next_unit_index = NONE;
next_unit_index = (long)result;
}
/* With no living player, keep observing the dead unit/current position. */
/* January preserves the command pointer as the fallback object index
* when no player is found. This is a bug; a corrected build should
* initialize next_unit_index to NONE instead. */
if (next_unit_index != camera->unit_index && next_unit_index != NONE)
{
+4 -17
View File
@@ -121,7 +121,6 @@ symbols in this file:
#include "camera_scripting.h"
#include "dead_camera.h"
#include "coop_spectate.h" /* port: port/linux/game/coop_spectate.c */
#include "editor_flying_camera.h"
#include "first_person_camera.h"
#include "flying_camera.h"
@@ -709,12 +708,8 @@ static void director_choose_camera_game(
}
else
{
long player_index = local_player_get_player_index(local_player_index);
struct player_datum *player = player_get(player_index);
/* port: and a network co-op player who hasn't spawned yet, waiting for
the level's first checkpoint, watches too */
boolean use_dead_camera = player->unit_index == NONE &&
(player->statistics.deaths > 0 || players_coop_waiting_to_start(player_index));
struct player_datum *player = player_get(local_player_get_player_index(local_player_index));
boolean use_dead_camera = player->unit_index == NONE && player->statistics.deaths > 0;
/* (port: not a client in another's game, the host's rules: a flying
camera would see all of it, and one behind the player round its
@@ -726,25 +721,17 @@ static void director_choose_camera_game(
director_choose_game_perspective(local_player_index, initialize);
if (use_dead_camera)
{
/* port: a dead network co-op player watches a living teammate
(port/linux/game/coop_spectate.c), or its own body if nobody is alive */
long watched_unit_index = coop_spectating() ? coop_spectate_unit(local_player_index) : NONE;
if (director->camera_proc != (director_camera_update_proc)dead_camera_update ||
(watched_unit_index != NONE &&
((struct dead_camera *)director->camera_data)->unit_index != watched_unit_index))
if (director->camera_proc != (director_camera_update_proc)dead_camera_update)
{
dead_camera_new(
(struct dead_camera *)director->camera_data,
local_player_index,
watched_unit_index);
NONE);
director_set_camera(
local_player_index,
(director_camera_update_proc)dead_camera_update,
TRUE);
}
if (watched_unit_index != NONE)
coop_spectate_camera((struct dead_camera *)director->camera_data);
}
else if (director->camera_proc == (director_camera_update_proc)dead_camera_update)
{
-2
View File
@@ -74,7 +74,6 @@ symbols in this file:
#include "scenario/scenario_definitions.h"
#include "text/draw_string.h"
#include "text/text_group.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* ---------- constants */
@@ -266,7 +265,6 @@ void cinematic_set_title_delayed(
{
short title_slot_index;
network_coop_note_title(title_index, delay);
for (title_slot_index = 0;
title_slot_index < MAXIMUM_QUEUED_CINEMATIC_TITLES &&
cinematic_globals->queued_titles[title_slot_index].title_index != NONE;
-77
View File
@@ -98,7 +98,6 @@ symbols in this file:
#include "scenario/scenario_definitions.h"
#include "sound/game_sound.h"
#include "sound/sound_definitions.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* ---------- constants */
@@ -113,10 +112,6 @@ enum
_scenario_device_group_can_change_only_once_bit = 0,
};
/* port: the group flags network_coop.c syncs */
#define DEVICE_GROUP_NETWORK_FLAGS \
(FLAG(_device_group_can_change_only_once_bit) | FLAG(_device_group_changed_once_bit))
enum
{
_device_function_none = 0,
@@ -191,9 +186,6 @@ typedef char device_animation_frame_count_offset_assert[
void device_group_set_actual_value(
short group_index,
real actual_value);
static boolean device_group_change(
short group_index,
real desired_value);
static short device_group_new(
real initial_value,
unsigned long flags);
@@ -602,7 +594,6 @@ void device_group_set_actual_value(
group = datum_get(device_groups_data, group_index);
group->actual_value = actual_value;
network_coop_note_device_snap(group_index);
object_iterator_new(&iterator, _object_mask_device, 0);
while ((device = object_iterator_next(&iterator)) != NULL)
@@ -686,74 +677,6 @@ void device_render_debug(
boolean device_group_set_desired_value(
short group_index,
real desired_value)
{
/* port: on a co-op client only the host moves devices (network_coop.c) */
if (network_coop_devices_remote())
return FALSE;
return device_group_change(group_index, desired_value);
}
/* port: reads a group for network_coop.c; FALSE if it doesn't exist */
boolean device_group_network_get(
short group_index,
real *value,
word *flags,
boolean *runtime)
{
struct device_group_datum *group = group_index != NONE ? datum_try_and_get(device_groups_data, group_index) : NULL;
if (!group)
return FALSE;
*value = group->actual_value;
*flags = group->flags & DEVICE_GROUP_NETWORK_FLAGS;
*runtime = TEST_FLAG(group->flags, _device_group_runtime_bit);
return TRUE;
}
/* port: a co-op client applies the host's group state. snap jumps the
devices to the value; otherwise they move there as usual. */
void device_group_network_set(
short group_index,
real value,
word flags,
boolean snap)
{
struct device_group_datum *group = group_index != NONE ? datum_try_and_get(device_groups_data, group_index) : NULL;
if (!group)
return;
if (snap)
device_group_set_actual_value(group_index, value);
else
{
group->flags &= ~FLAG(_device_group_changed_once_bit);
device_group_change(group_index, value);
}
group->flags = (word)((group->flags & ~DEVICE_GROUP_NETWORK_FLAGS) | (flags & DEVICE_GROUP_NETWORK_FLAGS));
}
void device_port_set_state(
long device_index,
real power,
boolean move_to_position,
real position)
{
struct device_datum *device = device_get(device_index);
device->device.power = PIN(power, 0.0f, 1.0f);
if (move_to_position)
{
device->device.position = PIN(position, 0.0f, 1.0f);
device->device.position_velocity = 0.0f;
device->device.flags |= FLAG(_device_position_changed_bit);
}
}
static boolean device_group_change(
short group_index,
real desired_value)
{
boolean changed = FALSE;
-18
View File
@@ -129,24 +129,6 @@ void device_set_power(
boolean device_group_set_desired_value(
short group_index,
real desired_value);
/* port: co-op device sync (port/linux/game/network_coop.c) */
boolean device_group_network_get(
short group_index,
real *value,
word *flags,
boolean *runtime);
void device_group_network_set(
short group_index,
real value,
word flags,
boolean snap);
/* port: a co-op client puts a device at the host's power, and at its
position when move_to_position */
void device_port_set_state(
long device_index,
real power,
boolean move_to_position,
real position);
boolean device_can_change_position(
long device_index);
boolean device_frontfacing(
-49
View File
@@ -107,7 +107,6 @@ symbols in this file:
#include "units/units.h"
#include <stddef.h>
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* ---------- constants */
@@ -398,7 +397,6 @@ void scripted_player_effect_set_rotation(
real pitch,
real roll)
{
network_coop_note_player_effect(_coop_player_effect_rotation, yaw, pitch, roll);
player_effect_globals->scripted_effect.max_rotation.yaw = DEGREES_TO_RADIANS(yaw);
player_effect_globals->scripted_effect.max_rotation.pitch = DEGREES_TO_RADIANS(pitch);
player_effect_globals->scripted_effect.max_rotation.roll = DEGREES_TO_RADIANS(roll);
@@ -460,50 +458,6 @@ void player_effect_screen_fade_out(
return;
}
/* port: read and set the screen fade, so a co-op host can send it to its
clients (port/linux/game/network_coop.c): color, length, direction, and
the game time it started */
void player_effect_port_screen_fade_get(
real_rgb_color *color,
short *ticks,
boolean *fading_out,
long *start_time)
{
*color = player_effect_globals->screen_fade.color;
*ticks = player_effect_globals->screen_fade.ticks;
*fading_out = player_effect_globals->screen_fade.fading_out;
*start_time = player_effect_globals->screen_fade.start_time;
return;
}
void player_effect_port_screen_fade_set(
real_rgb_color const *color,
short ticks,
boolean fading_out,
long start_time)
{
player_effect_globals->screen_fade.color = *color;
player_effect_globals->screen_fade.ticks = ticks;
player_effect_globals->screen_fade.fading_out = fading_out;
player_effect_globals->screen_fade.start_time = start_time;
return;
}
boolean player_effect_port_scripted_active(
void)
{
return TEST_FLAG(player_effect_globals->global_flags, _scripted_player_effect_active_bit);
}
void player_effect_port_scripted_end(
void)
{
SET_FLAG(player_effect_globals->global_flags, _scripted_player_effect_active_bit, FALSE);
SET_FLAG(player_effect_globals->global_flags, _scripted_player_effect_stopping_bit, FALSE);
}
void player_effect_get_damage_indicators(
short local_player_index,
byte *damage_indicators)
@@ -655,7 +609,6 @@ void scripted_player_effect_set_translation(
{
real_vector3d *translation = &player_effect_globals->scripted_effect.max_translation;
network_coop_note_player_effect(_coop_player_effect_translation, horizontal, vertical, depth);
translation->i = horizontal;
translation->j = vertical;
translation->k = depth;
@@ -669,7 +622,6 @@ void scripted_player_effect_start(
{
short ticks;
network_coop_note_player_effect(_coop_player_effect_start, maximum_intensity, attack_time, 0.0f);
player_effect_globals->scripted_effect.max_intensity = maximum_intensity;
ticks = (short)fast_ftol(attack_time * TICKS_PER_SECOND);
@@ -692,7 +644,6 @@ void scripted_player_effect_stop(
{
short ticks = (short)fast_ftol(duration * TICKS_PER_SECOND);
network_coop_note_player_effect(_coop_player_effect_stop, duration, 0.0f, 0.0f);
player_effect_globals->scripted_effect.timer = ticks;
player_effect_globals->scripted_effect.total_time = ticks;
SET_FLAG(
-15
View File
@@ -56,21 +56,6 @@ void player_effect_screen_fade_out(
real green,
real blue,
short ticks);
/* port: screen fade sync for network co-op */
void player_effect_port_screen_fade_get(
real_rgb_color *color,
short *ticks,
boolean *fading_out,
long *start_time);
void player_effect_port_screen_fade_set(
real_rgb_color const *color,
short ticks,
boolean fading_out,
long start_time);
/* port: whether the scripts' screen shake is running, and ending it at once
(network co-op keeps a client's in step with the host's) */
boolean player_effect_port_scripted_active(void);
void player_effect_port_scripted_end(void);
void player_effect_get_damage_indicators(
short local_player_index,
byte *damage_indicators);
+2 -20
View File
@@ -162,7 +162,6 @@ struct game_options;
#include "networking/network_messages.h"
#include "networking/telnet_console.h"
#include "objects/objects.h"
#include "objects/widgets/antenna.h"
#include "objects/widgets/widgets.h"
#include "physics/breakable_surfaces.h"
#include "physics/collision_usage.h"
@@ -180,12 +179,9 @@ struct game_options;
#include "structures/structures.h"
#include "units/units.h"
#include "units/vehicles.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* network_game_globals.c's */
boolean network_game_distributed_client(void);
/* port: a client drives the host's actors' units as the host sent them (port/linux/game/network_actors.c) */
void network_actors_drive(void);
/* ---------- constants */
@@ -329,31 +325,19 @@ void game_tick(
positions, and could place objects of their own) */
if (!network_game_distributed_client())
ai_update();
else
network_actors_drive();
players_update_before_game();
seconds_per_tick = game_globals->players_are_double_speed
? 1.0f / (2 * TICKS_PER_SECOND)
: 1.0f / TICKS_PER_SECOND;
effects_update(seconds_per_tick);
/* An antenna's chain is a simulation of the Xbox's own step, an update a
tick: stepped a frame at a time, its springs, its carry and its points'
physics act against the frames' own movement of it, and a vehicle at
speed swings it far wider than a tick's step does. The frames between the
ticks are drawn from what each tick leaves (antenna.c). */
antennas_update(seconds_per_tick);
lock_global_random_seed();
rumble_update();
first_person_weapons_update();
unlock_global_random_seed();
game_engine_update();
editor_update();
/* port: in network co-op only the host runs the scripts. A client running
them would place the map's actors and objects a second time and make
decisions that belong to the host. */
if (!(network_game_distributed_client() && network_coop_active()))
hs_update();
hs_update();
recorded_animations_update();
objects_update();
players_update_after_game();
@@ -941,9 +925,7 @@ void remove_quitting_players_from_game(
struct player_datum *player;
long current_time;
/* port: also in network co-op, which has no game engine. Without this a
player who left kept their unit, and it respawned. */
if (!game_engine_running() && !network_coop_active())
if (!game_engine_running())
return;
current_time = game_time_get();
+2 -24
View File
@@ -88,8 +88,8 @@ struct game_allegiance_globals
short allegiance_count;
struct game_allegiance_record allegiances[8];
byte reserved92[2];
unsigned long ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS];
unsigned long friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS];
unsigned long ally_bitvector[4];
unsigned long friendly_bitvector[4];
};
typedef char game_allegiance_record_size_assert[
@@ -452,28 +452,6 @@ void game_allegiance_update(
return;
}
/* port: which teams are allies and which friends. A co-op client takes the
host's, as only the host runs the scripts that make allegiances. */
void game_allegiance_get_teams(
unsigned long ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS],
unsigned long friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS])
{
csmemcpy(ally_bitvector, game_allegiance_globals->ally_bitvector,
sizeof(game_allegiance_globals->ally_bitvector));
csmemcpy(friendly_bitvector, game_allegiance_globals->friendly_bitvector,
sizeof(game_allegiance_globals->friendly_bitvector));
}
void game_allegiance_set_teams(
unsigned long const ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS],
unsigned long const friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS])
{
csmemcpy(game_allegiance_globals->ally_bitvector, ally_bitvector,
sizeof(game_allegiance_globals->ally_bitvector));
csmemcpy(game_allegiance_globals->friendly_bitvector, friendly_bitvector,
sizeof(game_allegiance_globals->friendly_bitvector));
}
void game_allegiance_create(
short team1_index,
boolean team1_suspicious,
-9
View File
@@ -11,9 +11,6 @@ enum
_allegiance_incident_forgive
};
/* port: the longs of game_allegiance_get_teams' bit vectors */
#define GAME_ALLEGIANCE_BITVECTOR_LONGS 4
/* ---------- prototypes/GAME_ALLEGIANCE.C */
void game_allegiance_initialize(
@@ -26,12 +23,6 @@ void game_allegiance_dispose_from_old_map(
void);
void game_allegiance_update(
void);
void game_allegiance_get_teams(
unsigned long ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS],
unsigned long friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS]);
void game_allegiance_set_teams(
unsigned long const ally_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS],
unsigned long const friendly_bitvector[GAME_ALLEGIANCE_BITVECTOR_LONGS]);
void game_allegiance_create(
short team1_index,
boolean team1_suspicious,
+4 -20
View File
@@ -568,7 +568,6 @@ symbols in this file:
#include "networking/network_game_globals.h"
#include "networking/network_server_manager.h"
#include "networking/network_game_manager.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* (network_server_manager_internal.h's: the host's game record) */
struct network_game *network_game_server_get_game(struct network_game_server *server);
#include "objects.h"
@@ -7069,31 +7068,16 @@ short game_engine_friendly_damage(
{
struct game_variant_options const *options = game_variant_options_get();
struct player_datum *attacker;
short friendly_fire;
/* port: network co-op's friendly fire is Server Setup's FRIENDLY FIRE,
in the host's game settings (only the host deals damage), between its
players: their AI allies they always hurt, as in the campaign */
if (network_coop_active())
if (!game_engine || !global_variant.universal_variant.teams || attacker_player_index == NONE ||
options->friendly_fire == _friendly_fire_on)
{
struct network_game *game = network_game_get_game();
struct unit_datum *unit = (struct unit_datum *)object_try_and_get_and_verify_type(object_index,
_object_mask_unit);
if (!game || !unit || unit->unit.player_index == NONE)
return _friendly_damage_all;
friendly_fire = game->variant_options.friendly_fire;
return _friendly_damage_all;
}
else if (!game_engine || !global_variant.universal_variant.teams)
return _friendly_damage_all;
else
friendly_fire = options->friendly_fire;
if (attacker_player_index == NONE || friendly_fire == _friendly_fire_on)
return _friendly_damage_all;
attacker = (struct player_datum *)datum_try_and_get(player_data, attacker_player_index);
if (!attacker || attacker->unit_index == object_index)
return _friendly_damage_all;
switch (friendly_fire)
switch (options->friendly_fire)
{
case _friendly_fire_off: return _friendly_damage_none;
case _friendly_fire_shields_only: return _friendly_damage_shields;
-7
View File
@@ -482,14 +482,7 @@ void game_time_update(
}
}
/* The effects and the widgets that move every frame (game_frame) hang
off objects: they read the pose between the last two ticks, as the
renderer does, and the ticks above have taken their snapshots already
(port/linux/game/render_interpolation.c). The frames of one tick share
the fraction, so what they hang off is drawn where they put it. */
render_interpolation_frame_begin();
game_frame(game_time_get_speed()*time_delta_sec);
render_interpolation_frame_end();
}
else
{
+34 -54
View File
@@ -209,7 +209,6 @@ symbols in this file:
#include "units/vehicles.h"
#include "real_math.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* ---------- constants */
@@ -1788,49 +1787,59 @@ boolean player_control_action_test_look_relative_all_directions(
_player_control_look_relative_all_directions_flags);
}
/* The actions the scripts test for (player_control_action_test_*) that a
player's unit control, turning, sticks and trigger show. A local player's
come from its input blob; in network co-op the host's scripts count a
client's too (port/linux/game/network_distributed.c), so Pillar of
Autumn's tutorial goes on for whichever player does what it asks. */
void player_control_action_test_note(
unsigned long unit_control_flags,
real_euler_angles2d const *facing_delta,
real_vector2d const *throttle,
real primary_trigger)
static void player_control_action_test_check_reset_input_blob(
struct input_blob *input)
{
struct player_control_globals_data *globals = player_control_globals;
struct player_control_globals_data *globals;
if (TEST_FLAG(unit_control_flags, _unit_control_action_bit))
if (input->accept && cinematic_can_be_skipped())
{
main_skip_cinematic();
}
globals = player_control_globals;
if (TEST_FLAG(input->unit_control_flags, _unit_control_action_bit))
{
globals->action_flags |= FLAG(_player_control_action_bit);
}
if (TEST_FLAG(unit_control_flags, _unit_control_jump_bit))
if (TEST_FLAG(input->unit_control_flags, _unit_control_jump_bit))
{
globals->action_flags |= FLAG(_player_control_jump_bit);
}
if (TEST_FLAG(unit_control_flags, _unit_control_throw_grenade_bit))
if (input->accept)
{
globals->action_flags |= FLAG(_player_control_grenade_trigger_bit);
globals->action_flags |= FLAG(_player_control_accept_bit);
}
if (primary_trigger > 0.f)
if (input->back)
{
globals->action_flags |= FLAG(_player_control_back_bit);
}
if (input->primary_trigger > 0.f)
{
globals->action_flags |= FLAG(_player_control_primary_trigger_bit);
}
if (TEST_FLAG(input->unit_control_flags, _unit_control_throw_grenade_bit))
{
globals->action_flags |= FLAG(_player_control_grenade_trigger_bit);
}
if (TEST_FLAG(input->player_control_flags, _player_control_input_zoom_bit))
{
globals->action_flags |= FLAG(_player_control_zoom_bit);
}
if (facing_delta->pitch > 0.f)
if (input->facing_delta.pitch > 0.f)
{
globals->action_flags |= FLAG(_player_control_look_relative_up_bit);
}
else if (facing_delta->pitch < 0.f)
else if (input->facing_delta.pitch < 0.f)
{
globals->action_flags |= FLAG(_player_control_look_relative_down_bit);
}
if (facing_delta->yaw > 0.f)
if (input->facing_delta.yaw > 0.f)
{
globals->action_flags |= FLAG(_player_control_look_relative_left_bit);
}
else if (facing_delta->yaw < 0.f)
else if (input->facing_delta.yaw < 0.f)
{
globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
}
@@ -1854,51 +1863,22 @@ void player_control_action_test_note(
}
}
#endif
if (throttle->i > 0.f)
if (input->throttle.i > 0.f)
{
globals->action_flags |= FLAG(_player_control_move_relative_forward_bit);
}
else if (throttle->i < 0.f)
else if (input->throttle.i < 0.f)
{
globals->action_flags |= FLAG(_player_control_move_relative_backward_bit);
}
if (throttle->j > 0.f)
if (input->throttle.j > 0.f)
{
globals->action_flags |= FLAG(_player_control_move_relative_right_bit);
}
else if (throttle->j < 0.f)
else if (input->throttle.j < 0.f)
{
globals->action_flags |= FLAG(_player_control_move_relative_left_bit);
}
}
static void player_control_action_test_check_reset_input_blob(
struct input_blob *input)
{
struct player_control_globals_data *globals;
if (input->accept && (cinematic_can_be_skipped() || network_coop_skip_offered()))
{
/* port: in network co-op the players vote to skip (network_coop.c) */
if (!network_coop_vote_skip())
main_skip_cinematic();
}
player_control_action_test_note(input->unit_control_flags, &input->facing_delta, &input->throttle,
input->primary_trigger);
globals = player_control_globals;
if (input->accept)
{
globals->action_flags |= FLAG(_player_control_accept_bit);
}
if (input->back)
{
globals->action_flags |= FLAG(_player_control_back_bit);
}
if (TEST_FLAG(input->player_control_flags, _player_control_input_zoom_bit))
{
globals->action_flags |= FLAG(_player_control_zoom_bit);
}
if (!TEST_FLAG(globals->action_test_flags, _player_control_action_bit))
{
+27 -761
View File
@@ -236,8 +236,6 @@ symbols in this file:
#include "camera/observer.h"
#include "data.h"
#include "devices/devices.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
#include "cutscene/cinematics.h" /* port: network co-op's first spawns */
#include "editor/editor_stubs.h"
#include "effects/effects.h"
#include "effects/player_effects.h"
@@ -272,7 +270,6 @@ symbols in this file:
#include "tag_files/tag_groups.h"
#include "text/text_group.h"
#include "text/unicode.h"
#include "objects/object_definitions.h"
#include "units/biped_definitions.h"
#include "units/bipeds.h"
#include "units/units.h"
@@ -411,8 +408,6 @@ typedef char players_static_data_screen_flash_offset_assert[
/* ---------- prototypes */
static boolean players_respawn_network_coop(
void);
static long is_player_in_trigger(
short bsp_switch_trigger_volume_index,
long object_index);
@@ -462,40 +457,6 @@ static void player_handle_powerup_equipment(
/* ---------- globals */
/* port: network co-op timers, by player absolute index. Each holds the
game time plus one at which something started, or 0 when it isn't happening.
spawnable_since: when the player became somewhere others can spawn beside
(player_spawnable_beside); they wait a while after (players_coop_room_to_spawn). */
static struct
{
long spawnable_since[NETWORK_GAME_MAXIMUM_PLAYER_COUNT];
/* when the dead started waiting for a safe teammate to respawn beside */
long respawn_wait_since;
/* when each player's unit left the loaded structure BSP */
long stranded_since[NETWORK_GAME_MAXIMUM_PLAYER_COUNT];
/* the structure BSP loaded, and when it last switched, by a trigger,
a script or a skipped cutscene (players_coop_bsp_switch_ready) */
short structure_bsp_index;
long bsp_switch_time;
/* the structure BSPs the players have been in this map, by bit */
word visited_structure_bsps;
/* where each player last stood on the ground, and on which BSP: where
everyone comes back when the last checkpoint was on another one */
short ground_structure_bsp_index;
boolean has_ground_position[NETWORK_GAME_MAXIMUM_PLAYER_COUNT];
real_point3d ground_positions[NETWORK_GAME_MAXIMUM_PLAYER_COUNT];
} players_coop_state;
/* port: where each player was at the last checkpoint, in network co-op
(players_note_checkpoint) */
static struct
{
boolean valid;
short structure_bsp_index;
boolean has_position[NETWORK_GAME_MAXIMUM_PLAYER_COUNT];
real_point3d positions[NETWORK_GAME_MAXIMUM_PLAYER_COUNT];
} players_checkpoint;
struct players_globals *players_globals;
struct data_array *team_data;
struct data_array *player_data;
@@ -592,9 +553,6 @@ void players_initialize_for_new_map(
players_globals->all_dead = FALSE;
players_globals->pending_teleport_starting_location_index = NONE;
players_globals->respawn_failure = 0;
/* port: a new map has no network co-op checkpoint yet */
csmemset(&players_checkpoint, 0, sizeof(players_checkpoint));
csmemset(&players_coop_state, 0, sizeof(players_coop_state));
data_make_valid(player_data);
data_make_valid(team_data);
csmemset(
@@ -749,15 +707,6 @@ short local_player_count(
return players_globals->local_player_count;
}
/* port: local_player_count, 0 before the players' globals are made (the
input's first polls ask it: port/linux/game/menu_functions.c's
pc_menu_split_players) */
short players_port_local_player_count(
void)
{
return players_globals ? players_globals->local_player_count : 0;
}
short local_player_get_next(
short local_player_index)
{
@@ -967,9 +916,7 @@ boolean any_player_is_dead(
player_data);
while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL)
{
/* port: ignore co-op players still waiting for the first checkpoint,
or the checkpoint that lets them in could never be saved */
if (player->unit_index == NONE && !players_coop_waiting_to_start(iterator.datum_index))
if (player->unit_index == NONE)
return TRUE;
}
@@ -1313,31 +1260,6 @@ long find_best_starting_location_index(
return best_starting_location_index;
}
/* port: paints a network co-op player's unit in their profile colour. The
campaign Spartan is always green because its tag fixes that colour, which
overrides the colour the unit is created with; this replaces fixed colours. */
static void players_coop_color_unit(
long unit_index,
real_rgb_color const *color)
{
struct object_datum *object = object_get(unit_index);
struct object_definition *definition = object_definition_get(object->definition_index);
short index;
for (index = 0; index < definition->object.change_colors.count && index < NUMBER_OF_OBJECT_CHANGE_COLORS; index++)
{
struct object_change_color_definition *change_color = TAG_BLOCK_GET_ELEMENT(
&definition->object.change_colors, index, struct object_change_color_definition);
/* skip colours driven by a function, like the shield glow */
if (change_color->permutations.count > 0 && !change_color->scaled_by && !change_color->darken_by)
{
object->object.base_change_colors[index] = *color;
object->object.outgoing_change_colors[index] = *color;
}
}
}
static void player_spawn(
long player_index)
{
@@ -1448,8 +1370,6 @@ static void player_spawn(
&placement_data,
&change_color);
unit_index = object_new(&placement_data);
if (unit_index != NONE && network_coop_active())
players_coop_color_unit(unit_index, &change_color);
if (unit_index != NONE)
{
unit = unit_try_and_get(unit_index);
@@ -1878,17 +1798,14 @@ static boolean player_handle_action(
switch (player->action_result)
{
case _player_action_result_touch_device:
/* port: on a co-op client the host decides device use; the action is
relayed to it (network_coop.c) */
if (!network_coop_devices_remote())
device_touched(player->action_object_index, player->unit_index);
device_touched(player->action_object_index, player->unit_index);
result = TRUE;
break;
case _player_action_result_swap_for_powerup:
/* port: a distributed client swaps nothing: the host owns inventories
and decides pickups, and a remote player's relayed action also reaches
here */
/* port: a distributed client's inventories are the host's (the
powerup is swapped where the host decides pickups, and the relayed
action of a remote player reaches here too): it swaps nothing */
if (!players_decide_pickups())
{
result = TRUE;
@@ -1990,7 +1907,7 @@ static boolean player_teleport_internal(
match_assert(
"c:\\halo\\SOURCE\\game\\players.c",
0x4FB,
source_unit_index==NONE || local_player_count()>1 || game_connection()!=_game_connection_local);
source_unit_index==NONE || local_player_count()>1);
if (source_unit_index != NONE &&
object_get_ultimate_parent(source_unit_index) != source_unit_index)
{
@@ -2186,12 +2103,11 @@ static boolean player_teleport_internal(
else
{
error(2, "couldn't teleport player into a valid location");
/* port: only a local player is pseudo-killed, because the respawn
hands the unit back to a local player; another machine's stays put
(the Xbox halted for it). Network co-op respawns its own way, so its
players stay put too. */
if (player->local_player_index != NONE && !network_coop_active())
player_pseudo_kill(player_index, source_unit_index);
match_assert(
"c:\\halo\\SOURCE\\game\\players.c",
0x5AB,
player->local_player_index!=NONE);
player_pseudo_kill(player_index, source_unit_index);
}
return result;
@@ -2390,11 +2306,6 @@ boolean players_respawn_coop(
boolean result;
boolean dangerous;
/* port: network co-op checks safety around each teammate instead
(players_respawn_network_coop) */
if (game_connection() == _game_connection_network_server && network_coop_active())
return players_respawn_network_coop();
players_globals->respawn_failure = 0;
result = FALSE;
if (!players_globals->respawn_failed)
@@ -2490,579 +2401,6 @@ result_complete:
return result;
}
/* ---------- port: network co-op
A network game on a campaign map with no game engine is co-op. Players
spawn on the host and respawn beside each other, as in local co-op
(players_respawn_coop). When everyone is dead, local co-op reverts to the
last checkpoint (game_state_revert), but every machine would have to do
that in step. Instead the players respawn where they were at the last
checkpoint (main_save_map_private records it), or at the map's start if
that was on another BSP. */
static boolean players_coop_room_to_spawn(
void);
static boolean player_place_beside_teammate(
long player_index);
/* How long into a level the extra players wait before spawning. The opening
cutscene starts a tick or so in, and the first player may still be somewhere
with no room beside them (the drop pod on Halo). */
#define COOP_LEVEL_START_TICKS (2 * TICKS_PER_SECOND)
/* Co-op: TRUE while players after the first are held back from spawning:
during the level's first seconds, during a cutscene, or until a teammate has
been free on foot for a while (players_coop_room_to_spawn). */
static boolean players_coop_extras_held(
void)
{
return game_time_get() < COOP_LEVEL_START_TICKS || cinematic_in_progress() || !players_coop_room_to_spawn();
}
/* Co-op host: TRUE if a player who hasn't spawned on this level yet may
spawn now. The first player spawns at once (the level's script places them).
The others spawn beside a teammate (player_place_beside_teammate) once nothing
holds them back (players_coop_extras_held): when the opening cutscene ends or
is skipped, or straight away if they join later. */
static boolean players_coop_may_spawn(
void)
{
struct data_iterator iterator;
struct player_datum *player;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
if (player->unit_index != NONE)
return !players_coop_extras_held();
}
return TRUE;
}
/* TRUE if this player hasn't spawned on the level yet and is being held
back (players_coop_extras_held); they spectate a teammate meanwhile */
boolean players_coop_waiting_to_start(
long player_index)
{
struct player_datum *player = player_try_and_get(player_index);
return player && player->unit_index == NONE && player->statistics.deaths == 0 && network_coop_active() &&
players_coop_extras_held();
}
/* an enemy projectile this close (world units) makes a teammate unsafe
to respawn beside */
#define COOP_RESPAWN_DANGER_RADIUS 15.0f
/* how often the respawn's safety test runs while someone is dead */
#define COOP_RESPAWN_CHECK_TICKS (TICKS_PER_SECOND / 2)
/* after waiting this long for a safe teammate, the dead respawn beside any
teammate standing on the ground */
#define COOP_RESPAWN_FALLBACK_TICKS (10 * TICKS_PER_SECOND)
/* TRUE if an enemy projectile (a grenade, a plasma bolt) is flying near the
unit. Players' own shots and needles stuck in a body don't count. */
static boolean players_coop_danger_near(
long unit_index)
{
real_point3d const *center = &object_get(unit_index)->object.bounding_sphere_center;
real radius_squared = COOP_RESPAWN_DANGER_RADIUS * COOP_RESPAWN_DANGER_RADIUS;
struct object_iterator iterator;
struct object_datum *object;
object_iterator_new(&iterator, _object_mask_projectile, 0);
while ((object = object_iterator_next(&iterator)) != NULL)
{
if (object->object.owner_player_index == NONE && object->object.parent_object_index == NONE &&
distance_squared3d(center, &object->object.bounding_sphere_center) <= radius_squared)
{
return TRUE;
}
}
return FALSE;
}
/* TRUE if the unit (or the vehicle it rides) is inside the loaded structure
BSP. In network co-op players can be left outside it after a BSP switch. */
static boolean players_coop_unit_in_structure(
long unit_index)
{
return object_get(object_get_ultimate_parent(unit_index))->object.location.cluster_index != NONE;
}
/* TRUE if a living player's unit is on solid ground: inside the structure
BSP, not airborne and not in a moving vehicle */
static boolean players_coop_unit_grounded(
long unit_index)
{
long root_index = object_get_ultimate_parent(unit_index);
if (!players_coop_unit_in_structure(unit_index))
return FALSE;
if (root_index == unit_index)
{
struct biped_datum *biped = biped_try_and_get(unit_index);
return !biped || !TEST_FLAG(biped->biped.flags, _biped_airborne_bit);
}
else
{
struct players_vehicle_datum *vehicle = (struct players_vehicle_datum *)
object_try_and_get_and_verify_type(root_index, _object_mask_vehicle);
return !vehicle || vehicle->vehicle.unknown_state <= 0;
}
}
/* TRUE if it is safe to respawn beside a living player's unit: grounded,
not under attack, and no enemy projectile nearby */
static boolean players_coop_unit_safe(
long unit_index)
{
return players_coop_unit_grounded(unit_index) && !ai_port_enemies_attacking_unit(unit_index) &&
!players_coop_danger_near(unit_index);
}
/* the unit of the first living player that passes `test`, or NONE */
static long players_coop_unit_where(
boolean (*test)(long unit_index))
{
struct data_iterator iterator;
struct player_datum *player;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
if (player->unit_index != NONE && test(player->unit_index))
return player->unit_index;
}
return NONE;
}
/* Network co-op respawn: the dead come back beside the first teammate who
is safe, or after COOP_RESPAWN_FALLBACK_TICKS beside one at least on the
ground, so a long fight can't keep them out. The campaign's own test
(players_respawn_coop) is map-wide (any projectile or enemy attack
anywhere), which with many players spread out would almost never pass.
Returns whether everyone waiting came back. */
static boolean players_respawn_network_coop(
void)
{
struct data_iterator iterator;
struct player_datum *player;
long safe_unit_index;
boolean result = TRUE;
if (!players_coop_state.respawn_wait_since)
players_coop_state.respawn_wait_since = game_time_get() + 1;
/* main.c calls this every tick while someone is dead; the safety test
walks all the map's projectiles, so twice a second is enough */
if (game_time_get() % COOP_RESPAWN_CHECK_TICKS != 0)
return FALSE;
safe_unit_index = players_coop_unit_where(players_coop_unit_safe);
if (safe_unit_index == NONE &&
game_time_get() + 1 - players_coop_state.respawn_wait_since >= COOP_RESPAWN_FALLBACK_TICKS)
{
safe_unit_index = players_coop_unit_where(players_coop_unit_grounded);
}
if (safe_unit_index == NONE)
return FALSE;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
/* players still waiting for the level's first checkpoint spawn then */
if (player->unit_index != NONE || player->quit_out_of_game ||
players_coop_waiting_to_start(iterator.datum_index))
{
continue;
}
player_spawn(iterator.datum_index);
if (player->unit_index == NONE)
result = FALSE;
/* (beside the safe teammate, else any with room: never left at the
level's start, where player_spawn put them) */
else if (!player_teleport(iterator.datum_index, safe_unit_index,
&object_get(safe_unit_index)->object.bounding_sphere_center) &&
!player_place_beside_teammate(iterator.datum_index))
{
result = FALSE;
}
}
if (result)
players_coop_state.respawn_wait_since = 0;
return result;
}
/* What a new player can spawn beside: the player's unit on foot, or the
vehicle it rides if a player drives it (a Warthog). NONE while scripts hold
the controls (Pillar of Autumn's cryo tube) or while it rides an AI vehicle
(Silent Cartographer's Pelican), which would put the new player inside. */
static long player_spawnable_beside(
struct player_datum const *player)
{
long vehicle_index;
long driver_index;
if (player->unit_index == NONE || !player_input_enabled() || !players_coop_unit_in_structure(player->unit_index))
return NONE;
vehicle_index = object_get_ultimate_parent(player->unit_index);
if (vehicle_index == player->unit_index)
return vehicle_index;
driver_index = object_try_and_get_and_verify_type(vehicle_index, _object_mask_unit) ?
unit_get(vehicle_index)->unit.driver_object_index : NONE;
return driver_index != NONE && unit_get(driver_index)->unit.player_index != NONE ? vehicle_index : NONE;
}
/* How long a teammate must have been somewhere spawnable before others
spawn beside them, so nobody lands inside a vehicle they just left. */
#define COOP_DISEMBARK_TICKS (4 * TICKS_PER_SECOND)
/* Where the player comes back: their position at the last checkpoint, or,
when that was on another structure BSP (everyone died before the next one),
where they last stood on the ground on this one. NULL if the player has
neither (they joined since); they come back beside a teammate. */
static real_point3d const *players_checkpoint_position(
long player_index)
{
short index = DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index);
if (players_checkpoint.valid && players_checkpoint.structure_bsp_index == global_structure_bsp_index_get())
return players_checkpoint.has_position[index] ? &players_checkpoint.positions[index] : NULL;
if (players_coop_state.ground_structure_bsp_index == global_structure_bsp_index_get() &&
players_coop_state.has_ground_position[index])
{
return &players_coop_state.ground_positions[index];
}
return NULL;
}
/* TRUE if a biped stands on something (not airborne), in the BSP or not */
static boolean players_coop_unit_grounded_anywhere(
long unit_index)
{
struct biped_datum *biped = biped_try_and_get(unit_index);
return biped && !TEST_FLAG(biped->biped.flags, _biped_airborne_bit);
}
/* TRUE if the unit is outside the loaded structure BSP, where it would fall
forever: on foot, or in a vehicle that is empty or player-driven. AI-driven
vehicles are ignored because the intro Pelicans fly outside the BSP. */
static boolean players_coop_unit_stranded(
long unit_index)
{
long vehicle_index = object_get_ultimate_parent(unit_index);
long driver_index;
if (players_coop_unit_in_structure(unit_index))
return FALSE;
/* (on foot: only while falling, not standing on something the BSP
doesn't hold, such as an elevator in its shaft) */
if (vehicle_index == unit_index)
return !players_coop_unit_grounded_anywhere(unit_index);
driver_index = object_try_and_get_and_verify_type(vehicle_index, _object_mask_unit) ?
unit_get(vehicle_index)->unit.driver_object_index : NONE;
return driver_index == NONE || unit_get(driver_index)->unit.player_index != NONE;
}
/* How long a player can be falling outside the BSP before being brought
back. A BSP switch settles in a tick or two; waiting longer avoids moving
someone who is only crossing a seam or riding over a gap. */
#define COOP_STRANDED_TICKS (2 * TICKS_PER_SECOND)
/* After a BSP switch, how long the co-op host lets no other trigger switch
it while the players are brought into the new one, and the longest it then
waits for every machine to have loaded it */
#define COOP_BSP_SWITCH_SETTLE_TICKS (2 * TICKS_PER_SECOND)
#define COOP_BSP_SWITCH_WAIT_TICKS (10 * TICKS_PER_SECOND)
/* Going back to a BSP the team has already been in needs two thirds of the
living players at the trigger: inside it, or this near the player in it
(world units) */
#define COOP_BACKTRACK_GATHER_DISTANCE 4.0f
/* Co-op host, each tick: a player stranded for COOP_STRANDED_TICKS is moved
beside a grounded teammate inside the BSP, else any teammate inside it, else
to the last checkpoint. Skipped while scripts hold the controls, since
cutscenes place the players themselves. A BSP switch already moves everyone
(players_reconnect_to_structure_bsp); this catches anyone it missed. */
static void players_coop_rescue_stranded(
void)
{
struct data_iterator iterator;
struct player_datum *player;
if (!network_coop_active() || game_connection() != _game_connection_network_server)
return;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
long *since = &players_coop_state.stranded_since[DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)];
long teammate_index;
boolean rescued;
if (player->unit_index == NONE || !player_input_enabled() || !players_coop_unit_stranded(player->unit_index))
{
*since = 0;
continue;
}
if (*since == 0)
*since = game_time_get() + 1;
if (game_time_get() + 1 - *since < COOP_STRANDED_TICKS)
continue;
teammate_index = players_coop_unit_where(players_coop_unit_grounded);
if (teammate_index == NONE)
teammate_index = players_coop_unit_where(players_coop_unit_in_structure);
if (teammate_index != NONE)
{
rescued = player_teleport(iterator.datum_index, teammate_index,
&object_get(teammate_index)->object.bounding_sphere_center);
}
else
{
real_point3d const *position = players_checkpoint_position(iterator.datum_index);
rescued = position && player_teleport(iterator.datum_index, NONE, position);
}
if (rescued)
*since = 0;
}
}
/* co-op, each tick: records when each player became somewhere spawnable,
and where each last stood on the ground */
static void players_coop_note_on_foot(
void)
{
struct data_iterator iterator;
struct player_datum *player;
if (!network_coop_active())
return;
if (players_coop_state.ground_structure_bsp_index != global_structure_bsp_index_get())
{
players_coop_state.ground_structure_bsp_index = global_structure_bsp_index_get();
csmemset(players_coop_state.has_ground_position, 0, sizeof(players_coop_state.has_ground_position));
}
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
short index = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index);
long *since = &players_coop_state.spawnable_since[index];
if (player_spawnable_beside(player) == NONE)
*since = 0;
else if (*since == 0)
*since = game_time_get() + 1;
if (player->unit_index != NONE && object_get_ultimate_parent(player->unit_index) == player->unit_index &&
players_coop_unit_grounded(player->unit_index))
{
players_coop_state.ground_positions[index] = object_get(player->unit_index)->object.bounding_sphere_center;
players_coop_state.has_ground_position[index] = TRUE;
}
}
}
/* Co-op host: TRUE if a new player has somewhere to spawn, i.e. a teammate
who has been spawnable for COOP_DISEMBARK_TICKS. While every teammate rides
an AI vehicle (Silent Cartographer's Pelican) or is held by the scripts
(Pillar of Autumn's cryo tube), the others spectate. */
static boolean players_coop_room_to_spawn(
void)
{
struct data_iterator iterator;
struct player_datum *player;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
long since = players_coop_state.spawnable_since[DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)];
if (player_spawnable_beside(player) != NONE && since != 0 && game_time_get() + 1 - since >= COOP_DISEMBARK_TICKS)
return TRUE;
}
return FALSE;
}
/* Co-op host: moves a newly spawned player beside a teammate who is on foot
or in a player-driven vehicle (player_spawnable_beside). player_teleport tries
a few spots around each. Teammates are tried in turn so a big lobby spreads
out. FALSE if nobody has room; the player stays where they spawned. */
static boolean player_place_beside_teammate(
long player_index)
{
struct data_iterator iterator;
struct player_datum *other;
if (player_get(player_index)->unit_index == NONE)
return FALSE;
data_iterator_new(&iterator, player_data);
while ((other = data_iterator_next(&iterator)) != NULL)
{
if (iterator.datum_index != player_index && player_spawnable_beside(other) != NONE &&
player_teleport(player_index, other->unit_index, &object_get(other->unit_index)->object.bounding_sphere_center))
{
return TRUE;
}
}
return FALSE;
}
void players_note_checkpoint(
void)
{
struct data_iterator iterator;
struct player_datum *player;
csmemset(&players_checkpoint, 0, sizeof(players_checkpoint));
players_checkpoint.structure_bsp_index = global_structure_bsp_index_get();
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
short index = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index);
if (player->unit_index != NONE)
{
players_checkpoint.positions[index] = object_get(player->unit_index)->object.bounding_sphere_center;
players_checkpoint.has_position[index] = TRUE;
players_checkpoint.valid = TRUE;
}
}
return;
}
/* Everyone back after all died: each where they were at the last checkpoint
(players_checkpoint_position), then those with no such spot beside the
others. Players who come back are spawned first, at the level's start. */
void players_respawn_at_checkpoint(
void)
{
struct data_iterator iterator;
struct player_datum *player;
boolean unplaced[NETWORK_GAME_MAXIMUM_PLAYER_COUNT] = { FALSE };
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
real_point3d const *position;
if (player->unit_index != NONE || player->quit_out_of_game)
continue;
player_spawn(iterator.datum_index);
position = players_checkpoint_position(iterator.datum_index);
unplaced[DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)] = player->unit_index != NONE &&
!(position && player_teleport(iterator.datum_index, NONE, position));
}
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
if (unplaced[DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.datum_index)])
player_place_beside_teammate(iterator.datum_index);
}
return;
}
/* port: whether the unit stands in a trigger that switches away from the
loaded structure BSP */
static boolean players_coop_in_bsp_switch_trigger(
long unit_index)
{
struct scenario *scenario = global_scenario_get();
short index;
for (index = 0; index < scenario->bsp_switch_trigger_volumes.count; index++)
{
struct scenario_bsp_switch_trigger_volume *volume = TAG_BLOCK_GET_ELEMENT(
&scenario->bsp_switch_trigger_volumes, index, struct scenario_bsp_switch_trigger_volume);
if (volume->source_structure_bsp_index == global_structure_bsp_index &&
scenario_trigger_volume_test_object(volume->trigger_volume_index, unit_index))
{
return TRUE;
}
}
return FALSE;
}
/* port: whether a network co-op host lets any trigger switch the BSP yet.
After a switch it waits COOP_BSP_SWITCH_SETTLE_TICKS while the players are
brought in, then until every player's machine has loaded the new BSP, so no
client falls two switches behind. A client stuck longer than
COOP_BSP_SWITCH_WAIT_TICKS no longer holds the team. */
static boolean players_coop_bsp_switch_ready(
void)
{
long since = game_time_get() - players_coop_state.bsp_switch_time;
struct data_iterator iterator;
struct player_datum *player;
if (since < COOP_BSP_SWITCH_SETTLE_TICKS)
return FALSE;
if (since >= COOP_BSP_SWITCH_WAIT_TICKS)
return TRUE;
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
if (player->unit_index != NONE && !network_coop_player_has_structure_bsp(iterator.datum_index))
return FALSE;
}
return TRUE;
}
/* port: whether a network co-op host lets the unit on this trigger switch
the BSP, which brings the whole team along. Not while the unit's machine
is still loading the BSP the host has. Anyone can lead the team into a BSP
it hasn't been in; going back to one needs two thirds of the team there,
so one player can't drag everyone back through the level. */
static boolean players_coop_bsp_switch_allowed(
short bsp_switch_trigger_volume_index,
long unit_index)
{
struct scenario_bsp_switch_trigger_volume *volume = TAG_BLOCK_GET_ELEMENT(
&global_scenario_get()->bsp_switch_trigger_volumes, bsp_switch_trigger_volume_index,
struct scenario_bsp_switch_trigger_volume);
real_point3d const *center = &object_get(unit_index)->object.bounding_sphere_center;
struct data_iterator iterator;
struct player_datum *player;
short living_count = 0;
short gathered_count = 0;
if (!network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index))
return FALSE;
if (!VALID_INDEX(volume->destination_structure_bsp_index, MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO) ||
!TEST_FLAG(players_coop_state.visited_structure_bsps, volume->destination_structure_bsp_index))
{
return TRUE;
}
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
struct object_datum *teammate = object_try_and_get(player->unit_index);
if (!teammate || TEST_FLAG(teammate->object.damage_flags, _object_dead_bit))
continue;
living_count++;
if (scenario_trigger_volume_test_object(volume->trigger_volume_index, player->unit_index) ||
distance_squared3d(center, &teammate->object.bounding_sphere_center) <=
COOP_BACKTRACK_GATHER_DISTANCE * COOP_BACKTRACK_GATHER_DISTANCE)
{
gathered_count++;
}
}
return gathered_count * 3 >= living_count * 2;
}
static void player_teleport_on_bsp_switch(
long player_index,
long source_unit_index,
@@ -3084,16 +2422,7 @@ static void player_teleport_on_bsp_switch(
position);
if (biped)
{
/* port: in network co-op a loading zone brings the whole team to
whoever crossed it, as split screen does. Only one BSP is ever
loaded, so anyone left elsewhere would stand outside it, or on a
trigger that switches it straight back (players_update_after_game
waits for players_coop_bsp_switch_ready before another switch). */
if (network_coop_active())
{
outside_switch_trigger = TRUE;
}
else if (players_globals->pending_teleport_starting_location_index != NONE &&
if (players_globals->pending_teleport_starting_location_index != NONE &&
!scenario_trigger_volume_test_object(
TAG_BLOCK_GET_ELEMENT(
&global_scenario_get()->bsp_switch_trigger_volumes,
@@ -3128,10 +2457,6 @@ static void player_teleport_on_bsp_switch(
player_index,
source_unit_index,
position);
/* port: a big co-op team runs out of room around the
crosser, so the rest go beside whoever is already in */
if (!teleport_succeeded && network_coop_active())
teleport_succeeded = player_place_beside_teammate(player_index);
}
else
{
@@ -3156,13 +2481,13 @@ void players_reconnect_to_structure_bsp(
long source_unit_index;
boolean teleport_position_valid;
boolean found_player;
short local_player_index;
short cutscene_flag_index;
long player_index;
long player_unit_index;
/* port: also on a network co-op host, whose other players are remote */
if (players_globals->pending_teleport_starting_location_index != NONE &&
(players_globals->local_player_count > 1 ||
(network_coop_active() && game_connection() == _game_connection_network_server)))
players_globals->local_player_count > 1)
{
scenario = global_scenario_get();
bsp_switch = TAG_BLOCK_GET_ELEMENT(
@@ -3242,46 +2567,28 @@ void players_reconnect_to_structure_bsp(
}
}
/* port: in network co-op, if whoever crossed isn't in the new BSP yet,
the others gather to a player who is and stands on no switch trigger,
so nobody is left to switch it straight back */
if (!found_player && network_coop_active())
{
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL && !found_player)
{
struct object_datum *unit = object_try_and_get(player->unit_index);
if (unit && scenario_leaf_index_from_point(&unit->object.bounding_sphere_center) != NONE &&
!players_coop_in_bsp_switch_trigger(player->unit_index))
{
source_unit_index = player->unit_index;
teleport_position = unit->object.position;
found_player = TRUE;
}
}
}
match_vassert(
"c:\\halo\\SOURCE\\game\\players.c",
0x63E,
found_player,
"no players in the bsp");
/* port: all players, not just local ones (the same set in split screen) */
if (found_player)
{
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
local_player_index = local_player_get_next(NONE);
while (local_player_index != NONE)
{
player_index = local_player_get_player_index(local_player_index);
player = player_get(player_index);
if (player->unit_index != NONE &&
player->unit_index != source_unit_index)
{
player_teleport_on_bsp_switch(
iterator.datum_index,
player_index,
source_unit_index,
&teleport_position);
player->cluster_index = NONE;
player_get(player_index)->cluster_index = NONE;
}
local_player_index = local_player_get_next(local_player_index);
}
}
players_globals->pending_teleport_starting_location_index = NONE;
@@ -4325,8 +3632,6 @@ void players_update_before_game(
short action_index;
profile_enter(PLAYERS_UPDATE_BEFORE_GAME_PROFILE);
players_coop_note_on_foot();
players_coop_rescue_stranded();
if (update_client_dequeue(actions))
{
data_iterator_new(&iterator, player_data);
@@ -4397,24 +3702,10 @@ void players_update_before_game(
else if (network_game_distributed_client())
game_engine_client_respawn_countdown(iterator.datum_index);
}
/* port: in co-op only the host spawns players; clients get their units
from the network. One who quit doesn't come back. */
else if (!main_menu_is_active() && !network_game_distributed_client() && !player->quit_out_of_game)
else if (!main_menu_is_active())
{
if (player->statistics.deaths == 0)
{
/* port: in network co-op the first player spawns at the map's
start; everyone else, late joiners included, spawns beside a
player already in the game once nothing holds them back
(players_coop_may_spawn) */
if (game_connection() != _game_connection_network_server)
player_spawn(iterator.datum_index);
else if (players_coop_may_spawn())
{
player_spawn(iterator.datum_index);
player_place_beside_teammate(iterator.datum_index);
}
}
player_spawn(iterator.datum_index);
else if (!players_globals->all_dead)
main_respawn(players_globals->respawn_failed);
}
@@ -4626,13 +3917,6 @@ void players_update_after_game(
if (players_globals->double_speed_ticks == 0)
game_set_players_are_double_speed(FALSE);
}
if (VALID_INDEX(global_structure_bsp_index, MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO))
SET_FLAG(players_coop_state.visited_structure_bsps, global_structure_bsp_index, TRUE);
if (players_coop_state.structure_bsp_index != global_structure_bsp_index)
{
players_coop_state.structure_bsp_index = global_structure_bsp_index;
players_coop_state.bsp_switch_time = game_time_get();
}
data_iterator_new(&iterator, player_data);
while (player = data_iterator_next(&iterator))
@@ -4679,11 +3963,7 @@ void players_update_after_game(
{
root_object_index = object_get_ultimate_parent(player->unit_index);
root_object = object_get(root_object_index);
/* port: a co-op client only switches BSP when the host does
(network_distributed.c), and the host not again until everyone
has the new one */
if (!TEST_FLAG(root_object->object.flags, _object_outside_of_map_bit) &&
!(network_coop_active() && (network_game_distributed_client() || !players_coop_bsp_switch_ready())))
if (!TEST_FLAG(root_object->object.flags, _object_outside_of_map_bit))
{
scenario = global_scenario_get();
for (bsp_switch_trigger_volume_index = 0;
@@ -4698,9 +3978,7 @@ void players_update_after_game(
global_structure_bsp_index &&
scenario_trigger_volume_test_object(
bsp_switch_trigger_volume->trigger_volume_index,
player->unit_index) &&
(!network_coop_active() ||
players_coop_bsp_switch_allowed(bsp_switch_trigger_volume_index, player->unit_index)))
player->unit_index))
{
if (players_globals->local_player_triggered_switch !=
_local_player_triggered_switch_none &&
@@ -4719,7 +3997,6 @@ void players_update_after_game(
bsp_switch_trigger_volume_index;
main_switch_structure_bsp(
bsp_switch_trigger_volume->destination_structure_bsp_index);
players_coop_state.bsp_switch_time = game_time_get();
}
}
}
@@ -4743,7 +4020,7 @@ void players_update_after_game(
if (players_globals->all_dead)
{
if (!game_engine_running() && !players_lost_map_started && !network_game_distributed_client())
if (!game_engine_running() && !players_lost_map_started)
{
main_lost_map();
players_lost_map_started = TRUE;
@@ -4844,17 +4121,6 @@ boolean player_name_valid(
return FALSE;
for (index = 0; index < count && (cleaned[index] || name[index]); index++)
{
/* (but the spaces it trims from the end: the Xbox's menus pad a new
profile's name, "New001 ") */
if (!cleaned[index])
{
for (; index < count && name[index]; index++)
{
if (name[index] != ' ')
return FALSE;
}
break;
}
if (cleaned[index] != name[index])
return FALSE;
}
-16
View File
@@ -295,11 +295,6 @@ void player_control_new_unit(
long unit_index);
void player_control_action_test_reset(
void);
void player_control_action_test_note(
unsigned long unit_control_flags,
real_euler_angles2d const *facing_delta,
real_vector2d const *throttle,
real primary_trigger);
boolean player_control_action_test_accept(
void);
boolean player_control_action_test_back(
@@ -378,17 +373,6 @@ short players_get_respawn_failure(
boolean players_respawn_coop(
void);
/* port: network co-op checkpoints (players.c): remember where the players
were at the last checkpoint, and respawn everyone there. Players other than
the first are held back from spawning on a new level: until its opening
cutscene ends and a teammate has been free on foot a few seconds (not
riding a vehicle, nor held by the scripts). */
boolean players_coop_waiting_to_start(
long player_index);
void players_note_checkpoint(
void);
void players_respawn_at_checkpoint(
void);
void players_reconnect_to_structure_bsp(
void);
+9 -32
View File
@@ -14088,29 +14088,6 @@ static boolean hs_expression_changes_no_game(
static boolean hs_compile_and_evaluate_command(
char const *expression);
/* port: the text after the host's command `word` ("ban", "kick") that an
expression starts with (spaces and an opening parenthesis before it, in
either case, then a space or the end), else NULL */
static char const *hs_host_player_command(
char const *expression,
char const *word)
{
char const *text = expression;
long index;
while (*text == ' ' || *text == '\t' || *text == '(')
text++;
for (index = 0; word[index]; index++)
{
char character = text[index] >= 'A' && text[index] <= 'Z' ? text[index] - 'A' + 'a' : text[index];
if (character != word[index])
return NULL;
}
text += index;
return *text == ' ' || *text == '\t' || *text == 0 ? text : NULL;
}
/* port: a command someone typed (the console, the telnet console, a cheat
button, init.txt): what it logs is its answer, shown whatever
config.toml's game.console_log is (terminal_command_running) */
@@ -14145,20 +14122,20 @@ static boolean hs_compile_and_evaluate_command(
console_warning("not while playing in another's game: the host decides the game");
return FALSE;
}
/* port: the host's ban and kick commands ("ban <player name>", "kick
<player name>", or the name's start: Tab completes it), which are no
script's */
/* port: the host's ban command ("ban <player name>", or its start: Tab
completes it), which is no script's */
{
char const *text = hs_host_player_command(expression, "ban");
boolean kick = FALSE;
char const *text = expression;
if (!text && (text = hs_host_player_command(expression, "kick")) != NULL)
kick = TRUE;
if (text)
while (*text == ' ' || *text == '\t' || *text == '(')
text++;
if ((text[0] == 'b' || text[0] == 'B') && (text[1] == 'a' || text[1] == 'A') &&
(text[2] == 'n' || text[2] == 'N') && (text[3] == ' ' || text[3] == '\t' || text[3] == 0))
{
char name[64];
long length = 0;
text += 3;
while (*text == ' ' || *text == '\t' || *text == '"')
text++;
while (*text && *text != '"' && *text != ')' && length < (long)sizeof(name) - 1)
@@ -14166,7 +14143,7 @@ static boolean hs_compile_and_evaluate_command(
while (length > 0 && (name[length - 1] == ' ' || name[length - 1] == '\t'))
length--;
name[length] = 0;
return kick ? network_game_server_kick_player(name) : network_game_server_ban_player(name);
return network_game_server_ban_player(name);
}
}
csstrncpy(buffer, expression, sizeof(buffer));
-3
View File
@@ -179,9 +179,6 @@ void hs_runtime_initialize(
void);
void hs_runtime_initialize_for_new_map(
void);
/* port: network co-op (port/linux/game/network_coop.c) */
void hs_runtime_port_shift_sleep_times(
long ticks);
void hs_runtime_update(
void);
long hs_runtime_evaluate(
-15
View File
@@ -104,7 +104,6 @@ symbols in this file:
#include "sound/sound_definitions.h"
#include "units/units.h"
#include "object_lists.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* ---------- constants */
@@ -675,8 +674,6 @@ void hs_effect_new(
struct scenario_cutscene_flag *cutscene_flag;
real_vector3d forward;
/* port: and on network co-op's clients (port/linux/game/network_coop.c) */
network_coop_note_effect(effect_definition_index, cutscene_flag_index);
cutscene_flag = TAG_BLOCK_GET_ELEMENT(
&global_scenario_get()->cutscene_flags,
cutscene_flag_index,
@@ -710,9 +707,6 @@ void hs_effect_new_from_object_marker(
{
struct object_marker marker;
/* port: and on network co-op's clients (port/linux/game/network_coop.c) */
network_coop_note_object_effect(effect_definition_index, object_index, marker_name);
if (object_get_marker_by_name(
object_index,
marker_name,
@@ -1004,9 +998,6 @@ void hs_teleport_players_not_in_trigger_volume(
short cutscene_flag_index)
{
long player_index;
/* port: in network co-op the first player moved goes to the flag and
the rest around them, instead of all into the same spot */
long first_unit_index = NONE;
for (player_index = data_next_index(player_data, NONE);
player_index != NONE;
@@ -1020,17 +1011,11 @@ void hs_teleport_players_not_in_trigger_volume(
trigger_volume_index,
player->unit_index))
{
if (first_unit_index != NONE && network_coop_active())
{
player_teleport(player_index, first_unit_index, &object_get(first_unit_index)->object.position);
continue;
}
hs_object_orient(
player->unit_index,
cutscene_flag_index,
TRUE,
TRUE);
first_unit_index = player->unit_index;
}
}
-18
View File
@@ -1120,24 +1120,6 @@ void render_debug_trigger_volumes(
return;
}
/* port: moves every sleeping script thread's wake time on by `ticks`, after
a network co-op host reverts but keeps its clock (network_coop.c) */
void hs_runtime_port_shift_sleep_times(
long ticks)
{
struct data_iterator iterator;
struct hs_thread_datum *thread;
data_iterator_new(&iterator, hs_thread_data);
while ((thread = data_iterator_next(&iterator)) != NULL)
{
if (thread->sleep_until > 0)
thread->sleep_until += ticks;
if (thread->previous_sleep_until > 0)
thread->previous_sleep_until += ticks;
}
}
void hs_runtime_update(
void)
{
+4 -14
View File
@@ -107,8 +107,6 @@ symbols in this file:
#include "sound/game_sound.h"
#include "tag_files/tag_files.h"
#include "text/draw_string.h"
#include "coop_spectate.h" /* port: port/linux/game/coop_spectate.c */
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
#include "text/font_group.h"
#include "text/text_group.h"
#include "units/unit_definitions.h"
@@ -873,7 +871,6 @@ void hud_autosave(
: hud_globals->checkpoint_end_index;
short local_player_index;
network_coop_note_hud(_coop_hud_checkpoint, active);
scripted_hud_messages_clear();
if (active && hud_globals->checkpoint_sound.index != NONE)
{
@@ -1002,7 +999,7 @@ static void hud_draw_players(
return;
}
/* port: in multiplayer and network co-op, players' names above their heads
/* port: in multiplayer, players' names above their heads
(display.player_names: "all", "allies", "enemies" or "none"). An ally's goes
above the triangle the game draws over teammates; an enemy's only within the
motion sensor's reach, while the view sees them and they are not
@@ -1275,8 +1272,7 @@ static void hud_draw_player_names_now(
if ((setting == _player_names_none && !vr_apart) || player_index == NONE)
return;
team_index = player_get(player_index)->team_index;
/* the campaign always draws teammate triangles (hud_draw_players) */
indicators = game_engine_display_team_indicators() || !game_engine_running();
indicators = game_engine_display_team_indicators();
enemy_range = hud_player_name_enemy_range();
/* (the players the motion tracker would show this local player) */
game_engine_motion_sensor_viewer(render.local_player_index);
@@ -1447,15 +1443,9 @@ void hud_draw_screen(
hud_draw_players();
}
/* port: players' names above their heads, in multiplayer and network co-op */
if ((game_engine_running() || network_coop_active()) && !cinematic_in_progress())
/* port: players' names above their heads, in multiplayer */
if (game_engine_running() && !cinematic_in_progress())
hud_draw_player_names();
/* port: who a dead network co-op player is watching */
if (player->unit_index == NONE && coop_spectating() && !cinematic_in_progress())
coop_spectate_draw(render.local_player_index);
/* port: the network co-op vote to skip a cinematic */
if (cinematic_in_progress())
coop_skip_vote_draw(render.local_player_index);
if (!game_time_get_paused() &&
render.local_player_index == local_player_get_next(NONE))
-46
View File
@@ -145,7 +145,6 @@ symbols in this file:
#include "text/font_group.h"
#include "text/text_group.h"
#include "text/unicode.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
#include <stddef.h>
@@ -607,7 +606,6 @@ void scripted_hud_set_state_message(
{
struct scenario *scenario = global_scenario_get();
network_coop_note_hud(_coop_hud_help_text, message_index);
if (hud_scripted_globals->show_hud_help_text &&
scenario->hud_messages.index != NONE)
{
@@ -628,7 +626,6 @@ void scripted_hud_set_flashing_state(
{
long time;
network_coop_note_hud(_coop_hud_help_flash, flash);
if (flash && !hud_messaging_globals->use_flash)
{
time = game_time_get();
@@ -659,7 +656,6 @@ void scripted_hud_set_objective(
{
struct scenario *scenario = global_scenario_get();
network_coop_note_hud(_coop_hud_objective, message_index);
if (scenario->hud_messages.index != NONE)
{
struct hud_message_text_definition *hud_messages =
@@ -741,47 +737,6 @@ void scripted_hud_show_timer(
return;
}
/* port: the script timer and the scenario's message count, for network
co-op (port/linux/game/network_coop.c) */
void hud_messaging_port_timer_get(
struct hud_timer_state *state)
{
struct hud_timer_data_definition const *timer = &hud_messaging_globals->timer;
state->reference_time = timer->reference_time;
state->ticks = timer->ticks;
state->flash_cutoff = timer->flash_cutoff;
state->x = timer->position.n[0];
state->y = timer->position.n[1];
state->corner = timer->corner;
state->paused = timer->paused;
state->enabled = timer->enabled;
}
void hud_messaging_port_timer_set(
struct hud_timer_state const *state)
{
struct hud_timer_data_definition *timer = &hud_messaging_globals->timer;
timer->reference_time = state->reference_time;
timer->ticks = state->ticks;
timer->flash_cutoff = state->flash_cutoff;
timer->position.n[0] = state->x;
timer->position.n[1] = state->y;
timer->corner = PIN(state->corner, 0, 4);
timer->paused = state->paused;
timer->enabled = state->enabled;
}
short hud_messaging_port_message_count(
void)
{
struct scenario *scenario = global_scenario_get();
return scenario->hud_messages.index != NONE ?
(short)HUD_MESSAGE_TEXT_DEFINITION_GET(scenario->hud_messages.index)->messages.count : 0;
}
void scripted_hud_pause_timer(
boolean paused)
{
@@ -1174,7 +1129,6 @@ void scripted_hud_messages_clear(
struct hud_messaging_datum_definition *datum = hud_messaging_globals->message_data;
long datum_count = NUMBER_OF_HUD_MESSAGING_DATUMS;
network_coop_note_hud(_coop_hud_messages_clear, 0);
do
{
struct hud_message_definition *message = datum->messages;
-18
View File
@@ -27,24 +27,6 @@ void hud_messaging_dispose_from_old_map(
void);
void hud_messaging_dispose(
void);
/* port: network co-op (port/linux/game/network_coop.c) */
struct hud_timer_state
{
long reference_time;
short ticks;
short flash_cutoff;
short x, y;
short corner;
boolean paused;
boolean enabled;
};
void hud_messaging_port_timer_get(
struct hud_timer_state *state);
void hud_messaging_port_timer_set(
struct hud_timer_state const *state);
short hud_messaging_port_message_count(
void);
void scripted_hud_set_state_message(
short message_index);
void hud_messaging_globals_update(
-9
View File
@@ -123,7 +123,6 @@ symbols in this file:
#include "scenario/scenario.h"
#include "scenario/scenario_definitions.h"
#include "units/units.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
/* ---------- constants */
@@ -497,7 +496,6 @@ void hud_activate_team_nav_point_with_flag(
short flag_index,
float vertical_offset)
{
network_coop_note_nav_point(_coop_nav_point_team_flag, nav_index, team_index, flag_index, vertical_offset);
hud_activate_team_nav_point(nav_index, team_index, _hud_nav_point_type_flag, flag_index, vertical_offset);
return;
@@ -509,7 +507,6 @@ void hud_activate_team_nav_point_with_object(
long object_index,
float vertical_offset)
{
network_coop_note_nav_point(_coop_nav_point_team_object, nav_index, team_index, object_index, vertical_offset);
hud_activate_team_nav_point(nav_index, team_index, _hud_nav_point_type_object, object_index, vertical_offset);
return;
@@ -556,7 +553,6 @@ void hud_deactivate_team_nav_point_with_flag(
short team_index,
short flag_index)
{
network_coop_note_nav_point(_coop_nav_point_team_flag, NONE, team_index, flag_index, 0.0f);
hud_deactivate_team_nav_point(team_index, _hud_nav_point_type_flag, flag_index);
return;
@@ -566,7 +562,6 @@ void hud_deactivate_team_nav_point_with_object(
short team_index,
long object_index)
{
network_coop_note_nav_point(_coop_nav_point_team_object, NONE, team_index, object_index, 0.0f);
hud_deactivate_team_nav_point(team_index, _hud_nav_point_type_object, object_index);
return;
@@ -580,7 +575,6 @@ void hud_unit_activate_nav_point_with_flag(
{
long player_index = player_index_from_unit_index(unit_index);
network_coop_note_nav_point(_coop_nav_point_unit_flag, nav_index, unit_index, flag_index, vertical_offset);
if (player_index!=NONE)
{
hud_activate_nav_point(nav_index, player_index, _hud_nav_point_type_flag, flag_index, vertical_offset);
@@ -597,7 +591,6 @@ void hud_unit_activate_nav_point_with_object(
{
long player_index = player_index_from_unit_index(unit_index);
network_coop_note_nav_point(_coop_nav_point_unit_object, nav_index, unit_index, object_index, vertical_offset);
if (player_index!=NONE)
{
hud_activate_nav_point(nav_index, player_index, _hud_nav_point_type_object, object_index, vertical_offset);
@@ -612,7 +605,6 @@ void hud_unit_deactivate_nav_point_with_flag(
{
long player_index = player_index_from_unit_index(unit_index);
network_coop_note_nav_point(_coop_nav_point_unit_flag, NONE, unit_index, flag_index, 0.0f);
if (player_index!=NONE)
{
hud_deactivate_nav_point(player_index, _hud_nav_point_type_flag, flag_index);
@@ -627,7 +619,6 @@ void hud_unit_deactivate_nav_point_with_object(
{
long player_index = player_index_from_unit_index(unit_index);
network_coop_note_nav_point(_coop_nav_point_unit_object, NONE, unit_index, object_index, 0.0f);
if (player_index!=NONE)
{
hud_deactivate_nav_point(player_index, _hud_nav_point_type_object, object_index);
-14
View File
@@ -381,20 +381,6 @@ void scripted_hud_blink_motion_sensor(
return;
}
/* port: the scripts' show and blink flags, for network co-op
(port/linux/game/network_coop.c) */
long hud_unit_port_script_flags(
void)
{
return unit_hud_globals->script_flags;
}
void hud_unit_port_set_script_flags(
long flags)
{
unit_hud_globals->script_flags = flags;
}
void hud_play_unit_sounds(
struct player_datum const *player,
boolean show_hud)
-5
View File
@@ -31,11 +31,6 @@ void hud_tick_shield(
long player_index,
real amount);
/* port: network co-op (port/linux/game/network_coop.c) */
long hud_unit_port_script_flags(
void);
void hud_unit_port_set_script_flags(
long flags);
void scripted_hud_show_health(
boolean show);
void scripted_hud_blink_health(
-14
View File
@@ -483,20 +483,6 @@ void scripted_hud_show_crosshair(
return;
}
/* port: the scripts' crosshair flag, for network co-op
(port/linux/game/network_coop.c) */
long hud_weapon_port_script_flags(
void)
{
return weapon_hud_globals->script_flags;
}
void hud_weapon_port_set_script_flags(
long flags)
{
weapon_hud_globals->script_flags = flags;
}
static struct weapon_hud_state *get_hud_state(
short local_player_index)
{
-5
View File
@@ -14,11 +14,6 @@ void hud_dispose_weapon_interface_from_old_map(
void hud_dispose_weapon_interface(
void);
/* port: network co-op (port/linux/game/network_coop.c) */
long hud_weapon_port_script_flags(
void);
void hud_weapon_port_set_script_flags(
long flags);
void scripted_hud_show_crosshair(
boolean show);
+5 -8
View File
@@ -1227,9 +1227,6 @@ static void interface_splitscreen_render(
{
rectangle2d bounds;
short window_count;
/* port: the screen's width, wider than the Xbox's 640 on a wide screen,
whose middle the windows split at (compute_window_bounds) */
short width = (short)halo_screen_width();
if (game_engine_force_single_screen() || cinematic_in_progress())
return;
@@ -1242,7 +1239,7 @@ static void interface_splitscreen_render(
bounds.y0 = 239;
bounds.x0 = 0;
bounds.y1 = 241;
bounds.x1 = width;
bounds.x1 = 640;
draw_quad(&bounds, 0xFF000000);
if (window_count <= 2)
@@ -1251,18 +1248,18 @@ static void interface_splitscreen_render(
if (window_count == 3)
{
bounds.y0 = 240;
bounds.x0 = width / 2 - 1;
bounds.x0 = 319;
bounds.y1 = 480;
bounds.x1 = width / 2 + 1;
bounds.x1 = 321;
draw_quad(&bounds, 0xFF000000);
return;
}
bounds.y0 = 0;
bounds.x0 = width / 2 - 1;
bounds.x0 = 319;
bounds.y1 = 480;
bounds.x1 = width / 2 + 1;
bounds.x1 = 321;
match_assert(
"c:\\halo\\SOURCE\\interface\\interface.c",
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff. Show more