Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0d454ae9b | ||
|
|
96a75df218 | ||
|
|
1a732f577f |
No files matched your search
@@ -242,7 +242,10 @@ jobs:
|
||||
mkdir release
|
||||
for build in artifacts/*/; do
|
||||
name="$(basename "$build")"
|
||||
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||||
# stored, not deflated: the updater in builds up to 57 waits for
|
||||
# an end of stream the game's zlib 1.1 does not always report,
|
||||
# and fails on a deflated entry that unpacked correctly
|
||||
(cd "$build" && zip -0 -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||||
done
|
||||
ls -l release
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ xbox/
|
||||
|
||||
# Locally extracted proprietary game data (never commit)
|
||||
assets/
|
||||
# (but the high-res assets, drawn for the port: tools/hud_assets.py)
|
||||
!port/assets/
|
||||
original/
|
||||
|
||||
# CI build outputs (tools/ci_build.py)
|
||||
|
||||
@@ -168,8 +168,7 @@ the Xbox:
|
||||
- The HUD stays at the edges of the screen.
|
||||
- The menus, the loading bar and the screens after a game have 640
|
||||
columns, at the center of the screen.
|
||||
- Black bars and fades cover all of the screen, and so do the menus' dims
|
||||
and backgrounds (the pause menu's dim, dialogs, the menus' gradient).
|
||||
- Black bars and fades cover all of the screen.
|
||||
|
||||
## VR
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
# Steam Frame
|
||||
|
||||
The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
|
||||
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
|
||||
What differs is found at run time, not by a build of its own.
|
||||
|
||||
## The device
|
||||
|
||||
- Steam Settings > System > Enable Developer Mode; then start "Lepton
|
||||
Development" from the library. While the headset sleeps Lepton stops, and
|
||||
adb with it.
|
||||
- adb over the network: `adb connect <frame-ip>:5555`; over USB:
|
||||
`adb forward tcp:5555 tcp:5555` then `adb connect localhost:5555`.
|
||||
Always name the device (`adb -s <frame-ip>:5555 ...`): a Quest may be
|
||||
connected as well.
|
||||
- Install with `adb install -r --no-streaming`. It prints "Lepton post
|
||||
installation failed due to APP_PATH being unset", but installs. Start the
|
||||
game with `am start -n com.halo.decomp/.LauncherActivity`.
|
||||
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
|
||||
data must live elsewhere (stage 2). `/sdcard/Documents` and
|
||||
`/sdcard/Download` are the SteamOS user's folders and are kept.
|
||||
|
||||
## What the port does for it
|
||||
|
||||
- SDL3's clipboard handler takes a clipboard service for granted, which
|
||||
Lepton has not: `tools/android_build.py` patches it to do without.
|
||||
- SteamVR lists an implicit OpenXR layer for Vulkan apps whose library an
|
||||
app cannot load: the APK carries a pass-through layer under its name
|
||||
(`port/android/xr_layer/fdm_injection_stub.c`).
|
||||
|
||||
## Stages
|
||||
|
||||
| # | Stage | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Base: the fixes above on the current main | done |
|
||||
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
|
||||
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
|
||||
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
|
||||
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
|
||||
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
|
||||
| 7 | Multiplayer, voice, cross-play with the Quest | |
|
||||
| 8 | Installing it, for others | |
|
||||
|
||||
## The game's data
|
||||
|
||||
Lepton uninstalls the app and empties its folders whenever it starts again.
|
||||
The game's data lives in the SteamOS user's Documents instead: when
|
||||
`/home/steamos/Documents/HaloCE` holds `maps/ui.map` and is writable, the
|
||||
launcher and the game take it as their data folder (config.toml, saves and
|
||||
debug.txt go there too). Once:
|
||||
|
||||
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
|
||||
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
|
||||
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
|
||||
|
||||
After a Lepton restart only the APK is installed again. (Use the path under
|
||||
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
|
||||
app and the latter.)
|
||||
|
||||
## The controllers
|
||||
|
||||
SteamVR offers the Frame's controllers as themselves
|
||||
(`XR_VALVE_frame_controller_interaction`,
|
||||
`/interaction_profiles/valve/frame_controller_valve`; Valve's "Steam Frame
|
||||
Input" page). Without bindings for it, SteamVR maps the Quest's onto them,
|
||||
and the left Touch controller's X and Y land on the D-pad. With them the
|
||||
game gets the Xbox's layout, as the right controller has it: A jump, B
|
||||
flashlight (held: switch grenades), X action and reload, Y switch weapon,
|
||||
menu pause; the left one's View is Back (scores), its D-pad the D-pad, the
|
||||
bumpers switch grenades (left) and the flashlight (right). The VR options'
|
||||
page of buttons shows this layout on the Frame.
|
||||
|
||||
## Findings
|
||||
|
||||
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
|
||||
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
|
||||
(menus, HUD, instructions) are not shown; the projection layer is. Froze
|
||||
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
|
||||
- 2026-10-04: SteamVR draws a quad's texels by their alpha alone; the
|
||||
game's menus and HUD have none in their additive parts, so nothing showed.
|
||||
On SteamVR (by the runtime's name) the overlays' alpha now comes from their
|
||||
colour (host_xr.c; `HALO_XR_QUAD_ALPHA=colour|keep` overrides). With it,
|
||||
menus, HUD and the campaign ran well (the user: "lief 1a"); no freeze.
|
||||
- 2026-10-04: no online games found: the public MQTT brokers had banned the
|
||||
home connection for a while (too many connections from tests that day);
|
||||
broker.emqx.io came back within minutes. Lepton's network is the Frame's
|
||||
own (same IP), DNS works.
|
||||
- 2026-10-04: the refresh rate is SteamOS's setting; the runtime offers
|
||||
only that one (the game asks for the highest at or below
|
||||
`vr.refresh_rate`, and logs the rates offered). At 72 Hz the game holds
|
||||
72 fps (about 11.6 ms a frame with multiview, eyes 7.6 ms; without it
|
||||
eyes about 9 ms). At 90 Hz it cannot (11.1 ms a frame), and SteamVR's
|
||||
making up of the missed frames showed as a doubled picture when turning
|
||||
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
|
||||
0.85) for 90.
|
||||
@@ -19,6 +19,9 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<!-- the voice chat in multiplayer (host_voice.c) -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<!-- installing a new version of the game (Updater) -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.halo.decomp;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -35,7 +38,7 @@ import java.nio.channels.FileChannel;
|
||||
*/
|
||||
public class LauncherActivity extends Activity {
|
||||
private static final int PICK_IMAGE = 1;
|
||||
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
|
||||
private static final int ASK_MICROPHONE = 2;
|
||||
|
||||
private File dataRoot;
|
||||
private TextView status;
|
||||
@@ -47,12 +50,6 @@ public class LauncherActivity extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
dataRoot = getExternalFilesDir(null);
|
||||
// the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
// starts again; the game's data lives in the SteamOS user's
|
||||
// Documents instead, when it is there (port/android/STEAM_FRAME.md;
|
||||
// the same choice as host_main.c's)
|
||||
if (new File(SHARED_DATA_ROOT, "maps/ui.map").isFile() && new File(SHARED_DATA_ROOT).canWrite())
|
||||
dataRoot = new File(SHARED_DATA_ROOT);
|
||||
// created by the app, so that files pushed into it with adb stay
|
||||
// readable (a directory adb creates there belongs to the shell user)
|
||||
if (dataRoot != null)
|
||||
@@ -60,12 +57,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 +140,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 +164,7 @@ public class LauncherActivity extends Activity {
|
||||
}
|
||||
|
||||
private void startGame() {
|
||||
passOnMicrophonePermission();
|
||||
startActivity(new Intent(this, HaloActivity.class));
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -68,9 +68,6 @@ int host_sdl_gl_swap_window(unsigned int window);
|
||||
int host_sdl_poll_event(void *event);
|
||||
int host_sdl_set_clipboard_text(const char *text);
|
||||
void host_sdl_get_clipboard_text(char *buffer, unsigned int size);
|
||||
/* the keyboard's keys by name (the controls' bindings, xinput_sdl.c) */
|
||||
void host_sdl_scancode_name(int scancode, char *buffer, unsigned int size);
|
||||
int host_sdl_scancode_from_name(const char *name);
|
||||
int host_sdl_show_toast(const char *message, int duration, int gravity, int x, int y);
|
||||
int host_sdl_show_simple_message_box(unsigned int flags, const char *title, const char *message);
|
||||
int host_sdl_get_gamepads(unsigned int *ids, int capacity);
|
||||
@@ -134,6 +131,21 @@ int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
|
||||
/* darkens the eye images from their edges, strength 0..1 (0: off) */
|
||||
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 */
|
||||
|
||||
@@ -39,19 +39,6 @@ const char *SDL_GetError(void)
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const char *SDL_GetScancodeName(SDL_Scancode scancode)
|
||||
{
|
||||
static __thread char buffer[64];
|
||||
|
||||
host_sdl_scancode_name((int)scancode, buffer, sizeof(buffer));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
SDL_Scancode SDL_GetScancodeFromName(const char *name)
|
||||
{
|
||||
return (SDL_Scancode)host_sdl_scancode_from_name(name);
|
||||
}
|
||||
|
||||
Uint64 SDL_GetTicks(void)
|
||||
{
|
||||
return (Uint64)host_sdl_ticks();
|
||||
|
||||
@@ -90,8 +90,6 @@ int host_errno(void)
|
||||
|
||||
static char data_root[512];
|
||||
static char save_root[512];
|
||||
/* where a device that empties the app's folders keeps the game's data */
|
||||
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
|
||||
|
||||
void host_android_path(int which, char *buffer, uint32_t size)
|
||||
{
|
||||
@@ -271,12 +269,6 @@ static void *game_main(void *unused)
|
||||
external = SDL_GetAndroidExternalStoragePath();
|
||||
if (!external)
|
||||
host_fatal("Android storage is unavailable: %s", SDL_GetError());
|
||||
/* the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
starts again; the game's data lives in the SteamOS user's Documents
|
||||
instead, when it is there (port/android/STEAM_FRAME.md; the same choice
|
||||
as LauncherActivity's) */
|
||||
if (directory_has_maps(SHARED_DATA_ROOT) && access(SHARED_DATA_ROOT, W_OK) == 0)
|
||||
external = SHARED_DATA_ROOT;
|
||||
snprintf(data_root, sizeof(data_root), "%s", external);
|
||||
snprintf(save_root, sizeof(save_root), "%s/save", external);
|
||||
/* readable by adb (the shell user), for managing saves */
|
||||
|
||||
@@ -98,16 +98,6 @@ void host_sdl_get_error(char *buffer, uint32_t size)
|
||||
SDL_strlcpy(buffer, SDL_GetError(), size);
|
||||
}
|
||||
|
||||
void host_sdl_scancode_name(int32_t scancode, char *buffer, uint32_t size)
|
||||
{
|
||||
SDL_strlcpy(buffer, SDL_GetScancodeName((SDL_Scancode)scancode), size);
|
||||
}
|
||||
|
||||
int32_t host_sdl_scancode_from_name(const char *name)
|
||||
{
|
||||
return (int32_t)SDL_GetScancodeFromName(name);
|
||||
}
|
||||
|
||||
int64_t host_sdl_ticks(void)
|
||||
{
|
||||
return (int64_t)SDL_GetTicks();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -102,14 +102,6 @@ static struct
|
||||
XrAction button_b; /* B on the right controller, Y on the left */
|
||||
XrAction menu;
|
||||
XrAction thumbrest;
|
||||
/* the Steam Frame's controllers' own (XR_VALVE_frame_controller_interaction) */
|
||||
int has_frame_controller;
|
||||
int frame_layout;
|
||||
XrAction button_x;
|
||||
XrAction button_y;
|
||||
XrAction view;
|
||||
XrAction bumper;
|
||||
XrAction dpad[4];
|
||||
XrAction haptic;
|
||||
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
|
||||
|
||||
@@ -130,11 +122,6 @@ static struct
|
||||
GLint rect_uniform;
|
||||
GLint cut_disc_uniform;
|
||||
GLint vignette_uniform;
|
||||
GLint alpha_from_colour_uniform;
|
||||
/* the runtime draws a quad's texels by their alpha alone, ignoring the
|
||||
colour of those with none (SteamVR on the Steam Frame): the game's
|
||||
overlays, whose additive parts have none, get alpha from their colour */
|
||||
int quad_alpha_from_colour;
|
||||
GLint tangents_uniform;
|
||||
float vignette;
|
||||
/* the same copy from a layer of a texture array (the eyes drawn at
|
||||
@@ -236,7 +223,6 @@ static const char copy_fragment_shader[] =
|
||||
"precision highp float;\n"
|
||||
"uniform sampler2D source;\n"
|
||||
"uniform float cut_disc;\n"
|
||||
"uniform float alpha_from_colour; /* 1: as opaque as it is bright (a quad's, SteamVR) */\n"
|
||||
"uniform float vignette; /* 0..1: the eye images darken from their edges */\n"
|
||||
"uniform vec4 tangents; /* the eye's edges: left, right, down, up */\n"
|
||||
"in vec2 coordinate;\n"
|
||||
@@ -250,8 +236,6 @@ static const char copy_fragment_shader[] =
|
||||
"{\n"
|
||||
" vec4 texel = texture(source, coordinate);\n"
|
||||
" color = vec4(to_linear(texel.rgb), texel.a);\n"
|
||||
" if (alpha_from_colour > 0.5)\n"
|
||||
" color.a = max(color.a, max(color.r, max(color.g, color.b)));\n"
|
||||
" if (cut_disc > 0.5)\n"
|
||||
" {\n"
|
||||
" float inside = 1.0 - smoothstep(0.95, 1.0, length(disc));\n"
|
||||
@@ -342,7 +326,6 @@ static int copy_initialize(void)
|
||||
xr.rect_uniform = glGetUniformLocation(xr.program, "rect");
|
||||
xr.cut_disc_uniform = glGetUniformLocation(xr.program, "cut_disc");
|
||||
xr.vignette_uniform = glGetUniformLocation(xr.program, "vignette");
|
||||
xr.alpha_from_colour_uniform = glGetUniformLocation(xr.program, "alpha_from_colour");
|
||||
xr.tangents_uniform = glGetUniformLocation(xr.program, "tangents");
|
||||
xr.copy_array_layer = -1;
|
||||
{
|
||||
@@ -597,14 +580,6 @@ static int actions_initialize(void)
|
||||
xr.button_b = action_create("button_upper", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.menu = action_create("menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.thumbrest = action_create("thumbrest", "Thumb rest", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.button_x = action_create("button_x", "X", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.button_y = action_create("button_y", "Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.view = action_create("view", "View", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.bumper = action_create("bumper", "Bumper", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[0] = action_create("dpad_up", "D-pad up", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[1] = action_create("dpad_down", "D-pad down", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[2] = action_create("dpad_left", "D-pad left", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[3] = action_create("dpad_right", "D-pad right", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.haptic = action_create("haptic", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT);
|
||||
|
||||
{
|
||||
@@ -669,44 +644,6 @@ static int actions_initialize(void)
|
||||
{ &xr.haptic, "/user/hand/right/output/haptic" },
|
||||
};
|
||||
|
||||
/* the Steam Frame's controllers, as themselves: the right one has
|
||||
A, B, X, Y and a menu button, the left one a D-pad and a View button
|
||||
(port/android/STEAM_FRAME.md) */
|
||||
static const struct binding frame_controller[] =
|
||||
{
|
||||
{ &xr.grip_pose, "/user/hand/left/input/grip/pose" },
|
||||
{ &xr.grip_pose, "/user/hand/right/input/grip/pose" },
|
||||
{ &xr.aim_pose, "/user/hand/left/input/aim/pose" },
|
||||
{ &xr.aim_pose, "/user/hand/right/input/aim/pose" },
|
||||
{ &xr.trigger, "/user/hand/left/input/trigger/value" },
|
||||
{ &xr.trigger, "/user/hand/right/input/trigger/value" },
|
||||
{ &xr.squeeze, "/user/hand/left/input/squeeze/value" },
|
||||
{ &xr.squeeze, "/user/hand/right/input/squeeze/value" },
|
||||
{ &xr.thumbstick, "/user/hand/left/input/thumbstick" },
|
||||
{ &xr.thumbstick, "/user/hand/right/input/thumbstick" },
|
||||
{ &xr.thumbstick_click, "/user/hand/left/input/thumbstick/click" },
|
||||
{ &xr.thumbstick_click, "/user/hand/right/input/thumbstick/click" },
|
||||
{ &xr.button_a, "/user/hand/right/input/a/click" },
|
||||
{ &xr.button_b, "/user/hand/right/input/b/click" },
|
||||
{ &xr.button_x, "/user/hand/right/input/x/click" },
|
||||
{ &xr.button_y, "/user/hand/right/input/y/click" },
|
||||
{ &xr.menu, "/user/hand/right/input/menu/click" },
|
||||
{ &xr.view, "/user/hand/left/input/view/click" },
|
||||
{ &xr.bumper, "/user/hand/left/input/bumper/click" },
|
||||
{ &xr.bumper, "/user/hand/right/input/bumper/click" },
|
||||
{ &xr.dpad[0], "/user/hand/left/input/dpad_up/click" },
|
||||
{ &xr.dpad[1], "/user/hand/left/input/dpad_down/click" },
|
||||
{ &xr.dpad[2], "/user/hand/left/input/dpad_left/click" },
|
||||
{ &xr.dpad[3], "/user/hand/left/input/dpad_right/click" },
|
||||
{ &xr.haptic, "/user/hand/left/output/haptic" },
|
||||
{ &xr.haptic, "/user/hand/right/output/haptic" },
|
||||
};
|
||||
|
||||
if (xr.has_frame_controller)
|
||||
{
|
||||
suggest("/interaction_profiles/valve/frame_controller_valve", frame_controller,
|
||||
sizeof(frame_controller) / sizeof(frame_controller[0]));
|
||||
}
|
||||
suggest("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
|
||||
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]));
|
||||
suggest("/interaction_profiles/khr/simple_controller", simple, sizeof(simple) / sizeof(simple[0]));
|
||||
@@ -776,30 +713,6 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
sync.activeActionSets = &active;
|
||||
if (!XR_SUCCEEDED(xrSyncActions(xr.session, &sync)))
|
||||
return;
|
||||
{
|
||||
/* (the controllers' profile the runtime chose, in the log as it
|
||||
changes) */
|
||||
static XrPath logged[HALO_XR_HAND_COUNT];
|
||||
|
||||
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
|
||||
{
|
||||
XrInteractionProfileState state = { XR_TYPE_INTERACTION_PROFILE_STATE };
|
||||
|
||||
if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(xr.session, xr.hands[hand].path, &state)) &&
|
||||
state.interactionProfile != logged[hand])
|
||||
{
|
||||
char name[XR_MAX_PATH_LENGTH] = "none";
|
||||
uint32_t length = 0;
|
||||
|
||||
logged[hand] = state.interactionProfile;
|
||||
if (state.interactionProfile != XR_NULL_PATH)
|
||||
xrPathToString(xr.instance, state.interactionProfile, sizeof(name), &length, name);
|
||||
if (hand == HALO_XR_HAND_RIGHT)
|
||||
xr.frame_layout = strstr(name, "frame_controller") != NULL;
|
||||
host_logf(HOST_LOG_INFO, "xr: the %s hand's controller: %s", hand ? "right" : "left", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
|
||||
{
|
||||
struct halo_xr_hand *out = &frame->hands[hand];
|
||||
@@ -844,31 +757,7 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
(action_boolean(xr.menu, hand) ? HALO_XR_BUTTON_MENU : 0) |
|
||||
(action_boolean(xr.thumbrest, hand) ? HALO_XR_BUTTON_THUMBREST : 0) |
|
||||
(out->trigger > 0.5f ? HALO_XR_BUTTON_TRIGGER : 0) |
|
||||
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0) |
|
||||
(action_boolean(xr.view, hand) ? HALO_XR_BUTTON_VIEW : 0) |
|
||||
(action_boolean(xr.bumper, hand) ? HALO_XR_BUTTON_BUMPER : 0) |
|
||||
(action_boolean(xr.dpad[0], hand) ? HALO_XR_BUTTON_DPAD_UP : 0) |
|
||||
(action_boolean(xr.dpad[1], hand) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
|
||||
(action_boolean(xr.dpad[2], hand) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
|
||||
(action_boolean(xr.dpad[3], hand) ? HALO_XR_BUTTON_DPAD_RIGHT : 0);
|
||||
}
|
||||
frame->controller_layout = xr.frame_layout ? HALO_XR_LAYOUT_FRAME : HALO_XR_LAYOUT_TOUCH;
|
||||
if (xr.frame_layout)
|
||||
{
|
||||
/* (the Frame's right X, Y and menu button: the game's X, Y and Start,
|
||||
which the Quest has on the left) */
|
||||
struct halo_xr_hand *left = &frame->hands[HALO_XR_HAND_LEFT];
|
||||
struct halo_xr_hand *right = &frame->hands[HALO_XR_HAND_RIGHT];
|
||||
|
||||
if (action_boolean(xr.button_x, HALO_XR_HAND_RIGHT))
|
||||
left->buttons |= HALO_XR_BUTTON_A;
|
||||
if (action_boolean(xr.button_y, HALO_XR_HAND_RIGHT))
|
||||
left->buttons |= HALO_XR_BUTTON_B;
|
||||
if (right->buttons & HALO_XR_BUTTON_MENU)
|
||||
{
|
||||
right->buttons &= ~HALO_XR_BUTTON_MENU;
|
||||
left->buttons |= HALO_XR_BUTTON_MENU;
|
||||
}
|
||||
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,13 +774,6 @@ static void refresh_rate_choose(float wanted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
char line[128] = "";
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
snprintf(line + strlen(line), sizeof(line) - strlen(line), " %.0f", rates[index]);
|
||||
host_logf(HOST_LOG_INFO, "xr: refresh rates offered:%s Hz", line);
|
||||
}
|
||||
/* the highest rate not above the one wanted (0: the highest) */
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
@@ -912,12 +794,6 @@ the eyes reads it. Shown only while it is. */
|
||||
|
||||
#define WRIST_IMAGE_SIZE 512
|
||||
#define CROSSHAIR_IMAGE_SIZE 256
|
||||
/* the names layer: HALO_XR_MAXIMUM_NAMES slots of 4:1, one over another */
|
||||
#define NAMES_IMAGE_WIDTH 512
|
||||
#define NAMES_IMAGE_HEIGHT (NAMES_IMAGE_WIDTH / 4 * HALO_XR_MAXIMUM_NAMES)
|
||||
/* a name's slot, as the game draws it: wider than its image's 4:1 (the
|
||||
game's 640x480 drawn square): the height a slot shows at, to its width */
|
||||
#define NAME_SLOT_ASPECT (30.0f / 160.0f)
|
||||
#define WRIST_BEHIND_PALM 0.05f
|
||||
#define WRIST_DOWN_HANDLE 0.02f
|
||||
#define WRIST_ABOVE_HAND 0.04f
|
||||
@@ -1013,12 +889,6 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
/* the loader and the runtime keep the activity */
|
||||
activity = (*environment)->NewGlobalRef(environment, activity);
|
||||
|
||||
/* (The Steam Frame lists an implicit API layer of its own for Vulkan
|
||||
apps, XR_APILAYER_VALVE_fdm_injection, whose library an app cannot load
|
||||
from its namespace, failing xrCreateInstance; the Android loader reads
|
||||
no variable to turn it off, so the app carries a pass-through layer
|
||||
under that library's name: port/android/xr_layer/fdm_injection_stub.c) */
|
||||
|
||||
if (!XR_SUCCEEDED(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
|
||||
(PFN_xrVoidFunction *)&initialize_loader)) || !initialize_loader)
|
||||
{
|
||||
@@ -1046,23 +916,6 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
extensions[index].next = NULL;
|
||||
}
|
||||
xrEnumerateInstanceExtensionProperties(NULL, extension_count, &extension_count, extensions);
|
||||
{
|
||||
/* (the runtime's extensions, in the log: what a headset offers) */
|
||||
char line[512] = "";
|
||||
|
||||
for (index = 0; index < extension_count; index++)
|
||||
{
|
||||
if (strlen(line) + strlen(extensions[index].extensionName) + 2 >= sizeof(line))
|
||||
{
|
||||
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
|
||||
line[0] = 0;
|
||||
}
|
||||
strcat(line, " ");
|
||||
strcat(line, extensions[index].extensionName);
|
||||
}
|
||||
if (line[0])
|
||||
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
|
||||
}
|
||||
if (!has_extension(extensions, extension_count, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME))
|
||||
{
|
||||
host_logf(HOST_LOG_ERROR, "xr: the runtime has no OpenGL ES support");
|
||||
@@ -1074,9 +927,6 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
xr.has_performance_settings = has_extension(extensions, extension_count, XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
|
||||
if (xr.has_performance_settings)
|
||||
enabled[enabled_count++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME;
|
||||
xr.has_frame_controller = has_extension(extensions, extension_count, "XR_VALVE_frame_controller_interaction");
|
||||
if (xr.has_frame_controller)
|
||||
enabled[enabled_count++] = "XR_VALVE_frame_controller_interaction";
|
||||
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;
|
||||
@@ -1104,22 +954,6 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
}
|
||||
xrGetSystemProperties(xr.instance, xr.system, &properties);
|
||||
strncpy(xr.system_name, properties.systemName, sizeof(xr.system_name) - 1);
|
||||
{
|
||||
/* (SteamVR: the quads' alpha from their colour; HALO_XR_QUAD_ALPHA
|
||||
"colour" or "keep" says otherwise, for trying) */
|
||||
XrInstanceProperties instance = { XR_TYPE_INSTANCE_PROPERTIES };
|
||||
const char *choice = getenv("HALO_XR_QUAD_ALPHA");
|
||||
|
||||
if (XR_SUCCEEDED(xrGetInstanceProperties(xr.instance, &instance)))
|
||||
{
|
||||
xr.quad_alpha_from_colour = strstr(instance.runtimeName, "SteamVR") != NULL;
|
||||
host_logf(HOST_LOG_INFO, "xr: runtime %s", instance.runtimeName);
|
||||
}
|
||||
if (choice)
|
||||
xr.quad_alpha_from_colour = !strcmp(choice, "colour");
|
||||
if (xr.quad_alpha_from_colour)
|
||||
host_logf(HOST_LOG_INFO, "xr: the overlays' alpha from their colour");
|
||||
}
|
||||
|
||||
views[0].type = views[1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
|
||||
views[0].next = views[1].next = NULL;
|
||||
@@ -1198,15 +1032,8 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
|
||||
{
|
||||
int size = layer == HALO_XR_LAYER_WRIST || layer == HALO_XR_LAYER_SCOPE ? WRIST_IMAGE_SIZE :
|
||||
layer == HALO_XR_LAYER_CROSSHAIR ? CROSSHAIR_IMAGE_SIZE : 0;
|
||||
int width = size ? size : xr.eye_width;
|
||||
int height = size ? size : xr.eye_height;
|
||||
|
||||
if (layer == HALO_XR_LAYER_NAMES)
|
||||
{
|
||||
width = NAMES_IMAGE_WIDTH;
|
||||
height = NAMES_IMAGE_HEIGHT;
|
||||
}
|
||||
if (!swapchain_create(&xr.swapchains[layer], width, height))
|
||||
if (!swapchain_create(&xr.swapchains[layer], size ? size : xr.eye_width, size ? size : xr.eye_height))
|
||||
return 0;
|
||||
}
|
||||
if (!copy_initialize() || !actions_initialize())
|
||||
@@ -1476,8 +1303,6 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
glUniform1i(xr.source_uniform, 0);
|
||||
glUniform4f(xr.rect_uniform, left, top, right, bottom);
|
||||
glUniform1f(xr.cut_disc_uniform, clear_alpha == HALO_XR_COPY_DISC ? 1.0f : 0.0f);
|
||||
glUniform1f(xr.alpha_from_colour_uniform, xr.quad_alpha_from_colour && clear_alpha == HALO_XR_COPY_ALPHA &&
|
||||
layer != HALO_XR_LAYER_EYE_LEFT && layer != HALO_XR_LAYER_EYE_RIGHT ? 1.0f : 0.0f);
|
||||
if ((layer == HALO_XR_LAYER_EYE_LEFT || layer == HALO_XR_LAYER_EYE_RIGHT) && xr.vignette > 0.001f)
|
||||
{
|
||||
const XrFovf *fov = &xr.views[layer - HALO_XR_LAYER_EYE_LEFT].fov;
|
||||
@@ -1529,8 +1354,7 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
XrCompositionLayerQuad wrist = { XR_TYPE_COMPOSITION_LAYER_QUAD };
|
||||
XrCompositionLayerQuad scope = { XR_TYPE_COMPOSITION_LAYER_QUAD };
|
||||
XrCompositionLayerQuad crosshair = { XR_TYPE_COMPOSITION_LAYER_QUAD };
|
||||
XrCompositionLayerQuad names[HALO_XR_MAXIMUM_NAMES];
|
||||
const XrCompositionLayerBaseHeader *layers[5 + HALO_XR_MAXIMUM_NAMES];
|
||||
const XrCompositionLayerBaseHeader *layers[5];
|
||||
uint32_t layer_count = 0;
|
||||
int eye;
|
||||
|
||||
@@ -1634,34 +1458,6 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
layers[layer_count++] = (const XrCompositionLayerBaseHeader *)&crosshair;
|
||||
}
|
||||
|
||||
if (xr.frame_state.shouldRender && submit->name_count > 0 && xr.swapchains[HALO_XR_LAYER_NAMES].filled)
|
||||
{
|
||||
struct xr_swapchain *swapchain = &xr.swapchains[HALO_XR_LAYER_NAMES];
|
||||
int slot_height = swapchain->height / HALO_XR_MAXIMUM_NAMES;
|
||||
int name;
|
||||
|
||||
for (name = 0; name < submit->name_count && name < HALO_XR_MAXIMUM_NAMES; name++)
|
||||
{
|
||||
XrCompositionLayerQuad *quad_name = &names[name];
|
||||
|
||||
memset(quad_name, 0, sizeof(*quad_name));
|
||||
quad_name->type = XR_TYPE_COMPOSITION_LAYER_QUAD;
|
||||
quad_name->layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
||||
quad_name->space = xr.local_space;
|
||||
quad_name->eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
quad_name->subImage.swapchain = swapchain->handle;
|
||||
/* (slot 0 at the image's top: GL's rows run bottom up) */
|
||||
quad_name->subImage.imageRect.offset.x = 0;
|
||||
quad_name->subImage.imageRect.offset.y = swapchain->height - (name + 1) * slot_height;
|
||||
quad_name->subImage.imageRect.extent.width = swapchain->width;
|
||||
quad_name->subImage.imageRect.extent.height = slot_height;
|
||||
quad_name->size.width = submit->name_widths[name];
|
||||
quad_name->size.height = submit->name_widths[name] * NAME_SLOT_ASPECT;
|
||||
pose_to_xr(&quad_name->pose, &submit->name_poses[name]);
|
||||
layers[layer_count++] = (const XrCompositionLayerBaseHeader *)quad_name;
|
||||
}
|
||||
}
|
||||
|
||||
end.displayTime = xr.frame_state.predictedDisplayTime;
|
||||
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
end.layerCount = layer_count;
|
||||
|
||||
@@ -38,8 +38,6 @@ host_sdl_gamepad_axis
|
||||
host_sdl_gamepad_button
|
||||
host_sdl_gamepad_type
|
||||
host_sdl_rumble_gamepad
|
||||
host_sdl_scancode_name
|
||||
host_sdl_scancode_from_name
|
||||
host_sdl_open_audio_stream
|
||||
host_sdl_put_audio_stream_data
|
||||
host_sdl_resume_audio_stream_device
|
||||
@@ -60,6 +58,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
|
||||
|
||||
|
||||
@@ -27,10 +27,7 @@ enum
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the left wrist */
|
||||
HALO_XR_LAYER_SCOPE = 4, /* the zoomed view, a disc over the weapon */
|
||||
HALO_XR_LAYER_CROSSHAIR = 5, /* the weapon's crosshair, where it aims */
|
||||
HALO_XR_LAYER_NAMES = 6, /* players' names, each a quad over a head */
|
||||
HALO_XR_LAYER_COUNT = 7,
|
||||
/* the names layer's slots, one a name, stacked from its top */
|
||||
HALO_XR_MAXIMUM_NAMES = 8,
|
||||
HALO_XR_LAYER_COUNT = 6,
|
||||
|
||||
/* host_xr_copy_layer's clear_alpha: the source's alpha kept, made
|
||||
opaque, or cut to a disc (premultiplied) */
|
||||
@@ -53,23 +50,6 @@ enum
|
||||
HALO_XR_BUTTON_TRIGGER = 1 << 4, /* trigger past its click point */
|
||||
HALO_XR_BUTTON_SQUEEZE = 1 << 5, /* grip past its click point */
|
||||
HALO_XR_BUTTON_THUMBREST = 1 << 6, /* thumb resting (touch) */
|
||||
/* the Steam Frame's controllers' (HALO_XR_LAYOUT_FRAME): the left one's
|
||||
View button and D-pad, either one's bumper (their right one's X, Y and
|
||||
menu button come as the left hand's A, B and MENU: the game's X, Y and
|
||||
Start, as on the Quest) */
|
||||
HALO_XR_BUTTON_VIEW = 1 << 7,
|
||||
HALO_XR_BUTTON_BUMPER = 1 << 8,
|
||||
HALO_XR_BUTTON_DPAD_UP = 1 << 9,
|
||||
HALO_XR_BUTTON_DPAD_DOWN = 1 << 10,
|
||||
HALO_XR_BUTTON_DPAD_LEFT = 1 << 11,
|
||||
HALO_XR_BUTTON_DPAD_RIGHT = 1 << 12,
|
||||
};
|
||||
|
||||
/* the controllers' layout, for what the game tells the player */
|
||||
enum
|
||||
{
|
||||
HALO_XR_LAYOUT_TOUCH = 0, /* the Quest's: X, Y and menu on the left */
|
||||
HALO_XR_LAYOUT_FRAME = 1, /* the Steam Frame's: A, B, X, Y and menu on the right, a D-pad on the left */
|
||||
};
|
||||
|
||||
/* halo_xr_hand.flags, halo_xr_view.flags, halo_xr_frame.head_flags */
|
||||
@@ -129,7 +109,6 @@ struct halo_xr_frame
|
||||
struct halo_xr_pose head;
|
||||
struct halo_xr_view views[HALO_XR_EYE_COUNT];
|
||||
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
|
||||
int controller_layout; /* HALO_XR_LAYOUT_* */
|
||||
};
|
||||
|
||||
/* halo_xr_submit.overlay_mode */
|
||||
@@ -168,12 +147,6 @@ struct halo_xr_submit
|
||||
int crosshair;
|
||||
float crosshair_size;
|
||||
struct halo_xr_pose crosshair_pose;
|
||||
/* the names layer was filled: its first name_count slots, each shown
|
||||
this wide (metres, a slot's height following from its shape), here in
|
||||
LOCAL space, facing the eyes */
|
||||
int name_count;
|
||||
float name_widths[HALO_XR_MAXIMUM_NAMES];
|
||||
struct halo_xr_pose name_poses[HALO_XR_MAXIMUM_NAMES];
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
FDM_INJECTION_STUB.C
|
||||
|
||||
An OpenXR API layer that does nothing, named as the Steam Frame's
|
||||
XR_APILAYER_VALVE_fdm_injection names its library
|
||||
(libVkLayer_VALVE_fdm_injection.so). The Frame lists that layer as implicit
|
||||
for every OpenXR app (/vendor/etc/openxr/1/api_layers/implicit.d), but an
|
||||
Android app (in Lepton) cannot load the system's library from its own
|
||||
namespace, and the Khronos loader then fails xrCreateInstance altogether;
|
||||
nor does the Android loader read the variable that would turn the layer
|
||||
off. With this one in the app's libraries the loader finds a layer of that
|
||||
name, which hands every call on to the next. (The real layer adds fragment
|
||||
density maps to Vulkan apps; this game draws with OpenGL ES.) On a Quest no
|
||||
manifest names it and it is never loaded.
|
||||
*/
|
||||
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_loader_negotiation.h>
|
||||
#include <stddef.h>
|
||||
|
||||
static PFN_xrGetInstanceProcAddr next_get_instance_proc_addr;
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
|
||||
PFN_xrVoidFunction *function)
|
||||
{
|
||||
if (!next_get_instance_proc_addr)
|
||||
return XR_ERROR_HANDLE_INVALID;
|
||||
return next_get_instance_proc_addr(instance, name, function);
|
||||
}
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_api_layer_instance(const XrInstanceCreateInfo *info,
|
||||
const XrApiLayerCreateInfo *layer_info, XrInstance *instance)
|
||||
{
|
||||
XrApiLayerCreateInfo next_layer_info;
|
||||
|
||||
if (!layer_info || !layer_info->nextInfo)
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
next_get_instance_proc_addr = layer_info->nextInfo->nextGetInstanceProcAddr;
|
||||
next_layer_info = *layer_info;
|
||||
next_layer_info.nextInfo = layer_info->nextInfo->next;
|
||||
return layer_info->nextInfo->nextCreateApiLayerInstance(info, &next_layer_info, instance);
|
||||
}
|
||||
|
||||
__attribute__((visibility("default")))
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
|
||||
const char *layer_name, XrNegotiateApiLayerRequest *request)
|
||||
{
|
||||
(void)layer_name;
|
||||
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
|
||||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
|
||||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
|
||||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
|
||||
{
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
|
||||
request->layerApiVersion = loader_info->maxApiVersion < XR_CURRENT_API_VERSION ? loader_info->maxApiVersion :
|
||||
XR_CURRENT_API_VERSION;
|
||||
request->getInstanceProcAddr = layer_get_instance_proc_addr;
|
||||
request->createApiLayerInstance = layer_create_api_layer_instance;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
Newtown
|
||||
Copyright (c) Roger White, 1994. Released by its author into the public domain.
|
||||
|
||||
Newtown-Regular.ttf is the TrueType file of Newtown, as Roger White
|
||||
distributed it (NEWTOW__.TTF, version 1.00, SHA-256
|
||||
376d82c5b660c059c182d848d0a96da9dbe7277d7c8820f6e9a2c99b1a256c65), here
|
||||
renamed. OpenCE (OpenCE-Regular.ttf) is made from it by tools/title_font.py.
|
||||
|
||||
The file's copyright notice ("All Rights Reserved - see .txt file for terms
|
||||
for distribution and use") and the readme it was first distributed with
|
||||
(personal use free, commercial use for a fee) date from 1994. Its author
|
||||
later released all of his fonts into the public domain. His website,
|
||||
rogersfonts.org.uk, said so from 2008 until it closed:
|
||||
|
||||
"Despite what is said in the readme files associated with the fonts all
|
||||
fonts on this site are now public domain, whether used for personal use
|
||||
or for commercial purposes. All that I ask is that the source is
|
||||
acknowledged if you are using any for commercial purposes."
|
||||
|
||||
The statement is in the Internet Archive's copies of the site's home page,
|
||||
the first of them from 24 July 2008:
|
||||
|
||||
https://web.archive.org/web/20080724224405/http://www.rogersfonts.org.uk/body_index.html
|
||||
https://web.archive.org/web/20110724011202/http://www.rogersfonts.org.uk/body_index.html
|
||||
|
||||
and Newtown was one of the site's fonts (its samples are archived from 2004,
|
||||
https://web.archive.org/web/20040909032418/http://www.rogersfonts.org.uk/assets/images/Newtown.gif).
|
||||
The site says its fonts were made from the author's own hand-drawn artwork,
|
||||
scanned and cleaned up.
|
||||
|
||||
As he asked, the source is acknowledged: OpenCE is based on Newtown by
|
||||
Roger White.
|
||||
@@ -1,95 +0,0 @@
|
||||
Copyright 2026 The OpenCE Project Authors, for its changes to Newtown's spacing.
|
||||
OpenCE is based on Newtown, by Roger White (1994), which its author released
|
||||
into the public domain (Newtown-LICENSE.txt).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,93 +0,0 @@
|
||||
Copyright 2021 The Overpass Project Authors (https://github.com/RedHatOfficial/Overpass)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,45 +0,0 @@
|
||||
# Fonts
|
||||
|
||||
The fonts the game's text is drawn with, at the display's resolution, in
|
||||
place of the bitmap fonts of the maps (`port/linux/src/text_hires.c`).
|
||||
`fonts.json` says which font draws each of the maps' font tags.
|
||||
|
||||
| File | Draws | From |
|
||||
| --- | --- | --- |
|
||||
| `Overpass-900.ttf` | `ui\large_ui`, `ui\interstate` | Overpass, weight 900 (Black) |
|
||||
| `Overpass-750.ttf` | `ui\small_ui` | Overpass, weight 750 |
|
||||
| `OpenCE-Regular.ttf` | the menus' titles, as pictures (`port/assets/titles`) | OpenCE: Newtown, respaced for this project (`tools/title_font.py`) |
|
||||
| `Newtown-Regular.ttf` | (OpenCE's source) | Newtown by Roger White, unmodified |
|
||||
|
||||
The maps' fonts are Interstate (Tobias Frere-Jones, The Font Bureau), a
|
||||
commercial typeface that cannot be shipped. Overpass (Red Hat, Delve
|
||||
Withrington) follows the same Highway Gothic road-sign alphabet and is
|
||||
under the SIL Open Font License 1.1 (`Overpass-OFL.txt`), without a
|
||||
Reserved Font Name. Matched against the maps' glyphs, its weights 900 and 750 overlap
|
||||
them as much as Interstate Bold does, at the same letter widths.
|
||||
|
||||
The menus' titles are pictures of text in the maps, set in another
|
||||
commercial typeface. OpenCE (Open Community Edition) stands in for it:
|
||||
Newtown, a typeface in the same style by Roger White (1994), which its author
|
||||
released into the public domain (`Newtown-LICENSE.txt` quotes his statement
|
||||
and where it is archived), respaced for this project. Only its spacing is
|
||||
changed, its outlines are Newtown's: each letter's side spaces and the
|
||||
kerning pairs are measured from where the letters sit in the maps' titles.
|
||||
OpenCE is under the SIL Open Font License 1.1 (`OpenCE-OFL.txt`), without a
|
||||
Reserved Font Name. It is not embedded: `tools/title_assets.py` draws the
|
||||
titles with it, and the builds embed those pictures. To measure its spacing
|
||||
again and rebuild it:
|
||||
|
||||
python3 tools/title_font.py --measure # paste the values into tools/title_font.py
|
||||
python3 tools/title_font.py
|
||||
python3 tools/title_assets.py --map assets/maps/ui.map
|
||||
|
||||
The Overpass files are static instances of Google Fonts' variable Overpass
|
||||
(`ofl/overpass/Overpass[wght].ttf`, google/fonts commit
|
||||
9710da1eacb3be272583c3224dcb70f9da6eadbb), made with fontTools:
|
||||
|
||||
python3 -m fontTools.varLib.instancer 'Overpass[wght].ttf' wght=900 -o Overpass-900.ttf
|
||||
python3 -m fontTools.varLib.instancer 'Overpass[wght].ttf' wght=750 -o Overpass-750.ttf
|
||||
|
||||
The builds embed the Overpass files (`tools/embed_assets.py`), and the
|
||||
release packages carry `Overpass-OFL.txt`, as the license asks.
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"comment": "Which font draws each of the maps' font tags (port/linux/src/text_hires.c).",
|
||||
"fonts": [
|
||||
{"tag": "ui\\large_ui", "file": "Overpass-900.ttf"},
|
||||
{"tag": "ui\\interstate", "file": "Overpass-900.ttf"},
|
||||
{"tag": "ui\\small_ui", "file": "Overpass-750.ttf"}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 701 B |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 846 B |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 502 B |
|
Before Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 268 B |
|
Before Width: | Height: | Size: 361 B |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
|
||||
<!-- ammo meter fill alpha: slanted bar whose top sweeps up into a tab on the right -->
|
||||
<path d="M7.56,32 H209.2 C289.7,32 340.8,7.2 353.7,0 H406.9 A8.3,8.3 0 0 1 414.1,11.4 L393.2,56 H5 A5,5 0 0 1 0,51 V48.1 Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 323 B |
@@ -1,66 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="128" viewBox="0 0 512 128">
|
||||
<!-- 3 rows x 20 slanted bullet rounds (alpha mask) -->
|
||||
<defs>
|
||||
<path id="b" d="M51.34,0 H51.79 L54.02,6 L42.83,30 H32.47 L43.71,5.89 Z" fill="#fff"/>
|
||||
</defs>
|
||||
<use href="#b"/>
|
||||
<use href="#b" x="20.63" y="0"/>
|
||||
<use href="#b" x="41.26" y="0"/>
|
||||
<use href="#b" x="61.89" y="0"/>
|
||||
<use href="#b" x="82.52" y="0"/>
|
||||
<use href="#b" x="103.16" y="0"/>
|
||||
<use href="#b" x="123.79" y="0"/>
|
||||
<use href="#b" x="144.42" y="0"/>
|
||||
<use href="#b" x="165.05" y="0"/>
|
||||
<use href="#b" x="185.68" y="0"/>
|
||||
<use href="#b" x="206.31" y="0"/>
|
||||
<use href="#b" x="226.94" y="0"/>
|
||||
<use href="#b" x="247.57" y="0"/>
|
||||
<use href="#b" x="268.20" y="0"/>
|
||||
<use href="#b" x="288.83" y="0"/>
|
||||
<use href="#b" x="309.47" y="0"/>
|
||||
<use href="#b" x="330.10" y="0"/>
|
||||
<use href="#b" x="350.73" y="0"/>
|
||||
<use href="#b" x="371.36" y="0"/>
|
||||
<use href="#b" x="391.99" y="0"/>
|
||||
<use href="#b" x="-15.85" y="34"/>
|
||||
<use href="#b" x="4.78" y="34"/>
|
||||
<use href="#b" x="25.41" y="34"/>
|
||||
<use href="#b" x="46.04" y="34"/>
|
||||
<use href="#b" x="66.67" y="34"/>
|
||||
<use href="#b" x="87.30" y="34"/>
|
||||
<use href="#b" x="107.93" y="34"/>
|
||||
<use href="#b" x="128.56" y="34"/>
|
||||
<use href="#b" x="149.19" y="34"/>
|
||||
<use href="#b" x="169.82" y="34"/>
|
||||
<use href="#b" x="190.46" y="34"/>
|
||||
<use href="#b" x="211.09" y="34"/>
|
||||
<use href="#b" x="231.72" y="34"/>
|
||||
<use href="#b" x="252.35" y="34"/>
|
||||
<use href="#b" x="272.98" y="34"/>
|
||||
<use href="#b" x="293.61" y="34"/>
|
||||
<use href="#b" x="314.24" y="34"/>
|
||||
<use href="#b" x="334.87" y="34"/>
|
||||
<use href="#b" x="355.50" y="34"/>
|
||||
<use href="#b" x="376.14" y="34"/>
|
||||
<use href="#b" x="-31.71" y="68"/>
|
||||
<use href="#b" x="-11.08" y="68"/>
|
||||
<use href="#b" x="9.55" y="68"/>
|
||||
<use href="#b" x="30.19" y="68"/>
|
||||
<use href="#b" x="50.82" y="68"/>
|
||||
<use href="#b" x="71.45" y="68"/>
|
||||
<use href="#b" x="92.08" y="68"/>
|
||||
<use href="#b" x="112.71" y="68"/>
|
||||
<use href="#b" x="133.34" y="68"/>
|
||||
<use href="#b" x="153.97" y="68"/>
|
||||
<use href="#b" x="174.60" y="68"/>
|
||||
<use href="#b" x="195.23" y="68"/>
|
||||
<use href="#b" x="215.86" y="68"/>
|
||||
<use href="#b" x="236.49" y="68"/>
|
||||
<use href="#b" x="257.13" y="68"/>
|
||||
<use href="#b" x="277.76" y="68"/>
|
||||
<use href="#b" x="298.39" y="68"/>
|
||||
<use href="#b" x="319.02" y="68"/>
|
||||
<use href="#b" x="339.65" y="68"/>
|
||||
<use href="#b" x="360.28" y="68"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="64" viewBox="0 0 512 64">
|
||||
<!-- rocket ammo: tail fin + body, grey translucent fill with white outline; pair repeated -->
|
||||
<defs>
|
||||
<g id="rocket" fill="#a6a6a6" fill-opacity=".651" stroke="#fff" stroke-width="4.1" stroke-miterlimit="10">
|
||||
<path d="M30.7,1.35 H51.4 L58.4,22 H90.6 L79.5,46 H48.1 L24.5,62.2 H3.5 L18.4,46.2 H11.6 L22.65,22 H29.35 Z"/>
|
||||
<path d="M88.5,48.4 L124.3,52 L191.9,46.1 L210.8,32.8 L202.7,21.9 L142.6,16 L101.8,19.8 Z"/>
|
||||
</g>
|
||||
</defs>
|
||||
<use href="#rocket"/>
|
||||
<use href="#rocket" x="214"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 644 B |
@@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
|
||||
<!-- slanted meter bar, grey translucent fill fading slightly darker downwards (rgb = alpha) -->
|
||||
<defs>
|
||||
<linearGradient id="g" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="44">
|
||||
<stop offset="0" stop-color="#c7c7c7" stop-opacity=".781"/>
|
||||
<stop offset="1" stop-color="#b4b4b4" stop-opacity=".705"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M20,0 H412 L392,44 H0 Z" fill="url(#g)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 506 B |
@@ -1,26 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="64" viewBox="0 0 512 64">
|
||||
<!-- 20 slanted blade ticks (alpha mask) -->
|
||||
<defs>
|
||||
<path id="t" d="M28.03,0 H40.39 L17.73,48.6 L0.58,59.39 L1.03,57.87 Z" fill="#fff"/>
|
||||
</defs>
|
||||
<use href="#t" x="0.000"/>
|
||||
<use href="#t" x="20.704"/>
|
||||
<use href="#t" x="41.408"/>
|
||||
<use href="#t" x="62.112"/>
|
||||
<use href="#t" x="82.816"/>
|
||||
<use href="#t" x="103.520"/>
|
||||
<use href="#t" x="124.224"/>
|
||||
<use href="#t" x="144.928"/>
|
||||
<use href="#t" x="165.632"/>
|
||||
<use href="#t" x="186.336"/>
|
||||
<use href="#t" x="207.040"/>
|
||||
<use href="#t" x="227.744"/>
|
||||
<use href="#t" x="248.448"/>
|
||||
<use href="#t" x="269.152"/>
|
||||
<use href="#t" x="289.856"/>
|
||||
<use href="#t" x="310.560"/>
|
||||
<use href="#t" x="331.264"/>
|
||||
<use href="#t" x="351.968"/>
|
||||
<use href="#t" x="372.672"/>
|
||||
<use href="#t" x="393.376"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 905 B |
@@ -1,20 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="64" viewBox="0 0 512 64">
|
||||
<!-- 12 tilted shotgun shells (alpha mask): tapered body with rim + separate tip -->
|
||||
<defs>
|
||||
<path id="s" transform="translate(15.87,38.77) rotate(25)" fill="#fff"
|
||||
d="M-8.04,-18.2 H8.04 V4.82 L6.23,12.94 H9.78 V17.06 H-9.78 V12.94 H-6.23 L-8.04,4.82 Z
|
||||
M-8.08,-21.95 H8.08 L4.93,-40.72 H-4.93 Z"/>
|
||||
</defs>
|
||||
<use href="#s" x="0.000"/>
|
||||
<use href="#s" x="35.273"/>
|
||||
<use href="#s" x="70.546"/>
|
||||
<use href="#s" x="105.819"/>
|
||||
<use href="#s" x="141.092"/>
|
||||
<use href="#s" x="176.365"/>
|
||||
<use href="#s" x="211.638"/>
|
||||
<use href="#s" x="246.911"/>
|
||||
<use href="#s" x="282.184"/>
|
||||
<use href="#s" x="317.457"/>
|
||||
<use href="#s" x="352.730"/>
|
||||
<use href="#s" x="388.003"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 841 B |