mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 14:00:25 +02:00
Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1929702f5b | ||
|
|
9d6b980ba6 | ||
|
|
9e4f8a2dcb | ||
|
|
53c268b8a6 | ||
|
|
592be6270a | ||
|
|
8817c37470 | ||
|
|
d635d83725 | ||
|
|
2a6dc2b738 | ||
|
|
6112dcfc5a | ||
|
|
62fa7e13ef | ||
|
|
9a12828762 | ||
|
|
7e82e7128e | ||
|
|
94882796b7 | ||
|
|
3dba558e4f | ||
|
|
1dc533fe18 | ||
|
|
8a8e70599f | ||
|
|
b115a41263 | ||
|
|
320464258a | ||
|
|
f5bb75c27c | ||
|
|
5cd1daff77 | ||
|
|
6329be8aa1 | ||
|
|
7a2f4caa50 | ||
|
|
938c37a669 | ||
|
|
fae3fbad69 | ||
|
|
5a35ea2b20 | ||
|
|
2e179ea90f | ||
|
|
9b517e554c | ||
|
|
595bbc5611 | ||
|
|
761210d405 | ||
|
|
7ba780ef59 | ||
|
|
d9c43e3478 | ||
|
|
c04ce7fd00 | ||
|
|
3f06b1b9d8 | ||
|
|
1b2d058a04 | ||
|
|
b5fa53ed95 | ||
|
|
1f2c10a3ca | ||
|
|
12f1155752 | ||
|
|
2b0327bc80 | ||
|
|
58aa352705 | ||
|
|
59c4b2160e | ||
|
|
1cf336f0ff | ||
|
|
d10969dc83 | ||
|
|
fe1d51dd58 | ||
|
|
197c199473 | ||
|
|
0acc3b1381 | ||
|
|
1e74e9b03e | ||
|
|
5d108579ed | ||
|
|
7a1ffca28b | ||
|
|
b843156fe8 | ||
|
|
99a31ca82d | ||
|
|
9666575b39 | ||
|
|
8e6b9a211e | ||
|
|
d0016bca89 | ||
|
|
dc383d10da | ||
|
|
a143d9490d | ||
|
|
e55c29a97e | ||
|
|
16361bdef1 | ||
|
|
e44619818b | ||
|
|
b449c43e22 | ||
|
|
991ab41016 | ||
|
|
5f9748d736 | ||
|
|
56b2a0e8e3 | ||
|
|
5f388f7818 |
No files matched your search
@@ -67,7 +67,9 @@ jobs:
|
||||
run: python tools/ci_build.py linux release
|
||||
|
||||
- name: Test
|
||||
run: python -m pytest -q tools/test_linux_port.py
|
||||
run: |
|
||||
python -m pytest -q tools/test_linux_port.py
|
||||
python tools/test_light_storage.py
|
||||
|
||||
- name: Compile cache
|
||||
run: ccache --show-stats
|
||||
@@ -200,6 +202,9 @@ jobs:
|
||||
- name: Release
|
||||
run: python tools/ci_build.py windows release
|
||||
|
||||
- name: Test light storage
|
||||
run: python tools/test_light_storage.py
|
||||
|
||||
- name: Compile cache
|
||||
run: ccache --show-stats
|
||||
|
||||
|
||||
@@ -123,3 +123,16 @@ page of buttons shows this layout on the Frame.
|
||||
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.
|
||||
- 2026-10-05: doubled pictures when turning at 72 Hz too, the game at the
|
||||
edge of its frame (12.5 of 13.9 ms; SteamVR reprojecting 2-8% of the
|
||||
frames). The time was not the drawing's but waits: the layer copies read
|
||||
the GL state back (glGet*) and released each image at once, and with
|
||||
Mesa's GL thread (zink) every read waits for the thread to run all that
|
||||
is queued, every release for the GPU (the left eye's copy took 4.5-7 ms
|
||||
of the frame, the right one's, just after it, 0.7). On SteamVR the copies
|
||||
are now deferred (host_xr.c): no state read back (the game sets its own
|
||||
again), the images released together as the frame ends, and the eyes'
|
||||
work flushed to the GPU as soon as they are copied.
|
||||
`debug.xr_deferred_copies` = 0 brings back the old copies for comparing,
|
||||
1 defers them on any runtime; `vr.log_performance` logs the frame's parts
|
||||
and, with the compositor's counters, each copy's time.
|
||||
@@ -0,0 +1,52 @@
|
||||
# VR roadmap
|
||||
|
||||
What the VR port (Quest 3, Steam Frame) has, and what comes next, in the
|
||||
agreed order. Updated with every step: a step is **done** once the user has
|
||||
tried it in the headset and it is merged into `main`; **built** means in a
|
||||
branch, not yet tried in the headset.
|
||||
|
||||
Several of the next steps follow
|
||||
[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) and the
|
||||
Steam Frame work it grew from,
|
||||
[astromaddie/HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) (both
|
||||
CC0): their ideas, rebuilt into this port's own structure, with the source
|
||||
named where code is taken over.
|
||||
|
||||
## Done (in `main`)
|
||||
|
||||
| Area | What |
|
||||
| --- | --- |
|
||||
| Headsets | Quest 3; Steam Frame (Lepton): its controllers' layout, a Steam library tile that installs, starts, updates and quits the game, data kept in `~/Documents/HaloCE` |
|
||||
| Rendering | Both eyes in one pass (multiview) on the Quest; one by one on SteamVR, where multiview's eyes did not fuse; lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
|
||||
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
|
||||
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
|
||||
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
|
||||
| Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button |
|
||||
| Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard |
|
||||
| Multiplayer | Up to date with OpenCE (network version 20): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
|
||||
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
|
||||
|
||||
## Next, in order
|
||||
|
||||
| # | Step | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Steam Frame CPU time: Mesa's trace markers off (after astromaddie/HaloCE-VR), no `glGet` stalls in the visibility counters' copies; then measured in the headset | built (branch `frame/performance-2`) |
|
||||
| 3a | Visible arms and hands: the arms by IK to the controllers, fingers by trigger and grip | |
|
||||
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | |
|
||||
| 3b | The whole body: torso from head and hands, legs and feet following in the room; full / arms only / hands only | |
|
||||
| 3c | Weapons held physically, holsters at shoulder and hip (the buttons stay) | |
|
||||
| 3d | Other VR players shown with their heads, hands and bodies (today their bodies move as flat players' do: only their aim and throws come from their hands) | |
|
||||
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | |
|
||||
|
||||
## Open points outside the order
|
||||
|
||||
- Multiview on the Steam Frame: why the eyes do not fuse there (suspected
|
||||
Mesa zink/Turnip); tools in branch `frame/doubled-images` (86335828),
|
||||
not merged.
|
||||
- Transparent geometry groups: the Xbox's 384 a window overflow in games
|
||||
of 30 and more players.
|
||||
- The VR on-screen keyboard shows a password in the clear while it is
|
||||
typed.
|
||||
- Headset tests of the latest merge (`merge/upstream-2026-10-06`): menus,
|
||||
bots, co-op, a password game between Frame and Quest, a bot making room
|
||||
for a joining person.
|
||||
@@ -119,7 +119,9 @@ headset before the game started, and set HALO_VR); 0 on failure */
|
||||
int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate);
|
||||
/* waits for the next frame and reports the predicted poses */
|
||||
int host_xr_begin_frame(struct halo_xr_frame *frame);
|
||||
/* copies a render target texture (0: clears) into a layer's image */
|
||||
/* copies a render target texture (0: clears) into a layer's image: 0 not
|
||||
copied, 1 copied with the GL state put back, 2 copied with the copy's GL
|
||||
state left (the deferred copies, host_xr.c), which the game sets again */
|
||||
int host_xr_copy_layer(int layer, unsigned int texture, int clear_alpha);
|
||||
/* the same for part of the texture: left, top, right, bottom, 0..1 from its
|
||||
top left */
|
||||
|
||||
@@ -92,6 +92,7 @@ void *host_resolve_import(const char *name);
|
||||
/* ---------- SDL / GL (host_sdl.c, host_gl.c) */
|
||||
|
||||
void *host_gl_resolve(const char *name);
|
||||
int host_gl_quiet_trace_markers(void);
|
||||
|
||||
/* ---------- VR (host_xr.c) */
|
||||
|
||||
|
||||
@@ -11,8 +11,52 @@ already have host types by then. Only strings need copying back.
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl32.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Valve's Mesa (the Steam Frame's Lepton) writes a marker to the kernel's
|
||||
trace_marker around every flush and context switch, a couple of hundred a
|
||||
frame, whether or not anything traces. The descriptors it holds open on that
|
||||
file are pointed at /dev/null instead (not closed: a reused descriptor would
|
||||
take the markers elsewhere). After astromaddie/HaloCE-VR (CC0). Returns how
|
||||
many were. */
|
||||
int host_gl_quiet_trace_markers(void)
|
||||
{
|
||||
DIR *directory = opendir("/proc/self/fd");
|
||||
struct dirent *entry;
|
||||
int quieted = 0, null_fd;
|
||||
|
||||
if (!directory)
|
||||
return 0;
|
||||
null_fd = open("/dev/null", O_WRONLY | O_CLOEXEC);
|
||||
while (null_fd >= 0 && (entry = readdir(directory)) != NULL)
|
||||
{
|
||||
char link[64], target[256];
|
||||
ssize_t length;
|
||||
int fd = atoi(entry->d_name);
|
||||
|
||||
if (entry->d_name[0] < '0' || entry->d_name[0] > '9' || fd == null_fd)
|
||||
continue;
|
||||
snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
|
||||
length = readlink(link, target, sizeof(target) - 1);
|
||||
if (length <= 0)
|
||||
continue;
|
||||
target[length] = 0;
|
||||
if (strstr(target, "tracing/trace_marker") && dup3(null_fd, fd, O_CLOEXEC) == fd)
|
||||
quieted++;
|
||||
}
|
||||
if (null_fd >= 0)
|
||||
close(null_fd);
|
||||
closedir(directory);
|
||||
if (quieted)
|
||||
host_logf(HOST_LOG_INFO, "trace markers: %d descriptors to /dev/null", quieted);
|
||||
return quieted;
|
||||
}
|
||||
|
||||
void *host_gl_resolve(const char *name)
|
||||
{
|
||||
@@ -88,14 +132,13 @@ half-drawn targets out of tile memory, for every lens flare. */
|
||||
static GLuint counter_snapshots[COUNTER_SNAPSHOTS];
|
||||
static uint32_t counter_snapshot_sizes[COUNTER_SNAPSHOTS];
|
||||
|
||||
/* (the copy bindings are left unbound after: the renderer binds them itself
|
||||
for each copy. Asking GL what they were would wait for Mesa's GL thread, on
|
||||
the Frame, to run everything queued) */
|
||||
void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
|
||||
{
|
||||
GLint read_binding = 0, write_binding = 0;
|
||||
|
||||
if (slot >= COUNTER_SNAPSHOTS || !buffer || !size)
|
||||
return;
|
||||
glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, &read_binding);
|
||||
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
|
||||
if (!counter_snapshots[slot])
|
||||
glGenBuffers(1, &counter_snapshots[slot]);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
|
||||
@@ -108,19 +151,17 @@ void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
|
||||
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, buffer);
|
||||
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, size);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, (GLuint)read_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, 0);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
}
|
||||
|
||||
/* copies a snapshot out; its frame must be finished (host_gl_wait_frame) */
|
||||
int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
|
||||
{
|
||||
GLint write_binding = 0;
|
||||
const void *mapping;
|
||||
|
||||
if (slot >= COUNTER_SNAPSHOTS || !counter_snapshots[slot] || size > counter_snapshot_sizes[slot])
|
||||
return 0;
|
||||
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
|
||||
mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, GL_MAP_READ_BIT);
|
||||
if (mapping)
|
||||
@@ -128,7 +169,7 @@ int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
|
||||
memcpy(out, mapping, size);
|
||||
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
|
||||
}
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
return mapping != NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -337,6 +337,15 @@ static void *game_main(void *unused)
|
||||
host_logf(HOST_LOG_INFO, "Mesa's GL thread %s (debug.gl_thread)", gl_thread != 0.0 ? "on" : "off");
|
||||
}
|
||||
}
|
||||
{
|
||||
/* (debug.xr_deferred_copies: the headset's layer copies deferred,
|
||||
host_xr.c, or one by one; -1 as the runtime has it, deferred on
|
||||
SteamVR) */
|
||||
double deferred = config_number(path, "debug.xr_deferred_copies", -1.0);
|
||||
|
||||
if (deferred >= 0.0)
|
||||
setenv("HALO_XR_DEFERRED", deferred != 0.0 ? "1" : "0", 1);
|
||||
}
|
||||
{
|
||||
int eye_width, eye_height;
|
||||
|
||||
|
||||
@@ -151,7 +151,11 @@ uint32_t host_sdl_gl_create_context(uint32_t window)
|
||||
{
|
||||
SDL_Window *object = handle_get(window, _handle_window);
|
||||
|
||||
return object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
uint32_t context = object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
|
||||
/* (the driver opens its trace markers with the context) */
|
||||
host_gl_quiet_trace_markers();
|
||||
return context;
|
||||
}
|
||||
|
||||
int host_sdl_gl_make_current(uint32_t window, uint32_t context)
|
||||
|
||||
+89
-18
@@ -58,6 +58,7 @@ struct xr_swapchain
|
||||
GLuint framebuffers[MAXIMUM_SWAPCHAIN_IMAGES];
|
||||
uint32_t acquired_index;
|
||||
int filled; /* this frame */
|
||||
int held; /* acquired, released only as the frame ends (xr.deferred) */
|
||||
};
|
||||
|
||||
struct xr_hand_actions
|
||||
@@ -136,7 +137,18 @@ static struct
|
||||
double copy_seconds[HALO_XR_LAYER_COUNT];
|
||||
double end_seconds;
|
||||
double acquire_seconds;
|
||||
double release_seconds;
|
||||
int timed_frames;
|
||||
/* The copies without waits in the frame (SteamVR: HALO_XR_DEFERRED,
|
||||
debug.xr_deferred_copies). Each copy otherwise reads the GL state back
|
||||
to restore it (glGet*), and releases its image at once: with Mesa's GL
|
||||
thread (the Steam Frame's zink) each read waits for the thread to run
|
||||
everything queued, and each release for the GPU to finish it, so the
|
||||
game's one thread stood still for the eyes' whole drawing in the middle
|
||||
of every frame. Deferred, a copy leaves its GL state for the game to set
|
||||
again (host_xr_copy_layer's 2) and keeps its image until the frame ends,
|
||||
where all are released at once. */
|
||||
int deferred;
|
||||
int has_refresh_rate;
|
||||
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate;
|
||||
PFN_xrGetDisplayRefreshRateFB get_refresh_rate;
|
||||
@@ -1248,6 +1260,10 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
|
||||
xr.get_refresh_rate(xr.session, &info->refresh_rate);
|
||||
}
|
||||
|
||||
/* (the runtime's GL binding brings contexts of its own, with their own
|
||||
trace markers: host_gl.c) */
|
||||
host_gl_quiet_trace_markers();
|
||||
|
||||
if (xr.has_metrics)
|
||||
{
|
||||
PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumerate = NULL;
|
||||
@@ -1283,6 +1299,16 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* (deferred on SteamVR; HALO_XR_DEFERRED "1" or "0" says otherwise:
|
||||
debug.xr_deferred_copies, host_main.c) */
|
||||
const char *choice = getenv("HALO_XR_DEFERRED");
|
||||
|
||||
xr.deferred = choice && *choice ? atoi(choice) != 0 : xr.steamvr;
|
||||
host_logf(HOST_LOG_INFO, "xr: layer copies %s", xr.deferred ?
|
||||
"deferred (no waits for the GL thread or the GPU in the frame)" : "restored and released one by one");
|
||||
}
|
||||
|
||||
info->eye_width = xr.eye_width;
|
||||
info->eye_height = xr.eye_height;
|
||||
info->overlay_width = xr.eye_width;
|
||||
@@ -1481,23 +1507,27 @@ static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float
|
||||
return 0;
|
||||
swapchain = &xr.swapchains[layer];
|
||||
if (swapchain->filled)
|
||||
return 1;
|
||||
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
|
||||
"xrAcquireSwapchainImage"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return xr.deferred ? 2 : 1;
|
||||
wait.timeout = XR_INFINITE_DURATION;
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
|
||||
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
|
||||
"xrAcquireSwapchainImage"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
|
||||
return 0;
|
||||
if (xr.has_metrics)
|
||||
xr.acquire_seconds += seconds_now() - start;
|
||||
}
|
||||
|
||||
gl_save(&saved);
|
||||
/* (deferred: nothing read back; the game sets its state again) */
|
||||
if (xr.deferred)
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
else
|
||||
gl_save(&saved);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, swapchain->framebuffers[swapchain->acquired_index]);
|
||||
glViewport(0, 0, swapchain->width, swapchain->height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
@@ -1562,13 +1592,22 @@ static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float
|
||||
glBindVertexArray(xr.vertex_array);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindSampler(0, xr.sampler);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
|
||||
if (xr.deferred)
|
||||
{
|
||||
/* (the game's render targets, all a copy reads, have no
|
||||
swizzle: only its uploaded textures get one, xbox_textures.c) */
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
|
||||
}
|
||||
if (clear_alpha == HALO_XR_COPY_OPAQUE)
|
||||
{
|
||||
/* an opaque layer: the game's alpha channel holds other things */
|
||||
@@ -1582,13 +1621,42 @@ static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float
|
||||
glClearColor(0.0f, 0.0f, 0.0f, clear_alpha ? 1.0f : 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
swapchain->filled = 1;
|
||||
if (xr.deferred)
|
||||
{
|
||||
swapchain->held = 1;
|
||||
/* (the eyes, most of the frame's work, on to the GPU while the
|
||||
game draws the rest) */
|
||||
if (layer == HALO_XR_LAYER_EYE_RIGHT)
|
||||
glFlush();
|
||||
return 2;
|
||||
}
|
||||
gl_restore(&saved);
|
||||
|
||||
xrReleaseSwapchainImage(swapchain->handle, &release);
|
||||
swapchain->filled = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* deferred: the images the frame's copies kept, released before it ends */
|
||||
static void release_held(void)
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
int layer;
|
||||
|
||||
for (layer = 0; layer < HALO_XR_LAYER_COUNT; layer++)
|
||||
{
|
||||
XrSwapchainImageReleaseInfo release = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
|
||||
struct xr_swapchain *swapchain = &xr.swapchains[layer];
|
||||
|
||||
if (!swapchain->held)
|
||||
continue;
|
||||
swapchain->held = 0;
|
||||
xr_ok(xrReleaseSwapchainImage(swapchain->handle, &release), "xrReleaseSwapchainImage");
|
||||
}
|
||||
if (xr.has_metrics)
|
||||
xr.release_seconds += seconds_now() - start;
|
||||
}
|
||||
|
||||
/* 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
|
||||
@@ -1659,6 +1727,7 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
if (!xr.frame_begun)
|
||||
return 0;
|
||||
xr.frame_begun = 0;
|
||||
release_held();
|
||||
|
||||
if (xr.frame_state.shouldRender && submit->eyes && xr.views_valid &&
|
||||
xr.swapchains[HALO_XR_LAYER_EYE_LEFT].filled && xr.swapchains[HALO_XR_LAYER_EYE_RIGHT].filled)
|
||||
@@ -1836,9 +1905,11 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
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;
|
||||
host_logf(HOST_LOG_INFO, "xr: per frame ms: copies (layer:ms)%s; of them acquiring images %.2f; "
|
||||
"releasing them at the end %.2f; xrEndFrame %.2f (copies %s)", copies,
|
||||
xr.acquire_seconds * 1e3 / frames, xr.release_seconds * 1e3 / frames, xr.end_seconds * 1e3 / frames,
|
||||
xr.deferred ? "deferred" : "one by one");
|
||||
xr.acquire_seconds = xr.release_seconds = xr.end_seconds = 0.0;
|
||||
xr.timed_frames = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,9 @@
|
||||
<bitmap name="main_menu/multiplayer_type_select/join_game/header_direct_link">
|
||||
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
<bitmap name="main_menu/multiplayer_type_select/join_game/header_password">
|
||||
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_password.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
<?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/join_game/password/password_screen" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game" bitmap="bitmaps/gradient">
|
||||
<on event="b" run="port password back"/>
|
||||
<on event="back" run="port password back"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/header_password"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/options_menu"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/header_password" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_password">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/help" type="text" controller="1" left="68" top="120" width="482" height="60" string_list="main_menu/multiplayer_type_select/join_game/password/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/join_game/password/help">
|
||||
<data input="port password update"/>
|
||||
<on event="created" run="port password init"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/op_password" x="54" y="73"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/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/join_game/password/button_ok" x="380" y="1"/>
|
||||
<child widget="common_button_cancel" x="510" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/op_password" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<on event="a" run="port password edit"/>
|
||||
<on event="start" run="port password edit"/>
|
||||
<on event="b"/>
|
||||
<on event="back"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/password_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/password_value" x="230" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/password_label" type="text" controller="1" width="230" height="22" string_list="main_menu/multiplayer_type_select/join_game/password/labels" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/password_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/button_ok" 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">
|
||||
<on event="a" run="port password join"/>
|
||||
<on event="start" run="port password join"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<strings name="main_menu/multiplayer_type_select/join_game/password/labels">
|
||||
<string text="PASSWORD:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/multiplayer_type_select/join_game/password/help_strings">
|
||||
<string text=""/>
|
||||
<string text="This game has a password. Type it, then press\nEnter to join."/>
|
||||
<string text="That is not the game's password. Try again."/>
|
||||
<string text="Could not join the game."/>
|
||||
</strings>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/header_server_browser" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_server_browser">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/header_direct_link" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_direct_link">
|
||||
|
||||
@@ -9,25 +9,27 @@
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/header_server_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/multiplayer_type_select/server_settings/header_server_settings">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/help" type="text" controller="1" left="68" top="358" width="482" height="60" string_list="main_menu/multiplayer_type_select/server_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/help" type="text" controller="1" left="68" top="366" width="482" height="60" string_list="main_menu/multiplayer_type_select/server_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/server_settings/help">
|
||||
<data input="server settings update"/>
|
||||
<on event="created" run="server settings init"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_server_name" x="54" y="73"/>
|
||||
<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"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="269"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="297"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="325"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_max_players" x="54" y="99"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="125"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_listing" x="54" y="151"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_password" x="54" y="177"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" x="54" y="203"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" x="54" y="229"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" x="54" y="255"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" x="54" y="255"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_collisions" x="54" y="281"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="203"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="229"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="255"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="281"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="307"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="333"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
|
||||
@@ -78,6 +80,19 @@
|
||||
<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_password" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<on event="a" run="ss edit server password"/>
|
||||
<on event="start" run="ss edit server password"/>
|
||||
<on event="b"/>
|
||||
<on event="back"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/password_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/password_value" x="230" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/password_label" type="text" controller="1" width="230" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="15" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/password_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
|
||||
</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"/>
|
||||
@@ -122,6 +137,17 @@
|
||||
<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_player_collisions" 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/player_collisions_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/player_collisions_spinner" x="286" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/player_collisions_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="14" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/player_collisions_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="network.coop_player_collisions" values="true|false" 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"/>
|
||||
@@ -211,6 +237,8 @@
|
||||
<string text="EXTRA ENEMIES:"/>
|
||||
<string text="PER PLAYER:"/>
|
||||
<string text="MULTIPLIER:"/>
|
||||
<string text="PLAYER COLLISIONS:"/>
|
||||
<string text="PASSWORD:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/multiplayer_type_select/server_settings/help_strings">
|
||||
<string text=""/>
|
||||
@@ -234,5 +262,8 @@
|
||||
<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."/>
|
||||
<string text="Players bump into each other, as in the campaign."/>
|
||||
<string text="Players walk through each other, so that no one\nblocks a doorway. The AI's characters still block."/>
|
||||
<string text="Players from the Server Browser must type it to\njoin (an invite link needs none). Enter sets it."/>
|
||||
</strings>
|
||||
</menus>
|
||||
+14
-2
@@ -16,7 +16,8 @@
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_volume" x="54" y="73"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_music_volume" x="54" y="103"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_effects_volume" x="54" y="133"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="163"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" x="54" y="163"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="193"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
|
||||
@@ -51,11 +52,20 @@
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/effects_volume_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0|1|2|3|4|5|6|7|8|9|10" setting="audio.effects_volume" values="0|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9|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"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" 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/audio_settings/reverb_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_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/audio_settings/reverb_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="audio.reverb" 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"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" 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/audio_settings/enabled_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_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/audio_settings/enabled_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="audio.enabled" 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"/>
|
||||
@@ -74,6 +84,7 @@
|
||||
<string text="MASTER VOLUME:"/>
|
||||
<string text="MUSIC VOLUME:"/>
|
||||
<string text="EFFECTS VOLUME:"/>
|
||||
<string text="REVERB:"/>
|
||||
<string text="SOUND:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/help_strings">
|
||||
@@ -81,6 +92,7 @@
|
||||
<string text="The volume of everything."/>
|
||||
<string text="The music's volume."/>
|
||||
<string text="The volume of every other sound: effects and\nspeech."/>
|
||||
<string text="Echo sounds as the place you are in does, and\nmuffle those behind walls, as the Xbox did."/>
|
||||
<string text="Play sound at all; from the next time the game\nstarts."/>
|
||||
</strings>
|
||||
</menus>
|
||||
+58
-10
@@ -9,20 +9,24 @@
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help" type="text" controller="1" left="68" top="364" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
</widget>
|
||||
<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_resolution" x="54" y="97" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" x="54" y="97" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" x="54" y="121" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="145"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="169" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="193"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="217" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="241"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="265"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing" x="54" y="289" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing_android" x="54" y="289" platform="android"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_shadow_resolution" x="54" y="313"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_per_pixel_lighting" x="54" y="337"/>
|
||||
<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">
|
||||
@@ -120,6 +124,42 @@
|
||||
<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"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing" 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/anti_aliasing_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_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="10" 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/anti_aliasing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|SMAA|SSAA 2X|MSAA 2X|MSAA 4X|MSAA 8X" setting="display.anti_aliasing" values="off|fxaa|smaa|ssaa2x|msaa2x|msaa4x|msaa8x" 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_anti_aliasing_android" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="android">
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_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="11" 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/anti_aliasing_android_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|MSAA 2X|MSAA 4X" setting="display.anti_aliasing" values="off|fxaa|msaa2x|msaa4x" 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_shadow_resolution" 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/shadow_resolution_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_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="12" 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/shadow_resolution_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="128|256|512|1024" setting="display.shadow_resolution" values="128|256|512|1024" 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_per_pixel_lighting" 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/per_pixel_lighting_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_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="13" 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/per_pixel_lighting_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.per_pixel_lighting" 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"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="a" run="port settings defaults"/>
|
||||
<on event="start" run="port settings defaults"/>
|
||||
@@ -141,6 +181,10 @@
|
||||
<string text="INSTANT AIM:"/>
|
||||
<string text="HIGH-RES HUD:"/>
|
||||
<string text="HIGH-RES TEXT:"/>
|
||||
<string text="ANTI-ALIASING:"/>
|
||||
<string text="ANTI-ALIASING:"/>
|
||||
<string text="SHADOW RESOLUTION:"/>
|
||||
<string text="PER-PIXEL LIGHTING:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings">
|
||||
<string text=""/>
|
||||
@@ -154,5 +198,9 @@
|
||||
<string text="In first person, turn the view the moment the\nmouse moves, not up to two ticks later."/>
|
||||
<string text="Draw the HUD from the high-res redraws; off\ndraws the game's own pictures."/>
|
||||
<string text="Draw text and titles with high-res fonts; off\ndraws the game's own."/>
|
||||
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nand SMAA are cheap; SSAA and MSAA are sharper."/>
|
||||
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nis cheap; MSAA is sharper."/>
|
||||
<string text="The size objects' shadows are drawn at: 128 as on\nthe Xbox; larger for smoother, as soft, edges."/>
|
||||
<string text="Light models for each pixel, without the facets\nof the Xbox's lighting for each vertex."/>
|
||||
</strings>
|
||||
</menus>
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -54,6 +54,7 @@
|
||||
"ce/port/blank__0.png",
|
||||
"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_password.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",
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<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="216.45" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 563 B |
+37
-18
@@ -56,13 +56,17 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
|
||||
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.
|
||||
15 world units of the player in it, about 45 metres), so one player can't
|
||||
drag the team back through the level; a player held back is told how
|
||||
many are there and how many it needs. 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, without a revert. A mission the scripts fail
|
||||
with players still alive reverts to the last checkpoint on the host, as a
|
||||
skipped cutscene does (below); a client never reverts on its own. 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
|
||||
@@ -73,12 +77,13 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
|
||||
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.
|
||||
that many times itself for any number of players. They stand around its
|
||||
starting locations on free ground (the same floor, clear of crates and
|
||||
other actors, with room to stand), or where none is left on rings about
|
||||
them as before, 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.
|
||||
@@ -159,7 +164,16 @@ 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).
|
||||
say which structure BSP it has loaded (co-op); version 17 breaks the host's
|
||||
glass and destructible scenery on every machine (and takes a client's hits on
|
||||
scenery), sends the cluster a co-op cutscene keeps active, and leaves a
|
||||
failed co-op mission's revert to the host; version 18 sends with an object
|
||||
the bitmap of its shaders it draws with when its actor variant set one (co-op:
|
||||
the Elite major's and commander's armor); version 19 sends with the game's
|
||||
settings whether co-op's players collide with each other (Server Setup's PLAYER
|
||||
COLLISIONS: each machine's players then pass through the others'); version 20
|
||||
lists a public game with a password with its invite's token sealed with the
|
||||
password's key (`p2p_lobby.c`), a listing of another layout.
|
||||
|
||||
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
|
||||
@@ -181,9 +195,13 @@ only ever jumps forward to it when behind, so an honest one is never ahead
|
||||
while going faster (one that caught up, or a host that stalled, is one or
|
||||
the other, not both). One more than a tenth faster and half a second ahead
|
||||
has its players' predictions refused at once (the host's copies go as its
|
||||
own ticks have them), and after ten seconds of it is dropped, its address
|
||||
kept out of the host's games while the host runs, and every machine is
|
||||
told who, in red on its console and in its `debug.txt`
|
||||
own ticks have them), and after ten seconds of it is dropped, and every
|
||||
machine is told who, in red on its console and in its `debug.txt`. Its
|
||||
address is kept out of the host's games while the host runs only when a
|
||||
message that came over its connection's stream was that far ahead too: a
|
||||
datagram is known to be the machine's only by the address it came from,
|
||||
which another machine can send one as, so on its datagrams alone it is
|
||||
dropped, logged, and may join again
|
||||
(`distributed_note_client_clock`, `network_game_server_kick_machine`,
|
||||
`_distributed_message_notice`). The host also adds a line to
|
||||
`cheaters.txt` beside its `debug.txt`: when, the player's address (an
|
||||
@@ -198,7 +216,8 @@ A joining machine tells the host its hardware id: a keyed hash (HMAC-SHA-256,
|
||||
16 bytes as hex) of what its machine is known by (Windows' SMBIOS UUID, else
|
||||
its MachineGuid; Linux's `/etc/machine-id`; Android's `ANDROID_ID`, which the
|
||||
launcher writes to `hardware_id.txt`: `p2p_hardware_id`), kept by the host
|
||||
as hex only. A player dropped for cheating, and one the host bans with the
|
||||
as hex only. A player dropped for cheating (on its stream's word, as
|
||||
above), and one the host bans with the
|
||||
console's `ban <player name>` (Tab completes the name; the host's alone), is
|
||||
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
|
||||
|
||||
+35
-8
@@ -228,21 +228,25 @@ the setting for one start of the game. It has priority over the file.
|
||||
| `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.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. |
|
||||
| `display.anti_aliasing` | `"off"` | `HALO_ANTI_ALIASING` | The smoothing of jagged edges, which the Xbox did not have. `"off"`: none, as on the Xbox. `"fxaa"` or `"smaa"`: a pass over the 3D view after the game draws it. The HUD and the menus stay sharp. SMAA is the sharper and costs more. `"ssaa2x"`: the game draws at two times the resolution in each direction (at most the GPU's largest texture), and the picture is scaled down. The GPU does four times the work. Not with `display.resolution_scaling = "original"`. `"msaa2x"`, `"msaa4x"` or `"msaa8x"`: each pixel of the 3D view has that many samples (at most the GPU's). On Android, `"smaa"` gives FXAA and `"ssaa2x"` none. A change applies from the next frame. Refer to "Anti-aliasing" in "What operates". |
|
||||
| `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.shadow_resolution` | `128` | `HALO_SHADOW_RESOLUTION` | The size of the maps that the shadows of the objects are drawn in, in pixels each way: `128`, `256`, `512` or `1024` (other values go down to one of these). The game draws the shadow of each object into a map of 128x128 pixels, blurs it and projects it onto the ground. On a large screen, the edges of these shadows show steps that move when the object moves. A larger map makes the edges smooth; the blur is made wider to match, so the shadows are as soft as on the Xbox. Each doubling adds two passes of the blur. `128`: as on the Xbox. |
|
||||
| `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. |
|
||||
| `display.scoreboard_team_layout` | `"teams"` | `HALO_SCOREBOARD_TEAM_LAYOUT` | How the multiplayer scoreboard (hold BACK, or tab) lists a team game's players. `"teams"`: a column for each team, red on the left and blue on the right. `"score"`: all the players in order of score. With more players than fit, the mouse wheel and Page Up / Page Down scroll the scoreboard. |
|
||||
| `display.scoreboard_background` | `true` | `HALO_SCOREBOARD_BACKGROUND` | `true`: the multiplayer scoreboard (hold BACK, or tab) has a panel behind its text, for clearer text. |
|
||||
| `display.scoreboard_background_color` | `"16, 16, 16, 150"` | `HALO_SCOREBOARD_BACKGROUND_COLOR` | The colour of the scoreboard's panel: `"red, green, blue, alpha"`, each from `0` to `255`. Alpha `0` is see-through, `255` is solid. |
|
||||
| `display.per_pixel_lighting` | `false` | `HALO_PER_PIXEL_LIGHTING` | `false`: the models (characters, weapons, vehicles, scenery) are lit at each vertex and the light is blended between them, as on the Xbox. The light across a curved surface then shows facets, and a point light that passes close lights only the vertices it reaches. `true`: the models are lit at each pixel by the same lights (the ambient light, two distant lights and two point lights), which changes their look. |
|
||||
| `audio.enabled` | `true` | `HALO_NO_AUDIO=1` sets `false` | `false`: no audio device. The sound continues without output. |
|
||||
| `audio.volume` | `1.0` | `HALO_VOLUME` | The master volume. |
|
||||
| `audio.music_volume` | `1.0` | `HALO_MUSIC_VOLUME` | The music's volume, of the master volume. |
|
||||
| `audio.effects_volume` | `1.0` | `HALO_EFFECTS_VOLUME` | The volume of the other sounds (effects and speech), of the master volume. |
|
||||
| `audio.reverb` | `true` | `HALO_REVERB` | `true`: the sounds of the world reverberate as the place the player is in does: the sound environments of the maps (a corridor, a cave, a large hall, outdoors) set the reverberation, as the I3DL2 reverb of the Xbox did. A sound behind a wall or a door is muffled in it too. `false`: no reverberation (sounds behind a wall are still muffled). |
|
||||
| `input.mouse_sensitivity` | `1.0` | `HALO_MOUSE_SENSITIVITY` | The multiplier for the mouse aim. |
|
||||
| `input.mouse_vertical_sensitivity` | `0.0` | `HALO_MOUSE_VERTICAL_SENSITIVITY` | The multiplier for the vertical mouse aim. `0`: the same as `input.mouse_sensitivity`. |
|
||||
| `input.invert_mouse` | `false` | `HALO_MOUSE_INVERT=1` sets `true` | `true`: the vertical mouse aim is inverted. |
|
||||
@@ -261,6 +265,7 @@ the setting for one start of the game. It has priority over the file.
|
||||
| `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_player_collisions` | `true` | `HALO_NET_COOP_PLAYER_COLLISIONS` | Whether the players of an online co-op game bump into each other. `false`: they walk through each other, so that one cannot block a doorway or stand on another; they still bump into the AI's characters. PLAYER COLLISIONS in co-op's Server Setup writes its choice here. |
|
||||
| `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. |
|
||||
@@ -460,10 +465,18 @@ 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.
|
||||
|
||||
A PUBLIC game can also have a PASSWORD (a row of Server Setup, below
|
||||
LISTING). The Server Browser shows a lock at the left of a game with a
|
||||
password. A player who selects that game must type the password, and JOIN
|
||||
GAME joins only with the correct password. The invite link of the game joins
|
||||
it without the password. The host keeps the password only while the game
|
||||
runs.
|
||||
|
||||
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.
|
||||
If it cannot reach the host in 30 seconds, it marks the game FAILED. REFRESH
|
||||
asks the hosts for their listings again.
|
||||
If it cannot reach the host in 30 seconds, it marks the game FAILED. A game
|
||||
that is full or starting shows CLOSED. REFRESH asks the hosts for their
|
||||
listings again.
|
||||
|
||||
How it operates (`src/p2p_lobby.c`):
|
||||
|
||||
@@ -485,6 +498,14 @@ How it operates (`src/p2p_lobby.c`):
|
||||
seconds.
|
||||
- When a public game becomes private, the host makes a new invite. Thus a
|
||||
player who saw the listing cannot join with the old invite.
|
||||
- The listing of a game with a password does not hold the invite in clear
|
||||
text. The secret part of the invite (its token) is encrypted with a key
|
||||
from the password (Argon2id, salted with the key of the host, then
|
||||
XChaCha20-Poly1305). The browser makes the key from the password that the
|
||||
player types, and opens the invite only if the password is correct. When
|
||||
the host sets or changes the password, it makes a new invite. A player who
|
||||
has the listing can try passwords on their own machine without the host,
|
||||
so use a long password.
|
||||
|
||||
A public game does not publish the address of the host. But any machine
|
||||
with the invite can ask the host to connect, and the host then sends its
|
||||
@@ -546,12 +567,15 @@ Only machines with the invite can find the game:
|
||||
flood of requests can make players join more slowly. A player asks again
|
||||
for 90 seconds.
|
||||
- The host drops a player whose game runs faster than time (a speed hack)
|
||||
for ten seconds, and keeps that address out of its games. Each player
|
||||
sees who in red on the console. The host adds a line to `cheaters.txt`
|
||||
(beside `debug.txt`) with the address and hardware id of the player, and
|
||||
the Discord name and id that the game of the player told it (a player can
|
||||
change these). The host also bans the player: it adds the line to
|
||||
`bans.txt`, and refuses a machine whose address or hardware id is in it.
|
||||
for ten seconds. Each player sees who in red on the console. The host
|
||||
adds a line to `cheaters.txt` (beside `debug.txt`) with the address and
|
||||
hardware id of the player, and the Discord name and id that the game of
|
||||
the player told it (a player can change these). If the messages on the
|
||||
player's connection were also ahead, the host keeps that address out of
|
||||
its games and bans the player: it adds the line to `bans.txt`, and refuses
|
||||
a machine whose address or hardware id is in it. If only the player's
|
||||
datagrams were ahead, the player can join again: another machine can send
|
||||
datagrams with the player's address.
|
||||
- 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
|
||||
@@ -627,6 +651,7 @@ The game sends the activity only to a Discord client of the same user.
|
||||
| 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. |
|
||||
| Anti-aliasing | Off unless `display.anti_aliasing` is set (`src/d3d8_gl.c`, `src/xgpu_post.c`). FXAA (written in the port) and SMAA (`port/third_party/smaa`, MIT licensed, at its HIGH preset, compiled as GLSL) are passes over the 3D view of each window, after the lens flares and before the HUD and the menus (`render/render.c`). Their programs are built when the setting is chosen. Supersampling draws the render targets the size of the screen at two times the resolution in each direction, and the display blit scales them down. Multisampling draws the back buffer and its depth buffer into multisampled renderbuffers, and with them any target that is drawn together with one of them (a mirror's view, in the secondary target with the back buffer's depth buffer), so that the attachments of a framebuffer are all multisampled or none is. A target's pixels are resolved into its texture before something reads the texture (as a texture, or at the display blit). Visibility tests count samples, divided by the samples of a pixel. An alpha-tested surface (foliage, grates) covers the samples of a pixel in proportion to its alpha past the reference (`gl_SampleMask`, not on Android). |
|
||||
| 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. |
|
||||
@@ -715,6 +740,8 @@ Other changes:
|
||||
| `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`). |
|
||||
| `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. |
|
||||
| `rasterizer/xbox/rasterizer_xbox_shadows.c` | With larger shadow maps (`display.shadow_resolution`), the blur's taps stay half a texel apart, and more passes widen it to cover the same part of the map as on the Xbox. |
|
||||
| `render/render.c` | The 3D view of each window is antialiased before the HUD is drawn (`display.anti_aliasing`). |
|
||||
| `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. |
|
||||
|
||||
|
||||
+184
-30
@@ -7,9 +7,15 @@ 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.
|
||||
each squad is that many times as large, for any number of players.
|
||||
|
||||
Extra enemies are placed in widening rings around the squad's starting
|
||||
locations, on a spot on the same floor, reachable without passing through
|
||||
a wall, crate or machine, with room to stand and nobody already there. A
|
||||
starting location with no room left passes its enemy to the squad's next
|
||||
one; if none has room, the enemy goes where extra enemies went before free
|
||||
ground was looked for (rings around the starting location, whoever stands
|
||||
there), so a squad still gets every enemy the setting asks for.
|
||||
|
||||
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
|
||||
@@ -60,9 +66,12 @@ enum
|
||||
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,
|
||||
/* the rings tried around a starting location, the first holding
|
||||
SPREAD_PLACES_PER_RING places and each further one that many more */
|
||||
SPREAD_RINGS = 5,
|
||||
SPREAD_PLACES_PER_RING = 6,
|
||||
/* the rings coop_enemies_fallback_position tries, a place on each */
|
||||
FALLBACK_RINGS = 8,
|
||||
MAXIMUM_RIDING_VEHICLES = 32,
|
||||
MAXIMUM_SEATED_RIDERS = 16,
|
||||
MAXIMUM_KEPT_RIDERS = 512,
|
||||
@@ -71,11 +80,25 @@ enum
|
||||
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 */
|
||||
/* (coop_enemies.h's count of the places on all the rings) */
|
||||
typedef char coop_enemies_spread_spots_assert[
|
||||
COOP_ENEMIES_SPREAD_SPOTS == SPREAD_PLACES_PER_RING * SPREAD_RINGS * (SPREAD_RINGS + 1) / 2 ? 1 : -1];
|
||||
|
||||
/* distance between rings */
|
||||
#define SPREAD_SPACING 0.8f
|
||||
/* height above the floor at which the way to a spot is checked */
|
||||
#define SPREAD_STEP_HEIGHT 0.6f
|
||||
#define SPREAD_GROUND_DEPTH 2.0f
|
||||
/* how far a spot's floor may be above or below the starting location's:
|
||||
a step, not a ledge */
|
||||
#define SPREAD_FLOOR_DIFFERENCE 0.5f
|
||||
/* room a spot needs above its floor */
|
||||
#define SPREAD_HEADROOM 1.6f
|
||||
/* how close across the ground a spot may be to a biped already there
|
||||
(about two bipeds' collision radius), and how far around it to look */
|
||||
#define SPREAD_CLEARANCE 0.6f
|
||||
#define SPREAD_SEARCH_RADIUS 1.0f
|
||||
/* how far below a fallback place the ground is looked for */
|
||||
#define FALLBACK_GROUND_DEPTH 2.0f
|
||||
/* a kept rider's place beside a rider who got out, each a little apart */
|
||||
#define RIDER_SPACING 0.5f
|
||||
|
||||
@@ -160,35 +183,105 @@ static short coop_enemies_player_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)
|
||||
/* what a spot is tested against: the level, and the objects that block
|
||||
the way (crates and other scenery, machines, vehicles) */
|
||||
#define SPREAD_COLLISION_FLAGS (FLAG(_collision_test_structure_bit) | FLAG(_collision_test_objects_bit) | \
|
||||
_collision_test_objects_sight_blocking_flags | FLAG(_collision_test_front_facing_surfaces_bit) | \
|
||||
FLAG(_collision_test_back_facing_surfaces_bit))
|
||||
|
||||
/* whether anything is in the way from `from`, along `vector` */
|
||||
static boolean coop_enemies_blocked(
|
||||
real_point3d const *from,
|
||||
real_vector3d const *vector)
|
||||
{
|
||||
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);
|
||||
return collision_test_vector(SPREAD_COLLISION_FLAGS, from, vector, NONE, &collision);
|
||||
}
|
||||
|
||||
/* the ground below a point, within SPREAD_GROUND_DEPTH of it */
|
||||
static boolean coop_enemies_ground(
|
||||
/* the floor under a point, looked for from SPREAD_STEP_HEIGHT above it to
|
||||
SPREAD_FLOOR_DIFFERENCE below it */
|
||||
static boolean coop_enemies_floor(
|
||||
real_point3d const *point,
|
||||
real_point3d *ground)
|
||||
real_point3d *floor)
|
||||
{
|
||||
struct collision_result collision;
|
||||
real_vector3d vector = { 0.0f, 0.0f, -(SPREAD_STEP_HEIGHT + SPREAD_GROUND_DEPTH) };
|
||||
real_point3d from = *point;
|
||||
real_vector3d down = { 0.0f, 0.0f, -(2.0f * SPREAD_STEP_HEIGHT + SPREAD_FLOOR_DIFFERENCE) };
|
||||
|
||||
if (!collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
|
||||
point, &vector, NONE, &collision))
|
||||
from.z += SPREAD_STEP_HEIGHT;
|
||||
if (!collision_test_vector(SPREAD_COLLISION_FLAGS, &from, &down, NONE, &collision))
|
||||
return FALSE;
|
||||
*floor = collision.point;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* whether a biped stands within SPREAD_CLEARANCE of a spot on the floor,
|
||||
or the spot is inside a vehicle */
|
||||
static boolean coop_enemies_occupied(
|
||||
real_point3d const *floor)
|
||||
{
|
||||
real_point3d body = { floor->x, floor->y, floor->z + SPREAD_STEP_HEIGHT };
|
||||
struct location location;
|
||||
long object_indices[32];
|
||||
short object_count;
|
||||
short index;
|
||||
|
||||
scenario_location_from_point(&location, &body);
|
||||
if (location.cluster_index == NONE)
|
||||
return TRUE;
|
||||
object_count = objects_in_sphere(0, _object_mask_biped | _object_mask_vehicle, &location, &body,
|
||||
SPREAD_SEARCH_RADIUS, object_indices, NUMBEROF(object_indices));
|
||||
for (index = 0; index < object_count; index++)
|
||||
{
|
||||
struct object_datum *object = object_get(object_indices[index]);
|
||||
|
||||
if (object->object.type == _object_type_vehicle)
|
||||
{
|
||||
if (point_in_sphere(&body, &object->object.bounding_sphere_center, object->object.bounding_sphere_radius))
|
||||
return TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
real dx = object->object.position.x - floor->x;
|
||||
real dy = object->object.position.y - floor->y;
|
||||
|
||||
if (dx * dx + dy * dy < SPREAD_CLEARANCE * SPREAD_CLEARANCE)
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Whether an enemy can stand at `spot`: reachable from `from` (beside the
|
||||
starting location) with nothing in the way, a floor within a step of
|
||||
`floor_height` (the starting location's), room above it, and nobody there.
|
||||
The floor goes in `position`. */
|
||||
static boolean coop_enemies_spot_free(
|
||||
real_point3d const *from,
|
||||
real_point3d const *spot,
|
||||
real floor_height,
|
||||
real_point3d *position)
|
||||
{
|
||||
/* (level, at step height: down to the starting location's floor, a spot
|
||||
whose floor is a little higher would end the look inside it) */
|
||||
real_vector3d way = { spot->x - from->x, spot->y - from->y, 0.0f };
|
||||
real_vector3d up = { 0.0f, 0.0f, SPREAD_HEADROOM };
|
||||
real_point3d floor;
|
||||
real_point3d feet;
|
||||
|
||||
if (coop_enemies_blocked(from, &way) || !coop_enemies_floor(spot, &floor) ||
|
||||
fabsf(floor.z - floor_height) > SPREAD_FLOOR_DIFFERENCE)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
*ground = collision.point;
|
||||
/* (headroom is checked from just above the floor, so the floor itself
|
||||
doesn't count as in the way) */
|
||||
feet = floor;
|
||||
feet.z += 0.1f;
|
||||
if (coop_enemies_blocked(&feet, &up) || coop_enemies_occupied(&floor))
|
||||
return FALSE;
|
||||
*position = floor;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -263,6 +356,7 @@ static void coop_enemies_rider_place(
|
||||
short out_count = 0;
|
||||
short index, round;
|
||||
real angle;
|
||||
real_point3d spread;
|
||||
|
||||
*position = object->object.position;
|
||||
*facing = (real)atan2(object->object.forward.j, object->object.forward.i);
|
||||
@@ -299,7 +393,13 @@ static void coop_enemies_rider_place(
|
||||
}
|
||||
}
|
||||
}
|
||||
/* (never on the rider's own place: a ring about it, further each round) */
|
||||
/* free ground around that spot, as the squad's own extra enemies get;
|
||||
failing that, a ring around it, further out each round */
|
||||
if (coop_enemies_spread_position(position, (short)(number + 1), NULL, &spread))
|
||||
{
|
||||
*position = spread;
|
||||
return;
|
||||
}
|
||||
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);
|
||||
@@ -357,6 +457,48 @@ short coop_enemies_extra_count(
|
||||
}
|
||||
|
||||
boolean coop_enemies_spread_position(
|
||||
real_point3d const *origin,
|
||||
short number,
|
||||
long *taken,
|
||||
real_point3d *position)
|
||||
{
|
||||
real_point3d from = *origin;
|
||||
real_point3d floor;
|
||||
real floor_height = coop_enemies_floor(origin, &floor) ? floor.z : origin->z;
|
||||
short ring, first_spot = 0;
|
||||
|
||||
from.z = floor_height + SPREAD_STEP_HEIGHT;
|
||||
for (ring = 0; ring < SPREAD_RINGS; ring++)
|
||||
{
|
||||
short place_count = (short)(SPREAD_PLACES_PER_RING * (ring + 1));
|
||||
real radius = SPREAD_SPACING * (real)(ring + 1);
|
||||
short place;
|
||||
|
||||
/* (each enemy starts at a different place on the ring, so a squad
|
||||
spreads all the way round rather than filling one side first) */
|
||||
for (place = 0; place < place_count; place++)
|
||||
{
|
||||
short around = (short)((number + place) % place_count);
|
||||
short spot_index = (short)(first_spot + around);
|
||||
real angle = (real)around * (_pi * 2.0f / (real)place_count);
|
||||
real_point3d spot = from;
|
||||
|
||||
if (taken && BIT_VECTOR_TEST_FLAG(taken, spot_index))
|
||||
continue;
|
||||
spot.x += (real)cos(angle) * radius;
|
||||
spot.y += (real)sin(angle) * radius;
|
||||
spot.z = floor_height;
|
||||
if (coop_enemies_spot_free(&from, &spot, floor_height, position))
|
||||
return TRUE;
|
||||
if (taken)
|
||||
BIT_VECTOR_SET_FLAG(taken, spot_index, TRUE);
|
||||
}
|
||||
first_spot = (short)(first_spot + place_count);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
boolean coop_enemies_fallback_position(
|
||||
real_point3d const *origin,
|
||||
short number,
|
||||
real_point3d *position)
|
||||
@@ -367,18 +509,30 @@ boolean coop_enemies_spread_position(
|
||||
short attempt;
|
||||
|
||||
from.z += SPREAD_STEP_HEIGHT;
|
||||
for (attempt = 0; attempt < SPREAD_ATTEMPTS; attempt++, ring++)
|
||||
for (attempt = 0; attempt < FALLBACK_RINGS; 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;
|
||||
real_vector3d way = { 0.0f, 0.0f, 0.0f };
|
||||
real_vector3d down = { 0.0f, 0.0f, -(SPREAD_STEP_HEIGHT + FALLBACK_GROUND_DEPTH) };
|
||||
struct collision_result collision;
|
||||
|
||||
to.x += (real)cos(angle) * radius;
|
||||
to.y += (real)sin(angle) * radius;
|
||||
if (coop_enemies_open(&from, &to) && coop_enemies_ground(&to, position))
|
||||
way.i = (real)cos(angle) * radius;
|
||||
way.j = (real)sin(angle) * radius;
|
||||
to.x += way.i;
|
||||
to.y += way.j;
|
||||
/* (the level only: the way open, and ground below) */
|
||||
if (!collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit) |
|
||||
FLAG(_collision_test_back_facing_surfaces_bit), &from, &way, NONE, &collision) &&
|
||||
collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
|
||||
&to, &down, NONE, &collision))
|
||||
{
|
||||
*position = collision.point;
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -21,10 +21,27 @@ 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) */
|
||||
/* the places coop_enemies_spread_position tries around a starting location */
|
||||
#define COOP_ENEMIES_SPREAD_SPOTS 90
|
||||
|
||||
/* encounters.c: finds a spot for a squad's `number`th extra enemy
|
||||
(counting from 1) around the starting location `origin`, on its floor,
|
||||
clear of walls and objects, with room to stand and nobody there. FALSE if
|
||||
there's no room. `taken` (COOP_ENEMIES_SPREAD_SPOTS bits, or NULL) marks the
|
||||
places found taken, which stay so while the squad's enemies are placed: they
|
||||
aren't tried again. */
|
||||
boolean coop_enemies_spread_position(
|
||||
real_point3d const *origin,
|
||||
short number,
|
||||
long *taken,
|
||||
real_point3d *position);
|
||||
/* encounters.c: where the `number`th extra enemy (counting from 1) goes
|
||||
when no starting location has room: a place on rings around `origin` with
|
||||
the way open and ground below, whoever stands there (as extra enemies were
|
||||
placed before free ground was looked for), so the squad still gets every
|
||||
enemy the setting asks for. FALSE if none (the starting location itself
|
||||
then) */
|
||||
boolean coop_enemies_fallback_position(
|
||||
real_point3d const *origin,
|
||||
short number,
|
||||
real_point3d *position);
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
COOP_SCRIPTS.C
|
||||
|
||||
Campaign scripts made to work for every player in network co-op.
|
||||
|
||||
The campaign's scripts were written for one or two players. Only the host
|
||||
runs them (game.c), against every player's unit, so most of what they do
|
||||
already happens for everyone: a trigger volume tested against (players)
|
||||
fires for whoever walks into it, and what it starts is sent to the clients
|
||||
(network_coop.c). Three habits of the scripts don't carry over:
|
||||
|
||||
- Waiting for every player at once (volume_test_objects_all on the
|
||||
players): the Maw's run to the Pillar of Autumn's bridge, a30's Pelican
|
||||
pickups. Split-screen players stand together; network players don't, so
|
||||
the wait would never end. In co-op one player arriving is enough.
|
||||
- Waiting until it's safe (game_safe_to_save) before a cutscene: the
|
||||
game's test fails while any enemy sees any player. In co-op one player
|
||||
out of danger is enough.
|
||||
- Naming players one at a time: the scripts reach the players through
|
||||
player0 and player1 (the first two in the players list), to put them in
|
||||
a Pelican, take them out of it, stage them for a cutscene or give them a
|
||||
starting profile. Everyone past the second would be left out, so they
|
||||
follow player0: wherever the scripts put player0, they go too.
|
||||
*/
|
||||
|
||||
/* ---------- headers */
|
||||
|
||||
#include "cseries.h"
|
||||
#include "ai/ai.h"
|
||||
#include "game/game.h"
|
||||
#include "game/players.h"
|
||||
#include "hs/object_lists.h"
|
||||
#include "objects/objects.h"
|
||||
#include "objects/object_types.h"
|
||||
#include "units/bipeds.h"
|
||||
#include "units/units.h"
|
||||
|
||||
#include "coop_scripts.h"
|
||||
#include "network_coop.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
/* players the scripts name: player0 and player1 */
|
||||
#define NAMED_PLAYERS 2
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static boolean coop_scripts_host(
|
||||
void)
|
||||
{
|
||||
return game_connection() == _game_connection_network_server && network_coop_active();
|
||||
}
|
||||
|
||||
/* the seat label to look for when boarding a follower: player0's own up to
|
||||
and including "rider" (Pelicans have "p-riderlf", "p-riderrf", "p-riderlb"
|
||||
and so on), else up to the '-' ("b-driver" gives "b-") */
|
||||
static void follower_seat_name(
|
||||
char const *seat_name,
|
||||
char *follower_seat,
|
||||
size_t size)
|
||||
{
|
||||
char *rider;
|
||||
char *dash;
|
||||
|
||||
/* (lower case: vehicle_scripting_find_available_seats compares it with
|
||||
lowered seat labels) */
|
||||
snprintf(follower_seat, size, "%s", seat_name);
|
||||
strlwr(follower_seat);
|
||||
rider = strstr(follower_seat, "rider");
|
||||
dash = strchr(follower_seat, '-');
|
||||
if (rider)
|
||||
rider[strlen("rider")] = 0;
|
||||
else if (dash)
|
||||
dash[1] = 0;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
boolean coop_scripts_any_player_will_do(
|
||||
long object_list_index)
|
||||
{
|
||||
long reference_index;
|
||||
long object_index;
|
||||
boolean any = FALSE;
|
||||
|
||||
if (!coop_scripts_host())
|
||||
return FALSE;
|
||||
for (object_index = object_list_get_first(object_list_index, &reference_index);
|
||||
object_index != NONE;
|
||||
object_index = object_list_get_next(object_list_index, &reference_index))
|
||||
{
|
||||
if (player_index_from_unit_index(object_index) == NONE)
|
||||
return FALSE;
|
||||
any = TRUE;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
/* the units of the players following player0, if `unit_index` is
|
||||
player0's; how many */
|
||||
static short players_following(
|
||||
long unit_index,
|
||||
long *unit_indices,
|
||||
short maximum_count)
|
||||
{
|
||||
struct data_iterator iterator;
|
||||
struct player_datum *player;
|
||||
short position = 0;
|
||||
short count = 0;
|
||||
|
||||
if (unit_index == NONE || !coop_scripts_host())
|
||||
return 0;
|
||||
/* (in the order (players) lists them: player0 is the first with a unit) */
|
||||
data_iterator_new(&iterator, player_data);
|
||||
while ((player = data_iterator_next(&iterator)) != NULL)
|
||||
{
|
||||
if (player->unit_index == NONE)
|
||||
continue;
|
||||
if (position == 0 && player->unit_index != unit_index)
|
||||
return 0;
|
||||
if (position >= NAMED_PLAYERS && count < maximum_count)
|
||||
unit_indices[count++] = player->unit_index;
|
||||
position++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void coop_scripts_teleport_followers(
|
||||
long unit_index)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
player_teleport(player_index_from_unit_index(followers[index]), unit_index,
|
||||
&object_get(unit_index)->object.position);
|
||||
}
|
||||
}
|
||||
|
||||
void coop_scripts_suspend_followers(
|
||||
long unit_index,
|
||||
boolean suspended)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
unit_scripting_suspended(followers[index], suspended);
|
||||
}
|
||||
|
||||
void coop_scripts_exit_followers(
|
||||
long unit_index,
|
||||
long vehicle_index)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
if (object_get(followers[index])->object.parent_object_index == vehicle_index)
|
||||
unit_scripting_exit_vehicle(followers[index]);
|
||||
}
|
||||
}
|
||||
|
||||
void coop_scripts_board_followers(
|
||||
long unit_index,
|
||||
long vehicle_index,
|
||||
char const *seat_name)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
char follower_seat[64];
|
||||
short index;
|
||||
|
||||
if (count == 0 || vehicle_index == NONE)
|
||||
return;
|
||||
follower_seat_name(seat_name, follower_seat, sizeof(follower_seat));
|
||||
/* (each list freed at once: there are few, and the scripts' own are only
|
||||
collected after the tick's threads have run) */
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
long list_index = object_list_new();
|
||||
|
||||
if (list_index == NONE)
|
||||
break;
|
||||
object_list_add(list_index, followers[index]);
|
||||
vehicle_scripting_load_magic(vehicle_index, follower_seat, list_index);
|
||||
object_list_delete(list_index);
|
||||
}
|
||||
}
|
||||
|
||||
boolean coop_scripts_safe_to_save(
|
||||
void)
|
||||
{
|
||||
struct data_iterator iterator;
|
||||
struct player_datum *player;
|
||||
|
||||
if (game_safe_to_save())
|
||||
return TRUE;
|
||||
if (!coop_scripts_host())
|
||||
return FALSE;
|
||||
data_iterator_new(&iterator, player_data);
|
||||
while ((player = data_iterator_next(&iterator)) != NULL)
|
||||
{
|
||||
struct biped_datum *biped = biped_try_and_get(player->unit_index);
|
||||
|
||||
if (biped && !TEST_FLAG(biped->object.damage_flags, _object_dead_bit) &&
|
||||
!TEST_FLAG(biped->biped.flags, _biped_airborne_bit) && !ai_port_enemies_can_see_unit(player->unit_index))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void coop_scripts_player_add_equipment(
|
||||
long unit_index,
|
||||
short starting_profile_index,
|
||||
boolean reset_equipment)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
short index;
|
||||
|
||||
player_add_equipment(unit_index, starting_profile_index, reset_equipment);
|
||||
for (index = 0; index < count; index++)
|
||||
player_add_equipment(followers[index], starting_profile_index, reset_equipment);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
COOP_SCRIPTS.H
|
||||
|
||||
Campaign scripts made to work for every player in network co-op
|
||||
(coop_scripts.c).
|
||||
*/
|
||||
|
||||
#ifndef __COOP_SCRIPTS_H
|
||||
#define __COOP_SCRIPTS_H
|
||||
|
||||
/* ---------- prototypes */
|
||||
|
||||
/* hs_library_external.c's volume_test_objects_all: whether to count the
|
||||
list as inside once any of it is, because every object in it is a player */
|
||||
boolean coop_scripts_any_player_will_do(
|
||||
long object_list_index);
|
||||
|
||||
/* What the scripts do to player0, done to the players they can't name too
|
||||
(the "followers"). Each does nothing unless `unit_index` is player0's in a
|
||||
co-op host's game. */
|
||||
|
||||
/* hs_library_external.c's object_teleport: around player0 */
|
||||
void coop_scripts_teleport_followers(
|
||||
long unit_index);
|
||||
/* units.c's unit_suspended */
|
||||
void coop_scripts_suspend_followers(
|
||||
long unit_index,
|
||||
boolean suspended);
|
||||
/* units.c's unit_exit_vehicle: those riding player0's vehicle get out */
|
||||
void coop_scripts_exit_followers(
|
||||
long unit_index,
|
||||
long vehicle_index);
|
||||
/* units.c's vehicle_load_magic and unit_enter_vehicle: into the vehicle,
|
||||
each in a free seat of the same kind as player0's (`seat_name`) */
|
||||
void coop_scripts_board_followers(
|
||||
long unit_index,
|
||||
long vehicle_index,
|
||||
char const *seat_name);
|
||||
|
||||
/* hs.c's game_safe_to_save, for the scripts: the game's own test, or in
|
||||
co-op, any one player alive, on the ground and unseen by enemies. The
|
||||
scripts wait on it before a cutscene, and with players spread out a fight
|
||||
somewhere else would hold up the one a player has reached. Checkpoints
|
||||
keep the game's own test. */
|
||||
boolean coop_scripts_safe_to_save(
|
||||
void);
|
||||
|
||||
/* hs.c's player_add_equipment, for the scripts: gives the starting profile
|
||||
to player0's followers too */
|
||||
void coop_scripts_player_add_equipment(
|
||||
long unit_index,
|
||||
short starting_profile_index,
|
||||
boolean reset_equipment);
|
||||
|
||||
#endif
|
||||
@@ -1970,6 +1970,10 @@ Xbox's networking, run by the engine's port entry points
|
||||
#define KEY_MODIFIER_CONTROL_BIT 1
|
||||
#define LOBBY_NAME "pc\\main_menu\\multiplayer_type_select\\lobby\\lobby_screen"
|
||||
#define PREVIEW_NAME "pc\\main_menu\\multiplayer_type_select\\lobby\\preview_screen"
|
||||
/* the server browser's password screen (tools/port_settings.py), and the
|
||||
longest password */
|
||||
#define PASSWORD_NAME "pc\\main_menu\\multiplayer_type_select\\join_game\\password\\password_screen"
|
||||
#define PASSWORD_LENGTH 32
|
||||
/* the lobby's panel's lines (the gametype, the players, the countdown, the
|
||||
invite) */
|
||||
#define LOBBY_TEXT_LENGTH (ROW_TEXT_LENGTH * 4)
|
||||
@@ -2036,6 +2040,7 @@ boolean ui_widget_port_unjoin_player(struct widget_instance *widget, struct even
|
||||
boolean *widget_deleted);
|
||||
void network_game_server_port_set_settings(wchar_t const *name, long maximum_players);
|
||||
void network_game_server_port_set_cooperative_friendly_fire(short friendly_fire);
|
||||
void network_game_server_port_set_cooperative_player_collisions(boolean player_collisions);
|
||||
void *global_network_game_client_get(void);
|
||||
void *global_network_game_server_get(void);
|
||||
boolean network_game_is_splitscreen_local(void);
|
||||
@@ -2092,6 +2097,8 @@ static struct
|
||||
boolean cooperative_maximum_players_set;
|
||||
/* co-op's FRIENDLY FIRE shown (network.coop_friendly_fire's) */
|
||||
short cooperative_friendly_fire;
|
||||
/* co-op's PLAYER COLLISIONS shown, OFF (network.coop_player_collisions's) */
|
||||
boolean cooperative_no_player_collisions;
|
||||
/* the browser's games */
|
||||
struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES];
|
||||
short game_count, game_chosen;
|
||||
@@ -2104,6 +2111,9 @@ static struct
|
||||
/* Server Setup's LISTING: PRIVATE (each new game starts with
|
||||
network.host_public's choice: PUBLIC unless set otherwise) */
|
||||
boolean game_private;
|
||||
/* Server Setup's PASSWORD (a public internet game's; empty: none), kept
|
||||
while the game runs, never written down */
|
||||
char game_password[PASSWORD_LENGTH + 1];
|
||||
} multiplayer = { 0, 0, 0, { 0 }, 0, { 0 }, 0, 0, 0, 0, { 0 }, NUMBEROF(maximum_players) - 1 };
|
||||
|
||||
/* ---- a text field (Direct Link's link, the game's name): the keyboard
|
||||
@@ -2116,6 +2126,8 @@ static struct
|
||||
char before[TEXT_FIELD_LENGTH];
|
||||
short maximum;
|
||||
void (*done)(char const *text);
|
||||
/* a password's: shown as stars (text_field_begin_masked) */
|
||||
boolean masked;
|
||||
} text_field;
|
||||
|
||||
/* when the field was last shown (a field not shown for a while is let go
|
||||
@@ -2161,6 +2173,7 @@ static void text_field_begin(struct widget_instance *row, char const *text, shor
|
||||
snprintf(text_field.before, sizeof(text_field.before), "%s", text);
|
||||
text_field.maximum = (short)MIN(maximum, TEXT_FIELD_LENGTH - 1);
|
||||
text_field.done = done;
|
||||
text_field.masked = FALSE;
|
||||
text_field_shown_time = system_milliseconds();
|
||||
while (input_get_key(&key))
|
||||
;
|
||||
@@ -2171,6 +2184,14 @@ static void text_field_begin(struct widget_instance *row, char const *text, shor
|
||||
#endif
|
||||
}
|
||||
|
||||
/* a password's field: as text_field_begin, its text shown as stars */
|
||||
static void text_field_begin_masked(struct widget_instance *row, char const *text, short maximum,
|
||||
void (*done)(char const *text))
|
||||
{
|
||||
text_field_begin(row, text, maximum, done);
|
||||
text_field.masked = TRUE;
|
||||
}
|
||||
|
||||
static void text_field_end(boolean keep)
|
||||
{
|
||||
void (*done)(char const *text) = text_field.done;
|
||||
@@ -2259,7 +2280,7 @@ static void text_field_show(struct widget_instance *value, char const *text, boo
|
||||
text = text_field.text;
|
||||
}
|
||||
for (index = 0; text[index] && index < TEXT_FIELD_LENGTH; index++)
|
||||
shown[index] = (wchar_t)(unsigned char)text[index];
|
||||
shown[index] = editing && text_field.masked ? L'*' : (wchar_t)(unsigned char)text[index];
|
||||
if (editing && (system_milliseconds() / 500) % 2)
|
||||
shown[index++] = L'_';
|
||||
shown[index] = 0;
|
||||
@@ -2643,6 +2664,8 @@ help_strings, tools/port_settings.py) */
|
||||
#define COOPERATIVE_EXTRA_ENEMIES_HELP 16
|
||||
#define COOPERATIVE_ENEMIES_PER_PLAYER_HELP 19
|
||||
#define COOPERATIVE_ENEMIES_MULTIPLIER_HELP 20
|
||||
#define COOPERATIVE_PLAYER_COLLISIONS_HELP 21
|
||||
#define SERVER_PASSWORD_HELP 23
|
||||
|
||||
/* co-op's EXTRA ENEMIES' choices (port_settings.COOP_ENEMIES_MODES, in this
|
||||
order): its amount's row is the choice's */
|
||||
@@ -2806,6 +2829,18 @@ static void server_settings_update(struct widget_instance *list)
|
||||
help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_MULTIPLIER_HELP;
|
||||
}
|
||||
}
|
||||
/* ... and its PLAYER COLLISIONS, below them: ON or OFF, its help its
|
||||
choice's */
|
||||
visible_set(named(list, "op_player_collisions", 0), cooperative);
|
||||
if (cooperative && (spinner = named(list, "player_collisions_spinner", 0)) != NULL)
|
||||
{
|
||||
short choice = (short)PIN(spinner->parameters.list.selected_index, 0, 1);
|
||||
struct widget_instance *help = list->parameters.list.extended_description;
|
||||
|
||||
multiplayer.cooperative_no_player_collisions = choice == 1;
|
||||
if (help && list->focused_child == named(list, "op_player_collisions", 0))
|
||||
help->parameters.text_box.string_list_index = (short)(COOPERATIVE_PLAYER_COLLISIONS_HELP + choice);
|
||||
}
|
||||
}
|
||||
/* LISTING (an internet game's): PUBLIC, listed in everyone's server
|
||||
browser, or PRIVATE, for this game. Its help is its choice's */
|
||||
@@ -2821,6 +2856,41 @@ static void server_settings_update(struct widget_instance *list)
|
||||
if (help && list->focused_child == named(list, "op_listing", 0))
|
||||
help->parameters.text_box.string_list_index = (short)(10 + spinner->parameters.list.selected_index);
|
||||
}
|
||||
/* PASSWORD (a PUBLIC internet game's): its stars, NONE if it has none */
|
||||
row = named(list, "op_password", 0);
|
||||
visible_set(row, named(list, "op_listing", 0) && named(list, "op_listing", 0)->visible &&
|
||||
!server_settings_private());
|
||||
if (row && !row->visible && text_field_editing(row))
|
||||
text_field_end(FALSE);
|
||||
if (row && row->visible)
|
||||
{
|
||||
struct widget_instance *help = list->parameters.list.extended_description;
|
||||
short index;
|
||||
|
||||
for (index = 0; multiplayer.game_password[index] && index < (short)sizeof(text) - 1; index++)
|
||||
text[index] = '*';
|
||||
text[index] = 0;
|
||||
text_field_show(named(list, "password_value", 0), index ? text : "NONE", text_field_editing(row));
|
||||
if (help && list->focused_child == row)
|
||||
help->parameters.text_box.string_list_index = SERVER_PASSWORD_HELP;
|
||||
}
|
||||
}
|
||||
|
||||
static void server_password_done(char const *text)
|
||||
{
|
||||
snprintf(multiplayer.game_password, sizeof(multiplayer.game_password), "%s", text);
|
||||
}
|
||||
|
||||
/* "ss edit server password" (its row's A: begins, or ends) */
|
||||
static boolean server_password_edit(struct widget_instance *row)
|
||||
{
|
||||
if (text_field_editing(row))
|
||||
{
|
||||
text_field_end(TRUE);
|
||||
return TRUE;
|
||||
}
|
||||
text_field_begin_masked(row, multiplayer.game_password, PASSWORD_LENGTH, server_password_done);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* "ss edit server name" (its row's A: begins, or ends) */
|
||||
@@ -2857,12 +2927,16 @@ static boolean server_start(void)
|
||||
text_field_end(TRUE);
|
||||
network_game_server_port_set_settings(multiplayer.game_name,
|
||||
maximum_players[PIN(*server_settings_maximum_players_index(), 0, NUMBEROF(maximum_players) - 1)]);
|
||||
/* (its listing's password: a PUBLIC internet game's, else none) */
|
||||
p2p_set_hosting_password(multiplayer.mode == _multiplayer_mode_host_internet && !server_settings_private() ?
|
||||
multiplayer.game_password : NULL);
|
||||
/* the gametype as Server Setup's options left it (co-op keeps its own,
|
||||
and its FRIENDLY FIRE) */
|
||||
if (hosting_cooperative())
|
||||
{
|
||||
gametype_setup_end();
|
||||
network_game_server_port_set_cooperative_friendly_fire(multiplayer.cooperative_friendly_fire);
|
||||
network_game_server_port_set_cooperative_player_collisions(!multiplayer.cooperative_no_player_collisions);
|
||||
}
|
||||
else if (!gametype_setup_apply())
|
||||
return campaign_fail();
|
||||
@@ -3123,6 +3197,11 @@ static struct
|
||||
/* the last that failed, and when */
|
||||
char failed_name[P2P_LISTING_NAME_SIZE + 1];
|
||||
unsigned long failed_time;
|
||||
/* a game with a password chosen: the password screen's (its own copy:
|
||||
the rows' are read again each frame), and a join it began, which the
|
||||
browser takes up again as it comes back */
|
||||
struct p2p_listing password_game;
|
||||
boolean password_joined;
|
||||
} lobby_browser;
|
||||
|
||||
static void lobby_browser_begin(void)
|
||||
@@ -3131,7 +3210,10 @@ static void lobby_browser_begin(void)
|
||||
lobby_browser.first = 0;
|
||||
lobby_browser.chosen = 0;
|
||||
lobby_browser.shown = FALSE;
|
||||
lobby_browser.joining = lobby_browser.ready = FALSE;
|
||||
/* (going on with the join the password screen began) */
|
||||
lobby_browser.joining = lobby_browser.password_joined;
|
||||
lobby_browser.ready = FALSE;
|
||||
lobby_browser.password_joined = FALSE;
|
||||
lobby_browser.begin_time = system_milliseconds();
|
||||
p2p_lobby_browse(TRUE);
|
||||
}
|
||||
@@ -3279,8 +3361,18 @@ static void lobby_browser_update(struct widget_instance *list)
|
||||
usnprintf(text, ROW_TEXT_LENGTH - 1, L"%d/%d", game->player_count, game->maximum_player_count);
|
||||
text_set(named(row, "server_item_players", 0), text);
|
||||
/* (no ping yet: its host is reached only on joining) */
|
||||
text_set(named(row, "server_item_ping", 0), game->failed ? L"FAILED" : game->in_progress ? L"LIVE" : L"-");
|
||||
visible_set(named(row, "server_item_locked", 0), !game->open);
|
||||
text_set(named(row, "server_item_ping", 0), game->failed ? L"FAILED" : !game->open ? L"CLOSED" :
|
||||
game->in_progress ? L"LIVE" : L"-");
|
||||
/* (the lock: a game with a password. Its bitmap's first frame is
|
||||
empty, the PC version's for no lock; the next is the lock, in the
|
||||
menus' blue) */
|
||||
{
|
||||
struct widget_instance *lock = named(row, "server_item_locked", 0);
|
||||
|
||||
visible_set(lock, game->locked);
|
||||
if (lock)
|
||||
lock->animation.current_frame_index = 1;
|
||||
}
|
||||
visible_set(named(row, "server_item_dedicated", 0), FALSE);
|
||||
visible_set(named(row, "server_item_classic", 0), FALSE);
|
||||
}
|
||||
@@ -3355,9 +3447,10 @@ static void lobby_browser_update(struct widget_instance *list)
|
||||
wchar_t gametype[P2P_LISTING_GAMETYPE_SIZE + 1];
|
||||
|
||||
text_to_wide(game->gametype, gametype, NUMBEROF(gametype));
|
||||
usnprintf(text, NUMBEROF(text) - 1, L"%s: %d %s of %d%s", gametype, game->player_count,
|
||||
usnprintf(text, NUMBEROF(text) - 1, L"%s: %d %s of %d%s%s", gametype, game->player_count,
|
||||
game->player_count == 1 ? L"player" : L"players", game->maximum_player_count,
|
||||
!game->open ? L", full or starting" : game->in_progress ? L", under way" : L"");
|
||||
!game->open ? L", full or starting" : game->in_progress ? L", under way" : L"",
|
||||
game->locked ? L", password" : L"");
|
||||
}
|
||||
else
|
||||
text[0] = 0;
|
||||
@@ -3387,9 +3480,26 @@ static void lobby_browser_update(struct widget_instance *list)
|
||||
browser_focus(list);
|
||||
}
|
||||
|
||||
/* the game's host reached by its invite (joined, once it is reached:
|
||||
lobby_browser_update) */
|
||||
static boolean lobby_browser_join(struct p2p_listing const *game, short controller)
|
||||
{
|
||||
if (!p2p_join_invite(game->invite))
|
||||
return FALSE;
|
||||
csmemcpy(lobby_browser.identifier, game->identifier, sizeof(lobby_browser.identifier));
|
||||
csmemcpy(lobby_browser.name, game->name, sizeof(lobby_browser.name));
|
||||
lobby_browser.joining = TRUE;
|
||||
lobby_browser.ready = FALSE;
|
||||
lobby_browser.join_time = system_milliseconds();
|
||||
lobby_browser.controller = controller;
|
||||
ui_play_audio_feedback_sound(SOUND_FORWARD);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* the server browser's rows and buttons: Refresh asks the hosts again; a
|
||||
row (or Join) joins its game's invite; the A press posted once its host is
|
||||
reached joins its game (or shows its lobby, if under way) */
|
||||
row (or Join) joins its game's invite, a game with a password once its
|
||||
screen has it; the A press posted once its host is reached joins its game
|
||||
(or shows its lobby, if under way) */
|
||||
static boolean lobby_browser_select(struct widget_instance *widget, short controller, short row,
|
||||
boolean *widget_deleted)
|
||||
{
|
||||
@@ -3434,15 +3544,117 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
|
||||
if (index >= lobby_browser.count)
|
||||
return campaign_fail();
|
||||
game = &lobby_browser.games[index];
|
||||
if (!game->open || !config_boolean("network.online") || !p2p_join_invite(game->invite))
|
||||
if (!game->open || !config_boolean("network.online"))
|
||||
return campaign_fail();
|
||||
csmemcpy(lobby_browser.identifier, game->identifier, sizeof(lobby_browser.identifier));
|
||||
csmemcpy(lobby_browser.name, game->name, sizeof(lobby_browser.name));
|
||||
lobby_browser.joining = TRUE;
|
||||
lobby_browser.ready = FALSE;
|
||||
lobby_browser.join_time = system_milliseconds();
|
||||
lobby_browser.controller = controller;
|
||||
ui_play_audio_feedback_sound(SOUND_FORWARD);
|
||||
/* (a game with a password: its screen asks for it, then joins) */
|
||||
if (game->locked)
|
||||
{
|
||||
lobby_browser.password_game = *game;
|
||||
lobby_browser.controller = controller;
|
||||
return ui_widget_port_open(widget, PASSWORD_NAME, widget_deleted);
|
||||
}
|
||||
return lobby_browser_join(game, controller) || campaign_fail();
|
||||
}
|
||||
|
||||
/* ---- the server browser's password screen (a game with a password
|
||||
chosen: lobby_browser_select, tools/port_settings.py): its password typed at
|
||||
once, shown as stars; JOIN GAME, or Enter, joins the game with it if it is
|
||||
the game's (the browser, back again, goes on with the join), else its help
|
||||
says it is not and the typing begins again */
|
||||
|
||||
/* its help's lines (tools/port_settings.py's) */
|
||||
enum
|
||||
{
|
||||
_password_help_ask = 1,
|
||||
_password_help_wrong,
|
||||
_password_help_failed,
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
char text[PASSWORD_LENGTH + 1];
|
||||
short help;
|
||||
} password_screen;
|
||||
|
||||
static void password_screen_done(char const *text)
|
||||
{
|
||||
snprintf(password_screen.text, sizeof(password_screen.text), "%s", text);
|
||||
}
|
||||
|
||||
/* "port password init" */
|
||||
static boolean password_screen_initialize(struct widget_instance *list)
|
||||
{
|
||||
struct widget_instance *row = named(list, "op_password", 0);
|
||||
|
||||
password_screen.text[0] = 0;
|
||||
password_screen.help = _password_help_ask;
|
||||
if (row)
|
||||
text_field_begin_masked(row, "", PASSWORD_LENGTH, password_screen_done);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* "port password update" */
|
||||
static void password_screen_update(struct widget_instance *list)
|
||||
{
|
||||
struct widget_instance *help = list->parameters.list.extended_description;
|
||||
char stars[PASSWORD_LENGTH + 1];
|
||||
short index;
|
||||
|
||||
for (index = 0; password_screen.text[index] && index < PASSWORD_LENGTH; index++)
|
||||
stars[index] = '*';
|
||||
stars[index] = 0;
|
||||
text_field_show(named(list, "password_value", 0), stars, text_field_editing(named(list, "op_password", 0)));
|
||||
if (help)
|
||||
help->parameters.text_box.string_list_index = password_screen.help;
|
||||
}
|
||||
|
||||
/* "port password join" (JOIN GAME, and Enter: password_screen_edit) */
|
||||
static boolean password_screen_join(struct widget_instance *widget, boolean *widget_deleted)
|
||||
{
|
||||
struct p2p_listing game = lobby_browser.password_game;
|
||||
struct widget_instance *row = named(screen_of(widget), "op_password", 0);
|
||||
|
||||
if (text_field_editing(NULL))
|
||||
text_field_end(TRUE);
|
||||
if (!p2p_listing_unlock(&game, password_screen.text))
|
||||
{
|
||||
password_screen.help = _password_help_wrong;
|
||||
password_screen.text[0] = 0;
|
||||
if (row)
|
||||
text_field_begin_masked(row, "", PASSWORD_LENGTH, password_screen_done);
|
||||
return campaign_fail();
|
||||
}
|
||||
if (!lobby_browser_join(&game, lobby_browser.controller))
|
||||
{
|
||||
password_screen.help = _password_help_failed;
|
||||
return campaign_fail();
|
||||
}
|
||||
password_screen.text[0] = 0;
|
||||
lobby_browser.password_joined = TRUE;
|
||||
ui_widget_port_go_back(widget);
|
||||
*widget_deleted = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* "port password back" (B, Escape): back to the browser at once, though the
|
||||
password is being typed */
|
||||
static boolean password_screen_back(struct widget_instance *widget, boolean *widget_deleted)
|
||||
{
|
||||
if (text_field_editing(NULL))
|
||||
text_field_end(FALSE);
|
||||
password_screen.text[0] = 0;
|
||||
ui_widget_port_go_back(widget);
|
||||
*widget_deleted = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* "port password edit" (the field's row's A: the typing begins; Enter, its
|
||||
end, joins) */
|
||||
static boolean password_screen_edit(struct widget_instance *row, boolean *widget_deleted)
|
||||
{
|
||||
if (text_field_editing(row))
|
||||
return password_screen_join(row, widget_deleted);
|
||||
text_field_begin_masked(row, password_screen.text, PASSWORD_LENGTH, password_screen_done);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -4950,6 +5162,26 @@ boolean pc_menu_event_function_invoke(
|
||||
{
|
||||
return server_name_edit(widget);
|
||||
}
|
||||
else if (!strcmp(name, "ss edit server password"))
|
||||
{
|
||||
return server_password_edit(widget);
|
||||
}
|
||||
else if (!strcmp(name, "port password init"))
|
||||
{
|
||||
return password_screen_initialize(widget);
|
||||
}
|
||||
else if (!strcmp(name, "port password edit"))
|
||||
{
|
||||
return password_screen_edit(widget, widget_deleted);
|
||||
}
|
||||
else if (!strcmp(name, "port password join"))
|
||||
{
|
||||
return password_screen_join(widget, widget_deleted);
|
||||
}
|
||||
else if (!strcmp(name, "port password back"))
|
||||
{
|
||||
return password_screen_back(widget, widget_deleted);
|
||||
}
|
||||
else if (!strcmp(name, "ss copy invite"))
|
||||
{
|
||||
return invite_copy();
|
||||
@@ -5129,6 +5361,8 @@ void pc_menu_game_data_function_invoke(
|
||||
gametype_list_update(widget);
|
||||
else if (!strcmp(name, "server settings update"))
|
||||
server_settings_update(widget);
|
||||
else if (!strcmp(name, "port password update"))
|
||||
password_screen_update(widget);
|
||||
else if (!strcmp(name, "gamespy screen update"))
|
||||
browser_update(widget);
|
||||
else if (!strcmp(name, "gt edit list update"))
|
||||
|
||||
@@ -361,6 +361,8 @@ static char const *const port_function_names[] =
|
||||
/* (Settings' OK: the Xbox's fails when the profile has no changes, the
|
||||
settings' screens having written theirs to config.toml) */
|
||||
"player profile save changes",
|
||||
/* (the server browser's password screen) */
|
||||
"port password init", "port password edit", "port password join", "port password back",
|
||||
};
|
||||
|
||||
/* the PC version's game data functions that the Xbox's have not, from
|
||||
@@ -378,6 +380,7 @@ static char const *const port_game_data_input_names[] =
|
||||
/* (the gametype editor's: the Xbox's stops the game on the buttons'
|
||||
row; the Xbox's read only player_ui's gametype, not Server Setup's) */
|
||||
"game settings lists text update", "get edit game settings name", "mp edit profile set rule text",
|
||||
"port password update",
|
||||
};
|
||||
|
||||
static struct
|
||||
@@ -1246,6 +1249,14 @@ static struct cache_file_tag_instance *instances_grow(long count, long *first_in
|
||||
|
||||
if (!instances)
|
||||
return NULL;
|
||||
/* (every tag's absolute index fits a tag index's 16 bits, short of
|
||||
NONE's, and the table's size cannot wrap: the map's count was checked
|
||||
as it loaded, cache_files.c, and is again) */
|
||||
if (existing < 0 || count < 0 || existing > UNSIGNED_SHORT_MAX - count)
|
||||
{
|
||||
platform_log("menus: %ld tags and the map's %ld are too many for a tag table", count, existing);
|
||||
return NULL;
|
||||
}
|
||||
grown = allocate((existing + count) * sizeof(*grown));
|
||||
if (!grown)
|
||||
return NULL;
|
||||
|
||||
+111
-10
@@ -29,6 +29,8 @@ that clients need to see is sent from this file:
|
||||
every tick in rotation, those that have moved since the map loaded more
|
||||
often. A scenario group is identified by its index, which is the same on
|
||||
every machine; a device's own group by the device.
|
||||
- Breakable surfaces (glass) the host breaks, each once as it breaks and
|
||||
a few at a time again in rotation (for a late joiner or a lost message).
|
||||
- Named objects (scenery and devices) the scripts create or destroy.
|
||||
network_objects.c already handles units, vehicles, weapons and equipment.
|
||||
Twice a second the host sends which object names currently exist. A
|
||||
@@ -53,7 +55,9 @@ host skips once more than half the machines have voted. The skip itself is
|
||||
the campaign's: the host reverts to the state saved when the cutscene began
|
||||
and the script carries on past it. Clients catch up through the object,
|
||||
device and name syncs. The host keeps its clock running through the revert,
|
||||
because the netcode needs it to only go forward (network_coop_skip_reverted).
|
||||
because the netcode needs it to only go forward (network_coop_reverted).
|
||||
A failed mission (game_lost, or the scripts' game_revert) reverts the same
|
||||
way, rather than leaving the players stuck in the failure's cutscene.
|
||||
|
||||
Objects (scenery and devices) that aren't synced by network_objects.c are
|
||||
found on a client by their scenario name, or, unnamed, by their object
|
||||
@@ -88,11 +92,15 @@ index and tag, since the map placed them at the same index everywhere.
|
||||
#include "models/model_animation_definitions.h"
|
||||
#include "models/model_definitions.h"
|
||||
#include "models/models.h"
|
||||
#include "networking/network_game_globals.h"
|
||||
#include "networking/network_game_manager.h"
|
||||
#include "objects/object_definitions.h"
|
||||
#include "objects/objects.h"
|
||||
#include "objects/object_types.h"
|
||||
#include "objects/damage.h"
|
||||
#include "objects/scenery.h"
|
||||
#include "physics/breakable_surfaces.h"
|
||||
#include "structures/structure_bsp_definitions.h"
|
||||
#include "physics/collisions.h"
|
||||
#include "rasterizer/rasterizer_cinematics.h"
|
||||
#include "saved games/game_state.h"
|
||||
@@ -184,9 +192,13 @@ enum
|
||||
_coop_event_effect,
|
||||
_coop_event_attach,
|
||||
/* the host skipped the cutscene: a client stops its dialogue */
|
||||
_coop_event_cutscene_skipped,
|
||||
/* the host reverted to its last saved state */
|
||||
_coop_event_reverted,
|
||||
/* a unit opened (value TRUE) or closed: a dropship's doors */
|
||||
_coop_event_unit_open,
|
||||
/* a breakable surface broke: value its index, frame its BSP, reals the
|
||||
damage's epicenter */
|
||||
_coop_event_surface_broken,
|
||||
};
|
||||
|
||||
/* distributed_coop_event.type of an effect: at a cutscene flag (value), or on
|
||||
@@ -270,7 +282,11 @@ struct distributed_coop_presentation
|
||||
/* whether the scripts have set the players' maximum vitality, and to
|
||||
what (network_coop_set_players_vitality) */
|
||||
byte players_vitality_set;
|
||||
byte pad[2];
|
||||
/* the cluster the scripts keep active (object_pvs_activate) plus one,
|
||||
0 for none: a cutscene filmed far from every player (b30's Elite at the
|
||||
shaft door) is active on the clients too. Plus one so the zeroed padding
|
||||
of a host without it reads as none. */
|
||||
short activating_cluster;
|
||||
real players_maximum_body_vitality;
|
||||
real players_maximum_shield_vitality;
|
||||
/* which teams are allies and friends (game_allegiance.c): the scripts
|
||||
@@ -1429,6 +1445,7 @@ static void host_presentation(
|
||||
presentation->input_disabled = (byte)!player_input_enabled();
|
||||
presentation->scripted_shake = (byte)player_effect_port_scripted_active();
|
||||
presentation->players_vitality_set = (byte)players_vitality.set;
|
||||
presentation->activating_cluster = (short)(objects_get_activating_cluster_index() + 1);
|
||||
presentation->players_maximum_body_vitality = players_vitality.maximum_body;
|
||||
presentation->players_maximum_shield_vitality = players_vitality.maximum_shield;
|
||||
game_allegiance_get_teams(presentation->ally_teams, presentation->friendly_teams);
|
||||
@@ -1544,7 +1561,10 @@ static void client_apply_player_effect(
|
||||
switch (event->type)
|
||||
{
|
||||
case _coop_player_effect_translation:
|
||||
scripted_player_effect_set_translation(reals[0], reals[1], reals[2]);
|
||||
/* (the camera's shake, which moves it no further than an observer
|
||||
accepts) */
|
||||
scripted_player_effect_set_translation(PIN(reals[0], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND),
|
||||
PIN(reals[1], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND), PIN(reals[2], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND));
|
||||
break;
|
||||
case _coop_player_effect_rotation:
|
||||
scripted_player_effect_set_rotation(reals[0], reals[1], reals[2]);
|
||||
@@ -1802,9 +1822,17 @@ static void client_apply_event(
|
||||
case _coop_event_attach:
|
||||
client_apply_attach(event);
|
||||
break;
|
||||
case _coop_event_cutscene_skipped:
|
||||
case _coop_event_reverted:
|
||||
client_stop_script_sounds();
|
||||
break;
|
||||
case _coop_event_surface_broken:
|
||||
if (event->frame == global_structure_bsp_index)
|
||||
{
|
||||
real_point3d epicenter = { event->reals[0], event->reals[1], event->reals[2] };
|
||||
|
||||
breakable_surface_port_break(event->value, &epicenter);
|
||||
}
|
||||
break;
|
||||
case _coop_event_unit_open:
|
||||
if (distributed_object_index_valid(event->object_index) && network_objects_client_has(event->object_index) &&
|
||||
object_try_and_get_and_verify_type(event->object_index, _object_mask_unit))
|
||||
@@ -1935,6 +1963,16 @@ boolean network_coop_active(
|
||||
global_scenario && global_scenario->type == _scenario_type_solo && !game_engine_running();
|
||||
}
|
||||
|
||||
boolean network_coop_player_collisions(
|
||||
void)
|
||||
{
|
||||
struct network_game *game;
|
||||
|
||||
if (!network_coop_active() || !(game = network_game_get_game()))
|
||||
return TRUE;
|
||||
return !TEST_FLAG(game->cooperative_flags, _network_game_cooperative_no_player_collisions_bit);
|
||||
}
|
||||
|
||||
boolean network_coop_devices_remote(
|
||||
void)
|
||||
{
|
||||
@@ -2430,6 +2468,51 @@ void network_coop_note_effect(
|
||||
event->value = cutscene_flag_index;
|
||||
}
|
||||
|
||||
void network_coop_note_surface_broken(
|
||||
short breakable_surface_index,
|
||||
real_point3d const *epicenter)
|
||||
{
|
||||
struct distributed_coop_event *event = event_new(_coop_event_surface_broken);
|
||||
|
||||
if (!event)
|
||||
return;
|
||||
event->value = breakable_surface_index;
|
||||
event->frame = global_structure_bsp_index;
|
||||
event->reals[0] = epicenter->x;
|
||||
event->reals[1] = epicenter->y;
|
||||
event->reals[2] = epicenter->z;
|
||||
}
|
||||
|
||||
/* host: a few broken surfaces again each refresh, round them all, for a
|
||||
machine that joined since or lost the message (broken from their middle) */
|
||||
static void host_send_broken_surfaces(
|
||||
void)
|
||||
{
|
||||
enum { SURFACES_PER_REFRESH = 8 };
|
||||
static short cursor;
|
||||
struct structure_bsp *structure_bsp = global_structure_bsp_get();
|
||||
short count = (short)structure_bsp->breakable_surfaces.count;
|
||||
short sent = 0;
|
||||
short step;
|
||||
|
||||
if (!host_resend.refresh || count <= 0)
|
||||
return;
|
||||
for (step = 0; step < count && sent < SURFACES_PER_REFRESH; step++)
|
||||
{
|
||||
short index = (short)((cursor + step) % count);
|
||||
|
||||
if (!breakable_surface_extant(index))
|
||||
{
|
||||
struct structure_breakable_surface *surface = TAG_BLOCK_GET_ELEMENT(&structure_bsp->breakable_surfaces,
|
||||
index, struct structure_breakable_surface);
|
||||
|
||||
network_coop_note_surface_broken(index, &surface->centroid);
|
||||
sent++;
|
||||
}
|
||||
}
|
||||
cursor = (short)((cursor + step) % count);
|
||||
}
|
||||
|
||||
void network_coop_note_object_effect(
|
||||
long effect_definition_index,
|
||||
long object_index,
|
||||
@@ -2465,7 +2548,7 @@ boolean network_coop_vote_skip(
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* The skip reverted the host's game state, clock included. The clock is
|
||||
/* The host reverted its game state, clock included. The clock is
|
||||
put back, since clients and the netcode expect it to only go forward, and
|
||||
the script threads' wake times move by the same amount so they still wake
|
||||
when they would have. The revert also renumbered the input queues from the
|
||||
@@ -2474,7 +2557,7 @@ from the restored one, or no tick would run until they caught up. And the
|
||||
revert is stamped with the restored time, since the script's game_reverted
|
||||
compares the stamp with the clock: otherwise it would play the cinematic
|
||||
it was meant to skip. */
|
||||
void network_coop_skip_reverted(
|
||||
void network_coop_reverted(
|
||||
long now)
|
||||
{
|
||||
long ticks;
|
||||
@@ -2491,8 +2574,15 @@ void network_coop_skip_reverted(
|
||||
}
|
||||
/* (the dropships' riders kept are of the game state reverted from) */
|
||||
coop_enemies_reset();
|
||||
error(_error_silent, "co-op: cutscene skipped; reverted %ld ticks, clock kept at %ld", ticks, now);
|
||||
event_new(_coop_event_cutscene_skipped);
|
||||
error(_error_silent, "co-op: reverted %ld ticks, clock kept at %ld", ticks, now);
|
||||
event_new(_coop_event_reverted);
|
||||
}
|
||||
|
||||
void network_coop_skip_done(
|
||||
void)
|
||||
{
|
||||
if (!coop_host())
|
||||
return;
|
||||
skip_vote_clear();
|
||||
skip_vote.requested = FALSE;
|
||||
skip_vote.cooldown_until = game_time_get() + SKIP_COOLDOWN_TICKS;
|
||||
@@ -2537,6 +2627,7 @@ void network_coop_host_tick(
|
||||
host_send_attachments();
|
||||
host_send_looping_sounds();
|
||||
host_send_hud_state();
|
||||
host_send_broken_surfaces();
|
||||
host_send_events();
|
||||
}
|
||||
|
||||
@@ -2598,7 +2689,16 @@ void network_coop_note_player_structure_bsp(
|
||||
boolean network_coop_player_has_structure_bsp(
|
||||
long player_index)
|
||||
{
|
||||
return !network_coop_active() || player_get(player_index)->local_player_index != NONE ||
|
||||
struct player_datum *player;
|
||||
|
||||
if (!network_coop_active())
|
||||
return TRUE;
|
||||
/* (no player, or none of the tracked: whose BSP is not known, which
|
||||
does not keep their predictions out) */
|
||||
player = player_index != NONE ? player_try_and_get(player_index) : NULL;
|
||||
if (!player || DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) >= MAXIMUM_TRACKED_PLAYERS)
|
||||
return TRUE;
|
||||
return player->local_player_index != NONE ||
|
||||
host_player_structure_bsps[DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index)] == global_structure_bsp_index_get();
|
||||
}
|
||||
|
||||
@@ -2904,6 +3004,7 @@ static void client_presentation_apply(
|
||||
!coop_spectate_watching_rider(0)) || coop_spectate_nothing_to_watch(0));
|
||||
if (coop_presentation.cinematic_started)
|
||||
cinematic_show_letterbox(TEST_FLAG(presentation->flags, _presentation_letterbox_bit));
|
||||
objects_port_set_activating_cluster((short)(presentation->activating_cluster - 1));
|
||||
if (coop_presentation.host_camera)
|
||||
{
|
||||
real_vector3d forward, up;
|
||||
|
||||
@@ -45,6 +45,9 @@ enum
|
||||
/* 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 the players collide with each other: FALSE in a co-op game whose
|
||||
host set Server Setup's PLAYER COLLISIONS off (bipeds.c, collisions.c) */
|
||||
boolean network_coop_player_collisions(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 */
|
||||
@@ -88,6 +91,10 @@ void network_coop_note_attach(long parent_index, char const *parent_marker_name,
|
||||
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);
|
||||
/* breakable_surfaces.c: a breakable surface (glass) broke, from damage at
|
||||
`epicenter`. Each machine's own shots break glass too, but with their own
|
||||
random damage, so the host's breaks are the ones everyone keeps. */
|
||||
void network_coop_note_surface_broken(short breakable_surface_index, real_point3d const *epicenter);
|
||||
void network_coop_note_scenery_animation(long object_index, long animation_graph_index, short animation_index,
|
||||
short frame_index);
|
||||
|
||||
@@ -96,9 +103,12 @@ 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);
|
||||
/* main.c: after a co-op host reverts to its last saved state (to skip a
|
||||
cinematic, or because the mission failed); now is the game time before the
|
||||
revert */
|
||||
void network_coop_reverted(long now);
|
||||
/* main.c: the skip vote is over once the skip has reverted */
|
||||
void network_coop_skip_done(void);
|
||||
/* the vote count to show, if a skippable cinematic is playing */
|
||||
boolean network_coop_skip_vote_status(short *votes, short *voters, boolean *voted);
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ being hit looks and feels like on the clients).
|
||||
- The damage to a unit goes to the machines of the players it concerns
|
||||
(the unit's, its riders', the damage's owner's) and those sent the
|
||||
unit's player this tick, which see it; a killing blow to all.
|
||||
- Scenery and devices that can be destroyed (lights, panels) are the
|
||||
map's, at the same index on every machine. A client reports its hits on
|
||||
them like any other, and every client deals the host's damage to its own
|
||||
copy, which breaks and plays its effects as the host's did.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
@@ -59,6 +63,8 @@ being hit looks and feels like on the clients).
|
||||
void damage_replay_player_effect(long player_index, struct damage_data *damage, real total_damage);
|
||||
void damage_replay_aftermath(long object_index, struct damage_data *damage, unsigned long being_damaged_flags,
|
||||
real shield_damage, real body_damage, real body_damage_multiplier, short body_part);
|
||||
void damage_replay_static(long object_index, struct damage_data *damage, short node_index, short region_index,
|
||||
short material_index);
|
||||
void damage_replay_kill(long object_index, struct damage_data *damage, short node_index, short region_index,
|
||||
short material_index);
|
||||
/* network_distributed.c's */
|
||||
@@ -142,8 +148,14 @@ enum
|
||||
_damage_event_player_effect,
|
||||
_damage_event_aftermath,
|
||||
_damage_event_kill,
|
||||
/* damage to destructible scenery or a device, to every machine */
|
||||
_damage_event_static,
|
||||
};
|
||||
|
||||
/* the objects every machine has from the map, at the same index, which
|
||||
damage can break (static_target) */
|
||||
#define STATIC_TARGET_TYPES (_object_mask_scenery | _object_mask_device)
|
||||
|
||||
/* what a player's damage is, by how the game deals it (the shape of their
|
||||
report of it) */
|
||||
enum
|
||||
@@ -467,7 +479,8 @@ static boolean distributed_damage_to_data(
|
||||
if (!tag_index_is_group(damage->definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
|
||||
return FALSE;
|
||||
damage_data_new(result, damage->definition_index);
|
||||
result->flags = damage->flags;
|
||||
/* (the flags the game has) */
|
||||
result->flags = damage->flags & (FLAG(NUMBER_OF_DAMAGE_DATA_FLAGS) - 1);
|
||||
result->owner_player_index = distributed_player_from_byte(damage->owner_player_index);
|
||||
result->owner_team_index = damage->owner_team_index;
|
||||
result->owner_object_index = distributed_object_index_valid(damage->owner_object_index) &&
|
||||
@@ -478,11 +491,22 @@ static boolean distributed_damage_to_data(
|
||||
result->scale = damage->scale;
|
||||
result->multiplier = damage->multiplier;
|
||||
result->material_effect_scale = damage->material_effect_scale;
|
||||
result->material_type = damage->material_type;
|
||||
/* (a material the game has, else none, damage_data_new's: it indexes
|
||||
the materials' modifiers) */
|
||||
if (damage->material_type >= 0 && damage->material_type < NUMBER_OF_MATERIAL_TYPES)
|
||||
result->material_type = damage->material_type;
|
||||
scenario_location_from_point(&result->location, &result->epicenter);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* whether the object is scenery or a device that damage can destroy */
|
||||
static boolean static_target(
|
||||
long object_index)
|
||||
{
|
||||
return object_try_and_get_and_verify_type(object_index, STATIC_TARGET_TYPES) &&
|
||||
object_get_maximum_body_vitality(object_index, TRUE) > 0.0f;
|
||||
}
|
||||
|
||||
/* ---------- object_cause_damage (damage.c) */
|
||||
|
||||
/* whether the damage is a weapon's own at the unit that fires it (weapons.c:
|
||||
@@ -629,7 +653,8 @@ boolean network_damage_deals(
|
||||
damage_replay_player_effect(unit->unit.player_index, &effect, 0.0f);
|
||||
}
|
||||
/* a hit of this machine's own player's, on the host's object */
|
||||
if (distributed_player_is_local(damage->owner_player_index) && network_objects_client_has(object_index) &&
|
||||
if (distributed_player_is_local(damage->owner_player_index) &&
|
||||
(network_objects_client_has(object_index) || static_target(object_index)) &&
|
||||
damage_report_count < MAXIMUM_HIT_REPORTS_PER_TICK)
|
||||
{
|
||||
struct distributed_hit_report *report = &damage_reports[damage_report_count++];
|
||||
@@ -717,6 +742,33 @@ void network_damage_player_effect(
|
||||
|
||||
/* the host: an object damaged, and what that did (object_damage_aftermath
|
||||
done, so that a kill's killer is known) */
|
||||
/* a damage event of this tick for the object, or NULL once the tick's are
|
||||
spent (the last RESERVED_KILL_EVENTS are kept for killing blows) */
|
||||
static struct distributed_damage_event *damage_event_new(
|
||||
byte kind,
|
||||
long object_index,
|
||||
struct damage_data const *damage,
|
||||
short node_index,
|
||||
short region_index,
|
||||
short material_index,
|
||||
boolean kill)
|
||||
{
|
||||
struct distributed_damage_event *event;
|
||||
|
||||
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS))
|
||||
return NULL;
|
||||
event = &damage_events[damage_event_count++];
|
||||
csmemset(event, 0, sizeof(*event));
|
||||
event->kind = kind;
|
||||
event->player_index = NO_PLAYER;
|
||||
event->object_index = object_index;
|
||||
distributed_damage_from_data(damage, &event->damage);
|
||||
event->node_index = node_index;
|
||||
event->region_index = region_index;
|
||||
event->material_index = material_index;
|
||||
return event;
|
||||
}
|
||||
|
||||
void network_damage_aftermath(
|
||||
long object_index,
|
||||
struct damage_data const *damage,
|
||||
@@ -736,6 +788,11 @@ void network_damage_aftermath(
|
||||
|
||||
if (game_connection() != _game_connection_network_server)
|
||||
return;
|
||||
if (static_target(object_index))
|
||||
{
|
||||
damage_event_new(_damage_event_static, object_index, damage, node_index, region_index, material_index, FALSE);
|
||||
return;
|
||||
}
|
||||
/* (units only: items and the like the objects' states place) */
|
||||
unit = (struct unit_datum *)object_try_and_get_and_verify_type(object_index, _object_mask_unit);
|
||||
if (!unit)
|
||||
@@ -749,22 +806,15 @@ void network_damage_aftermath(
|
||||
(victim_player_index != NONE ||
|
||||
(TEST_FLAG(_object_mask_biped, unit->object.type) &&
|
||||
!TEST_FLAG(being_damaged_flags, _object_being_damaged_body_destroyed_bit)));
|
||||
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS))
|
||||
event = damage_event_new(_damage_event_aftermath, object_index, damage, node_index, region_index, material_index,
|
||||
kill);
|
||||
if (!event)
|
||||
return;
|
||||
event = &damage_events[damage_event_count++];
|
||||
csmemset(event, 0, sizeof(*event));
|
||||
event->kind = _damage_event_aftermath;
|
||||
event->player_index = NO_PLAYER;
|
||||
event->object_index = object_index;
|
||||
distributed_damage_from_data(damage, &event->damage);
|
||||
event->being_damaged_flags = being_damaged_flags;
|
||||
event->shield_damage = shield_damage;
|
||||
event->body_damage = body_damage;
|
||||
event->body_damage_multiplier = body_damage_multiplier;
|
||||
event->body_part = body_part;
|
||||
event->node_index = node_index;
|
||||
event->region_index = region_index;
|
||||
event->material_index = material_index;
|
||||
/* a killing blow, a player's with who the host says dealt it */
|
||||
if (kill)
|
||||
{
|
||||
@@ -1719,6 +1769,8 @@ static boolean distributed_report_valid(
|
||||
return FALSE;
|
||||
target = (struct object_datum *)object_try_and_get_and_verify_type(report->object_index,
|
||||
_object_mask_biped | _object_mask_vehicle | _object_mask_weapon | _object_mask_equipment);
|
||||
if (!target && static_target(report->object_index))
|
||||
target = object_get(report->object_index);
|
||||
if (!target || !tag_index_is_group(report->damage.definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
|
||||
return FALSE;
|
||||
/* numbers the game can take */
|
||||
@@ -2160,6 +2212,15 @@ void network_damage_handle_events(
|
||||
players_show_telefragged(victim_player_index);
|
||||
}
|
||||
break;
|
||||
case _damage_event_static:
|
||||
if (distributed_object_index_valid(event->object_index) && static_target(event->object_index) &&
|
||||
distributed_damage_indices_valid(event->object_index, event->node_index, event->region_index,
|
||||
event->material_index))
|
||||
{
|
||||
damage_replay_static(event->object_index, &damage, event->node_index, event->region_index,
|
||||
event->material_index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ void game_engine_read_network_state(byte const *buffer, long size);
|
||||
/* physics.c's (world units a tick, each tick) */
|
||||
extern real global_gravity;
|
||||
/* network_server_manager.c's, cseries_windows.c's, console.c's, p2p.c's */
|
||||
void network_game_server_kick_machine(long machine_index);
|
||||
void network_game_server_kick_machine(long machine_index, boolean kept_out);
|
||||
unsigned long network_game_server_machine_address(long machine_index);
|
||||
char const *network_game_server_machine_hardware_id(long machine_index);
|
||||
void p2p_hardware_id_sanitize(char *destination, int size, const char *source);
|
||||
@@ -588,7 +588,10 @@ static long distributed_received_times[MAXIMUM_SENDERS][NUMBER_OF_DISTRIBUTED_ME
|
||||
/* the host: each client machine's clock, measured (CLIENT_CLOCK_WINDOW_MILLISECONDS):
|
||||
its latest tick, and its tick and the host's time as the window began
|
||||
(NONE: none begun); how many windows in a row it went fast, and whether
|
||||
the last did */
|
||||
the last did; and whether, since they began, a message that came over its
|
||||
stream was that far ahead too (a datagram's source address is all that
|
||||
says which machine sent it, and anyone can send one as from another's: a
|
||||
machine is banned only on its stream's word, distributed_note_client_clock) */
|
||||
/* the host: each client machine's Discord user, as it told it, the text
|
||||
kept only of what is allowed (p2p_discord_sanitize) */
|
||||
static struct distributed_client_identity distributed_client_identities[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
|
||||
@@ -602,7 +605,11 @@ static struct distributed_client_clock
|
||||
unsigned long window_time;
|
||||
short fast_windows;
|
||||
boolean fast;
|
||||
boolean ahead_on_stream;
|
||||
} distributed_client_clocks[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
|
||||
/* the host: whether the message being handled came over its machine's
|
||||
stream (network_distributed_handle_stream_message) */
|
||||
static boolean distributed_handling_stream_message;
|
||||
/* a client: the host's latest tick it has had a message of */
|
||||
static long distributed_host_time = NONE;
|
||||
/* the host: each client's round trip, in ticks, and its jitter */
|
||||
@@ -1622,6 +1629,14 @@ static void distributed_correct_own_unit(
|
||||
short local_player_index = player->local_player_index;
|
||||
real_point3d position = state->position;
|
||||
|
||||
/* (co-op: the host has its player in a BSP this machine hasn't loaded
|
||||
yet, the switch still on its way. Moved there, it would fall with no
|
||||
floor under it, so it stays on the floor it has until the BSP loads.) */
|
||||
if (network_coop_active() && scenario_leaf_index_from_point(&state->position) == NONE &&
|
||||
scenario_leaf_index_from_point(&object_get(unit_index)->object.position) != NONE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (TEST_FLAG(state->flags, _distributed_unit_predicted_bit) &&
|
||||
local_player_index >= 0 && local_player_index < MAXIMUM_LOCAL_PLAYERS)
|
||||
{
|
||||
@@ -2089,6 +2104,7 @@ static void distributed_apply_predictions(
|
||||
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 &&
|
||||
unit_get(unit_index)->unit.player_index != NONE &&
|
||||
network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index))
|
||||
{
|
||||
distributed_take_prediction(player_index, unit_index, &bound);
|
||||
@@ -3709,6 +3725,8 @@ static void distributed_note_client_clock(
|
||||
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES)
|
||||
return;
|
||||
clock = &distributed_client_clocks[machine_index];
|
||||
if (distributed_handling_stream_message && tick - game_time_get() > CLIENT_CLOCK_AHEAD_TICKS)
|
||||
clock->ahead_on_stream = TRUE;
|
||||
if (clock->window_tick == NONE)
|
||||
{
|
||||
clock->latest_tick = tick;
|
||||
@@ -3730,6 +3748,7 @@ static void distributed_note_client_clock(
|
||||
if (!clock->fast)
|
||||
{
|
||||
clock->fast_windows = 0;
|
||||
clock->ahead_on_stream = FALSE;
|
||||
}
|
||||
else if (++clock->fast_windows == 1)
|
||||
{
|
||||
@@ -3746,7 +3765,7 @@ static void distributed_note_client_clock(
|
||||
char discord_id[DISCORD_ID_SIZE];
|
||||
char discord_name[DISCORD_NAME_SIZE];
|
||||
char discord[DISCORD_ID_SIZE + DISCORD_NAME_SIZE + 16] = "";
|
||||
char reason[64];
|
||||
char reason[96];
|
||||
char text[MAXIMUM_NOTICE_LENGTH];
|
||||
|
||||
distributed_machine_player_names(machine_index, names, sizeof(names));
|
||||
@@ -3756,14 +3775,30 @@ static void distributed_note_client_clock(
|
||||
distributed_client_identities[machine_index].discord_name, 1);
|
||||
if (discord_id[0] || discord_name[0])
|
||||
snprintf(discord, sizeof(discord), " (Discord: %s, %s)", discord_name, discord_id);
|
||||
snprintf(reason, sizeof(reason), "speed hack (game ran %.2f times as fast)", rate);
|
||||
snprintf(reason, sizeof(reason), "speed hack (game ran %.2f times as fast)%s", rate,
|
||||
clock->ahead_on_stream ? "" : " (its datagrams only: not banned)");
|
||||
snprintf(text, sizeof(text), "%s%s kicked by the host: their game ran %.2f times as fast (a speed hack)",
|
||||
names, discord, rate);
|
||||
distributed_send_notice(text);
|
||||
distributed_log_cheater(machine_index, names, reason);
|
||||
/* banned, and kept out, only when its stream said so
|
||||
too; on its datagrams' word alone (which another machine
|
||||
could have sent as from its address), logged and dropped,
|
||||
and it may join again */
|
||||
if (clock->ahead_on_stream)
|
||||
{
|
||||
distributed_log_cheater(machine_index, names, reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
char address[32];
|
||||
|
||||
distributed_machine_address_text(machine_index, address, sizeof(address));
|
||||
distributed_write_player_record(CHEATERS_FILE, address, machine_index, names, reason);
|
||||
}
|
||||
}
|
||||
network_game_server_kick_machine(machine_index);
|
||||
network_game_server_kick_machine(machine_index, clock->ahead_on_stream);
|
||||
clock->fast_windows = 0;
|
||||
clock->ahead_on_stream = FALSE;
|
||||
}
|
||||
}
|
||||
clock->window_tick = clock->latest_tick;
|
||||
@@ -4101,3 +4136,16 @@ void network_distributed_handle_message(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* (the host: network_server_message_handler.c) a client machine's message
|
||||
of the distributed kind that came over its stream, which no other machine
|
||||
can send as it: as network_distributed_handle_message */
|
||||
void network_distributed_handle_stream_message(
|
||||
long machine_index,
|
||||
word const *message,
|
||||
word size)
|
||||
{
|
||||
distributed_handling_stream_message = TRUE;
|
||||
network_distributed_handle_message(machine_index, message, size);
|
||||
distributed_handling_stream_message = FALSE;
|
||||
}
|
||||
@@ -60,6 +60,7 @@ 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 "camera/observer.h"
|
||||
#include "cutscene/cinematics.h"
|
||||
#include "network_coop.h"
|
||||
#include "network_distributed.h"
|
||||
@@ -244,7 +245,11 @@ struct distributed_object_change
|
||||
byte change;
|
||||
byte flags;
|
||||
byte owner_player_index;
|
||||
byte pad;
|
||||
/* which bitmap of its shaders it draws with, when not its tag's (0). An
|
||||
AI unit's actor variant sets it after the unit is made
|
||||
(actor_customize_unit): the Elite major's and commander's armor are 1
|
||||
and 2. Builds before it sent 0 here. */
|
||||
byte forced_shader_permutation_index;
|
||||
long object_index;
|
||||
long definition_index;
|
||||
short owner_team_index;
|
||||
@@ -1003,6 +1008,7 @@ static void distributed_change_from_object(
|
||||
change->owner_player_index = distributed_player_to_byte(object->object.owner_player_index);
|
||||
change->owner_team_index = object->object.owner_team_index;
|
||||
change->variant_number = object->object.variant_number;
|
||||
change->forced_shader_permutation_index = (byte)PIN(object->object.forced_shader_permutation_index, 0, 0xFF);
|
||||
change->position = object->object.position;
|
||||
change->forward = object->object.forward;
|
||||
change->up = object->object.up;
|
||||
@@ -1292,8 +1298,20 @@ static short distributed_host_object_period(
|
||||
count = objects_host_viewers.player_unit_count;
|
||||
origins = objects_host_viewers.player_unit_origins;
|
||||
}
|
||||
/* in a co-op cutscene every client looks through the host's camera, so
|
||||
what it films is sent as often as what's next to the client's players
|
||||
(b30's sword Elite walking out of the door) */
|
||||
if (network_coop_active() && cinematic_in_progress())
|
||||
{
|
||||
real_point3d const *camera = &observer_get_camera(0)->position;
|
||||
real dx = position->x - camera->x;
|
||||
real dy = position->y - camera->y;
|
||||
real dz = position->z - camera->z;
|
||||
|
||||
nearest = dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
/* nobody is alive anywhere, so nothing needs to be sent often */
|
||||
if (!count)
|
||||
if (!count && nearest < 0.0f)
|
||||
return MAXIMUM_OBJECT_PERIOD_TICKS;
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
@@ -2142,6 +2160,8 @@ 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));
|
||||
if (change->forced_shader_permutation_index)
|
||||
object->object.forced_shader_permutation_index = change->forced_shader_permutation_index;
|
||||
/* (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) */
|
||||
{
|
||||
|
||||
@@ -16,7 +16,10 @@ also need the tick after the pair it spans: two ticks behind.)
|
||||
Rotations are blended as quaternions (normalised lerp, taking the shorter
|
||||
way round), positions and scales linearly. Anything that moves further than
|
||||
a tick of motion plausibly allows (teleports, respawns, camera cuts) snaps
|
||||
instead of sweeping across the world.
|
||||
instead of sweeping across the world, and so does a pose whose nodes moved
|
||||
too far for one tick: two snapshots of different poses (a model swapped,
|
||||
another weapon's skeleton, a pose left from seconds before) blended node by
|
||||
node stretch vertices across the screen.
|
||||
|
||||
Particles, contrails and other effects already move every frame
|
||||
(game_frame), so they need nothing here.
|
||||
@@ -56,6 +59,17 @@ unsigned long config_changes(void);
|
||||
/* world units (10 feet each) a node may move in one tick before it snaps:
|
||||
well beyond any vehicle, short of any teleport */
|
||||
#define OBJECT_SNAP_DISTANCE 10.0f
|
||||
/* ... a node may move in the object's root node's frame in one tick before
|
||||
the pose is taken for a new one, not blended to: further than any limb or
|
||||
part of a model moves in 33 ms (37 m/s), short of the game changing a pose
|
||||
at once (an actor waking from dormancy, a model swapped), which blended
|
||||
sweeps the vertices through poses it never had. (In the root's frame, so
|
||||
that the whole object turning moves nothing: measured in the world, a fast
|
||||
turn swung the head and weapon far enough to snap, and characters moved
|
||||
like robots.) */
|
||||
#define NODE_SNAP_DISTANCE 0.4f
|
||||
/* ... a first-person node may move relative to the camera */
|
||||
#define FIRST_PERSON_SNAP_DISTANCE 0.25f
|
||||
/* a correction's difference left drawn after each tick (of 1) */
|
||||
#define CORRECTION_DECAY 0.6f
|
||||
/* ... and small enough to be none */
|
||||
@@ -311,16 +325,6 @@ static void matrix_blend_rotations(
|
||||
result->up.k = lerp(a->up.k, b->up.k, t);
|
||||
}
|
||||
|
||||
/* a matrix a fraction t of the way from a to b */
|
||||
static void matrix_blend(real_matrix4x3 const *a, real_matrix4x3 const *b, real t, real_matrix4x3 *result)
|
||||
{
|
||||
struct interpolation_rotation rotation_a, rotation_b;
|
||||
|
||||
rotation_from_matrix(a, &rotation_a);
|
||||
rotation_from_matrix(b, &rotation_b);
|
||||
matrix_blend_rotations(a, b, &rotation_a, &rotation_b, t, result);
|
||||
}
|
||||
|
||||
/* the vector's parts' sum, large enough to be a correction (so written that
|
||||
one not a number is none) */
|
||||
static boolean correction_significant(real_vector3d const *correction)
|
||||
@@ -530,14 +534,22 @@ real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
|
||||
real_matrix4x3 const *latest = record->nodes + record->latest * record->node_capacity;
|
||||
real_matrix4x3 *blended = record->nodes + 2 * record->node_capacity;
|
||||
short node_index;
|
||||
|
||||
/* (so written that a position not a number snaps) */
|
||||
if (!(distance_squared(&previous[0].position, &latest[0].position) <=
|
||||
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE))
|
||||
boolean snap = !(distance_squared(&previous[0].position, &latest[0].position) <=
|
||||
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE);
|
||||
|
||||
/* a node moved further in the root's frame than a tick allows: the
|
||||
two snapshots are different poses, not one moving (so written that
|
||||
a position not a number snaps) */
|
||||
for (node_index = 1; !snap && node_index < record->node_count; node_index++)
|
||||
{
|
||||
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
|
||||
real_point3d previous_local, latest_local;
|
||||
|
||||
matrix4x3_inverse_transform_point(&previous[0], &previous[node_index].position, &previous_local);
|
||||
matrix4x3_inverse_transform_point(&latest[0], &latest[node_index].position, &latest_local);
|
||||
snap = !(distance_squared(&previous_local, &latest_local) <= NODE_SNAP_DISTANCE * NODE_SNAP_DISTANCE);
|
||||
}
|
||||
else
|
||||
if (!snap)
|
||||
{
|
||||
struct interpolation_rotation *previous_rotations =
|
||||
record->rotations + (record->latest ^ 1) * record->node_capacity;
|
||||
@@ -566,6 +578,8 @@ real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
|
||||
&latest_rotations[node_index], interpolation_fraction, &blended[node_index]);
|
||||
}
|
||||
}
|
||||
if (snap)
|
||||
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
|
||||
if (correction_significant(&record->correction) || correction_significant(&record->correction_pending))
|
||||
{
|
||||
real_vector3d drawn;
|
||||
@@ -804,6 +818,8 @@ void render_interpolation_first_person(
|
||||
struct interpolated_first_person *first_person;
|
||||
real_matrix4x3 camera_matrix;
|
||||
real_matrix4x3 inverse_camera;
|
||||
struct interpolation_rotation previous_rotations[MAXIMUM_INTERPOLATED_NODES];
|
||||
struct interpolation_rotation latest_rotations[MAXIMUM_INTERPOLATED_NODES];
|
||||
short node_index;
|
||||
|
||||
if (!interpolation_rendering ||
|
||||
@@ -817,8 +833,11 @@ void render_interpolation_first_person(
|
||||
matrix4x3_inverse(&camera_matrix, &inverse_camera);
|
||||
if (first_person->tick != interpolation_tick)
|
||||
{
|
||||
/* the pose drawn last, at the end of the previous tick */
|
||||
first_person->has_previous = first_person->node_count == node_count;
|
||||
/* the pose drawn last, if that was at the end of the tick just
|
||||
before: not one left from before the view was last away from first
|
||||
person (zoomed, in a vehicle, dead, in a cinematic), seconds old */
|
||||
first_person->has_previous = first_person->tick == interpolation_tick - 1 &&
|
||||
first_person->node_count == node_count;
|
||||
memcpy(first_person->previous, first_person->latest, sizeof(first_person->previous));
|
||||
first_person->tick = interpolation_tick;
|
||||
}
|
||||
@@ -827,13 +846,31 @@ void render_interpolation_first_person(
|
||||
first_person->node_count = node_count;
|
||||
if (!first_person->has_previous)
|
||||
return;
|
||||
/* a node that jumped further in the camera's frame than a tick allows:
|
||||
the last pose was another weapon's skeleton (of as many nodes), not this
|
||||
one moving (so written that a position not a number snaps) */
|
||||
for (node_index = 0; node_index < node_count; node_index++)
|
||||
{
|
||||
real_matrix4x3 const *previous = &first_person->previous[node_index];
|
||||
real_matrix4x3 const *latest = &first_person->latest[node_index];
|
||||
|
||||
if (!(distance_squared(&previous->position, &latest->position) <=
|
||||
FIRST_PERSON_SNAP_DISTANCE * FIRST_PERSON_SNAP_DISTANCE))
|
||||
{
|
||||
return;
|
||||
}
|
||||
rotation_from_matrix(previous, &previous_rotations[node_index]);
|
||||
rotation_from_matrix(latest, &latest_rotations[node_index]);
|
||||
}
|
||||
for (node_index = 0; node_index < node_count; node_index++)
|
||||
{
|
||||
real_matrix4x3 blended;
|
||||
|
||||
matrix_blend(
|
||||
matrix_blend_rotations(
|
||||
&first_person->previous[node_index],
|
||||
&first_person->latest[node_index],
|
||||
&previous_rotations[node_index],
|
||||
&latest_rotations[node_index],
|
||||
interpolation_fraction,
|
||||
&blended);
|
||||
matrix4x3_multiply(&camera_matrix, &blended, &node_matrices[node_index]);
|
||||
|
||||
@@ -41,8 +41,17 @@ the display's shape, or 640 (port/linux/src/d3d8_gl.c) */
|
||||
long halo_screen_width(void);
|
||||
/* takes up a new width between frames (F11); returns the width */
|
||||
long halo_screen_commit(void);
|
||||
/* the shadow maps' pixels for each of their 128 texels each way, a power of
|
||||
two (display.shadow_resolution) */
|
||||
long halo_shadow_map_scale(void);
|
||||
/* while TRUE, drawing shifts right to center 640-column layouts */
|
||||
void halo_screen_ui_offset(unsigned char centered);
|
||||
/* names a model lighting vertex program, whose draws can be lit for each
|
||||
pixel (port/linux/src/d3d8_gl.c, display.per_pixel_lighting) */
|
||||
void halo_vertex_shader_lighting(unsigned long handle);
|
||||
/* display.anti_aliasing's pass over a window's 3D view, before the HUD and
|
||||
menus (source/render/render.c): the window's bounds on the screen */
|
||||
void halo_screen_anti_alias(short x0, short y0, short x1, short y1);
|
||||
/* the mouse in the menus (source/interface/ui_widget.c) */
|
||||
#include "halo_ui_pointer.h"
|
||||
|
||||
|
||||
@@ -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 20
|
||||
/* ... the advertisement's reserved bytes: the version (a little-endian word),
|
||||
then flags */
|
||||
#define HALO_PORT_ADVERTISED_VERSION_OFFSET 0
|
||||
|
||||
@@ -63,6 +63,9 @@ void halo_vr_multiview_sky(int sky);
|
||||
/* the shared camera's projection's [2][2] and [3][2], for what the game
|
||||
projects itself (sprites, lens flare tests) */
|
||||
void halo_vr_multiview_projection(float projection_22, float projection_32);
|
||||
/* lens flares are being drawn: placed by their depth, though not depth
|
||||
tested */
|
||||
void halo_vr_multiview_lens_flares(int drawing);
|
||||
|
||||
/* clears the back buffer to transparent black for the overlay, after the
|
||||
eyes; menus_active chooses where the overlay is shown */
|
||||
|
||||
+814
-84
File diff suppressed because it is too large.
Load diff
+809
-112
File diff suppressed because it is too large.
Load diff
@@ -86,6 +86,10 @@ this list to generate the guest's entry points */
|
||||
X(glCheckFramebufferStatus) \
|
||||
X(glBlitFramebuffer) \
|
||||
X(glDrawBuffers) \
|
||||
X(glGenRenderbuffers) \
|
||||
X(glBindRenderbuffer) \
|
||||
X(glRenderbufferStorageMultisample) \
|
||||
X(glFramebufferRenderbuffer) \
|
||||
X(glReadBuffer) \
|
||||
X(glInvalidateFramebuffer) \
|
||||
X(glGenBuffers) \
|
||||
@@ -195,6 +199,10 @@ this list to generate the guest's entry points */
|
||||
X(glCheckFramebufferStatus) \
|
||||
X(glBlitFramebuffer) \
|
||||
X(glDrawBuffers) \
|
||||
X(glGenRenderbuffers) \
|
||||
X(glBindRenderbuffer) \
|
||||
X(glRenderbufferStorageMultisample) \
|
||||
X(glFramebufferRenderbuffer) \
|
||||
X(glGenBuffers) \
|
||||
X(glDeleteBuffers) \
|
||||
X(glBindBuffer) \
|
||||
@@ -209,6 +217,11 @@ this list to generate the guest's entry points */
|
||||
X(glDisableVertexAttribArray) \
|
||||
X(glVertexAttribPointer) \
|
||||
X(glVertexAttribIPointer) \
|
||||
X(glVertexAttribFormat) \
|
||||
X(glVertexAttribIFormat) \
|
||||
X(glVertexAttribBinding) \
|
||||
X(glBindVertexBuffer) \
|
||||
X(glGetQueryBufferObjectuiv) \
|
||||
X(glVertexAttrib4fv) \
|
||||
X(glVertexAttribI4ui) \
|
||||
X(glDrawArrays) \
|
||||
@@ -306,6 +319,10 @@ pointers, sees the declarations without these aliases */
|
||||
#define glCheckFramebufferStatus halo_glCheckFramebufferStatus
|
||||
#define glBlitFramebuffer halo_glBlitFramebuffer
|
||||
#define glDrawBuffers halo_glDrawBuffers
|
||||
#define glGenRenderbuffers halo_glGenRenderbuffers
|
||||
#define glBindRenderbuffer halo_glBindRenderbuffer
|
||||
#define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample
|
||||
#define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer
|
||||
#define glReadBuffer halo_glReadBuffer
|
||||
#define glInvalidateFramebuffer halo_glInvalidateFramebuffer
|
||||
#define glGenBuffers halo_glGenBuffers
|
||||
@@ -413,6 +430,10 @@ pointers, sees the declarations without these aliases */
|
||||
#define glCheckFramebufferStatus halo_glCheckFramebufferStatus
|
||||
#define glBlitFramebuffer halo_glBlitFramebuffer
|
||||
#define glDrawBuffers halo_glDrawBuffers
|
||||
#define glGenRenderbuffers halo_glGenRenderbuffers
|
||||
#define glBindRenderbuffer halo_glBindRenderbuffer
|
||||
#define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample
|
||||
#define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer
|
||||
#define glGenBuffers halo_glGenBuffers
|
||||
#define glDeleteBuffers halo_glDeleteBuffers
|
||||
#define glBindBuffer halo_glBindBuffer
|
||||
@@ -427,6 +448,11 @@ pointers, sees the declarations without these aliases */
|
||||
#define glDisableVertexAttribArray halo_glDisableVertexAttribArray
|
||||
#define glVertexAttribPointer halo_glVertexAttribPointer
|
||||
#define glVertexAttribIPointer halo_glVertexAttribIPointer
|
||||
#define glVertexAttribFormat halo_glVertexAttribFormat
|
||||
#define glVertexAttribIFormat halo_glVertexAttribIFormat
|
||||
#define glVertexAttribBinding halo_glVertexAttribBinding
|
||||
#define glBindVertexBuffer halo_glBindVertexBuffer
|
||||
#define glGetQueryBufferObjectuiv halo_glGetQueryBufferObjectuiv
|
||||
#define glVertexAttrib4fv halo_glVertexAttrib4fv
|
||||
#define glVertexAttribI4ui halo_glVertexAttribI4ui
|
||||
#define glDrawArrays halo_glDrawArrays
|
||||
|
||||
@@ -125,7 +125,9 @@ HALO_COMMON(hs_global_data, 4); /* struct data_array *hs_global_data */
|
||||
HALO_COMMON(hs_thread_data, 4); /* struct data_array *hs_thread_data */
|
||||
HALO_COMMON(hud_msg_def, 4); /* struct hud_messaging_parameters_definition *hud_msg_def */
|
||||
HALO_COMMON(interrupt_result, 24); /* HRESULT interrupt_result */
|
||||
HALO_COMMON(light_cluster_partition, 12); /* struct cluster_partition light_cluster_partition */
|
||||
/* object_lights.c owns light_cluster_partition. On COFF a weak fallback
|
||||
can override its tentative COMMON definition; let the owner allocate the
|
||||
native struct cluster_partition, including its reference-cache fields. */
|
||||
HALO_COMMON(light_data, 4); /* struct data_array *light_data */
|
||||
HALO_COMMON(local_player_index_for_draw_string_and_hack_in_icons, 4); /* short local_player_index_for_draw_string_and_hack_in_icons */
|
||||
HALO_COMMON(looping_sound_data, 4); /* struct data_array *looping_sound_data */
|
||||
|
||||
@@ -911,6 +911,11 @@ static struct
|
||||
int failed;
|
||||
} art[MAXIMUM_ART];
|
||||
static long art_count;
|
||||
/* the lowest and highest texture the art stands for: every texture the
|
||||
renderer binds asks (xbox_textures.c), every frame of a game too, and the
|
||||
menus' lie together, made as the map loaded, mostly apart from the map's
|
||||
(in its texture cache) */
|
||||
static unsigned long art_lowest, art_highest;
|
||||
|
||||
void halo_menus_art_register(void const *texture, char const *png)
|
||||
{
|
||||
@@ -944,6 +949,10 @@ void halo_menus_art_register(void const *texture, char const *png)
|
||||
memset(&art[index], 0, sizeof(art[index]));
|
||||
art[index].data = data;
|
||||
art[index].png = strdup(png);
|
||||
if (art_count == 1 || data < art_lowest)
|
||||
art_lowest = data;
|
||||
if (art_count == 1 || data > art_highest)
|
||||
art_highest = data;
|
||||
}
|
||||
|
||||
void halo_menus_art_forget(void)
|
||||
@@ -957,12 +966,15 @@ void halo_menus_art_forget(void)
|
||||
glDeleteTextures(1, &art[index].texture);
|
||||
}
|
||||
art_count = 0;
|
||||
art_lowest = art_highest = 0;
|
||||
}
|
||||
|
||||
unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
|
||||
{
|
||||
long index;
|
||||
|
||||
if (!art_count || data < art_lowest || data > art_highest)
|
||||
return 0;
|
||||
for (index = 0; index < art_count; index++)
|
||||
{
|
||||
if (art[index].data == data)
|
||||
|
||||
@@ -518,6 +518,59 @@ static const char *comparison_operator(unsigned long function)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- per-pixel model lighting
|
||||
|
||||
The game lights its models for each vertex: its model lighting programs
|
||||
(nv2a_vsh.c nv2a_vertex_shader_lighting) sum the ambient light, two distant
|
||||
lights and two point lights into the diffuse color v0. Halo's models have
|
||||
few vertices, so the light across a curved surface comes out in facets, and
|
||||
a point light that passes close lights only the vertices it reaches.
|
||||
model_lighting computes the same sum for each pixel, from the normal and
|
||||
world position the vertex program hands on and the same constants
|
||||
(XGPU_MODEL_LIGHT_COUNT): the same falloff, facing, cone and translucency
|
||||
terms, clamped where the programs clamp them. The diffuse alpha, which the
|
||||
programs write apart (0, or planar fog's), is kept. */
|
||||
|
||||
static void model_lighting(struct xgpu_text *text, BOOL point_lights)
|
||||
{
|
||||
int light;
|
||||
|
||||
xgpu_text_append(text,
|
||||
"uniform vec4 model_lights[%d];\n"
|
||||
"in vec4 xWorldNormal;\n"
|
||||
"%s"
|
||||
"vec3 model_lighting()\n"
|
||||
"{\n"
|
||||
/* the normal's direction between the vertices, at the length the
|
||||
vertices' normals have, as the programs light by it unnormalized
|
||||
(shorter than 1 on some models: the first-person weapons) */
|
||||
"\tvec3 n = xWorldNormal.xyz * (xWorldNormal.w * inversesqrt(max(dot(xWorldNormal.xyz, xWorldNormal.xyz), 1.0e-24)));\n"
|
||||
/* the ambient light, the first distant light, which also lights the
|
||||
back by the translucency, and the second */
|
||||
"\tfloat facing = dot(n, -model_lights[7].xyz);\n"
|
||||
"\tvec3 light = model_lights[11].xyz + max(max(facing, -facing * model_lights[0].z), 0.0) * model_lights[8].xyz +\n"
|
||||
"\t\tmax(dot(n, -model_lights[9].xyz), 0.0) * model_lights[10].xyz;\n",
|
||||
XGPU_MODEL_LIGHT_COUNT, point_lights ? "in vec3 xWorldPosition;\n" : "");
|
||||
/* each point light: its position and 1 / radius squared, its cone's axis
|
||||
and falloff scale, its color and falloff offset */
|
||||
for (light = 0; point_lights && light < 2; light++)
|
||||
{
|
||||
int first = 1 + light * 3;
|
||||
|
||||
xgpu_text_append(text,
|
||||
"\t{\n"
|
||||
"\t\tvec3 to_light = model_lights[%d].xyz - xWorldPosition;\n"
|
||||
"\t\tfloat distance_squared = dot(to_light, to_light);\n"
|
||||
"\t\tvec3 l = to_light * inversesqrt(max(distance_squared, 1.0e-12));\n"
|
||||
"\t\tlight += max(1.0 - distance_squared * model_lights[%d].w, 0.0) * clamp(dot(l, n), 0.0, 1.0) *\n"
|
||||
"\t\t\tclamp(dot(l, -model_lights[%d].xyz) * model_lights[%d].w + model_lights[%d].w, 0.0, 1.0) *\n"
|
||||
"\t\t\tmodel_lights[%d].xyz;\n"
|
||||
"\t}\n",
|
||||
first, first, first + 1, first + 1, first + 2, first + 2);
|
||||
}
|
||||
xgpu_text_append(text, "\treturn clamp(light, 0.0, 1.0);\n}\n");
|
||||
}
|
||||
|
||||
char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
|
||||
{
|
||||
const DWORD *state = key->combiner_state;
|
||||
@@ -556,6 +609,8 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
|
||||
XGPU_PIXEL_UNIFORMS);
|
||||
for (stage = 0; stage < 4; stage++)
|
||||
xgpu_text_append(&text, "uniform %s tex%d;\n", sampler_declaration(key->sampler_type[stage]), stage);
|
||||
if (key->per_pixel_lighting)
|
||||
model_lighting(&text, key->per_pixel_lighting == 2);
|
||||
xgpu_text_append(&text,
|
||||
"float signed_byte(float x)\n"
|
||||
"{\n"
|
||||
@@ -572,6 +627,8 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
|
||||
"\tvec4 v1 = xD1;\n"
|
||||
"\tvec4 t0 = vec4(0.0), t1 = vec4(0.0), t2 = vec4(0.0), t3 = vec4(0.0);\n"
|
||||
"\tfloat dot0 = 0.0, dot1 = 0.0, dot2 = 0.0, dot3 = 0.0;\n");
|
||||
if (key->per_pixel_lighting)
|
||||
xgpu_text_append(&text, "\tv0.rgb = model_lighting();\n");
|
||||
|
||||
for (stage = 0; stage < 4; stage++)
|
||||
texture_stage(&text, key, stage);
|
||||
@@ -645,9 +702,27 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
|
||||
const char *comparison = comparison_operator(key->alpha_test_function);
|
||||
|
||||
if (!comparison)
|
||||
{
|
||||
xgpu_text_append(&text, "\tdiscard;\n");
|
||||
}
|
||||
else if (*comparison && key->alpha_test_samples && comparison[0] != '=' && comparison[0] != '!')
|
||||
{
|
||||
/* multisampled: the samples covered as alpha passes the reference
|
||||
over the pixel (its change across the pixel from fwidth), the rest
|
||||
left as they are; alpha itself is kept, the game keeping values of
|
||||
its own in destination alpha */
|
||||
xgpu_text_append(&text,
|
||||
"\tfloat test_alpha = clamp(result.a, 0.0, 1.0) * 255.0;\n"
|
||||
"\tfloat test_coverage = clamp(%s(test_alpha - alpha_reference) / max(fwidth(test_alpha), 1.0) + 0.5, 0.0, 1.0);\n"
|
||||
"\tint test_samples = int(test_coverage * %d.0 + 0.5);\n"
|
||||
"\tif (test_samples == 0) discard;\n"
|
||||
"\tgl_SampleMask[0] = (1 << test_samples) - 1;\n",
|
||||
comparison[0] == '<' ? "-" : "", (int)key->alpha_test_samples);
|
||||
}
|
||||
else if (*comparison)
|
||||
{
|
||||
xgpu_text_append(&text, "\tif (!(floor(clamp(result.a, 0.0, 1.0) * 255.0 + 0.5) %s alpha_reference)) discard;\n", comparison);
|
||||
}
|
||||
}
|
||||
if (*config_string("debug.gpu_debug_expression"))
|
||||
xgpu_text_append(&text, "\tresult = vec4(vec3(%s), 1.0);\n", config_string("debug.gpu_debug_expression"));
|
||||
|
||||
+228
-26
@@ -7,7 +7,7 @@ GLSL.
|
||||
Each instruction is four little-endian words. Word 1 to 3 hold a MAC
|
||||
(vector) operation and an ILU (scalar) operation that execute in parallel
|
||||
on the same three operands A, B and C, and the destinations of both; the
|
||||
bit positions of every field are spelled out in operand() and in
|
||||
bit positions of every field are spelled out in operand_fields() and in
|
||||
nv2a_vertex_shader_to_glsl(). When both units run, the ILU result goes to
|
||||
temporary r1, whatever the instruction's temporary register field says.
|
||||
|
||||
@@ -76,43 +76,55 @@ static void write_mask(unsigned long mask, char *out)
|
||||
out[count] = 0;
|
||||
}
|
||||
|
||||
static void operand(struct xgpu_text *text, const DWORD *instruction, char which, int relative)
|
||||
struct operand_fields
|
||||
{
|
||||
static const char swizzle_names[] = "xyzw";
|
||||
unsigned long negate, swizzle[4], index, mux;
|
||||
};
|
||||
|
||||
static void operand_fields(const DWORD *instruction, char which, struct operand_fields *fields)
|
||||
{
|
||||
switch (which)
|
||||
{
|
||||
case 'A':
|
||||
negate = field(instruction, 1, 8, 1);
|
||||
swizzle[0] = field(instruction, 1, 6, 2);
|
||||
swizzle[1] = field(instruction, 1, 4, 2);
|
||||
swizzle[2] = field(instruction, 1, 2, 2);
|
||||
swizzle[3] = field(instruction, 1, 0, 2);
|
||||
index = field(instruction, 2, 28, 4);
|
||||
mux = field(instruction, 2, 26, 2);
|
||||
fields->negate = field(instruction, 1, 8, 1);
|
||||
fields->swizzle[0] = field(instruction, 1, 6, 2);
|
||||
fields->swizzle[1] = field(instruction, 1, 4, 2);
|
||||
fields->swizzle[2] = field(instruction, 1, 2, 2);
|
||||
fields->swizzle[3] = field(instruction, 1, 0, 2);
|
||||
fields->index = field(instruction, 2, 28, 4);
|
||||
fields->mux = field(instruction, 2, 26, 2);
|
||||
break;
|
||||
case 'B':
|
||||
negate = field(instruction, 2, 25, 1);
|
||||
swizzle[0] = field(instruction, 2, 23, 2);
|
||||
swizzle[1] = field(instruction, 2, 21, 2);
|
||||
swizzle[2] = field(instruction, 2, 19, 2);
|
||||
swizzle[3] = field(instruction, 2, 17, 2);
|
||||
index = field(instruction, 2, 13, 4);
|
||||
mux = field(instruction, 2, 11, 2);
|
||||
fields->negate = field(instruction, 2, 25, 1);
|
||||
fields->swizzle[0] = field(instruction, 2, 23, 2);
|
||||
fields->swizzle[1] = field(instruction, 2, 21, 2);
|
||||
fields->swizzle[2] = field(instruction, 2, 19, 2);
|
||||
fields->swizzle[3] = field(instruction, 2, 17, 2);
|
||||
fields->index = field(instruction, 2, 13, 4);
|
||||
fields->mux = field(instruction, 2, 11, 2);
|
||||
break;
|
||||
default:
|
||||
negate = field(instruction, 2, 10, 1);
|
||||
swizzle[0] = field(instruction, 2, 8, 2);
|
||||
swizzle[1] = field(instruction, 2, 6, 2);
|
||||
swizzle[2] = field(instruction, 2, 4, 2);
|
||||
swizzle[3] = field(instruction, 2, 2, 2);
|
||||
index = (field(instruction, 2, 0, 2) << 2) | field(instruction, 3, 30, 2);
|
||||
mux = field(instruction, 3, 28, 2);
|
||||
fields->negate = field(instruction, 2, 10, 1);
|
||||
fields->swizzle[0] = field(instruction, 2, 8, 2);
|
||||
fields->swizzle[1] = field(instruction, 2, 6, 2);
|
||||
fields->swizzle[2] = field(instruction, 2, 4, 2);
|
||||
fields->swizzle[3] = field(instruction, 2, 2, 2);
|
||||
fields->index = (field(instruction, 2, 0, 2) << 2) | field(instruction, 3, 30, 2);
|
||||
fields->mux = field(instruction, 3, 28, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
xgpu_text_append(text, "%s", negate ? "-" : "");
|
||||
static void operand(struct xgpu_text *text, const DWORD *instruction, char which, int relative)
|
||||
{
|
||||
static const char swizzle_names[] = "xyzw";
|
||||
struct operand_fields fields;
|
||||
unsigned long *swizzle = fields.swizzle, index, mux;
|
||||
|
||||
operand_fields(instruction, which, &fields);
|
||||
index = fields.index;
|
||||
mux = fields.mux;
|
||||
xgpu_text_append(text, "%s", fields.negate ? "-" : "");
|
||||
switch (mux)
|
||||
{
|
||||
case _mux_temporary:
|
||||
@@ -139,6 +151,183 @@ static void operand(struct xgpu_text *text, const DWORD *instruction, char which
|
||||
swizzle_names[swizzle[0]], swizzle_names[swizzle[1]], swizzle_names[swizzle[2]], swizzle_names[swizzle[3]]);
|
||||
}
|
||||
|
||||
/* ---------- model lighting
|
||||
|
||||
The game's model lighting programs (rasterizer_xbox_vertex_shaders_data.inc
|
||||
9, 10, 17 and 27) sum into the diffuse color oD0.xyz the ambient light
|
||||
c[-69], two distant lights (direction c[-73] and c[-71], color c[-72] and
|
||||
c[-70]; the first also lights the back by the translucency c[-82].z) and, in
|
||||
10 and 17, two point lights (c[-79] to c[-77] and c[-76] to c[-74]: position
|
||||
and 1 / radius squared, the cone's axis and falloff scale, the color and
|
||||
falloff offset). The normal and world position they light by are
|
||||
temporaries that the programs reuse (10 and 17 overwrite both after the
|
||||
sum). In 9, 10 and 17 the normal is the skinned normal turned by c[-84].w
|
||||
for the back faces' pass, r0 (written by instruction 17) and the position
|
||||
the skinned position, r10 (by 12); in 27, of a single node, the normal is
|
||||
r3 (by 5). They are taken as the first instruction that lights by them
|
||||
reads them: in 9 the normal before instruction 19, in 10 and 17 the normal
|
||||
before 24 and the position before 19, in 27 the normal before 7. A program
|
||||
is taken only where its lighting is the sum nv2a_psh.c computes for each
|
||||
pixel (the diffuse color written once, by the sum; both distant lights by
|
||||
one normal; the translucency; each point light by one position, its cone
|
||||
by its axis reversed) and the registers keep the same normal and position
|
||||
through it. Nothing else of the programs goes into oD0.xyz; oD0.w is 0, or
|
||||
in 17 the planar fog's. */
|
||||
|
||||
static BOOL xyz_unswizzled(const struct operand_fields *operand)
|
||||
{
|
||||
return operand->swizzle[0] == 0 && operand->swizzle[1] == 1 && operand->swizzle[2] == 2;
|
||||
}
|
||||
|
||||
/* the operand is c[reg] (as the game numbers them: -73), its xyz unswizzled */
|
||||
static BOOL light_constant(const DWORD *instruction, const struct operand_fields *operand, int reg, BOOL negated)
|
||||
{
|
||||
return operand->mux == _mux_constant && !field(instruction, 3, 1, 1) &&
|
||||
field(instruction, 1, 13, 8) == (unsigned long)(XGPU_VERTEX_CONSTANT_BIAS + reg) &&
|
||||
operand->negate == (negated ? 1UL : 0UL) && xyz_unswizzled(operand);
|
||||
}
|
||||
|
||||
/* the operand is a temporary (not the position output), its xyz unswizzled */
|
||||
static BOOL light_temporary(const struct operand_fields *operand, BOOL negated)
|
||||
{
|
||||
return operand->mux == _mux_temporary && operand->index < 12 &&
|
||||
operand->negate == (negated ? 1UL : 0UL) && xyz_unswizzled(operand);
|
||||
}
|
||||
|
||||
/* any of x, y and z of temporary reg written by the instructions in [first, last) */
|
||||
static BOOL temporary_written(const DWORD *instructions, unsigned long first, unsigned long last, unsigned long reg)
|
||||
{
|
||||
unsigned long index;
|
||||
|
||||
for (index = first; index < last; index++)
|
||||
{
|
||||
const DWORD *instruction = instructions + index * 4;
|
||||
unsigned long mac = field(instruction, 1, 21, 4);
|
||||
unsigned long ilu = field(instruction, 1, 25, 3);
|
||||
unsigned long temporary = field(instruction, 3, 20, 4);
|
||||
|
||||
if (mac != _mac_nop && mac != _mac_arl && temporary == reg && (field(instruction, 3, 24, 4) & 0xe))
|
||||
return TRUE;
|
||||
/* (the ILU writes r1 when both units run) */
|
||||
if (ilu != _ilu_nop && (mac != _mac_nop ? 1 : temporary) == reg && (field(instruction, 3, 16, 4) & 0xe))
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
BOOL nv2a_vertex_shader_lighting(const DWORD *instructions, unsigned long instruction_count,
|
||||
struct nv2a_vertex_lighting *lighting)
|
||||
{
|
||||
unsigned long count, index, diffuse;
|
||||
unsigned long distant[2], distant_register[2] = { 0, 0 }, point[2], point_register[2] = { 0, 0 };
|
||||
BOOL cone[2] = { FALSE, FALSE }, translucency = FALSE, point_lights = FALSE;
|
||||
int light;
|
||||
|
||||
/* to the instruction that ends the program */
|
||||
for (count = 0; count < instruction_count;)
|
||||
{
|
||||
if (field(instructions + count++ * 4, 3, 0, 1))
|
||||
break;
|
||||
}
|
||||
diffuse = distant[0] = distant[1] = point[0] = point[1] = count;
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
const DWORD *instruction = instructions + index * 4;
|
||||
unsigned long mac = field(instruction, 1, 21, 4);
|
||||
unsigned long output_mask = field(instruction, 3, 12, 4);
|
||||
unsigned long constant = field(instruction, 1, 13, 8);
|
||||
struct operand_fields a, b, c;
|
||||
|
||||
operand_fields(instruction, 'A', &a);
|
||||
operand_fields(instruction, 'B', &b);
|
||||
operand_fields(instruction, 'C', &c);
|
||||
/* the diffuse color's x, y and z, written once and last by the sum:
|
||||
the second distant light's term times its color, plus the rest */
|
||||
if ((output_mask & 0xe) && field(instruction, 3, 11, 1) && field(instruction, 3, 3, 8) == 3)
|
||||
{
|
||||
if (diffuse != count || (output_mask & 0xe) != 0xe || field(instruction, 3, 2, 1) ||
|
||||
mac != _mac_mad || !light_constant(instruction, &b, -70, FALSE) || c.mux != _mux_temporary)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
diffuse = index;
|
||||
}
|
||||
for (light = 0; light < 2; light++)
|
||||
{
|
||||
/* each distant light's facing: the normal's dot product with the
|
||||
light's direction reversed */
|
||||
if (mac == _mac_dp3 && light_constant(instruction, &b, light ? -71 : -73, TRUE))
|
||||
{
|
||||
if (distant[light] != count || !light_temporary(&a, FALSE))
|
||||
return FALSE;
|
||||
distant[light] = index;
|
||||
distant_register[light] = a.index;
|
||||
}
|
||||
/* each point light's direction, from the world position */
|
||||
if (mac == _mac_add && light_constant(instruction, &a, light ? -76 : -79, FALSE) &&
|
||||
light_temporary(&c, TRUE))
|
||||
{
|
||||
if (point[light] != count)
|
||||
return FALSE;
|
||||
point[light] = index;
|
||||
point_register[light] = c.index;
|
||||
}
|
||||
/* and its cone: the direction's dot product with the light's axis
|
||||
reversed */
|
||||
if (mac == _mac_dp3 && b.mux == _mux_constant && !field(instruction, 3, 1, 1) &&
|
||||
constant == (unsigned long)(XGPU_VERTEX_CONSTANT_BIAS + (light ? -75 : -78)))
|
||||
{
|
||||
if (!light_constant(instruction, &b, light ? -75 : -78, TRUE))
|
||||
return FALSE;
|
||||
cone[light] = TRUE;
|
||||
}
|
||||
}
|
||||
/* the back lit by the first distant light: its facing reversed, times
|
||||
the translucency */
|
||||
if (mac == _mac_mul && a.mux == _mux_temporary && a.negate && b.mux == _mux_constant &&
|
||||
!field(instruction, 3, 1, 1) && constant == XGPU_VERTEX_CONSTANT_BIAS - 82 && b.swizzle[0] == 2)
|
||||
{
|
||||
translucency = TRUE;
|
||||
}
|
||||
if ((a.mux == _mux_constant || b.mux == _mux_constant || c.mux == _mux_constant) &&
|
||||
!field(instruction, 3, 1, 1) && constant >= XGPU_VERTEX_CONSTANT_BIAS - 79 &&
|
||||
constant <= XGPU_VERTEX_CONSTANT_BIAS - 74)
|
||||
{
|
||||
point_lights = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
if (diffuse == count || distant[0] >= diffuse || distant[1] >= diffuse ||
|
||||
distant_register[0] != distant_register[1] || !translucency)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
lighting->lights = 1;
|
||||
lighting->normal_register = distant_register[0];
|
||||
lighting->normal_instruction = distant[0] < distant[1] ? distant[0] : distant[1];
|
||||
if (temporary_written(instructions, lighting->normal_instruction, diffuse, lighting->normal_register))
|
||||
return FALSE;
|
||||
lighting->position_instruction = lighting->position_register = 0;
|
||||
if (point_lights)
|
||||
{
|
||||
unsigned long last = point[0] > point[1] ? point[0] : point[1];
|
||||
|
||||
if (point[0] >= diffuse || point[1] >= diffuse || point_register[0] != point_register[1] ||
|
||||
!cone[0] || !cone[1])
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
lighting->lights = 2;
|
||||
lighting->position_register = point_register[0];
|
||||
lighting->position_instruction = point[0] < point[1] ? point[0] : point[1];
|
||||
if (temporary_written(instructions, lighting->position_instruction, last, lighting->position_register))
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ---------- translation */
|
||||
|
||||
static const char shader_prologue[] =
|
||||
#ifdef HALO_ANDROID
|
||||
/* the #version line comes first, from the context's capabilities */
|
||||
@@ -195,7 +384,7 @@ static const char shader_prologue[] =
|
||||
"}\n";
|
||||
|
||||
char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count,
|
||||
unsigned long packed_attribute_mask, int multiview)
|
||||
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting, int multiview)
|
||||
{
|
||||
struct xgpu_text text = { 0 };
|
||||
unsigned long index;
|
||||
@@ -221,6 +410,11 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
|
||||
(void)multiview;
|
||||
#endif
|
||||
xgpu_text_append(&text, "%s", shader_prologue);
|
||||
/* (the pixel shader's model_lighting; the normal's length in w) */
|
||||
if (lighting)
|
||||
xgpu_text_append(&text, "out vec4 xWorldNormal;\n");
|
||||
if (lighting && lighting->lights == 2)
|
||||
xgpu_text_append(&text, "out vec3 xWorldPosition;\n");
|
||||
for (index = 0; index < XGPU_VERTEX_ATTRIBUTE_COUNT; index++)
|
||||
{
|
||||
if (packed_attribute_mask & (1UL << index))
|
||||
@@ -267,6 +461,14 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
|
||||
char mask[5];
|
||||
|
||||
xgpu_text_append(&text, "\t/* %lu */\n", index);
|
||||
/* the lighting's normal and position, before the program reuses them */
|
||||
if (lighting && index == lighting->normal_instruction)
|
||||
{
|
||||
xgpu_text_append(&text, "\txWorldNormal = vec4(r%lu.xyz, length(r%lu.xyz));\n",
|
||||
lighting->normal_register, lighting->normal_register);
|
||||
}
|
||||
if (lighting && lighting->lights == 2 && index == lighting->position_instruction)
|
||||
xgpu_text_append(&text, "\txWorldPosition = r%lu.xyz;\n", lighting->position_register);
|
||||
xgpu_text_append(&text, "\tA = "); operand(&text, instruction, 'A', relative); xgpu_text_append(&text, ";\n");
|
||||
xgpu_text_append(&text, "\tB = "); operand(&text, instruction, 'B', relative); xgpu_text_append(&text, ";\n");
|
||||
xgpu_text_append(&text, "\tC = "); operand(&text, instruction, 'C', relative); xgpu_text_append(&text, ";\n");
|
||||
|
||||
@@ -2421,7 +2421,13 @@ int p2p_public_games(struct p2p_public_game *games, int maximum)
|
||||
listing_count = p2p_lobby_games(listings, 32);
|
||||
for (index = 0; index < listing_count && count < maximum; index++)
|
||||
{
|
||||
struct p2p_public_game *game = &games[count++];
|
||||
struct p2p_public_game *game;
|
||||
|
||||
/* (one with a password: joined from the server browser, which asks
|
||||
for it, not from this list, which has no way to) */
|
||||
if (listings[index].locked)
|
||||
continue;
|
||||
game = &games[count++];
|
||||
|
||||
memset(game, 0, sizeof(*game));
|
||||
snprintf(game->name, sizeof(game->name), "%s", listings[index].name);
|
||||
@@ -2533,7 +2539,8 @@ void p2p_new_invite_if_listed(void)
|
||||
if (!p2p.has_token || !p2p.token_listed)
|
||||
return;
|
||||
make_invite();
|
||||
platform_log("Internet play: the game is private now, with a new invite: %s", p2p.invite);
|
||||
platform_log("Internet play: a new invite, so that the one listed lets no one in (private, or a new password): %s",
|
||||
p2p.invite);
|
||||
if (p2p.hosting)
|
||||
{
|
||||
p2p_signal_host(p2p.token);
|
||||
|
||||
@@ -149,6 +149,12 @@ in everyone's server browser: Create Game's PUBLIC; a private game is
|
||||
reached only by its invite. Going private makes a new invite, if the old one
|
||||
was listed */
|
||||
void p2p_set_hosting_public(int public);
|
||||
/* the password of the game hosted for the internet (Server Setup's
|
||||
PASSWORD; NULL or empty: none): its listing lets only those who know it
|
||||
join from the server browser (its invite link still joins it). Setting or
|
||||
changing it makes a new invite, if the old one was listed. It takes a few
|
||||
milliseconds (the password's key) */
|
||||
void p2p_set_hosting_password(const char *password);
|
||||
/* the hosted game's details as listed (printable ASCII is kept; NULL leaves
|
||||
one as it was): the game's server calls it as they change (calling it with
|
||||
the same again costs little) */
|
||||
@@ -167,10 +173,16 @@ enum
|
||||
/* an invite link's text (P2P_LINK_SIZE) */
|
||||
P2P_LISTING_INVITE_SIZE = 77,
|
||||
/* (struct p2p_public_game's invite is as large) */
|
||||
/* a password's game's: its host's key and its key's hash, and its token
|
||||
sealed (P2P_KEY_SIZE, P2P_KEY_HASH_SIZE, P2P_SEALED_TOKEN_SIZE) */
|
||||
P2P_LISTING_SIGNING_KEY_SIZE = 32,
|
||||
P2P_LISTING_KEY_HASH_SIZE = 16,
|
||||
P2P_LISTING_SEALED_TOKEN_SIZE = 56,
|
||||
};
|
||||
|
||||
struct p2p_listing
|
||||
{
|
||||
/* (empty for a locked one until p2p_listing_unlock opens it) */
|
||||
char invite[P2P_LISTING_INVITE_SIZE];
|
||||
/* the host's (its XNADDR's abEnet once reached) */
|
||||
unsigned char identifier[6];
|
||||
@@ -183,12 +195,20 @@ struct p2p_listing
|
||||
unsigned char failed;
|
||||
/* milliseconds, -1 if not known */
|
||||
short ping;
|
||||
/* it has a password: its invite is sealed with it (p2p_listing_unlock) */
|
||||
unsigned char locked;
|
||||
unsigned char key_hash[P2P_LISTING_KEY_HASH_SIZE];
|
||||
unsigned char signing_key[P2P_LISTING_SIGNING_KEY_SIZE];
|
||||
unsigned char sealed_token[P2P_LISTING_SEALED_TOKEN_SIZE];
|
||||
};
|
||||
/* a copy of the public games found, in the order shown (the most players
|
||||
first; then those not failed, the open ones, by name); returns their count */
|
||||
int p2p_lobby_games(struct p2p_listing *games, int maximum_count);
|
||||
/* a game the browser could not join: kept, marked failed, for this run */
|
||||
void p2p_lobby_mark_failed(const unsigned char *identifier);
|
||||
/* a locked game's invite (into its invite), if the password is its own:
|
||||
FALSE if not; TRUE for a game with none. It takes a few milliseconds */
|
||||
int p2p_listing_unlock(struct p2p_listing *listing, const char *password);
|
||||
|
||||
/* the sizes of a Discord user's id and name as kept (with their end), and
|
||||
the text kept of either as told: only the characters allowed (digits in an
|
||||
|
||||
@@ -20,6 +20,7 @@ tunnel's packets are sealed with keys only its two machines have.
|
||||
#include "monocypher.h"
|
||||
#include "monocypher-ed25519.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---------- SHA-256 */
|
||||
@@ -649,6 +650,50 @@ int p2p_ed25519_to_x25519(const unsigned char *public_key, unsigned char *x25519
|
||||
return !p2p_equal(product, zero, P2P_KEY_SIZE);
|
||||
}
|
||||
|
||||
/* ---------- passwords (a password-protected public game's listing holds
|
||||
its invite's token sealed with the password's key: p2p_lobby.c) */
|
||||
|
||||
/* (Argon2id of 4 MiB and three passes: tens of milliseconds, once for the
|
||||
host and once a guess for a joiner) */
|
||||
#define PASSWORD_KEY_BLOCKS 4096
|
||||
#define PASSWORD_KEY_PASSES 3
|
||||
|
||||
void p2p_password_key(const char *password, const unsigned char *salt, unsigned char *key)
|
||||
{
|
||||
crypto_argon2_config config = { CRYPTO_ARGON2_ID, PASSWORD_KEY_BLOCKS, PASSWORD_KEY_PASSES, 1 };
|
||||
crypto_argon2_inputs inputs;
|
||||
void *work_area = malloc((size_t)PASSWORD_KEY_BLOCKS * 1024);
|
||||
|
||||
inputs.pass = (const unsigned char *)password;
|
||||
inputs.pass_size = (unsigned int)strlen(password);
|
||||
inputs.salt = salt;
|
||||
inputs.salt_size = P2P_KEY_SIZE;
|
||||
if (!work_area)
|
||||
{
|
||||
/* (no key anyone could guess: nothing opens with it) */
|
||||
posix_random_bytes(key, P2P_PASSWORD_KEY_SIZE);
|
||||
return;
|
||||
}
|
||||
crypto_argon2(key, P2P_PASSWORD_KEY_SIZE, work_area, config, inputs, crypto_argon2_no_extras);
|
||||
crypto_wipe(work_area, (size_t)PASSWORD_KEY_BLOCKS * 1024);
|
||||
free(work_area);
|
||||
}
|
||||
|
||||
void p2p_seal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *token,
|
||||
unsigned char *sealed)
|
||||
{
|
||||
/* (a nonce of its own each time: 24 random bytes never repeat) */
|
||||
posix_random_bytes(sealed, 24);
|
||||
crypto_aead_lock(sealed + 24 + 16, sealed + 24, key, sealed, signing_key, P2P_KEY_SIZE, token, P2P_TOKEN_SIZE);
|
||||
}
|
||||
|
||||
int p2p_unseal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *sealed,
|
||||
unsigned char *token)
|
||||
{
|
||||
return crypto_aead_unlock(token, sealed + 24, key, sealed, signing_key, P2P_KEY_SIZE, sealed + 24 + 16,
|
||||
P2P_TOKEN_SIZE) == 0;
|
||||
}
|
||||
|
||||
int p2p_ed25519_verify(const unsigned char *public_key, const void *message, int size,
|
||||
const unsigned char *signature)
|
||||
{
|
||||
|
||||
@@ -246,6 +246,26 @@ int p2p_ed25519_verify(const unsigned char *public_key, const void *message, int
|
||||
/* the X25519 public key of an Ed25519 one; 0 if it has a small order */
|
||||
int p2p_ed25519_to_x25519(const unsigned char *public_key, unsigned char *x25519_public);
|
||||
|
||||
enum
|
||||
{
|
||||
/* a password-protected listing's token, sealed (p2p_seal_token): its
|
||||
nonce (24), its tag (16), then the token sealed */
|
||||
P2P_PASSWORD_KEY_SIZE = 32,
|
||||
P2P_SEALED_TOKEN_SIZE = 24 + 16 + 16,
|
||||
};
|
||||
|
||||
/* the key of a password (Argon2id: P2P_PASSWORD_KEY_SIZE bytes), for the
|
||||
host whose Ed25519 key salt is (P2P_KEY_SIZE bytes): it takes a few
|
||||
milliseconds, so that guessing passwords at a listing takes long */
|
||||
void p2p_password_key(const char *password, const unsigned char *salt, unsigned char *key);
|
||||
/* a token (P2P_TOKEN_SIZE bytes) sealed with a password's key, bound to the
|
||||
host's Ed25519 key (P2P_SEALED_TOKEN_SIZE bytes); and opened: 0 if the key
|
||||
is not the one it was sealed with (a wrong password), or it was altered */
|
||||
void p2p_seal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *token,
|
||||
unsigned char *sealed);
|
||||
int p2p_unseal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *sealed,
|
||||
unsigned char *token);
|
||||
|
||||
/* ---------- p2p_lobby.c: public games' listings */
|
||||
|
||||
/* the p2p thread's pass: the token of the game hosted for the internet
|
||||
|
||||
+119
-13
@@ -12,9 +12,11 @@ the host can list its invite, alter its listing, or list another's under
|
||||
false details:
|
||||
|
||||
"HL", format 1, the lobby version (2: HALO_PORT_NETWORK_VERSION; browsers
|
||||
hide others), flags (open, under way, teams, closed), sequence (4: newest
|
||||
wins), Unix time (4), the Ed25519 key (32), the invite's token (16),
|
||||
players, most players, the gametype's engine (1 each), the game's name,
|
||||
hide others), flags (open, under way, teams, closed, password), sequence
|
||||
(4: newest wins), Unix time (4), the Ed25519 key (32), the invite's token
|
||||
(16; a password's game's sealed with the password's key, 56:
|
||||
p2p_seal_token; a tombstone's zero), players, most players, the
|
||||
gametype's engine (1 each), the game's name,
|
||||
map and gametype (a length byte, then up to 32, 32 and 24 characters of
|
||||
printable ASCII), a stamp (8, reserved: zero), and the signature (64) of
|
||||
"hceu-lobby-1" and all before it.
|
||||
@@ -38,6 +40,14 @@ one deletes or keeps is no authority, so
|
||||
A host that stops publishes a tombstone, then clears its slot. Going private
|
||||
makes a new invite (p2p.c), so a listing seen before lets no one in.
|
||||
|
||||
A game with a password (Server Setup's PASSWORD: p2p_set_hosting_password)
|
||||
is listed with its token sealed with the password's key (Argon2id of the
|
||||
password, salted with the host's key: p2p_password_key), so that only who
|
||||
knows the password can join it from the server browser
|
||||
(p2p_listing_unlock); its invite link, which holds the token, still joins it
|
||||
as any invite does. Setting or changing the password makes a new invite, as
|
||||
going private does.
|
||||
|
||||
Checking signatures takes work, and anyone can send listings: at most
|
||||
VERIFY_BUDGET milliseconds of it each pass of the p2p thread, from a queue
|
||||
of MAXIMUM_QUEUED; the rest is dropped (hosts publish again).
|
||||
@@ -62,11 +72,11 @@ enum
|
||||
_listing_has_teams = 4,
|
||||
/* a tombstone: the host stopped */
|
||||
_listing_closed = 8,
|
||||
/* (reserved) */
|
||||
/* its token sealed with a password's key */
|
||||
_listing_password = 16,
|
||||
STAMP_SIZE = 8,
|
||||
/* the signed part's end: everything but the signature */
|
||||
MAXIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_TOKEN_SIZE + 3 +
|
||||
MAXIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_SEALED_TOKEN_SIZE + 3 +
|
||||
(1 + P2P_LISTING_NAME_SIZE) + (1 + P2P_LISTING_MAP_SIZE) + (1 + P2P_LISTING_GAMETYPE_SIZE) + STAMP_SIZE +
|
||||
P2P_SIGNATURE_SIZE,
|
||||
MINIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_TOKEN_SIZE + 3 + 3 + STAMP_SIZE +
|
||||
@@ -88,6 +98,10 @@ enum
|
||||
|
||||
static const char signature_label[] = "hceu-lobby-1";
|
||||
|
||||
/* (p2p.h's sizes of a locked listing's are p2p_internal.h's) */
|
||||
typedef char check_listing_sizes[P2P_LISTING_SIGNING_KEY_SIZE == P2P_KEY_SIZE &&
|
||||
P2P_LISTING_KEY_HASH_SIZE == P2P_KEY_HASH_SIZE && P2P_LISTING_SEALED_TOKEN_SIZE == P2P_SEALED_TOKEN_SIZE ? 1 : -1];
|
||||
|
||||
/* a listing, read */
|
||||
struct listing
|
||||
{
|
||||
@@ -97,6 +111,8 @@ struct listing
|
||||
unsigned long time;
|
||||
unsigned char key[P2P_KEY_SIZE];
|
||||
unsigned char token[P2P_TOKEN_SIZE];
|
||||
/* (_listing_password: its token, sealed) */
|
||||
unsigned char sealed_token[P2P_SEALED_TOKEN_SIZE];
|
||||
int player_count, maximum_player_count, engine_type;
|
||||
char name[P2P_LISTING_NAME_SIZE + 1];
|
||||
char map[P2P_LISTING_MAP_SIZE + 1];
|
||||
@@ -143,6 +159,9 @@ static struct
|
||||
int engine_type;
|
||||
int flags;
|
||||
int player_count, maximum_player_count;
|
||||
/* the password's key (p2p_set_hosting_password), if it has one */
|
||||
int has_password;
|
||||
unsigned char password_key[P2P_PASSWORD_KEY_SIZE];
|
||||
/* listed: with this token; its listing as last published */
|
||||
int listed;
|
||||
unsigned char token[P2P_TOKEN_SIZE];
|
||||
@@ -240,6 +259,8 @@ static int listing_make(unsigned char *bytes, int flags)
|
||||
unsigned char data[sizeof(signature_label) - 1 + MAXIMUM_LISTING_SIZE];
|
||||
int size = 0;
|
||||
|
||||
if (lobby.has_password && !(flags & _listing_closed))
|
||||
flags |= _listing_password;
|
||||
bytes[size++] = 'H';
|
||||
bytes[size++] = 'L';
|
||||
bytes[size++] = LISTING_FORMAT;
|
||||
@@ -252,8 +273,20 @@ static int listing_make(unsigned char *bytes, int flags)
|
||||
size += 4;
|
||||
memcpy(bytes + size, p2p_signing_key(), P2P_KEY_SIZE);
|
||||
size += P2P_KEY_SIZE;
|
||||
memcpy(bytes + size, lobby.token, P2P_TOKEN_SIZE);
|
||||
size += P2P_TOKEN_SIZE;
|
||||
if (flags & _listing_password)
|
||||
{
|
||||
p2p_seal_token(lobby.password_key, p2p_signing_key(), lobby.token, bytes + size);
|
||||
size += P2P_SEALED_TOKEN_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* (a tombstone's none: it may end a game with a password) */
|
||||
if (flags & _listing_closed)
|
||||
memset(bytes + size, 0, P2P_TOKEN_SIZE);
|
||||
else
|
||||
memcpy(bytes + size, lobby.token, P2P_TOKEN_SIZE);
|
||||
size += P2P_TOKEN_SIZE;
|
||||
}
|
||||
bytes[size++] = (unsigned char)(lobby.player_count > 255 ? 255 : lobby.player_count);
|
||||
bytes[size++] = (unsigned char)(lobby.maximum_player_count > 255 ? 255 : lobby.maximum_player_count);
|
||||
bytes[size++] = (unsigned char)lobby.engine_type;
|
||||
@@ -302,8 +335,20 @@ static int listing_read(const unsigned char *bytes, int size, struct listing *li
|
||||
offset += 4;
|
||||
memcpy(listing->key, bytes + offset, P2P_KEY_SIZE);
|
||||
offset += P2P_KEY_SIZE;
|
||||
memcpy(listing->token, bytes + offset, P2P_TOKEN_SIZE);
|
||||
offset += P2P_TOKEN_SIZE;
|
||||
if (listing->flags & _listing_password)
|
||||
{
|
||||
/* (its token sealed, which is longer: the counts after it too) */
|
||||
if (offset + P2P_SEALED_TOKEN_SIZE + 3 > size)
|
||||
return 0;
|
||||
memset(listing->token, 0, P2P_TOKEN_SIZE);
|
||||
memcpy(listing->sealed_token, bytes + offset, P2P_SEALED_TOKEN_SIZE);
|
||||
offset += P2P_SEALED_TOKEN_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(listing->token, bytes + offset, P2P_TOKEN_SIZE);
|
||||
offset += P2P_TOKEN_SIZE;
|
||||
}
|
||||
listing->player_count = bytes[offset++];
|
||||
listing->maximum_player_count = bytes[offset++];
|
||||
listing->engine_type = bytes[offset++];
|
||||
@@ -569,10 +614,22 @@ static void listing_take(const struct queued *queued, const struct listing *list
|
||||
memcpy(game->payload, queued->payload, (size_t)queued->size);
|
||||
game->payload_size = queued->size;
|
||||
shown = &game->listing;
|
||||
memcpy(bytes, queued->key_hash, P2P_KEY_HASH_SIZE);
|
||||
memcpy(bytes + P2P_KEY_HASH_SIZE, listing->token, P2P_TOKEN_SIZE);
|
||||
p2p_hex(bytes, sizeof(bytes), text);
|
||||
snprintf(shown->invite, sizeof(shown->invite), "halo://join/%s", text);
|
||||
shown->locked = (listing->flags & _listing_password) != 0;
|
||||
if (shown->locked)
|
||||
{
|
||||
/* (its invite once the password opens its token: p2p_listing_unlock) */
|
||||
shown->invite[0] = 0;
|
||||
memcpy(shown->key_hash, queued->key_hash, P2P_KEY_HASH_SIZE);
|
||||
memcpy(shown->signing_key, listing->key, P2P_KEY_SIZE);
|
||||
memcpy(shown->sealed_token, listing->sealed_token, P2P_SEALED_TOKEN_SIZE);
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(bytes, queued->key_hash, P2P_KEY_HASH_SIZE);
|
||||
memcpy(bytes + P2P_KEY_HASH_SIZE, listing->token, P2P_TOKEN_SIZE);
|
||||
p2p_hex(bytes, sizeof(bytes), text);
|
||||
snprintf(shown->invite, sizeof(shown->invite), "halo://join/%s", text);
|
||||
}
|
||||
p2p_identifier_from_hash(queued->key_hash, shown->identifier);
|
||||
memcpy(shown->name, listing->name, sizeof(shown->name));
|
||||
memcpy(shown->map, listing->map, sizeof(shown->map));
|
||||
@@ -669,6 +726,33 @@ void p2p_set_hosting_public(int public)
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
}
|
||||
|
||||
void p2p_set_hosting_password(const char *password)
|
||||
{
|
||||
unsigned char key[P2P_PASSWORD_KEY_SIZE];
|
||||
int has_password = password && *password;
|
||||
|
||||
/* (the key takes a while: not under the lock) */
|
||||
if (has_password)
|
||||
p2p_password_key(password, p2p_signing_key(), key);
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
if (has_password != lobby.has_password ||
|
||||
(has_password && !p2p_equal(key, lobby.password_key, P2P_PASSWORD_KEY_SIZE)))
|
||||
{
|
||||
/* (a new invite, if one was listed: who saw it, with no password or
|
||||
another, cannot join with it) */
|
||||
p2p_new_invite_if_listed();
|
||||
lobby.has_password = has_password;
|
||||
if (has_password)
|
||||
memcpy(lobby.password_key, key, sizeof(key));
|
||||
else
|
||||
memset(lobby.password_key, 0, sizeof(lobby.password_key));
|
||||
if (lobby.listed)
|
||||
republish_soon(0);
|
||||
}
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
memset(key, 0, sizeof(key));
|
||||
}
|
||||
|
||||
void p2p_set_game_listing(const char *name, const char *map, const char *gametype, int engine_type, int open,
|
||||
int in_progress, int has_teams)
|
||||
{
|
||||
@@ -765,6 +849,28 @@ int p2p_lobby_games(struct p2p_listing *games, int maximum_count)
|
||||
return count;
|
||||
}
|
||||
|
||||
int p2p_listing_unlock(struct p2p_listing *listing, const char *password)
|
||||
{
|
||||
unsigned char key[P2P_PASSWORD_KEY_SIZE];
|
||||
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
|
||||
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
|
||||
int opened;
|
||||
|
||||
if (!listing->locked)
|
||||
return 1;
|
||||
p2p_password_key(password ? password : "", listing->signing_key, key);
|
||||
memcpy(bytes, listing->key_hash, P2P_KEY_HASH_SIZE);
|
||||
opened = p2p_unseal_token(key, listing->signing_key, listing->sealed_token, bytes + P2P_KEY_HASH_SIZE);
|
||||
if (opened)
|
||||
{
|
||||
p2p_hex(bytes, sizeof(bytes), text);
|
||||
snprintf(listing->invite, sizeof(listing->invite), "halo://join/%s", text);
|
||||
}
|
||||
memset(key, 0, sizeof(key));
|
||||
memset(bytes, 0, sizeof(bytes));
|
||||
return opened;
|
||||
}
|
||||
|
||||
void p2p_lobby_mark_failed(const unsigned char *identifier)
|
||||
{
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
|
||||
@@ -104,8 +104,9 @@ enum
|
||||
PROOF_SIZE = NONCE_SIZE + TAG_SIZE,
|
||||
BUFFER_SIZE = 4096,
|
||||
MAXIMUM_MESSAGE_SIZE = 512,
|
||||
/* a listing's most (p2p_lobby.c's MAXIMUM_LISTING_SIZE, and some) */
|
||||
MAXIMUM_LISTING_SIZE = 256,
|
||||
/* a listing's most (p2p_lobby.c's MAXIMUM_LISTING_SIZE, 268 with a
|
||||
password's sealed token, and some) */
|
||||
MAXIMUM_LISTING_SIZE = 320,
|
||||
/* the publishes awaiting acknowledgement on a broker */
|
||||
MAXIMUM_IN_FLIGHT = 4,
|
||||
/* the topics a broker is subscribed to (_topic_*) */
|
||||
|
||||
@@ -96,6 +96,12 @@ static const struct config_setting config_settings[] =
|
||||
{ "display.max_fps", _config_integer, "0", "HALO_MAX_FPS", _environment_value, _platform_desktop,
|
||||
"With vsync off, the most frames a second: 0 for twice the display's\n"
|
||||
"refresh rate, -1 for no limit (which can hang some Intel graphics)." },
|
||||
{ "display.anti_aliasing", _config_string, "\"off\"", "HALO_ANTI_ALIASING", _environment_value, _platform_all,
|
||||
"Smoothing of jagged edges, which the Xbox did not have: \"off\"; \"fxaa\"\n"
|
||||
"or \"smaa\" smooth the 3D view once it is drawn (the HUD and menus stay\n"
|
||||
"sharp); \"ssaa2x\" draws at twice the resolution each way (four times\n"
|
||||
"the work); \"msaa2x\", \"msaa4x\" or \"msaa8x\" draw with that many samples\n"
|
||||
"a pixel. Android has \"fxaa\" for \"smaa\", and no \"ssaa2x\"." },
|
||||
{ "display.interpolation", _config_boolean, "true", "HALO_INTERPOLATION", _environment_value, _platform_all,
|
||||
"Draw a frame for every display refresh, blending between the game's 30\n"
|
||||
"ticks a second; false keeps the original 30 frames a second." },
|
||||
@@ -111,6 +117,10 @@ static const struct config_setting config_settings[] =
|
||||
"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." },
|
||||
{ "display.shadow_resolution", _config_integer, "128", "HALO_SHADOW_RESOLUTION", _environment_value,
|
||||
_platform_all,
|
||||
"The size the objects' shadows are drawn at, in pixels each way: 128 as\n"
|
||||
"on the Xbox, or 256, 512 or 1024 for smoother edges, as soft." },
|
||||
{ "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." },
|
||||
@@ -135,6 +145,12 @@ static const struct config_setting config_settings[] =
|
||||
_environment_value, _platform_all,
|
||||
"The scoreboard panel's colour: \"red, green, blue, alpha\", each 0 to 255\n"
|
||||
"(alpha 0 is see-through, 255 solid)." },
|
||||
{ "display.per_pixel_lighting", _config_boolean, "false", "HALO_PER_PIXEL_LIGHTING", _environment_value,
|
||||
_platform_all,
|
||||
"Light the models (characters, weapons, vehicles, scenery) for each\n"
|
||||
"pixel by the lights the game gives them, without the facets the light\n"
|
||||
"of each vertex shows across curved surfaces; false lights each vertex,\n"
|
||||
"as the Xbox does." },
|
||||
|
||||
{ "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"
|
||||
@@ -301,6 +317,10 @@ static const struct config_setting config_settings[] =
|
||||
{ "audio.effects_volume", _config_real, "1.0", "HALO_EFFECTS_VOLUME", _environment_value, _platform_all,
|
||||
"The volume of every other sound (effects and speech), 0.0 to 1.0 (of\n"
|
||||
"audio.volume)." },
|
||||
{ "audio.reverb", _config_boolean, "true", "HALO_REVERB", _environment_value, _platform_all,
|
||||
"Reverberate the world's sounds as the place the player is in does (the\n"
|
||||
"maps' sound environments, as the Xbox's I3DL2 reverb did); false keeps\n"
|
||||
"them dry." },
|
||||
|
||||
{ "input.mouse_sensitivity", _config_real, "1.0", "HALO_MOUSE_SENSITIVITY", _environment_value, _platform_desktop,
|
||||
"How far the view turns for the mouse's movement." },
|
||||
@@ -425,6 +445,12 @@ static const struct config_setting config_settings[] =
|
||||
"\"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_player_collisions", _config_boolean, "true", "HALO_NET_COOP_PLAYER_COLLISIONS", _environment_value,
|
||||
_platform_all,
|
||||
"Whether the players of an online co-op game bump into each other;\n"
|
||||
"false, they walk through each other (the AI's characters they still\n"
|
||||
"bump into). Server Setup's PLAYER COLLISIONS in co-op writes its\n"
|
||||
"choice here." },
|
||||
{ "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"
|
||||
@@ -505,9 +531,17 @@ 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.multiview_steamvr", _config_boolean, "false", "HALO_MULTIVIEW_STEAMVR", _environment_value,
|
||||
_platform_android,
|
||||
"On SteamVR (the Steam Frame) the eyes are drawn one by one, whatever\n"
|
||||
"vr.multiview says: true draws them at once there too, for testing." },
|
||||
{ "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.xr_deferred_copies", _config_integer, "-1", "HALO_XR_DEFERRED", _environment_value, _platform_android,
|
||||
"The headset's layer copies: 1 deferred (no waits for the GL thread or the\n"
|
||||
"GPU in the middle of the frame), 0 each one restored and released at once,\n"
|
||||
"-1 as the runtime needs (deferred on SteamVR). 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"
|
||||
|
||||
+44
-14
@@ -236,9 +236,20 @@ int d3d8_multiview_supported(void);
|
||||
int d3d8_multiview_begin(const float rows[4][4], const float resolve_rect[2][4]);
|
||||
void d3d8_multiview_rows(const float rows[4][4]);
|
||||
void d3d8_multiview_depth(float a, float b);
|
||||
void d3d8_multiview_projected_depth(int projected);
|
||||
unsigned int d3d8_multiview_end(void);
|
||||
void d3d8_clear_back_buffer(float red, float green, float blue, float alpha);
|
||||
void d3d8_clear_back_buffer_region(float left, float top, float right, float bottom);
|
||||
void d3d8_host_state_changed(void);
|
||||
|
||||
/* a layer copied (host_xr_copy_layer): 2 is a deferred copy, which left GL
|
||||
as it drew, for the renderer to set again */
|
||||
static int vr_copied(int result)
|
||||
{
|
||||
if (result == 2)
|
||||
d3d8_host_state_changed();
|
||||
return result != 0;
|
||||
}
|
||||
|
||||
/* The motion tracker, the HUD's one element at its bottom left, moves from
|
||||
the HUD to the left wrist (host_xr.c draws it there): this part of the
|
||||
@@ -503,6 +514,16 @@ void halo_vr_initialize(void)
|
||||
vr.active = 1;
|
||||
platform_log("VR: %s, eyes %dx%d at %.0f Hz", vr.info.system_name, vr.info.eye_width, vr.info.eye_height,
|
||||
vr.info.refresh_rate);
|
||||
/* SteamVR (the Steam Frame): each eye drawn apart. With both drawn at
|
||||
once the eyes' pictures did not fuse, now and then: each alone was
|
||||
sharp, the two together doubled as the head turned (2026-10-05), and
|
||||
one by one costs it no more (its time goes to the driver either way).
|
||||
debug.multiview_steamvr draws them at once there again, for finding why */
|
||||
if (vr.multiview && strstr(vr.info.system_name, "SteamVR") && !config_boolean("debug.multiview_steamvr"))
|
||||
{
|
||||
vr.multiview = 0;
|
||||
platform_log("VR: SteamVR: the eyes drawn one by one (debug.multiview_steamvr)");
|
||||
}
|
||||
}
|
||||
|
||||
int halo_vr_active(void)
|
||||
@@ -1312,9 +1333,8 @@ void halo_vr_present(unsigned int back_buffer_texture)
|
||||
if (vr.overlay_mode == HALO_XR_OVERLAY_HEAD_LOCKED && vr.motion_tracker_on_wrist && vr.aim_with_controller)
|
||||
{
|
||||
/* the motion tracker to the wrist, and out of the HUD */
|
||||
submit.wrist = host_xr_copy_layer_region(HALO_XR_LAYER_WRIST, back_buffer_texture, HALO_XR_COPY_ALPHA,
|
||||
WRIST_REGION_LEFT,
|
||||
WRIST_REGION_TOP, WRIST_REGION_RIGHT, WRIST_REGION_BOTTOM);
|
||||
submit.wrist = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_WRIST, back_buffer_texture,
|
||||
HALO_XR_COPY_ALPHA, WRIST_REGION_LEFT, WRIST_REGION_TOP, WRIST_REGION_RIGHT, WRIST_REGION_BOTTOM));
|
||||
submit.wrist_size = vr.motion_tracker_size;
|
||||
submit.wrist_turn = vr.motion_tracker_turn;
|
||||
submit.wrist_right = vr.motion_tracker_right;
|
||||
@@ -1345,8 +1365,8 @@ void halo_vr_present(unsigned int back_buffer_texture)
|
||||
submit.scope_size = vr.scope_size;
|
||||
submit.scope_pose = vr.scope_pose;
|
||||
}
|
||||
host_xr_copy_layer(HALO_XR_LAYER_OVERLAY, back_buffer_texture,
|
||||
vr.overlay_blend ? HALO_XR_COPY_ALPHA : HALO_XR_COPY_OPAQUE);
|
||||
vr_copied(host_xr_copy_layer(HALO_XR_LAYER_OVERLAY, back_buffer_texture,
|
||||
vr.overlay_blend ? HALO_XR_COPY_ALPHA : HALO_XR_COPY_OPAQUE));
|
||||
host_xr_end_frame(&submit);
|
||||
vr.last_overlay_mode = vr.overlay_mode;
|
||||
vr.last_frame_stereo = submit.eyes;
|
||||
@@ -1544,6 +1564,15 @@ void halo_vr_multiview_projection(float projection_22, float projection_32)
|
||||
d3d8_multiview_depth(-projection_22, projection_32);
|
||||
}
|
||||
|
||||
/* lens flares are being drawn (rasterizer_lights.c): each eye's by the
|
||||
flare's depth, as the flare's test was, not at infinity (a weapon's own,
|
||||
the plasma pistol's at its prongs, half a metre away: a ball each eye saw
|
||||
elsewhere); the sun's are far enough for infinity either way */
|
||||
void halo_vr_multiview_lens_flares(int drawing)
|
||||
{
|
||||
d3d8_multiview_projected_depth(drawing && multiview_drawing);
|
||||
}
|
||||
|
||||
/* the sky is being drawn (render.c): as though infinitely far */
|
||||
void halo_vr_multiview_sky(int sky)
|
||||
{
|
||||
@@ -1559,9 +1588,9 @@ void halo_vr_capture_multiview(void)
|
||||
if (!multiview_drawing)
|
||||
return;
|
||||
multiview_drawing = 0;
|
||||
if (host_xr_copy_layer_array(HALO_XR_LAYER_EYE_LEFT, layers, 0))
|
||||
if (vr_copied(host_xr_copy_layer_array(HALO_XR_LAYER_EYE_LEFT, layers, 0)))
|
||||
vr.eyes_captured |= 1;
|
||||
if (host_xr_copy_layer_array(HALO_XR_LAYER_EYE_RIGHT, layers, 1))
|
||||
if (vr_copied(host_xr_copy_layer_array(HALO_XR_LAYER_EYE_RIGHT, layers, 1)))
|
||||
vr.eyes_captured |= 2;
|
||||
if (vr.eyes_start_us)
|
||||
vr.eyes_us += now_us() - vr.eyes_start_us;
|
||||
@@ -1571,7 +1600,7 @@ void halo_vr_capture_eye(int eye)
|
||||
{
|
||||
if (!vr.active || eye < 0 || eye >= HALO_XR_EYE_COUNT)
|
||||
return;
|
||||
if (host_xr_copy_layer(HALO_XR_LAYER_EYE_LEFT + eye, d3d8_back_buffer_texture(), 1))
|
||||
if (vr_copied(host_xr_copy_layer(HALO_XR_LAYER_EYE_LEFT + eye, d3d8_back_buffer_texture(), 1)))
|
||||
vr.eyes_captured |= 1 << eye;
|
||||
if (eye == HALO_XR_EYE_COUNT - 1 && vr.eyes_start_us)
|
||||
vr.eyes_us += now_us() - vr.eyes_start_us;
|
||||
@@ -2402,9 +2431,9 @@ void halo_vr_capture_names(void)
|
||||
{
|
||||
if (vr.name_count > 0)
|
||||
{
|
||||
vr.names_captured = host_xr_copy_layer_region(HALO_XR_LAYER_NAMES, d3d8_back_buffer_texture(),
|
||||
vr.names_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_NAMES, d3d8_back_buffer_texture(),
|
||||
HALO_XR_COPY_ALPHA, NAME_SLOT_LEFT / 640.0f, 0.0f, (NAME_SLOT_LEFT + NAME_SLOT_WIDTH) / 640.0f,
|
||||
HALO_XR_MAXIMUM_NAMES * NAME_SLOT_HEIGHT / 480.0f);
|
||||
HALO_XR_MAXIMUM_NAMES * NAME_SLOT_HEIGHT / 480.0f));
|
||||
}
|
||||
d3d8_clear_back_buffer(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
@@ -2512,8 +2541,8 @@ int halo_vr_scope_pass(void)
|
||||
void halo_vr_capture_scope(float left, float top, float right, float bottom)
|
||||
{
|
||||
vr.scope_pass = 0;
|
||||
vr.scope_captured = host_xr_copy_layer_region(HALO_XR_LAYER_SCOPE, d3d8_back_buffer_texture(), HALO_XR_COPY_DISC,
|
||||
left, top, right, bottom);
|
||||
vr.scope_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_SCOPE, d3d8_back_buffer_texture(),
|
||||
HALO_XR_COPY_DISC, left, top, right, bottom));
|
||||
}
|
||||
|
||||
int halo_vr_hide_crosshair(void)
|
||||
@@ -2530,9 +2559,9 @@ int halo_vr_crosshair_apart(void)
|
||||
|
||||
void halo_vr_capture_crosshair(void)
|
||||
{
|
||||
vr.crosshair_captured = host_xr_copy_layer_region(HALO_XR_LAYER_CROSSHAIR, d3d8_back_buffer_texture(),
|
||||
vr.crosshair_captured = vr_copied(host_xr_copy_layer_region(HALO_XR_LAYER_CROSSHAIR, d3d8_back_buffer_texture(),
|
||||
HALO_XR_COPY_ALPHA, CROSSHAIR_REGION_LEFT, CROSSHAIR_REGION_TOP, CROSSHAIR_REGION_RIGHT,
|
||||
CROSSHAIR_REGION_BOTTOM);
|
||||
CROSSHAIR_REGION_BOTTOM));
|
||||
d3d8_clear_back_buffer(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
@@ -2567,6 +2596,7 @@ void halo_vr_center_camera(const float game_position[3], float position[3], floa
|
||||
}
|
||||
void halo_vr_capture_multiview(void) {}
|
||||
void halo_vr_multiview_sky(int sky) { (void)sky; }
|
||||
void halo_vr_multiview_lens_flares(int drawing) { (void)drawing; }
|
||||
void halo_vr_multiview_projection(float projection_22, float projection_32)
|
||||
{
|
||||
(void)projection_22; (void)projection_32;
|
||||
|
||||
@@ -656,6 +656,10 @@ struct texture_entry
|
||||
unsigned long last_used_frame;
|
||||
/* the high-res HUD texture drawn in its place (hud_hires.h), or -1 */
|
||||
long override;
|
||||
/* the newest generation of its pages (memory_watch_generation) as of the
|
||||
memory watch serial read before it was found: the same while no watched
|
||||
page has been written since (0: never found) */
|
||||
unsigned long watched_serial, watched_generation;
|
||||
};
|
||||
|
||||
#define TEXTURE_BUCKET_COUNT 4096
|
||||
@@ -668,7 +672,7 @@ static struct texture_entry *texture_buckets[TEXTURE_BUCKET_COUNT];
|
||||
texture that is not palettized is remembered with the memory watch serial it
|
||||
started at: while no watched page has been written since, and no texture
|
||||
has been dropped, the same lookup finds the same current texture. */
|
||||
#define RECENT_TEXTURE_COUNT 64
|
||||
#define RECENT_TEXTURE_COUNT 512
|
||||
|
||||
static struct
|
||||
{
|
||||
@@ -680,9 +684,14 @@ static struct
|
||||
static unsigned long texture_drop_serial = 1;
|
||||
static unsigned long texture_frame = 0;
|
||||
|
||||
/* (every bit of the three mixed into the top ones: textures are aligned,
|
||||
and few sizes and formats are common) */
|
||||
static unsigned long bucket_index(DWORD data, DWORD format_word, DWORD size_word)
|
||||
{
|
||||
return ((data >> 7) ^ (format_word * 2654435761UL) ^ size_word) % TEXTURE_BUCKET_COUNT;
|
||||
unsigned long hash = (unsigned long)data * 2654435761UL ^ (unsigned long)format_word * 2246822519UL ^
|
||||
(unsigned long)size_word * 3266489917UL;
|
||||
|
||||
return ((hash & 0xffffffffUL) >> 20) % TEXTURE_BUCKET_COUNT;
|
||||
}
|
||||
|
||||
/* palettized textures are cached per palette contents: the game rewrites
|
||||
@@ -757,25 +766,33 @@ GLuint xgpu_texture_get(const DWORD *resource, const D3DCOLOR *palette, GLenum *
|
||||
unsigned long recent = bucket_index(data, format_word, size_word) % RECENT_TEXTURE_COUNT;
|
||||
unsigned long watch_serial = memory_watch_serial();
|
||||
|
||||
/* (a texture that is not palettized has the one entry, until one is
|
||||
dropped: remembered, a page written since only means checking it) */
|
||||
entry = NULL;
|
||||
if (!palettized && recent_textures[recent].entry && recent_textures[recent].data == data &&
|
||||
recent_textures[recent].format_word == format_word && recent_textures[recent].size_word == size_word &&
|
||||
recent_textures[recent].watch_serial == watch_serial &&
|
||||
recent_textures[recent].drop_serial == texture_drop_serial)
|
||||
{
|
||||
entry = recent_textures[recent].entry;
|
||||
entry->last_used_frame = texture_frame;
|
||||
return texture_entry_result(entry, target, description);
|
||||
if (recent_textures[recent].watch_serial == watch_serial)
|
||||
{
|
||||
entry->last_used_frame = texture_frame;
|
||||
return texture_entry_result(entry, target, description);
|
||||
}
|
||||
}
|
||||
|
||||
for (entry = *bucket; entry; entry = entry->next)
|
||||
if (!entry)
|
||||
{
|
||||
if (entry->data == data && entry->format_word == format_word && entry->size_word == size_word)
|
||||
for (entry = *bucket; entry; entry = entry->next)
|
||||
{
|
||||
if (entry->palette_hash == hash)
|
||||
break;
|
||||
variant_count++;
|
||||
if (!oldest_variant || entry->last_used_frame < oldest_variant->last_used_frame)
|
||||
oldest_variant = entry;
|
||||
if (entry->data == data && entry->format_word == format_word && entry->size_word == size_word)
|
||||
{
|
||||
if (entry->palette_hash == hash)
|
||||
break;
|
||||
variant_count++;
|
||||
if (!oldest_variant || entry->last_used_frame < oldest_variant->last_used_frame)
|
||||
oldest_variant = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!entry && variant_count >= MAXIMUM_PALETTE_VARIANTS)
|
||||
@@ -806,7 +823,17 @@ GLuint xgpu_texture_get(const DWORD *resource, const D3DCOLOR *palette, GLenum *
|
||||
|
||||
if (no_cache < 0)
|
||||
no_cache = config_boolean("debug.texture_no_cache");
|
||||
generation = memory_watch_generation(entry->address, entry->size);
|
||||
/* (its pages' newest generation, found again only once a watched page
|
||||
has been written: a large texture's pages, scanned for every draw that
|
||||
bound it, were much of a frame with many objects) */
|
||||
if (entry->watched_serial && entry->watched_serial == watch_serial)
|
||||
generation = entry->watched_generation;
|
||||
else
|
||||
{
|
||||
generation = memory_watch_generation(entry->address, entry->size);
|
||||
entry->watched_serial = watch_serial;
|
||||
entry->watched_generation = generation;
|
||||
}
|
||||
if (!entry->generation || generation > entry->generation || no_cache)
|
||||
{
|
||||
/* protect first, so a write racing with the upload is noticed */
|
||||
|
||||
+68
-3
@@ -53,6 +53,11 @@ must call this afterwards. */
|
||||
|
||||
void xgpu_gl_state_invalidate(void);
|
||||
|
||||
/* a compiled shader, or a linked program of two, or 0 with the log
|
||||
written */
|
||||
GLuint xgpu_compile_shader(GLenum type, const char *code, const char *what);
|
||||
GLuint xgpu_link_program(GLuint vertex_shader, GLuint fragment_shader, const char *what);
|
||||
|
||||
/* ---------- generated source text */
|
||||
|
||||
struct xgpu_text
|
||||
@@ -71,14 +76,34 @@ void xgpu_text_append(struct xgpu_text *text, const char *format, ...) __attribu
|
||||
/* D3D constant register -96 is hardware register 0 */
|
||||
#define XGPU_VERTEX_CONSTANT_BIAS 96
|
||||
|
||||
/* where one of the game's model lighting programs (d3d8_gl.c
|
||||
halo_vertex_shader_lighting) has the normal, and the world position, that it
|
||||
lights the diffuse color by: the temporary register that holds each before
|
||||
the instruction given */
|
||||
struct nv2a_vertex_lighting
|
||||
{
|
||||
/* 1 by the ambient and distant lights, 2 by the point lights too */
|
||||
int lights;
|
||||
unsigned long normal_instruction, normal_register;
|
||||
/* (with the point lights only) */
|
||||
unsigned long position_instruction, position_register;
|
||||
};
|
||||
|
||||
/* finds them in a model lighting program, checking that it lights its
|
||||
diffuse color as nv2a_psh.c does for each pixel; FALSE if it does not */
|
||||
BOOL nv2a_vertex_shader_lighting(const DWORD *instructions, unsigned long instruction_count,
|
||||
struct nv2a_vertex_lighting *lighting);
|
||||
|
||||
/* GLSL for an NV2A vertex program (the instruction words after the program
|
||||
header). Attributes whose bit is set in packed_attribute_mask are fed as
|
||||
NORMPACKED3 32-bit integers and unpacked in the shader. Returns a malloc'd
|
||||
string. */
|
||||
NORMPACKED3 32-bit integers and unpacked in the shader. With lighting (else
|
||||
NULL), the normal and world position go to the pixel shader too, which
|
||||
lights the diffuse color for each pixel (nv2a_pixel_shader_key
|
||||
per_pixel_lighting). Returns a malloc'd string. */
|
||||
/* multiview: for a two-view (GL_OVR_multiview2) target, each view's
|
||||
position corrected from the shared one by mv_rows (d3d8_gl.c, VR) */
|
||||
char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count,
|
||||
unsigned long packed_attribute_mask, int multiview);
|
||||
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting, int multiview);
|
||||
|
||||
/* ---------- pixel shaders */
|
||||
|
||||
@@ -114,6 +139,15 @@ struct nv2a_pixel_shader_key
|
||||
behind it only where it covers it (the Xbox's point-sampled meters stop
|
||||
at their texels' edges; filtered ones have a fringe of faint texels) */
|
||||
unsigned char coverage_alpha;
|
||||
/* a model lighting program's draw lit for each pixel
|
||||
(display.per_pixel_lighting): nv2a_vertex_lighting's lights, or 0 for
|
||||
the diffuse color the vertex shader computed */
|
||||
unsigned char per_pixel_lighting;
|
||||
/* drawn into a multisampled target (display.anti_aliasing's
|
||||
multisampling), its samples a pixel: the alpha test covers samples in
|
||||
proportion to how far alpha is past the reference, not all of the pixel
|
||||
or none of it, so that cut-out edges (foliage, grates) are smoothed too */
|
||||
unsigned char alpha_test_samples;
|
||||
};
|
||||
|
||||
char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key);
|
||||
@@ -140,6 +174,13 @@ C0/C1 of each stage and the final combiner, and texture constants */
|
||||
"uniform vec4 texture_scale[4];\n" \
|
||||
XGPU_PIXEL_UNIFORMS_ES
|
||||
|
||||
/* the vertex constants the per-pixel model lighting reads, in a uniform of
|
||||
their own (the vertex shader's 192 would pass OpenGL ES's least fragment
|
||||
uniform space): [0] c[-82] (the translucency in z), [1] to [11] c[-79] to
|
||||
c[-69] (rasterizer_set_model_lighting's two point lights, two distant
|
||||
lights and the ambient light) */
|
||||
#define XGPU_MODEL_LIGHT_COUNT 12
|
||||
|
||||
/* ---------- textures */
|
||||
|
||||
struct xgpu_texture_description
|
||||
@@ -178,9 +219,33 @@ struct xgpu_render_target
|
||||
targets when the game draws at the display's resolution (d3d8_gl.c) */
|
||||
float scale[2];
|
||||
unsigned long gl_width, gl_height;
|
||||
/* with multisampling, the multisampled renderbuffer draws go to, its
|
||||
samples a pixel (0 when it has none), and whether it has been drawn into
|
||||
since the texture last had its pixels (d3d8_gl.c,
|
||||
render_target_multisample) */
|
||||
GLuint multisample;
|
||||
int samples;
|
||||
BOOL unresolved;
|
||||
};
|
||||
|
||||
/* the GL texture holding a render target with this physical address, or 0 */
|
||||
struct xgpu_render_target *xgpu_render_target_find(unsigned long data);
|
||||
|
||||
/* ---------- anti-aliasing
|
||||
|
||||
display.anti_aliasing's passes (xgpu_post.c): FXAA or SMAA antialias each
|
||||
window's 3D view in place before the HUD and menus are drawn over it, so
|
||||
that their text stays sharp. Supersampling and multisampling are the
|
||||
device's (d3d8_gl.c). */
|
||||
|
||||
/* the programs and textures of FXAA, or of SMAA, made now; FALSE if they
|
||||
cannot be (once FALSE, it stays so) */
|
||||
BOOL xgpu_post_prepare(BOOL smaa);
|
||||
|
||||
/* FXAA, or SMAA, on the corners x0, y0 to x1, y1 (from row 0) of a render
|
||||
target's framebuffer, width by height, GL_RGBA8; FALSE if its programs do
|
||||
not build */
|
||||
BOOL xgpu_post_anti_alias(BOOL smaa, GLuint framebuffer, unsigned long width, unsigned long height,
|
||||
const GLint corners[4]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,524 @@
|
||||
/*
|
||||
XGPU_POST.C
|
||||
|
||||
The post-process passes of display.anti_aliasing (xgpu.h): FXAA and SMAA,
|
||||
run on a render target in place, over the rectangle of a window's 3D view.
|
||||
Each pass draws one triangle covering the rectangle (its corners from
|
||||
gl_VertexID, with no vertex data) and finds its texels from gl_FragCoord: a
|
||||
target's rows are the picture's from the top, as the passes' own textures'
|
||||
are, so no coordinate is turned over. The target's pixels are copied first,
|
||||
so that no pass reads what it writes, and only colour is written back: the
|
||||
game keeps values of its own in destination alpha. The programs are built
|
||||
when the setting is chosen (xgpu_post_prepare), not in the middle of a frame.
|
||||
|
||||
FXAA is written here, after Timothy Lottes' description: the direction of an
|
||||
edge from the luma around a pixel, the edge's ends found by stepping along
|
||||
it, and the pixel blended across it by where it lies between them, or by how
|
||||
much it stands out from its neighbours. SMAA is port/third_party/smaa's,
|
||||
compiled as GLSL at its HIGH preset: edges from luma, blending weights from
|
||||
its lookup textures, then the blend. Android has FXAA only (d3d8_gl.c's
|
||||
anti_aliasing_values).
|
||||
*/
|
||||
|
||||
#include "xgpu.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifndef HALO_ANDROID
|
||||
#include "memory/zlib/zlib.h"
|
||||
|
||||
/* port/third_party/smaa, embedded by tools/embed_assets.py: the shader's
|
||||
text (ending in a NUL), and the lookup textures' bytes compressed with
|
||||
zlib; each size 0 where the build had no such file */
|
||||
extern const unsigned int xgpu_smaa_shader[];
|
||||
extern const unsigned long xgpu_smaa_shader_size;
|
||||
extern const unsigned int xgpu_smaa_area_texture[];
|
||||
extern const unsigned long xgpu_smaa_area_texture_size;
|
||||
extern const unsigned int xgpu_smaa_search_texture[];
|
||||
extern const unsigned long xgpu_smaa_search_texture_size;
|
||||
#endif
|
||||
|
||||
enum
|
||||
{
|
||||
_post_program_fxaa,
|
||||
_post_program_smaa_edges,
|
||||
_post_program_smaa_weights,
|
||||
_post_program_smaa_blend,
|
||||
NUMBER_OF_POST_PROGRAMS
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
GLuint vertex_shader;
|
||||
GLuint programs[NUMBER_OF_POST_PROGRAMS];
|
||||
/* each program's metrics uniform, and FXAA's bounds */
|
||||
GLint metrics[NUMBER_OF_POST_PROGRAMS];
|
||||
GLint bounds;
|
||||
/* FXAA's, and SMAA's, programs or textures could not be made */
|
||||
BOOL failed[2];
|
||||
/* the size of the textures below */
|
||||
unsigned long width, height;
|
||||
/* the target's pixels before the pass, and SMAA's edges and blending
|
||||
weights */
|
||||
GLuint color, edges, weights;
|
||||
GLuint color_framebuffer, edges_framebuffer, weights_framebuffer;
|
||||
/* SMAA's lookup textures */
|
||||
GLuint area, search;
|
||||
/* linear and clamped, as FXAA and SMAA sample */
|
||||
GLuint sampler;
|
||||
/* no attributes: the triangle comes from gl_VertexID */
|
||||
GLuint vertex_array;
|
||||
} post;
|
||||
|
||||
/* ---------- programs */
|
||||
|
||||
static void shader_header(struct xgpu_text *text)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
xgpu_text_append(text,
|
||||
"#version %s\n"
|
||||
"precision highp float;\n"
|
||||
"precision highp int;\n"
|
||||
"precision highp sampler2D;\n",
|
||||
xgpu_capabilities.shading_language);
|
||||
#else
|
||||
xgpu_text_append(text, "#version 450 core\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
/* one triangle over the whole viewport */
|
||||
static const char vertex_source[] =
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tgl_Position = vec4(float((gl_VertexID & 1) << 2) - 1.0, float((gl_VertexID & 2) << 1) - 1.0, 0.0, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
static const char fxaa_source[] =
|
||||
"uniform sampler2D color_texture;\n"
|
||||
"/* 1 / width, 1 / height, width, height */\n"
|
||||
"uniform vec4 metrics;\n"
|
||||
"/* the rectangle's outermost texel centres, as texture coordinates */\n"
|
||||
"uniform vec4 bounds;\n"
|
||||
"out vec4 result;\n"
|
||||
"\n"
|
||||
"const float EDGE_THRESHOLD = 0.125;\n"
|
||||
"const float EDGE_THRESHOLD_MINIMUM = 0.0312;\n"
|
||||
"const float SUBPIXEL_QUALITY = 0.75;\n"
|
||||
"const int STEPS = 12;\n"
|
||||
"const float STEP_LENGTHS[12] = float[12](1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 2.0, 2.0, 2.0, 4.0, 8.0);\n"
|
||||
"\n"
|
||||
"vec3 color_at(vec2 position)\n"
|
||||
"{\n"
|
||||
"\treturn textureLod(color_texture, clamp(position, bounds.xy, bounds.zw), 0.0).rgb;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"float luma_at(vec2 position)\n"
|
||||
"{\n"
|
||||
"\treturn dot(color_at(position), vec3(0.299, 0.587, 0.114));\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tvec2 position = gl_FragCoord.xy * metrics.xy;\n"
|
||||
"\tfloat luma = luma_at(position);\n"
|
||||
"\tfloat up = luma_at(position + vec2(0.0, -metrics.y));\n"
|
||||
"\tfloat down = luma_at(position + vec2(0.0, metrics.y));\n"
|
||||
"\tfloat left = luma_at(position + vec2(-metrics.x, 0.0));\n"
|
||||
"\tfloat right = luma_at(position + vec2(metrics.x, 0.0));\n"
|
||||
"\tfloat highest = max(luma, max(max(up, down), max(left, right)));\n"
|
||||
"\tfloat range = highest - min(luma, min(min(up, down), min(left, right)));\n"
|
||||
"\n"
|
||||
"\t/* no edge: the target has the pixel already */\n"
|
||||
"\tif (range < max(EDGE_THRESHOLD_MINIMUM, highest * EDGE_THRESHOLD))\n"
|
||||
"\t\tdiscard;\n"
|
||||
"\tfloat up_left = luma_at(position + vec2(-metrics.x, -metrics.y));\n"
|
||||
"\tfloat up_right = luma_at(position + vec2(metrics.x, -metrics.y));\n"
|
||||
"\tfloat down_left = luma_at(position + vec2(-metrics.x, metrics.y));\n"
|
||||
"\tfloat down_right = luma_at(position + vec2(metrics.x, metrics.y));\n"
|
||||
"\n"
|
||||
"\t/* how much the pixel stands out from its neighbourhood (a feature\n"
|
||||
"\tthinner than a pixel) */\n"
|
||||
"\tfloat average = (2.0 * (up + down + left + right) + up_left + up_right + down_left + down_right) / 12.0;\n"
|
||||
"\tfloat subpixel = smoothstep(0.0, 1.0, clamp(abs(average - luma) / range, 0.0, 1.0));\n"
|
||||
"\tsubpixel = subpixel * subpixel * SUBPIXEL_QUALITY;\n"
|
||||
"\n"
|
||||
"\t/* an edge along the rows where the luma changes more from row to row\n"
|
||||
"\tthan from column to column; across it, the side it changes more to */\n"
|
||||
"\tbool horizontal = abs(up_left + down_left - 2.0 * left) + 2.0 * abs(up + down - 2.0 * luma) +\n"
|
||||
"\t\tabs(up_right + down_right - 2.0 * right) >=\n"
|
||||
"\t\tabs(up_left + up_right - 2.0 * up) + 2.0 * abs(left + right - 2.0 * luma) +\n"
|
||||
"\t\tabs(down_left + down_right - 2.0 * down);\n"
|
||||
"\tfloat before = horizontal ? up : left;\n"
|
||||
"\tfloat after = horizontal ? down : right;\n"
|
||||
"\tbool before_steeper = abs(before - luma) >= abs(after - luma);\n"
|
||||
"\tfloat gradient = 0.25 * max(abs(before - luma), abs(after - luma));\n"
|
||||
"\tfloat edge_luma = 0.5 * ((before_steeper ? before : after) + luma);\n"
|
||||
"\tvec2 across = (horizontal ? vec2(0.0, metrics.y) : vec2(metrics.x, 0.0)) * (before_steeper ? -1.0 : 1.0);\n"
|
||||
"\tvec2 along = horizontal ? vec2(metrics.x, 0.0) : vec2(0.0, metrics.y);\n"
|
||||
"\n"
|
||||
"\t/* along the edge, half a pixel across, to where its luma ends either way */\n"
|
||||
"\tvec2 end_before = position + 0.5 * across - along;\n"
|
||||
"\tvec2 end_after = position + 0.5 * across + along;\n"
|
||||
"\tfloat luma_before = luma_at(end_before) - edge_luma;\n"
|
||||
"\tfloat luma_after = luma_at(end_after) - edge_luma;\n"
|
||||
"\tbool reached_before = abs(luma_before) >= gradient;\n"
|
||||
"\tbool reached_after = abs(luma_after) >= gradient;\n"
|
||||
"\tfor (int index = 1; index < STEPS && !(reached_before && reached_after); index++)\n"
|
||||
"\t{\n"
|
||||
"\t\tif (!reached_before)\n"
|
||||
"\t\t{\n"
|
||||
"\t\t\tend_before -= along * STEP_LENGTHS[index];\n"
|
||||
"\t\t\tluma_before = luma_at(end_before) - edge_luma;\n"
|
||||
"\t\t\treached_before = abs(luma_before) >= gradient;\n"
|
||||
"\t\t}\n"
|
||||
"\t\tif (!reached_after)\n"
|
||||
"\t\t{\n"
|
||||
"\t\t\tend_after += along * STEP_LENGTHS[index];\n"
|
||||
"\t\t\tluma_after = luma_at(end_after) - edge_luma;\n"
|
||||
"\t\t\treached_after = abs(luma_after) >= gradient;\n"
|
||||
"\t\t}\n"
|
||||
"\t}\n"
|
||||
"\n"
|
||||
"\t/* blended across the edge the more, the nearer the pixel is to an end;\n"
|
||||
"\tonly where the luma at that end changes the way the pixel's does */\n"
|
||||
"\tfloat distance_before = horizontal ? position.x - end_before.x : position.y - end_before.y;\n"
|
||||
"\tfloat distance_after = horizontal ? end_after.x - position.x : end_after.y - position.y;\n"
|
||||
"\tfloat pixel_offset = 0.5 - min(distance_before, distance_after) / (distance_before + distance_after);\n"
|
||||
"\tbool varies = ((distance_before < distance_after ? luma_before : luma_after) < 0.0) != (luma < edge_luma);\n"
|
||||
"\tresult = vec4(color_at(position + max(varies ? pixel_offset : 0.0, subpixel) * across), 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
#ifndef HALO_ANDROID
|
||||
/* the main functions of SMAA's passes, after SMAA.hlsl; its vertex
|
||||
functions' offsets are found for each pixel */
|
||||
static const char *const smaa_mains[] =
|
||||
{
|
||||
/* edges */
|
||||
"uniform sampler2D color_texture;\n"
|
||||
"out vec4 result;\n"
|
||||
"\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
|
||||
"\tvec4 offset[3];\n"
|
||||
"\tSMAAEdgeDetectionVS(texcoord, offset);\n"
|
||||
"\tresult = vec4(SMAALumaEdgeDetectionPS(texcoord, offset, color_texture), 0.0, 0.0);\n"
|
||||
"}\n",
|
||||
/* weights */
|
||||
"uniform sampler2D edges_texture;\n"
|
||||
"uniform sampler2D area_texture;\n"
|
||||
"uniform sampler2D search_texture;\n"
|
||||
"out vec4 result;\n"
|
||||
"\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
|
||||
"\tvec2 pixcoord;\n"
|
||||
"\tvec4 offset[3];\n"
|
||||
"\tSMAABlendingWeightCalculationVS(texcoord, pixcoord, offset);\n"
|
||||
"\tresult = SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, edges_texture, area_texture,\n"
|
||||
"\t\tsearch_texture, vec4(0.0));\n"
|
||||
"}\n",
|
||||
/* blend */
|
||||
"uniform sampler2D color_texture;\n"
|
||||
"uniform sampler2D weights_texture;\n"
|
||||
"out vec4 result;\n"
|
||||
"\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
|
||||
"\tvec4 offset;\n"
|
||||
"\tSMAANeighborhoodBlendingVS(texcoord, offset);\n"
|
||||
"\tresult = SMAANeighborhoodBlendingPS(texcoord, offset, color_texture, weights_texture);\n"
|
||||
"}\n",
|
||||
};
|
||||
#endif
|
||||
|
||||
/* a program, its samplers given their texture units; 0 if it does not
|
||||
build */
|
||||
static GLuint program_build(int which)
|
||||
{
|
||||
static const char *const names[NUMBER_OF_POST_PROGRAMS] =
|
||||
{
|
||||
"FXAA", "SMAA edge detection", "SMAA blending weight", "SMAA neighborhood blending"
|
||||
};
|
||||
static const struct
|
||||
{
|
||||
const char *name;
|
||||
GLint unit;
|
||||
} units[] =
|
||||
{
|
||||
{ "color_texture", 0 }, { "edges_texture", 0 }, { "area_texture", 1 },
|
||||
{ "search_texture", 2 }, { "weights_texture", 1 },
|
||||
};
|
||||
struct xgpu_text text = { 0 };
|
||||
GLuint fragment_shader, program;
|
||||
unsigned long index;
|
||||
|
||||
if (!post.vertex_shader)
|
||||
{
|
||||
shader_header(&text);
|
||||
xgpu_text_append(&text, "%s", vertex_source);
|
||||
post.vertex_shader = xgpu_compile_shader(GL_VERTEX_SHADER, text.buffer, "post-process");
|
||||
text.length = 0;
|
||||
}
|
||||
shader_header(&text);
|
||||
if (which == _post_program_fxaa)
|
||||
{
|
||||
xgpu_text_append(&text, "%s", fxaa_source);
|
||||
}
|
||||
#ifndef HALO_ANDROID
|
||||
else if (xgpu_smaa_shader_size)
|
||||
{
|
||||
xgpu_text_append(&text,
|
||||
"#define SMAA_GLSL_4 1\n"
|
||||
"uniform vec4 metrics;\n"
|
||||
"#define SMAA_RT_METRICS metrics\n"
|
||||
"#define SMAA_PRESET_HIGH 1\n"
|
||||
"%s\n%s",
|
||||
(const char *)xgpu_smaa_shader, smaa_mains[which - _post_program_smaa_edges]);
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
platform_log("anti-aliasing: the build has no SMAA");
|
||||
free(text.buffer);
|
||||
return 0;
|
||||
}
|
||||
fragment_shader = post.vertex_shader ?
|
||||
xgpu_compile_shader(GL_FRAGMENT_SHADER, text.buffer, names[which]) :
|
||||
0;
|
||||
free(text.buffer);
|
||||
if (!fragment_shader)
|
||||
return 0;
|
||||
program = xgpu_link_program(post.vertex_shader, fragment_shader, names[which]);
|
||||
glDeleteShader(fragment_shader);
|
||||
if (!program)
|
||||
return 0;
|
||||
glUseProgram(program);
|
||||
/* (a name the program does not have is location -1, which is ignored) */
|
||||
for (index = 0; index < sizeof(units) / sizeof(units[0]); index++)
|
||||
glUniform1i(glGetUniformLocation(program, units[index].name), units[index].unit);
|
||||
post.metrics[which] = glGetUniformLocation(program, "metrics");
|
||||
if (which == _post_program_fxaa)
|
||||
post.bounds = glGetUniformLocation(program, "bounds");
|
||||
return program;
|
||||
}
|
||||
|
||||
/* ---------- textures */
|
||||
|
||||
static GLuint texture_new(GLenum format, unsigned long width, unsigned long height, GLenum data_format,
|
||||
const void *data)
|
||||
{
|
||||
GLuint texture;
|
||||
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, (GLint)format, (GLsizei)width, (GLsizei)height, 0, data_format,
|
||||
GL_UNSIGNED_BYTE, data);
|
||||
return texture;
|
||||
}
|
||||
|
||||
static GLuint framebuffer_new(GLuint texture)
|
||||
{
|
||||
GLuint framebuffer;
|
||||
GLenum draw_buffer = GL_COLOR_ATTACHMENT0;
|
||||
|
||||
glGenFramebuffers(1, &framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
glDrawBuffers(1, &draw_buffer);
|
||||
return framebuffer;
|
||||
}
|
||||
|
||||
/* the passes' textures, of the target's size (SMAA's only for SMAA); color
|
||||
is the target's format, so that the copy is exact */
|
||||
static void textures_fit(BOOL smaa, unsigned long width, unsigned long height)
|
||||
{
|
||||
if (post.width != width || post.height != height)
|
||||
{
|
||||
GLuint textures[3] = { post.color, post.edges, post.weights };
|
||||
GLuint framebuffers[3] = { post.color_framebuffer, post.edges_framebuffer, post.weights_framebuffer };
|
||||
|
||||
glDeleteFramebuffers(3, framebuffers);
|
||||
glDeleteTextures(3, textures);
|
||||
post.color = post.edges = post.weights = 0;
|
||||
post.width = width;
|
||||
post.height = height;
|
||||
post.color = texture_new(GL_RGBA8, width, height, GL_RGBA, NULL);
|
||||
post.color_framebuffer = framebuffer_new(post.color);
|
||||
}
|
||||
if (smaa && !post.edges)
|
||||
{
|
||||
post.edges = texture_new(GL_RG8, width, height, GL_RG, NULL);
|
||||
post.weights = texture_new(GL_RGBA8, width, height, GL_RGBA, NULL);
|
||||
post.edges_framebuffer = framebuffer_new(post.edges);
|
||||
post.weights_framebuffer = framebuffer_new(post.weights);
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef HALO_ANDROID
|
||||
/* a lookup texture of SMAA's from its zlib stream; 0 if it does not
|
||||
inflate */
|
||||
static GLuint lookup_texture(const unsigned int *stream, unsigned long stream_size, GLenum format,
|
||||
GLenum data_format, unsigned long width, unsigned long height, unsigned long texel_size)
|
||||
{
|
||||
uLongf size = (uLongf)(width * height * texel_size);
|
||||
unsigned char *texels = malloc(size);
|
||||
GLuint texture = 0;
|
||||
int result;
|
||||
|
||||
if (!texels)
|
||||
return 0;
|
||||
result = uncompress(texels, &size, (const Bytef *)stream, (uLong)stream_size);
|
||||
/* (the game's zlib is 1.1, which can stop short of saying the stream
|
||||
has ended: the size says whether it is whole) */
|
||||
if ((result == Z_OK || result == Z_BUF_ERROR) && size == (uLongf)(width * height * texel_size))
|
||||
{
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
texture = texture_new(format, width, height, data_format, texels);
|
||||
}
|
||||
free(texels);
|
||||
return texture;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ---------- the passes */
|
||||
|
||||
static void texture_bind(GLuint unit, GLuint texture)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + unit);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindSampler(unit, post.sampler);
|
||||
}
|
||||
|
||||
/* a program's triangle drawn into a framebuffer over the rectangle;
|
||||
color_only keeps the framebuffer's alpha */
|
||||
static void pass_draw(int which, GLuint framebuffer, const GLint corners[4], BOOL color_only)
|
||||
{
|
||||
float metrics[4];
|
||||
|
||||
metrics[0] = 1.0f / (float)post.width;
|
||||
metrics[1] = 1.0f / (float)post.height;
|
||||
metrics[2] = (float)post.width;
|
||||
metrics[3] = (float)post.height;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
glViewport(corners[0], corners[1], corners[2] - corners[0], corners[3] - corners[1]);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, color_only ? GL_FALSE : GL_TRUE);
|
||||
glUseProgram(post.programs[which]);
|
||||
glUniform4fv(post.metrics[which], 1, metrics);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
/* an intermediate emptied, all of it, so that no edge of another window's
|
||||
pass is found: SMAA's passes write only where there is an edge */
|
||||
static void intermediate_clear(GLuint framebuffer)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
BOOL xgpu_post_prepare(BOOL smaa)
|
||||
{
|
||||
int first = smaa ? _post_program_smaa_edges : _post_program_fxaa;
|
||||
int last = smaa ? _post_program_smaa_blend : _post_program_fxaa;
|
||||
int which;
|
||||
|
||||
if (post.failed[smaa != FALSE])
|
||||
return FALSE;
|
||||
for (which = first; which <= last; which++)
|
||||
{
|
||||
if (!post.programs[which] && !(post.programs[which] = program_build(which)))
|
||||
{
|
||||
post.failed[smaa != FALSE] = TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
#ifndef HALO_ANDROID
|
||||
if (smaa && !post.area)
|
||||
{
|
||||
post.area = lookup_texture(xgpu_smaa_area_texture, xgpu_smaa_area_texture_size, GL_RG8, GL_RG,
|
||||
160, 560, 2);
|
||||
post.search = lookup_texture(xgpu_smaa_search_texture, xgpu_smaa_search_texture_size, GL_R8, GL_RED,
|
||||
64, 16, 1);
|
||||
if (!post.area || !post.search)
|
||||
{
|
||||
platform_log("anti-aliasing: cannot inflate SMAA's lookup textures");
|
||||
post.failed[1] = TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!post.sampler)
|
||||
{
|
||||
glGenSamplers(1, &post.sampler);
|
||||
glSamplerParameteri(post.sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glSamplerParameteri(post.sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glSamplerParameteri(post.sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glSamplerParameteri(post.sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glGenVertexArrays(1, &post.vertex_array);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL xgpu_post_anti_alias(BOOL smaa, GLuint framebuffer, unsigned long width, unsigned long height,
|
||||
const GLint corners[4])
|
||||
{
|
||||
/* (made already, as the setting was chosen) */
|
||||
if (!xgpu_post_prepare(smaa))
|
||||
return FALSE;
|
||||
textures_fit(smaa, width, height);
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_POLYGON_OFFSET_FILL);
|
||||
#ifndef HALO_ANDROID
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
#endif
|
||||
glBindVertexArray(post.vertex_array);
|
||||
|
||||
/* the target's pixels, read while the target is drawn into */
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, post.color_framebuffer);
|
||||
glBlitFramebuffer(corners[0], corners[1], corners[2], corners[3], corners[0], corners[1], corners[2], corners[3],
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
|
||||
if (!smaa)
|
||||
{
|
||||
float bounds[4];
|
||||
|
||||
/* (split screen: no texel of the next window is read) */
|
||||
bounds[0] = ((float)corners[0] + 0.5f) / (float)width;
|
||||
bounds[1] = ((float)corners[1] + 0.5f) / (float)height;
|
||||
bounds[2] = ((float)corners[2] - 0.5f) / (float)width;
|
||||
bounds[3] = ((float)corners[3] - 0.5f) / (float)height;
|
||||
texture_bind(0, post.color);
|
||||
glUseProgram(post.programs[_post_program_fxaa]);
|
||||
glUniform4fv(post.bounds, 1, bounds);
|
||||
pass_draw(_post_program_fxaa, framebuffer, corners, TRUE);
|
||||
}
|
||||
else
|
||||
{
|
||||
intermediate_clear(post.edges_framebuffer);
|
||||
texture_bind(0, post.color);
|
||||
pass_draw(_post_program_smaa_edges, post.edges_framebuffer, corners, FALSE);
|
||||
|
||||
intermediate_clear(post.weights_framebuffer);
|
||||
texture_bind(0, post.edges);
|
||||
texture_bind(1, post.area);
|
||||
texture_bind(2, post.search);
|
||||
pass_draw(_post_program_smaa_weights, post.weights_framebuffer, corners, FALSE);
|
||||
|
||||
texture_bind(0, post.color);
|
||||
texture_bind(1, post.weights);
|
||||
pass_draw(_post_program_smaa_blend, framebuffer, corners, TRUE);
|
||||
}
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
return TRUE;
|
||||
}
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com)
|
||||
Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com)
|
||||
Copyright (C) 2013 Belen Masia (bmasia@unizar.es)
|
||||
Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com)
|
||||
Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software. As clarification, there is no
|
||||
requirement that the copyright notice and permission be included in binary
|
||||
distributions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# SMAA
|
||||
|
||||
Enhanced Subpixel Morphological Antialiasing, by Jorge Jimenez, Jose I.
|
||||
Echevarria, Belen Masia, Fernando Navarro and Diego Gutierrez, MIT licensed
|
||||
(see `LICENSE`).
|
||||
|
||||
Upstream: https://github.com/iryoku/smaa, commit
|
||||
71c806a838bdd7d517df19192a20f0c61b3ca29d. `SMAA.hlsl` is copied
|
||||
unchanged. `area_tex.zlib` and `search_tex.zlib` are the bytes of
|
||||
`Textures/AreaTex.h` (160x560, two channels) and `Textures/SearchTex.h`
|
||||
(64x16, one channel), compressed with zlib:
|
||||
|
||||
python -c "import re, sys, zlib; t = open(sys.argv[1]).read(); \
|
||||
open(sys.argv[2], 'wb').write(zlib.compress(bytes(int(h, 16) for h in \
|
||||
re.findall(r'0x([0-9a-fA-F]{2})', t[t.index('{'):])), 9))" AreaTex.h area_tex.zlib
|
||||
|
||||
`display.anti_aliasing = "smaa"` antialiases the 3D view with it, compiled
|
||||
as GLSL (`port/linux/src/xgpu_post.c`); the files are embedded in the Linux
|
||||
and Windows platform layers by `tools/embed_assets.py`. Android has FXAA in
|
||||
its place.
|
||||
Vendored
+1361
File diff suppressed because it is too large.
Load diff
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -2082,6 +2082,13 @@ boolean ai_enemies_attacking_player(
|
||||
return ai_enemies_endanger_player(TRUE, NONE);
|
||||
}
|
||||
|
||||
/* port: whether enemies can see this player's unit */
|
||||
boolean ai_port_enemies_can_see_unit(
|
||||
long unit_index)
|
||||
{
|
||||
return ai_enemies_endanger_player(FALSE, unit_index);
|
||||
}
|
||||
|
||||
/* port: whether enemies are attacking this player's unit */
|
||||
boolean ai_port_enemies_attacking_unit(
|
||||
long unit_index)
|
||||
|
||||
@@ -143,6 +143,9 @@ boolean ai_consider_major_upgrade(
|
||||
real chance);
|
||||
|
||||
/* port: network co-op's respawn (players.c) */
|
||||
/* port: whether enemies can see this player's unit */
|
||||
boolean ai_port_enemies_can_see_unit(
|
||||
long unit_index);
|
||||
boolean ai_port_enemies_attacking_unit(
|
||||
long unit_index);
|
||||
boolean ai_enemies_attacking_player(
|
||||
|
||||
@@ -3255,7 +3255,8 @@ void ai_scripting_attach_unit(
|
||||
squad_index,
|
||||
struct squad_definition);
|
||||
|
||||
if (squad_definition->actor_palette_index != NONE)
|
||||
/* port: an entry the palette has (a map's index), as the others here */
|
||||
if (VALID_INDEX(squad_definition->actor_palette_index, scenario->ai_actor_palette.count))
|
||||
{
|
||||
struct tag_reference *actor_palette_entry = TAG_BLOCK_GET_ELEMENT(
|
||||
&scenario->ai_actor_palette,
|
||||
|
||||
+52
-9
@@ -3006,9 +3006,9 @@ 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) */
|
||||
/* port: extra_number, counting from 1, places one of network co-op's extra
|
||||
enemies (coop_enemies.c), spread around the squad's starting locations in
|
||||
turn; 0 places the squad's own actor, as on the Xbox */
|
||||
static boolean encounter_place_actor(
|
||||
long encounter_index,
|
||||
short squad_index,
|
||||
@@ -3033,16 +3033,59 @@ 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 */
|
||||
/* port: an extra enemy goes on free ground around its starting
|
||||
location, else around the squad's others in turn */
|
||||
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))
|
||||
/* (what was found taken around the starting locations while this
|
||||
squad's extra enemies are placed, not tried again for the rest of
|
||||
them: placing actors only takes room, and each enemy would try
|
||||
every taken place again, and every place of a full location) */
|
||||
static struct
|
||||
{
|
||||
starting_location = &spread_location;
|
||||
long encounter_index;
|
||||
long time;
|
||||
short squad_index;
|
||||
unsigned long full;
|
||||
long taken[32][BIT_VECTOR_SIZE_IN_LONGS(COOP_ENEMIES_SPREAD_SPOTS)];
|
||||
} full_locations = { NONE, NONE, NONE, 0 };
|
||||
short count = squad_definition->starting_locations.count;
|
||||
short tried;
|
||||
|
||||
if (extra_number == 1 || full_locations.encounter_index != encounter_index ||
|
||||
full_locations.squad_index != squad_index || full_locations.time != game_time_get())
|
||||
{
|
||||
full_locations.encounter_index = encounter_index;
|
||||
full_locations.squad_index = squad_index;
|
||||
full_locations.time = game_time_get();
|
||||
full_locations.full = 0;
|
||||
csmemset(full_locations.taken, 0, sizeof(full_locations.taken));
|
||||
}
|
||||
for (tried = 0; tried < count; tried++)
|
||||
{
|
||||
short candidate_index = (short)((starting_location_index + tried) % count);
|
||||
struct actor_starting_location *candidate = TAG_BLOCK_GET_ELEMENT(&squad_definition->starting_locations,
|
||||
candidate_index, struct actor_starting_location);
|
||||
|
||||
if (candidate_index < 32 && TEST_FLAG(full_locations.full, candidate_index))
|
||||
continue;
|
||||
spread_location = *candidate;
|
||||
if (coop_enemies_spread_position(&candidate->position, extra_number,
|
||||
candidate_index < 32 ? full_locations.taken[candidate_index] : NULL, &spread_location.position))
|
||||
break;
|
||||
if (candidate_index < 32)
|
||||
SET_FLAG(full_locations.full, candidate_index, TRUE);
|
||||
}
|
||||
/* (with no room anywhere, as extra enemies were placed before:
|
||||
on rings around the squad's starting location in turn, else on
|
||||
it) */
|
||||
if (tried == count)
|
||||
{
|
||||
spread_location = *starting_location;
|
||||
coop_enemies_fallback_position(&starting_location->position,
|
||||
(short)((extra_number - 1) / MAX(count, 1) + 1), &spread_location.position);
|
||||
}
|
||||
starting_location = &spread_location;
|
||||
}
|
||||
|
||||
if (starting_location->actor_variant_index != NONE)
|
||||
|
||||
Vendored
+268
-2
@@ -137,6 +137,16 @@ symbols in this file:
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
/* port: a vertex or index buffer in a cache file (a D3DResource: Common,
|
||||
Data, Lock), as cache_files_disable_writes counts them */
|
||||
CACHE_FILE_BUFFER_SIZE = 12,
|
||||
/* port: what cache_file_read rounds a read's size up to a multiple of
|
||||
(cache_files_windows.c) */
|
||||
CACHE_FILE_SECTOR_SIZE = 512,
|
||||
};
|
||||
|
||||
/* ---------- macros */
|
||||
|
||||
#define STRUCTURE_BSP_TAG 'sbsp'
|
||||
@@ -221,6 +231,18 @@ typedef char verify_cache_file_header_size[
|
||||
|
||||
static struct cache_file_tag_instance *cache_get_tag_instance(
|
||||
long tag_index);
|
||||
static boolean cache_file_region_contains(
|
||||
void const *region,
|
||||
unsigned long region_size,
|
||||
void const *address,
|
||||
long count,
|
||||
long element_size);
|
||||
static boolean cache_file_tag_header_verify(
|
||||
struct cache_file_tag_header *tag_header,
|
||||
long tag_data_size,
|
||||
char const *scenario_name);
|
||||
static boolean cache_file_structure_bsp_reference_verify(
|
||||
struct scenario_structure_bsp_reference *reference);
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
@@ -270,6 +292,156 @@ static struct cache_file_tag_instance *cache_get_tag_instance(
|
||||
return tag_instance;
|
||||
}
|
||||
|
||||
/* port: whether count elements of element_size bytes at address all lie in
|
||||
the region_size bytes at region (no elements always do): a map's pointers
|
||||
and counts are checked so before anything follows them */
|
||||
static boolean cache_file_region_contains(
|
||||
void const *region,
|
||||
unsigned long region_size,
|
||||
void const *address,
|
||||
long count,
|
||||
long element_size)
|
||||
{
|
||||
unsigned long offset = (unsigned long)address - (unsigned long)region;
|
||||
|
||||
if (count == 0)
|
||||
return TRUE;
|
||||
|
||||
return count > 0 &&
|
||||
(unsigned long)address >= (unsigned long)region &&
|
||||
offset <= region_size &&
|
||||
(unsigned long)count <= (region_size - offset) / (unsigned long)element_size;
|
||||
}
|
||||
|
||||
/* port: whether the tag header of the tags just read (tag_data_size bytes
|
||||
at the tag cache's base) can be trusted, as everything after trusts it:
|
||||
its tag table and vertex and index buffers lie in the tag data, its tags'
|
||||
count fits a tag index's absolute index, and it names a scenario tag */
|
||||
static boolean cache_file_tag_header_verify(
|
||||
struct cache_file_tag_header *tag_header,
|
||||
long tag_data_size,
|
||||
char const *scenario_name)
|
||||
{
|
||||
char const *problem = NULL;
|
||||
|
||||
if (tag_header->signature != CACHE_FILE_TAG_HEADER_SIGNATURE)
|
||||
{
|
||||
problem = "signature";
|
||||
}
|
||||
else if (tag_header->tag_count <= 0 || tag_header->tag_count > UNSIGNED_SHORT_MAX)
|
||||
{
|
||||
problem = "tag count";
|
||||
}
|
||||
else if (!cache_file_region_contains(
|
||||
tag_header,
|
||||
tag_data_size,
|
||||
tag_header->tag_instances,
|
||||
tag_header->tag_count,
|
||||
sizeof(struct cache_file_tag_instance)))
|
||||
{
|
||||
problem = "tag table";
|
||||
}
|
||||
else if (!cache_file_region_contains(
|
||||
tag_header,
|
||||
tag_data_size,
|
||||
tag_header->vertex_buffers,
|
||||
tag_header->vertex_buffer_count,
|
||||
CACHE_FILE_BUFFER_SIZE))
|
||||
{
|
||||
problem = "vertex buffers";
|
||||
}
|
||||
else if (!cache_file_region_contains(
|
||||
tag_header,
|
||||
tag_data_size,
|
||||
tag_header->index_buffers,
|
||||
tag_header->index_buffer_count,
|
||||
CACHE_FILE_BUFFER_SIZE))
|
||||
{
|
||||
problem = "index buffers";
|
||||
}
|
||||
else
|
||||
{
|
||||
long scenario_absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(tag_header->scenario_tag_index);
|
||||
|
||||
if (scenario_absolute_index >= tag_header->tag_count ||
|
||||
tag_header->tag_instances[scenario_absolute_index].tag_index != tag_header->scenario_tag_index ||
|
||||
tag_header->tag_instances[scenario_absolute_index].group_tag != SCENARIO_TAG)
|
||||
{
|
||||
problem = "scenario tag";
|
||||
}
|
||||
}
|
||||
|
||||
if (problem)
|
||||
{
|
||||
error(_error_silent, "the cache file '%s' is damaged: its tag header's %s is wrong", scenario_name, problem);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* port: whether a structure bsp reference (the scenario's) may be loaded:
|
||||
its bytes lie in the map and fit the tag cache after the tag data, where
|
||||
they are read to (rounded up to whole sectors, as the read is), and it
|
||||
names a structure bsp tag */
|
||||
static boolean cache_file_structure_bsp_reference_verify(
|
||||
struct scenario_structure_bsp_reference *reference)
|
||||
{
|
||||
byte *tag_cache_base_address = physical_memory_get_tag_cache_base_address();
|
||||
long tag_data_size = cache_file_globals.header.tag_data_size;
|
||||
long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(reference->structure_bsp.index);
|
||||
long read_size;
|
||||
|
||||
if (reference->file_offset < 0 ||
|
||||
reference->file_size < (long)sizeof(struct cache_file_structure_bsp_header) ||
|
||||
reference->file_size > TAG_CACHE_SIZE ||
|
||||
reference->file_offset > cache_file_globals.header.file_length - reference->file_size)
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"a structure bsp is damaged: %08x bytes at %08x, in %08x bytes",
|
||||
reference->file_size,
|
||||
reference->file_offset,
|
||||
cache_file_globals.header.file_length);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
read_size = (reference->file_size + CACHE_FILE_SECTOR_SIZE - 1) & ~(CACHE_FILE_SECTOR_SIZE - 1);
|
||||
if (!cache_file_region_contains(
|
||||
tag_cache_base_address + tag_data_size,
|
||||
TAG_CACHE_SIZE - tag_data_size,
|
||||
reference->base_address,
|
||||
read_size,
|
||||
1))
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"a structure bsp is damaged: its %08x bytes at %08x would load to %08lx, outside the tag cache",
|
||||
reference->file_size,
|
||||
reference->file_offset,
|
||||
(unsigned long)reference->base_address);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (reference->structure_bsp.index == NONE ||
|
||||
absolute_index >= cache_file_globals.tag_header->tag_count ||
|
||||
global_tag_instances[absolute_index].tag_index != reference->structure_bsp.index ||
|
||||
global_tag_instances[absolute_index].group_tag != STRUCTURE_BSP_TAG)
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"a structure bsp is damaged: %08x is not a structure bsp tag",
|
||||
reference->structure_bsp.index);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
char const *cache_files_map_directory(
|
||||
@@ -444,7 +616,9 @@ long tag_loaded(
|
||||
long group_tag,
|
||||
char const *name)
|
||||
{
|
||||
short absolute_index;
|
||||
/* port: a long, as the tags' count is (a short wrapped on a count past
|
||||
0x7FFF, and the walk never ended) */
|
||||
long absolute_index;
|
||||
long result = NONE;
|
||||
|
||||
if (cache_file_globals.tags_loaded)
|
||||
@@ -651,6 +825,27 @@ boolean cache_file_header_verify(
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* port: the map holds at least its header, and its tag data lies in it
|
||||
and fits the tag cache, which it is read into whole (a size rounded up
|
||||
to whole sectors still fits, the cache being whole sectors). Checked
|
||||
without overflow: the offset and size are each checked first */
|
||||
if (header->file_length < (long)sizeof(struct cache_file_header) ||
|
||||
header->tag_data_offset < 0 ||
|
||||
header->tag_data_size < 0 ||
|
||||
header->tag_data_size > TAG_CACHE_SIZE ||
|
||||
header->tag_data_offset > header->file_length - header->tag_data_size)
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"the cache file '%s' is damaged: %08x bytes of tag data at %08x, in %08x bytes",
|
||||
scenario_name,
|
||||
header->tag_data_size,
|
||||
header->tag_data_offset,
|
||||
header->file_length);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -846,6 +1041,26 @@ long scenario_tags_load(
|
||||
SwitchToThread();
|
||||
}
|
||||
|
||||
/* port: tags that did not all read, or whose header cannot be
|
||||
trusted, are not loaded: the map is refused, as one whose header
|
||||
is wrong is, and closed for the next to open */
|
||||
if (read_complete != TRUE)
|
||||
{
|
||||
error(_error_silent, "the cache file '%s' could not be read", scenario_name);
|
||||
cache_file_close();
|
||||
|
||||
return NONE;
|
||||
}
|
||||
if (!cache_file_tag_header_verify(
|
||||
tag_cache_base_address,
|
||||
cache_file_globals.header.tag_data_size,
|
||||
scenario_name))
|
||||
{
|
||||
cache_file_close();
|
||||
|
||||
return NONE;
|
||||
}
|
||||
|
||||
cache_file_globals.tag_header = tag_cache_base_address;
|
||||
match_vassert(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files.c",
|
||||
@@ -885,6 +1100,11 @@ long scenario_tags_load(
|
||||
}
|
||||
result = cache_file_globals.tag_header->scenario_tag_index;
|
||||
}
|
||||
/* port: a map refused is closed for the next to open */
|
||||
else
|
||||
{
|
||||
cache_file_close();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -895,6 +1115,18 @@ boolean scenario_structure_bsp_load(
|
||||
{
|
||||
struct cache_file_tag_instance *tag_instance;
|
||||
byte *tag_cache_base_address;
|
||||
/* port: the bsp's header, once read and checked */
|
||||
struct cache_file_structure_bsp_header *structure_bsp_header;
|
||||
|
||||
/* port: the tag data's size was checked as the map loaded
|
||||
(cache_file_header_verify); the bsp's reference is the map's, and is
|
||||
checked before anything is read where it says */
|
||||
if (cache_file_globals.header.tag_data_size < 0 ||
|
||||
cache_file_globals.header.tag_data_size > TAG_CACHE_SIZE ||
|
||||
!cache_file_structure_bsp_reference_verify(reference))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
tag_cache_base_address = physical_memory_get_tag_cache_base_address();
|
||||
csmemset(
|
||||
@@ -919,9 +1151,43 @@ boolean scenario_structure_bsp_load(
|
||||
sound_idle();
|
||||
}
|
||||
}
|
||||
|
||||
/* port: a bsp that did not all read, or whose header's pointers
|
||||
leave what was read, is not loaded */
|
||||
structure_bsp_header = reference->base_address;
|
||||
if (read_complete != TRUE ||
|
||||
structure_bsp_header->signature != CACHE_FILE_STRUCTURE_BSP_HEADER_SIGNATURE ||
|
||||
!cache_file_region_contains(
|
||||
reference->base_address,
|
||||
reference->file_size,
|
||||
structure_bsp_header->base_address,
|
||||
1,
|
||||
1) ||
|
||||
!cache_file_region_contains(
|
||||
reference->base_address,
|
||||
reference->file_size,
|
||||
structure_bsp_header->vertex_buffers,
|
||||
structure_bsp_header->vertex_buffer_count,
|
||||
CACHE_FILE_BUFFER_SIZE) ||
|
||||
!cache_file_region_contains(
|
||||
reference->base_address,
|
||||
reference->file_size,
|
||||
structure_bsp_header->index_buffers,
|
||||
structure_bsp_header->index_buffer_count,
|
||||
CACHE_FILE_BUFFER_SIZE))
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"a structure bsp is damaged: its %08x bytes at %08x %s",
|
||||
reference->file_size,
|
||||
reference->file_offset,
|
||||
read_complete != TRUE ? "could not be read" : "have a wrong header");
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
cache_file_globals.structure_bsp_header = reference->base_address;
|
||||
cache_file_globals.structure_bsp_header = structure_bsp_header;
|
||||
match_assert(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files.c",
|
||||
0xE0,
|
||||
|
||||
Vendored
+12
-2
@@ -22,6 +22,14 @@ enum cached_map_file_precache_state
|
||||
NUMBER_OF_CACHED_MAP_FILE_PRECACHE_STATES,
|
||||
};
|
||||
|
||||
/* port: what a read's completion flag is set to when the read failed or came
|
||||
up short (cache_file_read): not FALSE, so whatever waits on it stops
|
||||
waiting, but not TRUE either, so a caller that checks can tell */
|
||||
enum
|
||||
{
|
||||
_cache_file_read_failed = 2,
|
||||
};
|
||||
|
||||
/* ---------- macros */
|
||||
|
||||
/* ---------- structures */
|
||||
@@ -37,8 +45,10 @@ struct cache_file_structure_bsp_header;
|
||||
struct tag_iterator
|
||||
{
|
||||
byte reserved0[4];
|
||||
short absolute_index;
|
||||
byte reserved6[10];
|
||||
/* port: a long, as the tags' count is (a short wrapped on a count past
|
||||
0x7FFF, and the walk never ended); the struct keeps its size */
|
||||
long absolute_index;
|
||||
byte reserved8[8];
|
||||
long group_tag;
|
||||
};
|
||||
|
||||
|
||||
+37
-2
@@ -755,6 +755,14 @@ static void cache_copy_initialize_file_data(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_decompress_windows.c",
|
||||
964,
|
||||
self->read_bytes_left>=sizeof(self->header));
|
||||
/* port: a map that did not open, or is too short to hold a header, is a
|
||||
bad file, not copied (the assertion goes on in a release build, and an
|
||||
unopened file's size is -1, which it compared unsigned) */
|
||||
if (self->source_file == INVALID_HANDLE_VALUE ||
|
||||
self->read_bytes_left < (long)sizeof(self->header))
|
||||
{
|
||||
cache_copy_set_flag(_copy_bad_file_bit);
|
||||
}
|
||||
|
||||
csmemset(self->overlapped_in_use_flags, 0, sizeof(self->overlapped_in_use_flags));
|
||||
csmemset(self->overlapped_completed_flags, 0, sizeof(self->overlapped_completed_flags));
|
||||
@@ -1610,6 +1618,25 @@ static void cache_copy_run_decompression(
|
||||
if (self->write_requests_pending > 1)
|
||||
decompressor_timer_stop(_decompressor_timer_zlib_during_write_file);
|
||||
|
||||
/* port: a stream that ends before the size the header gives the map
|
||||
is a bad file: the rest of the cache file would be taken for it */
|
||||
if (zlib_result == Z_STREAM_END &&
|
||||
zlib_stream->total_out != (uLong)(self->header.size - sizeof(self->header)))
|
||||
{
|
||||
match_vassert(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_decompress_windows.c",
|
||||
1248,
|
||||
FALSE,
|
||||
csprintf(
|
||||
decompressor_globals.message,
|
||||
"decompression ended after %lu of %ld bytes",
|
||||
(unsigned long)zlib_stream->total_out,
|
||||
self->header.size - (long)sizeof(self->header)));
|
||||
cache_copy_set_flag(_copy_bad_file_bit);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (zlib_result == Z_OK || zlib_result == Z_STREAM_END)
|
||||
{
|
||||
if (!zlib_stream->avail_in)
|
||||
@@ -1661,14 +1688,22 @@ static unsigned long __stdcall simple_cache_copy_thread(
|
||||
cache_copy_initialize_read_buffers(self);
|
||||
cache_copy_initialize_file_data(self);
|
||||
|
||||
if (!cache_copy_stop_requested())
|
||||
/* port: nor is a bad file (cache_copy_initialize_file_data) read */
|
||||
if (!cache_copy_stop_requested() && !(self->flags & ALL_COPY_FAILURE_FLAGS))
|
||||
{
|
||||
decompressor_timer_start(_decompressor_timer_setup);
|
||||
cache_copy_initialize_read_data(self);
|
||||
cache_copy_initialize_zlib(self);
|
||||
decompressor_timer_stop(_decompressor_timer_setup);
|
||||
|
||||
if (cache_file_header_verify(&self->header, "cache decompressed", TRUE))
|
||||
/* port: a header that is not a map's is a bad file, so the copy
|
||||
fails (it ended as if it had worked, and the map was precached
|
||||
again, or, of no size, seemed never to end) */
|
||||
if (!cache_file_header_verify(&self->header, "cache decompressed", TRUE))
|
||||
{
|
||||
cache_copy_set_flag(_copy_bad_file_bit);
|
||||
}
|
||||
else
|
||||
{
|
||||
boolean keep_going = TRUE;
|
||||
|
||||
|
||||
Vendored
+59
-8
@@ -427,7 +427,9 @@ static struct cache_file_runtime_globals cache_file_globals;
|
||||
void tags_header_register_vertex_and_index_buffers(
|
||||
struct cache_file_tag_header *header)
|
||||
{
|
||||
short index;
|
||||
/* port: a long, as the buffers' counts are (a short wrapped on a count
|
||||
past 0x7FFF, and the loop never ended) */
|
||||
long index;
|
||||
|
||||
for (index = 0; index < header->vertex_buffer_count; index++)
|
||||
{
|
||||
@@ -450,7 +452,9 @@ void tags_header_register_vertex_and_index_buffers(
|
||||
void tags_header_deregister_vertex_and_index_buffers(
|
||||
struct cache_file_tag_header *header)
|
||||
{
|
||||
short index;
|
||||
/* port: a long, as the buffers' counts are (a short wrapped on a count
|
||||
past 0x7FFF, and the loop never ended) */
|
||||
long index;
|
||||
|
||||
for (index = 0; index < header->vertex_buffer_count; index++)
|
||||
{
|
||||
@@ -480,7 +484,9 @@ void tags_header_deregister_vertex_and_index_buffers(
|
||||
void structure_bsp_header_register_vertex_buffers(
|
||||
struct cache_file_structure_bsp_header *header)
|
||||
{
|
||||
short index;
|
||||
/* port: a long, as the buffers' counts are (a short wrapped on a count
|
||||
past 0x7FFF, and the loop never ended) */
|
||||
long index;
|
||||
|
||||
for (index = 0; index < header->vertex_buffer_count; index++)
|
||||
{
|
||||
@@ -504,7 +510,9 @@ void structure_bsp_header_register_vertex_buffers(
|
||||
void structure_bsp_header_deregister_vertex_buffers(
|
||||
struct cache_file_structure_bsp_header *header)
|
||||
{
|
||||
short index;
|
||||
/* port: a long, as the buffers' counts are (a short wrapped on a count
|
||||
past 0x7FFF, and the loop never ended) */
|
||||
long index;
|
||||
|
||||
rasterizer_globals.current_lock_operation = _rasterizer_lock_bsp_switch;
|
||||
|
||||
@@ -590,11 +598,28 @@ boolean cache_files_precache_map_begin(
|
||||
if (cache_file_read_header_from_dvd(cache_map_name, &header))
|
||||
{
|
||||
long buffer_size = cache_copy_buffer_size(copy_map);
|
||||
void *buffer = texture_cache_steal_memory(buffer_size);
|
||||
short map_file_index = cached_map_files_find_free_map(
|
||||
header.file_length,
|
||||
header.scenario_type);
|
||||
struct cached_map_file *map_file = cached_map_file_get(map_file_index);
|
||||
void *buffer;
|
||||
struct cached_map_file *map_file;
|
||||
|
||||
/* port: a map no cache file holds (of no type the cache files are
|
||||
for, or too big for its type's) is not precached; the texture
|
||||
cache's memory is taken only once one does */
|
||||
if (map_file_index == NONE)
|
||||
{
|
||||
error(_error_silent, "no cache file can hold map '%s' (%08x bytes, type %d)",
|
||||
cache_map_name, header.file_length, header.scenario_type);
|
||||
if (copy_map)
|
||||
{
|
||||
display_error_damaged_media();
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
buffer = texture_cache_steal_memory(buffer_size);
|
||||
map_file = cached_map_file_get(map_file_index);
|
||||
|
||||
memset(
|
||||
&map_file->header,
|
||||
@@ -757,6 +782,14 @@ boolean cache_file_open(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
|
||||
224,
|
||||
map_file_index!=NONE);
|
||||
/* port: a map in no cache file is not opened (its index would be
|
||||
NONE's, before the cache files) */
|
||||
if (map_file_index == NONE)
|
||||
{
|
||||
error(_error_silent, "the map '%s' is in no cache file", scenario_name);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
memset(
|
||||
cache_file_globals.requests,
|
||||
0,
|
||||
@@ -797,6 +830,16 @@ short cache_file_read(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
|
||||
276,
|
||||
offset>=0);
|
||||
/* port: the offset and size are the map's (a texture's or sound's,
|
||||
which nothing checks): a negative one is no read, failed at once (a
|
||||
negative size read the file to its end over what follows the buffer) */
|
||||
if (offset < 0 || size < 0)
|
||||
{
|
||||
error(_error_silent, "cache file read of %08x bytes at %08x refused", size, offset);
|
||||
*completion_flag_reference = _cache_file_read_failed;
|
||||
|
||||
return NONE;
|
||||
}
|
||||
if (size & (CACHE_FILE_SECTOR_SIZE - 1))
|
||||
{
|
||||
size = (size | (CACHE_FILE_SECTOR_SIZE - 1)) + 1;
|
||||
@@ -1056,7 +1099,13 @@ static void CALLBACK cache_file_read_io_completion_routine(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
|
||||
1389,
|
||||
finished_request->overlapped.hEvent);
|
||||
*(volatile boolean *)finished_request->overlapped.hEvent = TRUE;
|
||||
/* port: a read that failed or came up short completes as failed: what
|
||||
waits on it stops waiting, and a caller that checks (the map's tags and
|
||||
bsps) sees it, instead of taking what is in its buffer for the map */
|
||||
*(volatile boolean *)finished_request->overlapped.hEvent =
|
||||
error_code == ERROR_SUCCESS && bytes_transferred == (unsigned long)finished_request->size ?
|
||||
TRUE :
|
||||
_cache_file_read_failed;
|
||||
finished_request->pending = FALSE;
|
||||
finished_request->running = FALSE;
|
||||
|
||||
@@ -1222,9 +1271,11 @@ static short cached_map_files_find_free_map(
|
||||
* arm's assertion failure calls system_exit, which does not return in January
|
||||
* (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit).
|
||||
* Source-policy approval pending (2026-09-27 audit). */
|
||||
/* port: the type is the map's header's, and a release build's assertion
|
||||
goes on: a type no cache file is for finds none */
|
||||
default:
|
||||
match_vassert("c:\\halo\\SOURCE\\cache\\cache_files_windows.c", 1172, FALSE, NULL);
|
||||
break;
|
||||
return NONE;
|
||||
}
|
||||
|
||||
for (map_file_index = first_map_file_index;
|
||||
|
||||
Vendored
-1
@@ -46,7 +46,6 @@ symbols in this file:
|
||||
#define GAME_STATE_SIZE HALO_PORT_GAME_STATE_SIZE
|
||||
#define GAME_STATE_VERIFY_SIZE HALO_PORT_GAME_STATE_CPU_SIZE
|
||||
#define TAG_CACHE_BASE_ADDRESS 0x803A6000
|
||||
#define TAG_CACHE_SIZE 0x1600000
|
||||
#define TEXTURE_CACHE_SIZE 0x1600000
|
||||
#define SOUND_CACHE_SIZE 0x400000
|
||||
|
||||
|
||||
Vendored
+4
@@ -10,6 +10,10 @@ header included in hcex build.
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
/* port: the tag cache's size, which the loader checks a map's tag data and
|
||||
structure bsps against (cache_files.c) */
|
||||
#define TAG_CACHE_SIZE 0x1600000
|
||||
|
||||
/* ---------- macros */
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||
@@ -277,6 +277,11 @@ void error(
|
||||
va_end(argument_list);
|
||||
csstrcat(string, "\r\n");
|
||||
|
||||
/* (port: the file first: printing used to halt on a long line
|
||||
while this call still held the lock, and the reason never reached
|
||||
debug.txt) */
|
||||
write_to_error_file(string, TRUE);
|
||||
|
||||
/* (port: on screen as config.toml's game.console_log says: an
|
||||
assert that stops the game, and what a command someone typed
|
||||
logs (its answer), always; the rest, the game's chatter.
|
||||
@@ -287,7 +292,6 @@ void error(
|
||||
{
|
||||
terminal_printf(global_real_argb_white, "%s", string);
|
||||
}
|
||||
write_to_error_file(string, TRUE);
|
||||
|
||||
new_size = csstrlen(string);
|
||||
if (error_globals.message_buffer_size+new_size >= ERROR_MESSAGE_BUFFER_MAXIMUM_SIZE)
|
||||
|
||||
@@ -1189,10 +1189,18 @@ void profile_render_end(
|
||||
return;
|
||||
}
|
||||
|
||||
/* port: the texture timer is read only by the profile graph and the frame
|
||||
dumps; a texture set per draw, it is not timed while nothing reads it */
|
||||
#define PROFILE_TEXTURE_TIMED() \
|
||||
(profile_global_enable || profile_graph || profile_dump_frames || profile_dump_lost_frames)
|
||||
|
||||
void profile_texture_start(
|
||||
void)
|
||||
{
|
||||
profile_timesection_begin_now(&profile_globals.current_frame.texture);
|
||||
if (PROFILE_TEXTURE_TIMED())
|
||||
{
|
||||
profile_timesection_begin_now(&profile_globals.current_frame.texture);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1200,7 +1208,10 @@ void profile_texture_start(
|
||||
void profile_texture_end(
|
||||
void)
|
||||
{
|
||||
profile_timesection_end_now(&profile_globals.current_frame.texture);
|
||||
if (PROFILE_TEXTURE_TIMED())
|
||||
{
|
||||
profile_timesection_end_now(&profile_globals.current_frame.texture);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
+96
-22
@@ -479,6 +479,10 @@ static struct
|
||||
long bsp_switch_time;
|
||||
/* the structure BSPs the players have been in this map, by bit */
|
||||
word visited_structure_bsps;
|
||||
/* when each local player was last told they wait for the team to go
|
||||
back, and the count they were told (players_coop_show_backtrack_wait) */
|
||||
long backtrack_message_times[MAXIMUM_LOCAL_PLAYERS];
|
||||
short backtrack_message_counts[MAXIMUM_LOCAL_PLAYERS];
|
||||
/* 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;
|
||||
@@ -1310,6 +1314,16 @@ long find_best_starting_location_index(
|
||||
}
|
||||
}
|
||||
|
||||
/* port: a multiplayer map played alone (New Game's MULTIPLAYER maps:
|
||||
no game engine) has no starting location for no game type, every one
|
||||
being for its game types: the player starts at any of them, not at none
|
||||
(outside the map, with no pause menu) */
|
||||
if (best_starting_location_index == NONE && !game_engine_running() && starting_location_count > 0)
|
||||
{
|
||||
best_starting_location_index = (short)PIN(
|
||||
(short)(real_random_range(0.0f, 1.0f) * starting_location_count), 0, starting_location_count - 1);
|
||||
}
|
||||
|
||||
return best_starting_location_index;
|
||||
}
|
||||
|
||||
@@ -2781,9 +2795,13 @@ waits for every machine to have loaded it */
|
||||
#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
|
||||
living players with the player on the trigger: inside it, or this near
|
||||
them (world units, about 45 metres). Wide enough for a team walking through
|
||||
a doorway in a line, not for one player who ran back alone. */
|
||||
#define COOP_BACKTRACK_GATHER_DISTANCE 15.0f
|
||||
|
||||
/* how often a player waiting for the team to go back is reminded */
|
||||
#define COOP_BACKTRACK_MESSAGE_TICKS (3 * TICKS_PER_SECOND)
|
||||
|
||||
/* 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
|
||||
@@ -2970,9 +2988,9 @@ void players_respawn_at_checkpoint(
|
||||
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(
|
||||
/* port: the trigger that switches away from the loaded structure BSP the
|
||||
unit stands in, or NONE */
|
||||
static short players_coop_bsp_switch_trigger(
|
||||
long unit_index)
|
||||
{
|
||||
struct scenario *scenario = global_scenario_get();
|
||||
@@ -2986,11 +3004,11 @@ static boolean players_coop_in_bsp_switch_trigger(
|
||||
if (volume->source_structure_bsp_index == global_structure_bsp_index &&
|
||||
scenario_trigger_volume_test_object(volume->trigger_volume_index, unit_index))
|
||||
{
|
||||
return TRUE;
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
return NONE;
|
||||
}
|
||||
|
||||
/* port: whether a network co-op host lets any trigger switch the BSP yet.
|
||||
@@ -3019,14 +3037,16 @@ static boolean players_coop_bsp_switch_ready(
|
||||
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(
|
||||
/* port: whether this trigger leads back to a BSP the team has been in and
|
||||
the unit on it must wait for more of the team: two thirds of the living
|
||||
players must be with it (in the trigger, or within
|
||||
COOP_BACKTRACK_GATHER_DISTANCE), so one player can't drag everyone back
|
||||
through the level. Also how many are there and how many it needs. */
|
||||
static boolean players_coop_backtrack_wait(
|
||||
short bsp_switch_trigger_volume_index,
|
||||
long unit_index)
|
||||
long unit_index,
|
||||
short *gathered_count,
|
||||
short *needed_count)
|
||||
{
|
||||
struct scenario_bsp_switch_trigger_volume *volume = TAG_BLOCK_GET_ELEMENT(
|
||||
&global_scenario_get()->bsp_switch_trigger_volumes, bsp_switch_trigger_volume_index,
|
||||
@@ -3035,14 +3055,13 @@ static boolean players_coop_bsp_switch_allowed(
|
||||
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;
|
||||
*gathered_count = 0;
|
||||
*needed_count = 0;
|
||||
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;
|
||||
return FALSE;
|
||||
}
|
||||
data_iterator_new(&iterator, player_data);
|
||||
while ((player = data_iterator_next(&iterator)) != NULL)
|
||||
@@ -3056,11 +3075,64 @@ static boolean players_coop_bsp_switch_allowed(
|
||||
distance_squared3d(center, &teammate->object.bounding_sphere_center) <=
|
||||
COOP_BACKTRACK_GATHER_DISTANCE * COOP_BACKTRACK_GATHER_DISTANCE)
|
||||
{
|
||||
gathered_count++;
|
||||
(*gathered_count)++;
|
||||
}
|
||||
}
|
||||
/* (two thirds, rounded up) */
|
||||
*needed_count = (short)((living_count * 2 + 2) / 3);
|
||||
|
||||
return gathered_count * 3 >= living_count * 2;
|
||||
return *gathered_count < *needed_count;
|
||||
}
|
||||
|
||||
/* 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 host's BSP, nor back to one the team has been in until
|
||||
enough of it is there (players_coop_backtrack_wait) */
|
||||
static boolean players_coop_bsp_switch_allowed(
|
||||
short bsp_switch_trigger_volume_index,
|
||||
long unit_index)
|
||||
{
|
||||
short gathered_count;
|
||||
short needed_count;
|
||||
|
||||
return network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index) &&
|
||||
!players_coop_backtrack_wait(bsp_switch_trigger_volume_index, unit_index, &gathered_count, &needed_count);
|
||||
}
|
||||
|
||||
/* port: tells each of this machine's co-op players held back on a trigger
|
||||
how many of the team are there and how many it needs, at once and every
|
||||
COOP_BACKTRACK_MESSAGE_TICKS while they wait. Every machine works this out
|
||||
for its own players, as the host does for its decision. */
|
||||
static void players_coop_show_backtrack_wait(
|
||||
void)
|
||||
{
|
||||
short local_player_index;
|
||||
|
||||
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
|
||||
{
|
||||
long player_index = local_player_get_player_index(local_player_index);
|
||||
long unit_index = player_index != NONE ? player_get(player_index)->unit_index : NONE;
|
||||
short trigger_index = unit_index != NONE ? players_coop_bsp_switch_trigger(unit_index) : NONE;
|
||||
long *time = &players_coop_state.backtrack_message_times[local_player_index];
|
||||
short *count = &players_coop_state.backtrack_message_counts[local_player_index];
|
||||
short gathered_count;
|
||||
short needed_count;
|
||||
wchar_t text[64];
|
||||
|
||||
if (trigger_index == NONE ||
|
||||
!players_coop_backtrack_wait(trigger_index, unit_index, &gathered_count, &needed_count))
|
||||
{
|
||||
*time = 0;
|
||||
continue;
|
||||
}
|
||||
if (*time != 0 && *count == gathered_count && game_time_get() - *time < COOP_BACKTRACK_MESSAGE_TICKS)
|
||||
continue;
|
||||
*time = game_time_get();
|
||||
*count = gathered_count;
|
||||
usnprintf(text, NUMBEROF(text), L"Waiting for your team to go back: %d of %d here",
|
||||
gathered_count, needed_count);
|
||||
hud_print_message(local_player_index, text);
|
||||
}
|
||||
}
|
||||
|
||||
static void player_teleport_on_bsp_switch(
|
||||
@@ -3253,7 +3325,7 @@ void players_reconnect_to_structure_bsp(
|
||||
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))
|
||||
players_coop_bsp_switch_trigger(player->unit_index) == NONE)
|
||||
{
|
||||
source_unit_index = player->unit_index;
|
||||
teleport_position = unit->object.position;
|
||||
@@ -4633,6 +4705,8 @@ void players_update_after_game(
|
||||
players_coop_state.structure_bsp_index = global_structure_bsp_index;
|
||||
players_coop_state.bsp_switch_time = game_time_get();
|
||||
}
|
||||
if (network_coop_active())
|
||||
players_coop_show_backtrack_wait();
|
||||
|
||||
data_iterator_new(&iterator, player_data);
|
||||
while (player = data_iterator_next(&iterator))
|
||||
|
||||
+95
-16
@@ -2787,6 +2787,7 @@ symbols in this file:
|
||||
#include "ai/ai.h"
|
||||
#include "ai/ai_debug.h"
|
||||
#include "cache/cache_files.h"
|
||||
#include "cache/physical_memory_map.h" /* port: hs_scenario_syntax_data_valid */
|
||||
#include "cache/texture_cache.h"
|
||||
#include "camera/camera_scripting.h"
|
||||
#include "cutscene/cinematics.h"
|
||||
@@ -2837,6 +2838,7 @@ symbols in this file:
|
||||
#include "main/main.h"
|
||||
#include "units/units.h"
|
||||
#include "units/vehicles.h"
|
||||
#include "coop_scripts.h" /* port: port/linux/game/coop_scripts.c */
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
@@ -3606,6 +3608,10 @@ boolean hs_scenario_merge(
|
||||
struct scenario *source_scenario);
|
||||
static void hs_allocate(
|
||||
void);
|
||||
static boolean hs_scenario_syntax_data_valid(
|
||||
struct scenario const *scenario);
|
||||
static void hs_scenario_scripts_disable(
|
||||
struct scenario *scenario);
|
||||
static boolean hs_rebuild_source(
|
||||
void);
|
||||
static boolean hs_compile_source(
|
||||
@@ -12093,19 +12099,81 @@ boolean hs_scenario_merge(
|
||||
return success;
|
||||
}
|
||||
|
||||
/* port: the scenario's script data as the map holds it, the header of its
|
||||
data array too (which hs_scenario_postprocess and the data array code go by,
|
||||
the nodes' count above all): inside the tag cache, and the data array of
|
||||
script nodes it has room for. The shipped maps' all are, of 19001 nodes
|
||||
(their names differ: a30's is "static script node"). */
|
||||
static boolean hs_scenario_syntax_data_valid(
|
||||
struct scenario const *scenario)
|
||||
{
|
||||
long const syntax_data_size =
|
||||
sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node);
|
||||
byte const *tag_cache = (byte const *)physical_memory_get_tag_cache_base_address();
|
||||
byte const *address = (byte const *)scenario->hs_syntax_data.address;
|
||||
struct data_array const *data = (struct data_array const *)address;
|
||||
|
||||
if (scenario->hs_syntax_data.size != syntax_data_size ||
|
||||
!tag_cache ||
|
||||
address < tag_cache ||
|
||||
address > tag_cache+TAG_CACHE_SIZE-syntax_data_size ||
|
||||
((unsigned long)address & 3))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return data->signature == 'd@t@' &&
|
||||
data->maximum_count == MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO &&
|
||||
data->size == sizeof(struct hs_syntax_node) &&
|
||||
data->valid &&
|
||||
data->count >= 0 &&
|
||||
data->count <= data->maximum_count &&
|
||||
data->actual_count >= 0 &&
|
||||
data->actual_count <= data->count &&
|
||||
data->first_free_absolute_index >= 0 &&
|
||||
data->first_free_absolute_index <= data->maximum_count;
|
||||
}
|
||||
|
||||
/* port: the scenario runs no scripts, its script data not being sound: a
|
||||
cache file's blocks can't be resized (tag_block_resize), so the counts are
|
||||
let go of in place, and no global is initialized or script thread started
|
||||
(hs_runtime_initialize_for_new_map) against nodes that aren't there */
|
||||
static void hs_scenario_scripts_disable(
|
||||
struct scenario *scenario)
|
||||
{
|
||||
scenario->hs_scripts.count = 0;
|
||||
scenario->hs_globals.count = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static void hs_allocate(
|
||||
void)
|
||||
{
|
||||
struct scenario *scenario;
|
||||
|
||||
scenario = global_scenario_index != NONE ? global_scenario_get() : NULL;
|
||||
/* port: as the map holds it only when it is sound */
|
||||
if (scenario &&
|
||||
scenario->hs_syntax_data.size ==
|
||||
sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node))
|
||||
sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node) &&
|
||||
hs_scenario_syntax_data_valid(scenario))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* port: script data a map holds that isn't sound is the map's, in the
|
||||
tag cache: it isn't freed or replaced (what the Xbox did here, freeing
|
||||
tag memory), the map runs no scripts, and an array of the port's own,
|
||||
made once, stands in for it */
|
||||
if (scenario)
|
||||
{
|
||||
hs_scenario_scripts_disable(scenario);
|
||||
if (hs_syntax_data && hs_syntax_data_allocated)
|
||||
return;
|
||||
error(0, "the scenario's script data is missing or damaged; its scripts won't run");
|
||||
}
|
||||
|
||||
hs_syntax_data = data_new(
|
||||
"script node",
|
||||
MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO,
|
||||
@@ -12113,19 +12181,7 @@ static void hs_allocate(
|
||||
if (hs_syntax_data)
|
||||
{
|
||||
data_make_valid(hs_syntax_data);
|
||||
if (scenario)
|
||||
{
|
||||
match_free("c:\\halo\\SOURCE\\hs\\hs.c", 336, scenario->hs_syntax_data.address);
|
||||
scenario->hs_syntax_data.address = hs_syntax_data;
|
||||
scenario->hs_syntax_data.size =
|
||||
sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node);
|
||||
tag_data_resize(&scenario->hs_string_constants, 0x400);
|
||||
tag_block_resize(&scenario->hs_scripts, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
hs_syntax_data_allocated = TRUE;
|
||||
}
|
||||
hs_syntax_data_allocated = TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -12542,8 +12598,17 @@ struct hs_external_global_definition *hs_global_external_get(
|
||||
short hs_global_get_type(
|
||||
short global_index)
|
||||
{
|
||||
/* port: a global that isn't there (a map's syntax node names it) has
|
||||
no type */
|
||||
if (global_index & 0x8000)
|
||||
{
|
||||
if ((global_index & 0x7FFF) >= hs_external_global_count)
|
||||
return _hs_unparsed;
|
||||
|
||||
return hs_global_external_get(global_index & 0x7FFF)->type;
|
||||
}
|
||||
if ((global_index & 0x7FFF) >= global_scenario_get()->hs_globals.count)
|
||||
return _hs_unparsed;
|
||||
|
||||
return TAG_BLOCK_GET_ELEMENT(
|
||||
&global_scenario_get()->hs_globals,
|
||||
@@ -12964,7 +13029,8 @@ static long alphabetize(
|
||||
return _stricmp(*left, *right);
|
||||
}
|
||||
|
||||
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_safe_to_save_evaluate, game_safe_to_save)
|
||||
/* port: in network co-op, once any player is safe (coop_scripts.c) */
|
||||
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_safe_to_save_evaluate, coop_scripts_safe_to_save)
|
||||
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_all_quiet_evaluate, game_all_quiet)
|
||||
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_safe_to_speak_evaluate, game_safe_to_speak)
|
||||
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(game_is_cooperative_evaluate, game_is_cooperative)
|
||||
@@ -13782,7 +13848,8 @@ HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(player_control_action_test_look_relative
|
||||
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(player_control_action_test_look_relative_right_evaluate, player_control_action_test_look_relative_right)
|
||||
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(player_control_action_test_look_relative_all_directions_evaluate, player_control_action_test_look_relative_all_directions)
|
||||
HS_EVALUATE_RETURN_BOOLEAN_NO_ARGUMENTS(player_control_action_test_move_relative_all_directions_evaluate, player_control_action_test_move_relative_all_directions)
|
||||
HS_EVALUATE_VOID_FROM_ARGUMENTS(player_add_equipment_evaluate, struct hs_arguments_long_word_boolean, (player_add_equipment(arguments->value0, arguments->value1, arguments->value2)))
|
||||
/* port: in network co-op, also for the players the scripts can't name (coop_scripts.c) */
|
||||
HS_EVALUATE_VOID_FROM_ARGUMENTS(player_add_equipment_evaluate, struct hs_arguments_long_word_boolean, (coop_scripts_player_add_equipment(arguments->value0, arguments->value1, arguments->value2)))
|
||||
HS_EVALUATE_VOID_FROM_ARGUMENTS(debug_player_teleport_evaluate, struct hs_arguments_short_word, (debug_player_teleport(arguments->value0, arguments->value1)))
|
||||
HS_EVALUATE_VOID_STRING(main_set_map_name_evaluate, main_set_map_name)
|
||||
HS_EVALUATE_VOID_STRING(main_set_multiplayer_map_name_evaluate, main_set_multiplayer_map_name)
|
||||
@@ -13961,6 +14028,15 @@ boolean hs_scenario_postprocess(
|
||||
scenario = global_scenario_get();
|
||||
saved_syntax_data = hs_syntax_data;
|
||||
hs_allocate();
|
||||
/* port: script data that isn't sound isn't gone through (hs_allocate
|
||||
has an empty array stand in for it, and the map runs no scripts) */
|
||||
if (!hs_scenario_syntax_data_valid(scenario))
|
||||
{
|
||||
if (restore_syntax_data)
|
||||
hs_syntax_data = saved_syntax_data;
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
recompile = scenario->hs_scripts.count == 0 && scenario->hs_source_files.count>0;
|
||||
hs_syntax_data = (struct data_array *)scenario->hs_syntax_data.address;
|
||||
hs_syntax_data->data = (char *)hs_syntax_data+sizeof(struct data_array);
|
||||
@@ -13996,6 +14072,9 @@ boolean hs_scenario_postprocess(
|
||||
!tag_data_resize(&global_scenario_get()->hs_string_constants, 0x400))
|
||||
{
|
||||
error(0, "couldn't reset scripts.");
|
||||
/* port: a cache file's can't be resized: none run against the
|
||||
nodes just deleted */
|
||||
hs_scenario_scripts_disable(scenario);
|
||||
}
|
||||
success = FALSE;
|
||||
}
|
||||
|
||||
@@ -2486,8 +2486,12 @@ boolean hs_compile_postprocess(
|
||||
long predicate_index = hs_syntax_get(expression_index)->data;
|
||||
struct hs_syntax_node *predicate;
|
||||
|
||||
/* port: a predicate that isn't a node (a map's) is as corrupt
|
||||
as one that isn't a function name, where hs_syntax_get's NULL
|
||||
was gone through */
|
||||
if (predicate_index == NONE ||
|
||||
(predicate = hs_syntax_get(predicate_index))->type != _hs_function_name)
|
||||
!(predicate = datum_try_and_get(hs_syntax_data, predicate_index)) ||
|
||||
predicate->type != _hs_function_name)
|
||||
{
|
||||
hs_compile_globals.error = "corrupt syntax tree (you need to recompile scripts.)";
|
||||
success = FALSE;
|
||||
|
||||
@@ -105,6 +105,7 @@ symbols in this file:
|
||||
#include "units/units.h"
|
||||
#include "object_lists.h"
|
||||
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
|
||||
#include "coop_scripts.h" /* port: port/linux/game/coop_scripts.c */
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
@@ -844,10 +845,12 @@ boolean hs_trigger_volume_test_objects_all(
|
||||
short trigger_volume_index,
|
||||
long object_list_index)
|
||||
{
|
||||
/* port: in network co-op, waiting for every player means waiting for
|
||||
any one of them (coop_scripts.c) */
|
||||
return hs_trigger_volume_test_objects(
|
||||
trigger_volume_index,
|
||||
object_list_index,
|
||||
TRUE);
|
||||
!coop_scripts_any_player_will_do(object_list_index));
|
||||
}
|
||||
|
||||
boolean hs_trigger_volume_test_objects_any(
|
||||
@@ -986,6 +989,8 @@ void hs_object_teleport(
|
||||
short cutscene_flag_index)
|
||||
{
|
||||
hs_object_orient(object_index, cutscene_flag_index, TRUE, TRUE);
|
||||
/* port: co-op players the scripts can't name go with player0 */
|
||||
coop_scripts_teleport_followers(object_index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
+119
-8
@@ -315,6 +315,9 @@ enum
|
||||
{
|
||||
_hs_thread_in_function_call_bit = 0,
|
||||
_hs_thread_sleeping_bit,
|
||||
/* port: the thread's stack overflowed (hs_stack_overflow); hs_thread_main
|
||||
ends it */
|
||||
_hs_thread_stack_overflow_bit,
|
||||
_hs_global_external_bit = 15,
|
||||
};
|
||||
|
||||
@@ -494,7 +497,9 @@ static void hs_thread_delete(
|
||||
static long hs_thread_new(
|
||||
short type,
|
||||
long script_index);
|
||||
static void hs_stack_push(
|
||||
static boolean hs_stack_push(
|
||||
long thread_index);
|
||||
static void hs_stack_overflow(
|
||||
long thread_index);
|
||||
static void *hs_stack_allocate(
|
||||
long thread_index,
|
||||
@@ -1304,7 +1309,7 @@ static void hs_thread_delete(
|
||||
return;
|
||||
}
|
||||
|
||||
static void hs_stack_push(
|
||||
static boolean hs_stack_push(
|
||||
long thread_index)
|
||||
{
|
||||
struct hs_thread_datum *thread = hs_thread_get(thread_index);
|
||||
@@ -1315,10 +1320,39 @@ static void hs_stack_push(
|
||||
(byte *) (new_frame+1)<thread->stack_data+HS_THREAD_STACK_SIZE,
|
||||
"stack overflow.");
|
||||
|
||||
/* port: a frame past the thread's stack isn't pushed (it would be
|
||||
written over the next thread's datum): the thread is ended instead */
|
||||
if ((byte *)(new_frame+1)>=thread->stack_data+HS_THREAD_STACK_SIZE ||
|
||||
TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit))
|
||||
{
|
||||
hs_stack_overflow(thread_index);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
new_frame->previous = thread->stack;
|
||||
thread->stack = new_frame;
|
||||
new_frame->size = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* port: a script whose stack overflows is ended rather than let write past
|
||||
its thread's stack: the push or allocation is refused (its caller returns),
|
||||
and hs_thread_main lets the thread's frames go once the evaluation in hand
|
||||
returns */
|
||||
static void hs_stack_overflow(
|
||||
long thread_index)
|
||||
{
|
||||
struct hs_thread_datum *thread = hs_thread_get(thread_index);
|
||||
|
||||
if (!TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit))
|
||||
{
|
||||
error(_error_silent, "a problem occurred while executing the script %s: stack overflow. (it was stopped)",
|
||||
hs_thread_format(thread_index));
|
||||
SET_FLAG(thread->flags, _hs_thread_stack_overflow_bit, TRUE);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1836,6 +1870,10 @@ void hs_evaluate_begin(
|
||||
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x15,
|
||||
function_index==_hs_function_begin);
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!expression_index || !result)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
*expression_index = hs_syntax_get(hs_syntax_get(
|
||||
@@ -1963,6 +2001,10 @@ void hs_evaluate_logical(
|
||||
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0xcf,
|
||||
function_index==_hs_function_and || function_index==_hs_function_or);
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!expression_index || !value || !result)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
*expression_index = hs_syntax_get(hs_syntax_get(
|
||||
@@ -2014,6 +2056,10 @@ void hs_evaluate_if(
|
||||
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x77,
|
||||
function_index==_hs_function_if);
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!condition || !expression_index || !result)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
*condition = 0;
|
||||
@@ -2064,7 +2110,9 @@ void hs_evaluate_set(
|
||||
short type;
|
||||
long global_index;
|
||||
|
||||
hs_stack_allocate(thread_index, sizeof(long));
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!hs_stack_allocate(thread_index, sizeof(long)))
|
||||
return;
|
||||
type = hs_global_get_type((short)variable->data);
|
||||
|
||||
if (initialize)
|
||||
@@ -2107,6 +2155,10 @@ void hs_evaluate_inspect(
|
||||
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x2bc,
|
||||
function_index==_hs_function_inspect);
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!value)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
hs_evaluate(thread_index, hs_syntax_get(hs_syntax_get(
|
||||
@@ -2146,6 +2198,10 @@ void hs_evaluate_arithmetic(
|
||||
real *result = hs_stack_allocate(thread_index, sizeof(real));
|
||||
long result_long;
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!argument_index || !expression_index || !value || !result)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
*argument_index = 0;
|
||||
@@ -2219,6 +2275,10 @@ void hs_evaluate_object_cast_up(
|
||||
function_index>=_hs_function_object_to_unit &&
|
||||
function_index<=_hs_function_object_to_unit);
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!object_index)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
hs_evaluate(thread_index, hs_syntax_get(hs_syntax_get(
|
||||
@@ -2264,6 +2324,10 @@ void hs_evaluate_begin_random(
|
||||
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x45,
|
||||
function_index==_hs_function_begin_random);
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!argument_count || !evaluated || !result)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
long expression_index = hs_syntax_get(hs_syntax_get(
|
||||
@@ -2318,6 +2382,10 @@ void hs_evaluate_debug_string(
|
||||
(function_index>=_hs_function_debug_string__first) &&
|
||||
(function_index<=_hs_function_debug_string__last));
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!expression_index || !argument_count || !arguments)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
*expression_index = hs_syntax_get(hs_syntax_get(
|
||||
@@ -2384,6 +2452,10 @@ void hs_evaluate_sleep_until(
|
||||
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x1e5,
|
||||
function_index==_hs_function_sleep_until);
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!condition || !ticks || !timeout || !start_time || !argument_index)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
*(boolean *)condition = FALSE;
|
||||
@@ -2455,6 +2527,10 @@ void hs_evaluate_sleep(
|
||||
match_assert("c:\\halo\\source\\hs\\hs_library_internal_runtime.h", 0x189,
|
||||
function_index==_hs_function_sleep);
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!ticks || !script_index || !argument_index)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
hs_evaluate(thread_index, hs_syntax_get(hs_syntax_get(
|
||||
@@ -2524,6 +2600,7 @@ static void hs_thread_main(
|
||||
{
|
||||
struct hs_thread_datum *thread = hs_thread_get(thread_index);
|
||||
struct hs_script *script = NULL;
|
||||
long *root_result;
|
||||
|
||||
hs_runtime_globals.executing_thread_index = (short)thread_index;
|
||||
if (thread->type==_hs_thread_type_script)
|
||||
@@ -2547,8 +2624,10 @@ static void hs_thread_main(
|
||||
match_assert("c:\\halo\\SOURCE\\hs\\hs_runtime.c", 0x2c3, script);
|
||||
|
||||
thread->stack->size = 0;
|
||||
hs_evaluate(thread_index, script->root_expression_index,
|
||||
hs_stack_allocate(thread_index, sizeof(long)));
|
||||
/* port: (the base frame, just emptied, has room) */
|
||||
root_result = hs_stack_allocate(thread_index, sizeof(long));
|
||||
if (root_result)
|
||||
hs_evaluate(thread_index, script->root_expression_index, root_result);
|
||||
}
|
||||
|
||||
while (thread->stack!=(struct hs_stack_frame *)thread->stack_data &&
|
||||
@@ -2577,6 +2656,15 @@ static void hs_thread_main(
|
||||
{
|
||||
hs_script_evaluate(expression->index, thread_index, initialize);
|
||||
}
|
||||
|
||||
/* port: a stack overflow (hs_stack_overflow) ends the thread: its
|
||||
frames are let go, and a script's thread doesn't run again */
|
||||
if (TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit))
|
||||
{
|
||||
thread->stack = (struct hs_stack_frame *)thread->stack_data;
|
||||
thread->flags = 0;
|
||||
thread->sleep_until = thread->type==_hs_thread_type_script ? NONE : 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (thread->stack==(struct hs_stack_frame *)thread->stack_data)
|
||||
@@ -2612,6 +2700,10 @@ static void hs_script_evaluate(
|
||||
struct hs_thread_datum *thread = hs_thread_get(thread_index);
|
||||
long *result = hs_stack_allocate(thread_index, sizeof(long));
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow) */
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
if (initialize)
|
||||
hs_evaluate(thread_index, script->root_expression_index, result);
|
||||
else
|
||||
@@ -2636,6 +2728,17 @@ static void *hs_stack_allocate(
|
||||
frame->data+frame->size+size<=thread->stack_data+HS_THREAD_STACK_SIZE,
|
||||
"stack overflow.");
|
||||
|
||||
/* port: none past the thread's stack (hs_stack_overflow); the caller
|
||||
returns on NULL */
|
||||
if (size<0 ||
|
||||
size>thread->stack_data+HS_THREAD_STACK_SIZE-(frame->data+frame->size) ||
|
||||
TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit))
|
||||
{
|
||||
hs_stack_overflow(thread_index);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
result = frame->data+frame->size;
|
||||
frame->size += (short)size;
|
||||
|
||||
@@ -2676,9 +2779,12 @@ static void hs_evaluate(
|
||||
else
|
||||
{
|
||||
thread->stack->result = destination;
|
||||
hs_stack_push(thread_index);
|
||||
SET_FLAG(thread->flags, _hs_thread_in_function_call_bit, TRUE);
|
||||
thread->stack->expression_index = expression_index;
|
||||
/* port: not pushed on a stack overflow (hs_stack_overflow) */
|
||||
if (hs_stack_push(thread_index))
|
||||
{
|
||||
SET_FLAG(thread->flags, _hs_thread_in_function_call_bit, TRUE);
|
||||
thread->stack->expression_index = expression_index;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -3132,6 +3238,11 @@ static long *hs_arguments_evaluate(
|
||||
short *argument_index = hs_stack_allocate(thread_index, sizeof(short));
|
||||
long *expression_index = hs_stack_allocate(thread_index, sizeof(long));
|
||||
|
||||
/* port: none on a stack overflow (hs_stack_overflow): as for arguments
|
||||
still being evaluated, the caller returns on NULL */
|
||||
if (!values || !argument_index || !expression_index)
|
||||
return NULL;
|
||||
|
||||
if (initialize)
|
||||
{
|
||||
*argument_index = 0;
|
||||
|
||||
@@ -373,22 +373,16 @@ void terminal_printf(
|
||||
|
||||
if (line_index!=NONE)
|
||||
{
|
||||
short count;
|
||||
int count;
|
||||
struct output_line_datum *line = output_line_get(line_index);
|
||||
line->timer = 0;
|
||||
line->color = !color ? default_terminal_printf_color : *color;
|
||||
count = _vsnprintf(line->buffer, NUMBEROF(line->buffer)-2, format, arglist) - 1;
|
||||
match_vassert(
|
||||
"c:\\halo\\SOURCE\\interface\\terminal.c",
|
||||
413,
|
||||
count < (short)(NUMBEROF(line->buffer)-1),
|
||||
csprintf(
|
||||
temporary,
|
||||
"terminal_printf call generated %d characters; the buffer size is %d characters.",
|
||||
count,
|
||||
NUMBEROF(line->buffer)-1
|
||||
)
|
||||
);
|
||||
/* port: a line longer than the on-screen buffer is cut to fit (it
|
||||
halted here, from inside error(), so the real halt was never
|
||||
written down) */
|
||||
count = _vsnprintf(line->buffer, NUMBEROF(line->buffer)-2, format, arglist);
|
||||
if (count < 0 || count >= (int)(NUMBEROF(line->buffer)-2))
|
||||
line->buffer[NUMBEROF(line->buffer)-2] = 0;
|
||||
line->tabstop = strstr(line->buffer, "|t") != 0;
|
||||
telnet_console_print(line->buffer);
|
||||
}
|
||||
|
||||
@@ -8629,6 +8629,30 @@ static boolean ui_check_for_pause_game(
|
||||
{
|
||||
if (game_time_get_paused() == TRUE)
|
||||
ui_widgets_close_all();
|
||||
/* port: (and a multiplayer map's own, below, which pauses
|
||||
nothing, closes as in a multiplayer game) */
|
||||
else if (tag_loaded(UI_WIDGET_DEFINITION_TAG, "ui\\shell\\solo_game\\pause_game\\pause_game") == NONE)
|
||||
ui_widget_delete(widget_globals.active_widgets[controller_index]);
|
||||
}
|
||||
/* port: a multiplayer map played alone (New Game's MULTIPLAYER
|
||||
maps) has no campaign pause screen, but its own (LEAVE GAME
|
||||
goes to the main menu: network_game_remove_local_player) */
|
||||
else if (tag_loaded(UI_WIDGET_DEFINITION_TAG, "ui\\shell\\solo_game\\pause_game\\pause_game") == NONE &&
|
||||
tag_loaded(UI_WIDGET_DEFINITION_TAG, "ui\\shell\\multiplayer_game\\pause_game\\1p_pause_game") != NONE)
|
||||
{
|
||||
if (!ui_widget_load_by_name_or_tag(
|
||||
"ui\\shell\\multiplayer_game\\pause_game\\1p_pause_game",
|
||||
NONE,
|
||||
NULL,
|
||||
controller_index,
|
||||
NONE,
|
||||
NONE,
|
||||
NONE))
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"failed to load multiplayer pause game window");
|
||||
}
|
||||
}
|
||||
else if (!ui_widget_load_by_name_or_tag(
|
||||
"ui\\shell\\solo_game\\pause_game\\pause_game",
|
||||
|
||||
@@ -2431,6 +2431,14 @@ static boolean network_game_remove_local_player(
|
||||
match_vassert("c:\\halo\\SOURCE\\interface\\ui_widget_event_handler_functions.c", 4073,
|
||||
event && event->controller_index >= 0 && event->controller_index < 4,
|
||||
"valid controller index required to remove player from network game");
|
||||
/* port: a multiplayer map played alone (its pause screen: ui_widget.c's
|
||||
ui_check_for_pause_game) has no network game to leave: the main menu,
|
||||
the map not saved (as the campaign's pause screen quits it) */
|
||||
if (!global_network_game_client_get())
|
||||
{
|
||||
main_goto_main_menu();
|
||||
return TRUE;
|
||||
}
|
||||
network_game_client_local_player_quit(event->controller_index);
|
||||
/* port: a split screen player who quit, the others staying, is not
|
||||
joined to the next game */
|
||||
|
||||
+57
-19
@@ -1852,11 +1852,40 @@ void main_pregame_render(
|
||||
return;
|
||||
}
|
||||
|
||||
/* port: a network co-op host reverts to its last saved state with its
|
||||
clock kept going forward, and the clients follow through the co-op syncs
|
||||
(network_coop.c). FALSE without a saved state, which would reset the map
|
||||
on the host alone. */
|
||||
static boolean main_coop_host_revert(
|
||||
void)
|
||||
{
|
||||
long now = game_time_get();
|
||||
|
||||
if (!game_state_port_saved_game_valid())
|
||||
return FALSE;
|
||||
game_state_revert();
|
||||
network_coop_reverted(now);
|
||||
ui_widgets_disable_pause_game(30);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static boolean main_coop_host(
|
||||
void)
|
||||
{
|
||||
return game_connection() == _game_connection_network_server && network_coop_active();
|
||||
}
|
||||
|
||||
static void main_revert_map_private(
|
||||
void)
|
||||
{
|
||||
game_state_revert();
|
||||
ui_widgets_disable_pause_game(30);
|
||||
/* (a network client never reverts on its own: its game is the host's) */
|
||||
if (main_coop_host())
|
||||
main_coop_host_revert();
|
||||
else if (game_connection() != _game_connection_network_client)
|
||||
{
|
||||
game_state_revert();
|
||||
ui_widgets_disable_pause_game(30);
|
||||
}
|
||||
main_globals.revert_map = FALSE;
|
||||
return;
|
||||
}
|
||||
@@ -1864,22 +1893,29 @@ static void main_revert_map_private(
|
||||
static void main_skip_cinematic_private(
|
||||
void)
|
||||
{
|
||||
/* port: a network host never reverts without a saved state, which
|
||||
would reset the map on the host alone */
|
||||
if (cinematic_can_be_skipped() &&
|
||||
(game_connection() != _game_connection_network_server || game_state_port_saved_game_valid()))
|
||||
{
|
||||
long now = game_time_get();
|
||||
/* port: only a local game or a network co-op host reverts to skip; a
|
||||
network game's other machines would be left out of step */
|
||||
boolean skippable = cinematic_can_be_skipped();
|
||||
boolean skipped = FALSE;
|
||||
|
||||
game_state_revert();
|
||||
network_coop_skip_reverted(now);
|
||||
ui_widgets_disable_pause_game(30);
|
||||
main_globals.revert_map = FALSE;
|
||||
if (skippable && main_coop_host())
|
||||
{
|
||||
skipped = main_coop_host_revert();
|
||||
if (skipped)
|
||||
network_coop_skip_done();
|
||||
}
|
||||
else if (game_connection() == _game_connection_network_server)
|
||||
else if (skippable && game_connection() == _game_connection_local)
|
||||
{
|
||||
game_state_revert();
|
||||
ui_widgets_disable_pause_game(30);
|
||||
skipped = TRUE;
|
||||
}
|
||||
if (skipped)
|
||||
main_globals.revert_map = FALSE;
|
||||
else if (main_coop_host())
|
||||
{
|
||||
error(_error_silent, "co-op: cutscene not skipped (skippable %d, saved state %d)",
|
||||
cinematic_can_be_skipped(), game_state_port_saved_game_valid());
|
||||
skippable, game_state_port_saved_game_valid());
|
||||
}
|
||||
main_globals.skip_cinematic = FALSE;
|
||||
return;
|
||||
@@ -2018,12 +2054,14 @@ static void main_lost_map_private(
|
||||
{
|
||||
main_globals.lost_map = FALSE;
|
||||
main_globals.loss_timer = 0;
|
||||
/* port: network co-op can't revert to a checkpoint (every machine would
|
||||
have to), so the players respawn where they were at the last one */
|
||||
if (game_connection() == _game_connection_network_server)
|
||||
players_respawn_at_checkpoint();
|
||||
else
|
||||
/* port: in network co-op, everyone dying respawns the players where
|
||||
they were at the last checkpoint, without a revert. A mission the
|
||||
scripts failed (game_lost, with players still alive: d40's timer,
|
||||
a50's Keyes) does revert, or its failure cutscene would never end. */
|
||||
if (game_connection() != _game_connection_network_server)
|
||||
game_state_revert();
|
||||
else if (players_are_all_dead() || !main_coop_host_revert())
|
||||
players_respawn_at_checkpoint();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -322,6 +322,12 @@ long datum_new(
|
||||
absolute_index++;
|
||||
header = (struct datum_header *)((byte *)header+size);
|
||||
}
|
||||
/* port: none free from first_free_absolute_index on (and none before it
|
||||
is, as ever): the next search starts at the end, until a datum is deleted
|
||||
(datum_delete lowers it), rather than walking a full array again. A full
|
||||
array (the AI's props, with many actors) was walked on every new datum. */
|
||||
if (result == NONE)
|
||||
data->first_free_absolute_index = data->maximum_count;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -411,6 +411,12 @@ enum
|
||||
long as the host waits for a silent machine, network_server_manager.c):
|
||||
network_game_client_network_lost */
|
||||
NETWORK_GAME_CLIENT_LINK_DOWN_TIMEOUT = 15000,
|
||||
/* port: how long a frame the client spends on the messages that came
|
||||
(a host that floods it with them would hold each frame for as long as
|
||||
they took): those left wait in the queue, in order, for the next
|
||||
frame's, and the stream what the queue has no room for. One is always
|
||||
handled (network_game_client_process_messages) */
|
||||
MAXIMUM_MESSAGE_MILLISECONDS_PER_IDLE = 50,
|
||||
};
|
||||
|
||||
enum network_game_client_state
|
||||
@@ -745,6 +751,9 @@ static boolean add_advertised_game(
|
||||
struct message_server_game_advertise *advertisement);
|
||||
static boolean network_game_client_process_incoming_messages(
|
||||
struct network_game_client *client);
|
||||
static boolean network_game_client_process_messages(
|
||||
struct network_game_client *client,
|
||||
boolean budgeted);
|
||||
static boolean network_game_client_process_last_messages(
|
||||
struct network_game_client *client);
|
||||
static void network_game_client_update_precache_status(
|
||||
@@ -2450,11 +2459,21 @@ static boolean add_advertised_game(
|
||||
|
||||
static boolean network_game_client_process_incoming_messages(
|
||||
struct network_game_client *client)
|
||||
{
|
||||
return network_game_client_process_messages(client, TRUE);
|
||||
}
|
||||
|
||||
/* (budgeted: no longer than MAXIMUM_MESSAGE_MILLISECONDS_PER_IDLE, else
|
||||
every message queued) */
|
||||
static boolean network_game_client_process_messages(
|
||||
struct network_game_client *client,
|
||||
boolean budgeted)
|
||||
{
|
||||
boolean success = TRUE;
|
||||
word message_packet_size;
|
||||
struct transport_address source_address;
|
||||
word message_packet[MAXIMUM_NETWORK_MESSAGE_SIZE / sizeof(word)];
|
||||
unsigned long start_time = system_milliseconds();
|
||||
|
||||
message_packet_size = sizeof(message_packet);
|
||||
|
||||
@@ -2474,6 +2493,8 @@ static boolean network_game_client_process_incoming_messages(
|
||||
}
|
||||
|
||||
message_packet_size = sizeof(message_packet);
|
||||
if (budgeted && system_milliseconds() - start_time >= MAXIMUM_MESSAGE_MILLISECONDS_PER_IDLE)
|
||||
break;
|
||||
}
|
||||
|
||||
return success;
|
||||
@@ -2490,7 +2511,8 @@ static boolean network_game_client_process_last_messages(
|
||||
{
|
||||
short state = client->state;
|
||||
|
||||
if (!network_game_client_process_incoming_messages(client))
|
||||
/* (all of them: the connection goes) */
|
||||
if (!network_game_client_process_messages(client, FALSE))
|
||||
{
|
||||
network_event("network_game_client_process_incoming_messages() failed after the connection failed");
|
||||
}
|
||||
|
||||
@@ -662,6 +662,14 @@ boolean network_game_remove_player(
|
||||
game->players[player_index].machine_index == player->machine_index &&
|
||||
game->players[player_index].controller_index == player->controller_index)
|
||||
{
|
||||
/* port: a client's count is the host's (its settings), and
|
||||
need not be as many as the players it lists: none taken
|
||||
below none */
|
||||
if (game->player_count <= 0)
|
||||
{
|
||||
error(2, "tried to remove a player from a game of %d players", game->player_count);
|
||||
break;
|
||||
}
|
||||
network_game_invalidate_player(&game->players[player_index]);
|
||||
game->player_count--;
|
||||
result = TRUE;
|
||||
@@ -707,7 +715,9 @@ boolean network_game_remove_machine(
|
||||
}
|
||||
|
||||
network_game_invalidate_machine(game, machine->machine_index);
|
||||
game->machine_count--;
|
||||
/* (port: and none below none, as for its players) */
|
||||
if (game->machine_count > 0)
|
||||
game->machine_count--;
|
||||
result = TRUE;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -42,12 +42,22 @@ struct network_game_local_data
|
||||
byte __padding431[3];
|
||||
};
|
||||
|
||||
/* port: network_game's cooperative_flags */
|
||||
enum
|
||||
{
|
||||
/* the players pass through each other (Server Setup's PLAYER COLLISIONS:
|
||||
OFF, co-op's): network_coop_player_collisions */
|
||||
_network_game_cooperative_no_player_collisions_bit = 0,
|
||||
};
|
||||
|
||||
struct network_game
|
||||
{
|
||||
wchar_t name[16];
|
||||
struct network_game_map map;
|
||||
struct game_variant variant;
|
||||
byte __padding10C;
|
||||
/* port: co-op's switches (_network_game_cooperative_*_bit), in a byte the
|
||||
Xbox's game left as padding, which builds before them send as 0 */
|
||||
byte cooperative_flags;
|
||||
char minimum_players;
|
||||
/* port: 128 does not fit a signed char */
|
||||
byte maximum_players;
|
||||
|
||||
@@ -1116,9 +1116,12 @@ unsigned long network_game_server_machine_address(
|
||||
|
||||
/* port: the distributed netcode asks that a client machine be dropped (its
|
||||
game ran faster than this one's: network_distributed.c); it is, once this
|
||||
server next looks at its machines, not while its messages are read */
|
||||
server next looks at its machines, not while its messages are read. Its
|
||||
address kept out of this server's games (kept_out), else it may join
|
||||
again, as a kick's */
|
||||
void network_game_server_kick_machine(
|
||||
long machine_index)
|
||||
long machine_index,
|
||||
boolean kept_out)
|
||||
{
|
||||
struct network_game_server *server = global_network_game_server_get();
|
||||
|
||||
@@ -1128,7 +1131,7 @@ void network_game_server_kick_machine(
|
||||
{
|
||||
return;
|
||||
}
|
||||
network_game_server_kick_pending[machine_index] = _kick_kept_out;
|
||||
network_game_server_kick_pending[machine_index] = kept_out ? _kick_kept_out : _kick_rejoinable;
|
||||
}
|
||||
|
||||
/* (a client machine's player queued to add in game: one refused is as one
|
||||
@@ -3142,6 +3145,11 @@ struct network_machine *network_game_server_get_client_machine(
|
||||
if (machine_index)
|
||||
*machine_index = NONE;
|
||||
|
||||
/* port: none for a connection without a slot (NONE: the assert is not
|
||||
checked in release builds) */
|
||||
if (client_machine->machine_index < 0 || client_machine->machine_index >= MAXIMUM_NETWORK_MACHINE_COUNT)
|
||||
return NULL;
|
||||
|
||||
machine = &server->game.machines[client_machine->machine_index];
|
||||
if (machine_index)
|
||||
*machine_index = machine->machine_index;
|
||||
@@ -3178,6 +3186,10 @@ struct network_game_server_client_machine *network_game_server_get_client_machin
|
||||
match_assert(NETWORK_SERVER_MANAGER_FILE, 0x741,
|
||||
server && (index<MAXIMUM_NETWORK_MACHINE_COUNT));
|
||||
|
||||
/* (port: and in release builds) */
|
||||
if (index < 0 || index >= MAXIMUM_NETWORK_MACHINE_COUNT)
|
||||
return NULL;
|
||||
|
||||
return &server->client_machines[index];
|
||||
}
|
||||
|
||||
@@ -4032,6 +4044,18 @@ void network_game_server_port_set_cooperative_friendly_fire(
|
||||
network_event("network_game_server_port_set_cooperative_friendly_fire() failed to send updated game settings to clients");
|
||||
}
|
||||
|
||||
void network_game_server_port_set_cooperative_player_collisions(
|
||||
boolean player_collisions)
|
||||
{
|
||||
struct network_game_server *server = global_network_game_server_get();
|
||||
|
||||
if (!server)
|
||||
return;
|
||||
SET_FLAG(server->game.cooperative_flags, _network_game_cooperative_no_player_collisions_bit, !player_collisions);
|
||||
if (server->state == _network_game_server_state_pregame && !network_game_server_send_game_data_pregame(server))
|
||||
network_event("network_game_server_port_set_cooperative_player_collisions() failed to send updated game settings to clients");
|
||||
}
|
||||
|
||||
/* port: a gametype's PC options: the menus' (player_ui_set_game_variant_options)
|
||||
when it is the menus' gametype, else its defaults */
|
||||
static void network_game_server_variant_options(
|
||||
|
||||
@@ -47,7 +47,8 @@ void network_game_server_pause_countdown(
|
||||
void network_game_generate_join_game_token(
|
||||
byte *join_token);
|
||||
void network_game_server_kick_machine(
|
||||
long machine_index);
|
||||
long machine_index,
|
||||
boolean kept_out);
|
||||
/* the host's ban command (console.c, hs.c) */
|
||||
enum
|
||||
{
|
||||
@@ -72,6 +73,10 @@ void network_game_server_port_set_cooperative(
|
||||
Server Setup's FRIENDLY FIRE), kept for the levels after */
|
||||
void network_game_server_port_set_cooperative_friendly_fire(
|
||||
short friendly_fire);
|
||||
/* port: whether co-op's players collide with each other (Server Setup's
|
||||
PLAYER COLLISIONS), kept for the levels after */
|
||||
void network_game_server_port_set_cooperative_player_collisions(
|
||||
boolean player_collisions);
|
||||
boolean network_game_server_ban_player(
|
||||
char const *text);
|
||||
/* port: the host's kick command: as the ban command, but nothing kept (no
|
||||
|
||||
@@ -267,6 +267,7 @@ symbols in this file:
|
||||
|
||||
/* port/linux/game/network_distributed.c's */
|
||||
void network_distributed_handle_message(long machine_index, word const *message, word size);
|
||||
void network_distributed_handle_stream_message(long machine_index, word const *message, word size);
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
@@ -289,6 +290,11 @@ enum
|
||||
/* port: the least time between two advertisements of the game
|
||||
(milliseconds: network_game_server_handle_message_client_broadcast_game_search) */
|
||||
GAME_ADVERTISEMENT_INTERVAL = 250,
|
||||
/* port: the least time between two answers to one address's pings, and
|
||||
the addresses kept to tell, no more of them answered in that time
|
||||
(network_game_server_handle_message_client_ping) */
|
||||
PING_REPLY_INTERVAL = 250,
|
||||
MAXIMUM_PING_REPLY_ADDRESSES = 32,
|
||||
};
|
||||
|
||||
enum
|
||||
@@ -447,6 +453,11 @@ struct message_client_switch_to_pregame
|
||||
long unused;
|
||||
};
|
||||
|
||||
struct message_client_graceful_game_exit_postgame
|
||||
{
|
||||
long unused;
|
||||
};
|
||||
|
||||
struct message_server_machine_accepted
|
||||
{
|
||||
long random_seed;
|
||||
@@ -551,6 +562,11 @@ static boolean network_game_server_handle_message_client_switch_to_pregame(
|
||||
struct network_game_server_client_machine *client_machine,
|
||||
word *message,
|
||||
short message_size);
|
||||
static boolean network_game_server_handle_message_client_graceful_game_exit_postgame(
|
||||
struct network_game_server *server,
|
||||
struct network_game_server_client_machine *client_machine,
|
||||
word *message,
|
||||
short message_size);
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
@@ -793,7 +809,12 @@ short network_distributed_server_machines(
|
||||
connection = network_game_server_get_client_connection(machine);
|
||||
if (!connection || !network_connection_active(connection))
|
||||
continue;
|
||||
network_game_server_get_client_machine(server, machine, &game_machine_index);
|
||||
/* (port: and not a machine without a slot) */
|
||||
if (!network_game_server_get_client_machine(server, machine, &game_machine_index) ||
|
||||
game_machine_index == NONE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
machine_indices[count++] = game_machine_index;
|
||||
}
|
||||
return count;
|
||||
@@ -1292,15 +1313,19 @@ boolean network_game_server_handle_client_message(
|
||||
}
|
||||
break;
|
||||
|
||||
/* (port: its own handler, which decodes it as the
|
||||
postgame message it is: the pregame's, which this
|
||||
called, took none out of the pregame, nor could decode
|
||||
one) */
|
||||
case _message_client_graceful_game_exit_postgame:
|
||||
result = network_game_server_handle_message_client_graceful_game_exit_pregame(
|
||||
result = network_game_server_handle_message_client_graceful_game_exit_postgame(
|
||||
server,
|
||||
machine,
|
||||
message,
|
||||
message_buffer_size);
|
||||
if (!result)
|
||||
{
|
||||
network_event("network_game_server_handle_message_client_graceful_game_exit_pregame() failed");
|
||||
network_event("network_game_server_handle_message_client_graceful_game_exit_postgame() failed");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1328,7 +1353,8 @@ boolean network_game_server_handle_client_message(
|
||||
long machine_index;
|
||||
|
||||
network_game_server_get_client_machine(server, machine, &machine_index);
|
||||
network_distributed_handle_message(machine_index, message, message_buffer_size);
|
||||
/* (port: from its stream, which only it can send on) */
|
||||
network_distributed_handle_stream_message(machine_index, message, message_buffer_size);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1726,12 +1752,44 @@ static boolean network_game_server_handle_message_client_ping(
|
||||
struct message_server_pong pong;
|
||||
struct network_message *reply;
|
||||
boolean result = FALSE;
|
||||
/* port: an address answered no more often than PING_REPLY_INTERVAL, and
|
||||
no more than MAXIMUM_PING_REPLY_ADDRESSES in that time (the answer goes
|
||||
to the port the ping names, at the address it came from, which anyone
|
||||
can send one as: a flood of pings would be a flood of answers, at
|
||||
another's machine). A searching client pings once a second */
|
||||
static struct
|
||||
{
|
||||
unsigned long address;
|
||||
unsigned long time;
|
||||
} replied[MAXIMUM_PING_REPLY_ADDRESSES];
|
||||
unsigned long now = system_milliseconds();
|
||||
short index;
|
||||
short slot = NONE;
|
||||
|
||||
match_assert(
|
||||
"c:\\halo\\SOURCE\\networking\\network_server_message_handler.c",
|
||||
0x26A,
|
||||
server && source_address && client_message);
|
||||
|
||||
for (index = 0; index < MAXIMUM_PING_REPLY_ADDRESSES; index++)
|
||||
{
|
||||
boolean recent = replied[index].address && now - replied[index].time < PING_REPLY_INTERVAL;
|
||||
|
||||
if (replied[index].address == source_address->address.long_words[0])
|
||||
{
|
||||
if (recent)
|
||||
return TRUE;
|
||||
slot = index;
|
||||
break;
|
||||
}
|
||||
if (!recent && slot == NONE)
|
||||
slot = index;
|
||||
}
|
||||
if (slot == NONE)
|
||||
return TRUE;
|
||||
replied[slot].address = source_address->address.long_words[0];
|
||||
replied[slot].time = now;
|
||||
|
||||
pong.timestamp = client_message->timestamp;
|
||||
reply = create_network_game_message(
|
||||
_message_server_pong,
|
||||
@@ -1858,7 +1916,7 @@ static boolean network_game_server_handle_message_client_join_game_request(
|
||||
/* port: a machine joining the game in progress is named
|
||||
by its join (its settings request, which names a machine
|
||||
in the lobby, is refused in game) */
|
||||
if (late_join)
|
||||
if (late_join && client_machine)
|
||||
csmemcpy(client_machine->name, machine_name, sizeof(client_machine->name));
|
||||
|
||||
acceptance.machine_index = (short)machine_index;
|
||||
@@ -2404,9 +2462,10 @@ static boolean network_game_server_handle_message_client_graceful_game_exit_preg
|
||||
&packet_version,
|
||||
_network_game_packet_class_client_pregame))
|
||||
{
|
||||
if (network_game_server_remove_machine_from_game(
|
||||
server,
|
||||
network_game_server_get_client_machine(server, client_machine, NULL)))
|
||||
struct network_machine *machine = network_game_server_get_client_machine(server, client_machine, NULL);
|
||||
|
||||
/* (port: none for a machine without a slot) */
|
||||
if (machine && network_game_server_remove_machine_from_game(server, machine))
|
||||
{
|
||||
/* (not to machines loading the game) */
|
||||
if (network_game_server_lobby_is_open(server) &&
|
||||
@@ -2705,3 +2764,48 @@ static boolean network_game_server_handle_message_client_switch_to_pregame(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* port: a machine leaving the game's scores, as one leaving the lobby does
|
||||
(network_game_server_handle_message_client_graceful_game_exit_pregame) */
|
||||
static boolean network_game_server_handle_message_client_graceful_game_exit_postgame(
|
||||
struct network_game_server *server,
|
||||
struct network_game_server_client_machine *client_machine,
|
||||
word *message,
|
||||
short message_size)
|
||||
{
|
||||
if (network_game_server_get_state(server, NULL) == _network_game_server_state_postgame)
|
||||
{
|
||||
struct message_client_graceful_game_exit_postgame graceful_game_exit;
|
||||
short packet_type = _message_client_graceful_game_exit_postgame;
|
||||
short packet_version = NETWORK_GAME_MESSAGE_VERSION;
|
||||
|
||||
message_size -= sizeof(word);
|
||||
if (decode_network_game_message(
|
||||
&graceful_game_exit,
|
||||
message + 1,
|
||||
&message_size,
|
||||
&packet_type,
|
||||
&packet_version,
|
||||
_network_game_packet_class_client_postgame))
|
||||
{
|
||||
struct network_machine *machine = network_game_server_get_client_machine(server, client_machine, NULL);
|
||||
|
||||
if (!machine || !network_game_server_remove_machine_from_game(server, machine))
|
||||
{
|
||||
network_event(
|
||||
"network_game_server_remove_machine_from_game() failed in network_game_server_handle_message_client_graceful_game_exit_postgame()");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
network_event("server failed to decode a message_client_graceful_game_exit_postgame packet");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
network_event(
|
||||
"failed to handle a message_client_graceful_game_exit_postgame message because the server is not in post-game");
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
@@ -2654,6 +2654,20 @@ void damage_replay_kill(
|
||||
distributed_damage_authorized = FALSE;
|
||||
}
|
||||
|
||||
/* ... damage to scenery or a device, dealt as the host dealt it: the copy
|
||||
breaks, plays its effects and is destroyed as the host's was */
|
||||
void damage_replay_static(
|
||||
long object_index,
|
||||
struct damage_data *damage,
|
||||
short node_index,
|
||||
short region_index,
|
||||
short material_index)
|
||||
{
|
||||
distributed_damage_authorized = TRUE;
|
||||
object_cause_damage(damage, object_index, node_index, region_index, material_index, NULL);
|
||||
distributed_damage_authorized = FALSE;
|
||||
}
|
||||
|
||||
/* the host's: what damage_set_network_state takes */
|
||||
void damage_get_network_state(
|
||||
long object_index,
|
||||
|
||||
+195
-51
@@ -496,6 +496,31 @@ boolean debug_lights;
|
||||
boolean debug_object_lights;
|
||||
struct data_array *light_data;
|
||||
struct cluster_partition light_cluster_partition;
|
||||
|
||||
/* port: the clusters' lights kept for lighting objects (lights_port_cluster):
|
||||
as many as the light partition's lists can hold */
|
||||
enum
|
||||
{
|
||||
LIGHTS_PORT_MAXIMUM_ENTRIES = HALO_PORT_MAXIMUM_CLUSTER_REFERENCES,
|
||||
};
|
||||
struct lights_port_entry
|
||||
{
|
||||
long light_index;
|
||||
real_point3d position;
|
||||
real radius;
|
||||
};
|
||||
static struct
|
||||
{
|
||||
/* each light's radius or rasterizer light written (lights_preprocess_scene) */
|
||||
unsigned long radius_writes;
|
||||
/* what the entries were kept for; each cluster's, and where they are */
|
||||
unsigned long generation;
|
||||
short entry_count;
|
||||
unsigned long cluster_generations[MAXIMUM_CLUSTERS_PER_STRUCTURE];
|
||||
short cluster_first_entries[MAXIMUM_CLUSTERS_PER_STRUCTURE];
|
||||
short cluster_entry_counts[MAXIMUM_CLUSTERS_PER_STRUCTURE];
|
||||
struct lights_port_entry entries[LIGHTS_PORT_MAXIMUM_ENTRIES];
|
||||
} lights_port;
|
||||
struct lights_game_globals *lights_game_globals = NULL;
|
||||
short debug_rasterizer_light_count;
|
||||
struct lights_globals lights_globals;
|
||||
@@ -812,6 +837,7 @@ void lights_preprocess_scene(
|
||||
_point_light_attached_to_first_person_weapon_bit,
|
||||
FALSE);
|
||||
light->rasterizer_light_index = NONE;
|
||||
lights_port.radius_writes++;
|
||||
if (light->parent_light_index != NONE)
|
||||
{
|
||||
struct point_light_definition *definition = light_definition_get(
|
||||
@@ -967,6 +993,7 @@ void lights_preprocess_scene(
|
||||
light->radius = (definition->radius_modifier_lower_bound * inverse_intensity
|
||||
+ definition->radius_modifier_upper_bound * intensity)
|
||||
* definition->radius;
|
||||
lights_port.radius_writes++;
|
||||
if (light->radius != 0.0f)
|
||||
{
|
||||
struct rasterizer_light_submit_parameters light_parameters;
|
||||
@@ -1035,6 +1062,7 @@ void lights_preprocess_scene(
|
||||
else
|
||||
{
|
||||
light->radius = definition->radius;
|
||||
lights_port.radius_writes++;
|
||||
}
|
||||
|
||||
if (definition->lens_flare.index != NONE)
|
||||
@@ -1796,6 +1824,136 @@ void light_reconnect_to_map(
|
||||
return;
|
||||
}
|
||||
|
||||
/* a light of a cluster, for find_point_lights_for_object_in_cluster: as
|
||||
the Xbox's loop does each, marking it (so its other clusters pass it over) */
|
||||
static void find_point_light_for_object(
|
||||
long object_index,
|
||||
long light_index,
|
||||
real_point3d const *center,
|
||||
real radius,
|
||||
long *light_indices,
|
||||
real *light_intensities,
|
||||
real *light_attenuations,
|
||||
short *light_count,
|
||||
short maximum_light_count)
|
||||
{
|
||||
if (light_unmarked(light_index))
|
||||
{
|
||||
struct light_datum *light = light_get(light_index);
|
||||
|
||||
if (light->rasterizer_light_index != NONE
|
||||
&& (light->object_index != object_index
|
||||
|| !TEST_FLAG(light_definition_get(light->definition_index)->flags,
|
||||
_light_definition_dont_light_own_object_bit)))
|
||||
{
|
||||
real distance = distance3d(&light->position, center);
|
||||
|
||||
if (distance < radius + light->radius)
|
||||
{
|
||||
real attenuation = light_attenuation(light->radius, distance);
|
||||
real intensity = real_rgb_color_brightness(&light->color) * attenuation;
|
||||
short index;
|
||||
|
||||
if (*light_count < maximum_light_count)
|
||||
{
|
||||
index = (*light_count)++;
|
||||
}
|
||||
else
|
||||
{
|
||||
real minimum_intensity = REAL_MAX;
|
||||
short dimmest_index = NONE;
|
||||
|
||||
for (index = 0; index < *light_count; index++)
|
||||
{
|
||||
if (minimum_intensity > light_intensities[index])
|
||||
{
|
||||
minimum_intensity = light_intensities[index];
|
||||
dimmest_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
if (minimum_intensity < intensity)
|
||||
{
|
||||
index = dimmest_index;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < maximum_light_count)
|
||||
{
|
||||
light_indices[index] = light_index;
|
||||
light_intensities[index] = intensity;
|
||||
light_attenuations[index] = attenuation;
|
||||
}
|
||||
}
|
||||
}
|
||||
light_mark(light_index);
|
||||
}
|
||||
}
|
||||
|
||||
/* port: a cluster's lights in its list's order, with where each is and how
|
||||
far it reaches, kept while none of that changes: while the light partition's
|
||||
lists (cluster_partitions.c's port_modification_count) and the lights' radii
|
||||
and rasterizer lights (lights_port.radius_writes, written as a frame's lights
|
||||
are prepared) are as they were; a light only moves as it joins its clusters
|
||||
again. An object lit (each object drawn, each shadow) then passes over the
|
||||
lights too far to reach it without their datums or the list's: with hundreds
|
||||
of lights in a cluster (network co-op's extra enemies), reading them was
|
||||
most of a frame's lighting. FALSE: not kept (the list walked as before). */
|
||||
static boolean lights_port_cluster(
|
||||
short cluster_index,
|
||||
short *first_entry,
|
||||
short *entry_count)
|
||||
{
|
||||
unsigned long generation = 1 + light_cluster_partition.port_modification_count + lights_port.radius_writes;
|
||||
long reference_index;
|
||||
long light_index;
|
||||
short first;
|
||||
|
||||
if (cluster_index < 0 || cluster_index >= MAXIMUM_CLUSTERS_PER_STRUCTURE)
|
||||
return FALSE;
|
||||
if (lights_port.generation != generation)
|
||||
{
|
||||
lights_port.generation = generation;
|
||||
lights_port.entry_count = 0;
|
||||
}
|
||||
if (lights_port.cluster_generations[cluster_index] != generation)
|
||||
{
|
||||
first = lights_port.entry_count;
|
||||
for (light_index = cluster_partition_get_first_datum(&light_cluster_partition, &reference_index, cluster_index);
|
||||
light_index != NONE;
|
||||
light_index = cluster_partition_get_next_datum(&light_cluster_partition, &reference_index))
|
||||
{
|
||||
struct light_datum *light = light_get(light_index);
|
||||
struct lights_port_entry *entry;
|
||||
|
||||
/* (one not drawn this frame lights nothing: reaching an object,
|
||||
it would only be marked, for that object's other clusters to pass
|
||||
over) */
|
||||
if (light->rasterizer_light_index == NONE)
|
||||
continue;
|
||||
if (lights_port.entry_count >= LIGHTS_PORT_MAXIMUM_ENTRIES)
|
||||
{
|
||||
/* (more than are kept: this cluster's walked) */
|
||||
lights_port.entry_count = first;
|
||||
first = NONE;
|
||||
break;
|
||||
}
|
||||
entry = &lights_port.entries[lights_port.entry_count++];
|
||||
entry->light_index = light_index;
|
||||
entry->position = light->position;
|
||||
entry->radius = light->radius;
|
||||
}
|
||||
lights_port.cluster_generations[cluster_index] = generation;
|
||||
lights_port.cluster_first_entries[cluster_index] = first;
|
||||
lights_port.cluster_entry_counts[cluster_index] = first == NONE ? 0 : (short)(lights_port.entry_count - first);
|
||||
}
|
||||
if (lights_port.cluster_first_entries[cluster_index] == NONE)
|
||||
return FALSE;
|
||||
*first_entry = lights_port.cluster_first_entries[cluster_index];
|
||||
*entry_count = lights_port.cluster_entry_counts[cluster_index];
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void find_point_lights_for_object_in_cluster(
|
||||
long object_index,
|
||||
short cluster_index,
|
||||
@@ -1809,66 +1967,52 @@ static void find_point_lights_for_object_in_cluster(
|
||||
{
|
||||
long light_index;
|
||||
long reference_index;
|
||||
short first_entry, entry_count;
|
||||
|
||||
match_assert(
|
||||
"c:\\halo\\SOURCE\\objects\\object_lights.c",
|
||||
0x544,
|
||||
lights_globals.marker_initialized);
|
||||
/* port: the cluster's lights as kept (lights_port_cluster). A light too
|
||||
far to reach the object is passed over: the Xbox's loop tested it the
|
||||
same, on the same position and radius, and rejected it. Left unmarked, it
|
||||
is tested again in the object's other clusters, and rejected again.
|
||||
Most are far along one axis, which says so without the distance: a
|
||||
distance is never less than one axis of it but for rounding, a few parts
|
||||
in 2^24, well inside the margin here. */
|
||||
if (lights_port_cluster(cluster_index, &first_entry, &entry_count))
|
||||
{
|
||||
short entry_index;
|
||||
|
||||
for (entry_index = first_entry; entry_index < first_entry + entry_count; entry_index++)
|
||||
{
|
||||
struct lights_port_entry const *entry = &lights_port.entries[entry_index];
|
||||
real reach = radius + entry->radius;
|
||||
real bound = reach * 1.000001f;
|
||||
real offset;
|
||||
|
||||
offset = entry->position.x - center->x;
|
||||
if (offset > bound || -offset > bound)
|
||||
continue;
|
||||
offset = entry->position.y - center->y;
|
||||
if (offset > bound || -offset > bound)
|
||||
continue;
|
||||
offset = entry->position.z - center->z;
|
||||
if (offset > bound || -offset > bound)
|
||||
continue;
|
||||
if (!(distance3d(&entry->position, center) < reach))
|
||||
continue;
|
||||
find_point_light_for_object(object_index, entry->light_index, center, radius, light_indices,
|
||||
light_intensities, light_attenuations, light_count, maximum_light_count);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (light_index = cluster_partition_get_first_datum(&light_cluster_partition, &reference_index, cluster_index);
|
||||
light_index != NONE;
|
||||
light_index = cluster_partition_get_next_datum(&light_cluster_partition, &reference_index))
|
||||
{
|
||||
if (light_unmarked(light_index))
|
||||
{
|
||||
struct light_datum *light = light_get(light_index);
|
||||
|
||||
if (light->rasterizer_light_index != NONE
|
||||
&& (light->object_index != object_index
|
||||
|| !TEST_FLAG(light_definition_get(light->definition_index)->flags,
|
||||
_light_definition_dont_light_own_object_bit)))
|
||||
{
|
||||
real distance = distance3d(&light->position, center);
|
||||
|
||||
if (distance < radius + light->radius)
|
||||
{
|
||||
real attenuation = light_attenuation(light->radius, distance);
|
||||
real intensity = real_rgb_color_brightness(&light->color) * attenuation;
|
||||
short index;
|
||||
|
||||
if (*light_count < maximum_light_count)
|
||||
{
|
||||
index = (*light_count)++;
|
||||
}
|
||||
else
|
||||
{
|
||||
real minimum_intensity = REAL_MAX;
|
||||
short dimmest_index = NONE;
|
||||
|
||||
for (index = 0; index < *light_count; index++)
|
||||
{
|
||||
if (minimum_intensity > light_intensities[index])
|
||||
{
|
||||
minimum_intensity = light_intensities[index];
|
||||
dimmest_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
if (minimum_intensity < intensity)
|
||||
{
|
||||
index = dimmest_index;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < maximum_light_count)
|
||||
{
|
||||
light_indices[index] = light_index;
|
||||
light_intensities[index] = intensity;
|
||||
light_attenuations[index] = attenuation;
|
||||
}
|
||||
}
|
||||
}
|
||||
light_mark(light_index);
|
||||
}
|
||||
find_point_light_for_object(object_index, light_index, center, radius, light_indices, light_intensities,
|
||||
light_attenuations, light_count, maximum_light_count);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -31,6 +31,7 @@ OBJECTS.C
|
||||
#include "game/players.h"
|
||||
#include "items/weapons.h"
|
||||
#include "main/console.h"
|
||||
#include "interface/terminal.h"
|
||||
#include "math/periodic_functions.h"
|
||||
#include "memory/memory_pool.h"
|
||||
#include "models/model_animation_definitions.h"
|
||||
@@ -525,6 +526,18 @@ void object_pvs_set_camera_point(
|
||||
return;
|
||||
}
|
||||
|
||||
void objects_port_set_activating_cluster(
|
||||
short cluster_index)
|
||||
{
|
||||
if (cluster_index == NONE || cluster_index >= global_structure_bsp_get()->clusters.count)
|
||||
{
|
||||
object_globals->pvs_activation_type = _pvs_activation_normal;
|
||||
return;
|
||||
}
|
||||
object_globals->pvs_activation_type = _pvs_activation_cluster;
|
||||
object_globals->pvs_activation.cluster_index = cluster_index;
|
||||
}
|
||||
|
||||
void object_pvs_clear(void)
|
||||
{
|
||||
object_globals->pvs_activation_type = _pvs_activation_normal;
|
||||
@@ -4090,7 +4103,10 @@ void objects_garbage_collection(
|
||||
}
|
||||
|
||||
sprintf(tempbuffer, "garbage collection %scritical (%s)", status, warningbuf);
|
||||
console_printf(FALSE, "%s", tempbuffer);
|
||||
/* (port: chatter, shown as config.toml's game.console_log says:
|
||||
many enemies keep it coming, over the whole screen) */
|
||||
if (terminal_shows(_terminal_message_chatter))
|
||||
console_printf(FALSE, "%s", tempbuffer);
|
||||
error(_error_log, "%s", tempbuffer);
|
||||
update_time = TRUE;
|
||||
}
|
||||
@@ -4129,7 +4145,9 @@ void objects_garbage_collection(
|
||||
char tempbuffer[512];
|
||||
|
||||
sprintf(tempbuffer, "removing objects: %s", released_resultbuf);
|
||||
console_printf(FALSE, "%s", tempbuffer);
|
||||
/* (port: chatter, shown as config.toml's game.console_log says) */
|
||||
if (terminal_shows(_terminal_message_chatter))
|
||||
console_printf(FALSE, "%s", tempbuffer);
|
||||
error(_error_log, "%s", tempbuffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -386,6 +386,10 @@ void object_pvs_set_object(long object_index);
|
||||
void object_pvs_set_camera_point(short camera_point_index);
|
||||
void object_pvs_clear(void);
|
||||
short objects_get_activating_cluster_index(void);
|
||||
/* port: makes a cluster active as object_pvs_activate would (NONE: back to
|
||||
the players' own), for a network co-op client following the host's
|
||||
cutscene (port/linux/game/network_coop.c) */
|
||||
void objects_port_set_activating_cluster(short cluster_index);
|
||||
void object_definition_predict(long definition_index);
|
||||
void object_predict(long object_index);
|
||||
void object_beautify(long object_index, boolean beautiful);
|
||||
|
||||
@@ -25,6 +25,7 @@ BREAKABLE_SURFACES.C
|
||||
#include "sound/game_sound.h"
|
||||
#include "structures/structure_bsp_definitions.h"
|
||||
#include "tag_files/tag_groups.h"
|
||||
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||
@@ -37,6 +38,21 @@ struct breakable_surface_globals
|
||||
|
||||
/* ---------- prototypes */
|
||||
|
||||
/* port: cache_files.c's */
|
||||
boolean tag_index_is_group(long tag_index, long group_tag);
|
||||
|
||||
/* port: a networked break carries no damage effect (the host already dealt
|
||||
it). Tag 0 is the scenario, and reading it as a damage effect stops the game */
|
||||
static struct damage_breaking_effect_definition const *breakable_surface_breaking_effect(
|
||||
struct damage_data const *damage_data)
|
||||
{
|
||||
static struct damage_breaking_effect_definition const none;
|
||||
|
||||
if (damage_data && tag_index_is_group(damage_data->definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
|
||||
return &damage_effect_definition_get(damage_data->definition_index)->breaking_effect;
|
||||
return &none;
|
||||
}
|
||||
|
||||
/* January evaluates the j/k terms as one group in this translation unit.
|
||||
The shared helper must remain flat for its other exact consumers. */
|
||||
static __inline real breakable_surface_plane_distance(
|
||||
@@ -160,7 +176,7 @@ void breakable_surface_damage(
|
||||
if (globals->enabled)
|
||||
{
|
||||
if (breakable_surface_index != NONE &&
|
||||
damage_data->definition_index != NONE &&
|
||||
tag_index_is_group(damage_data->definition_index, DAMAGE_EFFECT_DEFINITION_TAG) &&
|
||||
damage_data->material_type != NONE)
|
||||
{
|
||||
struct breakable_surface_datum *surface = breakable_surface_get(breakable_surface_index);
|
||||
@@ -189,6 +205,8 @@ void breakable_surface_damage(
|
||||
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
|
||||
|
||||
breakable_surface_effect(breakable_surface_index, damage_data, seed_surface_index);
|
||||
/* port: and on network co-op's clients */
|
||||
network_coop_note_surface_broken(breakable_surface_index, &damage_data->epicenter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +222,11 @@ void breakable_surface_damage_area_of_effect(
|
||||
const struct damage_data *damage_data)
|
||||
{
|
||||
struct structure_bsp *structure_bsp = global_structure_bsp_get();
|
||||
struct damage_effect_definition *damage_effect_definition = damage_effect_definition_get(damage_data->definition_index);
|
||||
struct damage_effect_definition *damage_effect_definition;
|
||||
|
||||
if (!tag_index_is_group(damage_data->definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
|
||||
return;
|
||||
damage_effect_definition = damage_effect_definition_get(damage_data->definition_index);
|
||||
|
||||
if (globals->enabled &&
|
||||
(damage_effect_definition->damage.damage_lower_bound != 0.0f || damage_effect_definition->damage.damage_upper_bound != 0.0f))
|
||||
@@ -236,6 +258,8 @@ void breakable_surface_damage_area_of_effect(
|
||||
breakable_surface_get(breakable_surface_index)->vitality = 0.0f;
|
||||
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
|
||||
breakable_surface_effect(breakable_surface_index, damage_data, breakable_surface->collision_surface_index);
|
||||
/* port: and on network co-op's clients */
|
||||
network_coop_note_surface_broken(breakable_surface_index, &damage_data->epicenter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,6 +268,32 @@ void breakable_surface_damage_area_of_effect(
|
||||
return;
|
||||
}
|
||||
|
||||
void breakable_surface_port_break(
|
||||
short breakable_surface_index,
|
||||
real_point3d const *epicenter)
|
||||
{
|
||||
struct structure_bsp *structure_bsp = global_structure_bsp_get();
|
||||
struct structure_breakable_surface *breakable_surface;
|
||||
struct damage_data damage;
|
||||
|
||||
if (breakable_surface_index < 0 || breakable_surface_index >= structure_bsp->breakable_surfaces.count ||
|
||||
!breakable_surface_extant(breakable_surface_index))
|
||||
{
|
||||
return;
|
||||
}
|
||||
breakable_surface = TAG_BLOCK_GET_ELEMENT(&structure_bsp->breakable_surfaces, breakable_surface_index,
|
||||
struct structure_breakable_surface);
|
||||
/* (the shards fly away from the epicenter, all the effect reads) */
|
||||
csmemset(&damage, 0, sizeof(damage));
|
||||
damage.epicenter = *epicenter;
|
||||
/* (and where it is, which the break's sound plays in: zeros were leaf
|
||||
and cluster 0) */
|
||||
scenario_location_from_point(&damage.location, epicenter);
|
||||
breakable_surface_get(breakable_surface_index)->vitality = 0.0f;
|
||||
BIT_VECTOR_SET_FLAG((long *)breakable_surface_flags_get(), breakable_surface_index, FALSE);
|
||||
breakable_surface_effect(breakable_surface_index, &damage, breakable_surface->collision_surface_index);
|
||||
}
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static void breakable_surface_effect(
|
||||
@@ -473,7 +523,7 @@ static void breakable_surface_effect(
|
||||
struct new_particle_data particle;
|
||||
|
||||
velocity = *global_zero_vector3d;
|
||||
breaking_effect = &damage_effect_definition_get(damage_data->definition_index)->breaking_effect;
|
||||
breaking_effect = breakable_surface_breaking_effect(damage_data);
|
||||
vector_from_points3d(&damage_data->epicenter, &position, &outward_vector);
|
||||
distance = normalize3d(&outward_vector);
|
||||
|
||||
|
||||
@@ -74,6 +74,12 @@ struct breakable_surface_particle_effect
|
||||
|
||||
struct breakable_surface_datum *breakable_surface_get(
|
||||
short breakable_surface_index);
|
||||
/* port: breaks a surface as damage at `epicenter` would, shards and all,
|
||||
for a network co-op client the host told (port/linux/game/network_coop.c);
|
||||
nothing if it is already broken */
|
||||
void breakable_surface_port_break(
|
||||
short breakable_surface_index,
|
||||
real_point3d const *epicenter);
|
||||
void breakable_surfaces_initialize(
|
||||
void);
|
||||
void breakable_surfaces_dispose(
|
||||
|
||||
@@ -1687,6 +1687,10 @@ static void object_get_features_in_sphere(
|
||||
|
||||
if ((!TEST_FLAG(flags, _collision_test_skip_passthrough_bipeds_bit) ||
|
||||
!TEST_FLAG(biped->biped.flags, _biped_movement_passes_through_bipeds_bit)) &&
|
||||
/* port: (nor a player's, to a player in a co-op
|
||||
game without player collisions) */
|
||||
(!TEST_FLAG(flags, _collision_test_skip_player_bipeds_bit) ||
|
||||
biped->unit.player_index == NONE) &&
|
||||
(object->object.parent_object_index == NONE ||
|
||||
biped->unit.parent_seat_index == NONE))
|
||||
{
|
||||
|
||||
@@ -51,6 +51,9 @@ enum
|
||||
_collision_test_try_to_keep_location_valid_bit,
|
||||
_collision_test_skip_passthrough_bipeds_bit,
|
||||
_collision_test_use_vehicle_physics_bit,
|
||||
/* port: no players' bipeds (a player's own movement in a co-op game
|
||||
without player collisions: network_coop_player_collisions) */
|
||||
_collision_test_skip_player_bipeds_bit,
|
||||
|
||||
NUMBER_OF_COLLISION_TEST_FLAGS,
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ enum
|
||||
|
||||
enum
|
||||
{
|
||||
RASTERIZER_MEMORY_POOL_SIZE = 0x18000,
|
||||
/* port: the Xbox pool (0x18000) held a frame of 1024 particles. The
|
||||
native particle pool is larger, and a full co-op Library spends the Xbox
|
||||
pool before the frame is drawn */
|
||||
RASTERIZER_MEMORY_POOL_SIZE = 0x18000 * (HALO_PORT_MAXIMUM_PARTICLES / 1024),
|
||||
RASTERIZER_MAXIMUM_TRIANGLES_PER_TRIANGLE_BUFFER = 24576,
|
||||
RASTERIZER_MAXIMUM_DEBUG_PRIMITIVES = 131072,
|
||||
RASTERIZER_MAXIMUM_DEBUG_VERTICES = 393216,
|
||||
|
||||
@@ -98,6 +98,7 @@ symbols in this file:
|
||||
#include "rasterizer_console_vars.h"
|
||||
#include "objects/widgets/widget_types.h"
|
||||
#include "main/main.h"
|
||||
#include "halo_vr.h" /* port: VR's stereo of the flares */
|
||||
#include <xtl.h>
|
||||
#include "rasterizer/xbox/rasterizer_xbox.h"
|
||||
#include "main/main.h"
|
||||
@@ -869,6 +870,9 @@ void rasterizer_lens_flares_draw(
|
||||
{
|
||||
short lens_flare_index;
|
||||
|
||||
/* port: in VR's both eyes at once, each eye's flare where its depth
|
||||
puts it (port/linux/src/vr.c) */
|
||||
halo_vr_multiview_lens_flares(TRUE);
|
||||
rasterizer_widget_begin(_widget_type_internal_sprite, 0);
|
||||
|
||||
for (lens_flare_index = 0; lens_flare_index < local_lens_flare_count; lens_flare_index++)
|
||||
@@ -1123,6 +1127,7 @@ void rasterizer_lens_flares_draw(
|
||||
|
||||
rasterizer_set_stencil_mode(RASTERIZER_STENCIL_MODE_NONE);
|
||||
rasterizer_widget_end();
|
||||
halo_vr_multiview_lens_flares(FALSE);
|
||||
|
||||
if (rasterizer_debug_options.lens_flare_sun_glow_enabled)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,9 @@ RASTERIZER_MEMORY_POOL.C
|
||||
/* swapped? */
|
||||
static void *global_memory_pool;
|
||||
static long global_memory_index;
|
||||
/* port: whether the pool ran out, logged once (a frame that runs out
|
||||
usually means every frame does) */
|
||||
static boolean pool_exceeded_logged;
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
@@ -57,9 +60,10 @@ void *rasterizer_memory_alloc(
|
||||
memcpy(pointer, src, size);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!pool_exceeded_logged)
|
||||
{
|
||||
error(_error_silent, "### ERROR rasterizer memory pool exceeded");
|
||||
pool_exceeded_logged = TRUE;
|
||||
error(_error_silent, "### ERROR rasterizer memory pool exceeded (not logged again)");
|
||||
}
|
||||
|
||||
return pointer;
|
||||
|
||||
@@ -176,6 +176,10 @@ typedef char verify_rasterizer_shadows_shader_model_animation_offset[
|
||||
|
||||
static void rasterizer_shadow_convolve(
|
||||
void);
|
||||
static void rasterizer_shadow_convolve_pass(
|
||||
short source_target,
|
||||
short destination_target,
|
||||
real offset);
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
@@ -850,6 +854,9 @@ static void rasterizer_shadow_convolve(
|
||||
{
|
||||
short stage;
|
||||
real vertex_constants[8][4];
|
||||
/* port: the shadow maps' texels for each of the Xbox's 128 */
|
||||
long scale = halo_shadow_map_scale();
|
||||
long distance;
|
||||
|
||||
match_assert(
|
||||
"c:\\halo\\SOURCE\\rasterizer\\xbox\\rasterizer_xbox_shadows.c",
|
||||
@@ -953,6 +960,14 @@ static void rasterizer_shadow_convolve(
|
||||
vertex_constants[7][1] = 1.0f;
|
||||
vertex_constants[7][2] = 0.0f;
|
||||
vertex_constants[7][3] = -0.00390625f;
|
||||
/* port: half a texel of a larger map (rasterizer_shadow_convolve_pass) */
|
||||
if (scale > 1)
|
||||
{
|
||||
for (distance = 0; distance < 8; distance++)
|
||||
{
|
||||
vertex_constants[distance][3] /= (real)scale;
|
||||
}
|
||||
}
|
||||
IDirect3DDevice8_SetVertexShaderConstant(
|
||||
global_d3d_device,
|
||||
-81,
|
||||
@@ -997,7 +1012,78 @@ static void rasterizer_shadow_convolve(
|
||||
-1.0078125f,
|
||||
-0.9921875f);
|
||||
IDirect3DDevice8_End(global_d3d_device);
|
||||
|
||||
/* port: the passes that widen the blur of a larger map, back and
|
||||
forth between the maps, ending in the secondary */
|
||||
for (distance = 1; distance < scale; distance *= 2)
|
||||
{
|
||||
rasterizer_shadow_convolve_pass(3, 2, (real)distance / (real)(128 * scale));
|
||||
rasterizer_shadow_convolve_pass(2, 3, (real)distance / (real)(128 * scale));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* port: the shadow maps can be drawn larger than the Xbox's 128x128
|
||||
(display.shadow_resolution, port/linux/src/d3d8_gl.c): scale times as many
|
||||
texels each way, a power of two. The Xbox's blur takes four taps diagonally
|
||||
half a texel off each texel, on the corners it shares with its neighbours.
|
||||
Each tap averages the four texels there, and together they weigh the texel
|
||||
and its neighbours 1, 2, 1 each way: a tent over three texels, which is a box
|
||||
two texels wide taken twice. For the shadows to stay as soft, a larger map's
|
||||
blur must cover as much of the map: a tent scale times as wide, a box of
|
||||
2 * scale texels taken twice, which weighs the texels 2 * scale - |d| for |d|
|
||||
up to 2 * scale - 1 texels away. The Xbox's pass, with its taps half a texel
|
||||
of the larger map off, is the box of two texels taken twice. A further pass
|
||||
with its taps diagonally distance texels off, on texels' centres, averages
|
||||
texels 2 * distance apart each way, which doubles a box of 2 * distance
|
||||
texels; two passes each at distance 1, 2, ... scale / 2 make both boxes
|
||||
2 * scale texels wide. The quad's offsets of half a texel stay as they are:
|
||||
they are half of the game's units, which the port undoes in the same units
|
||||
at any size (port/linux/src/nv2a_vsh.c), so the quad covers the map. */
|
||||
static void rasterizer_shadow_convolve_pass(
|
||||
short source_target,
|
||||
short destination_target,
|
||||
real offset)
|
||||
{
|
||||
short stage;
|
||||
real vertex_constants[8][4];
|
||||
|
||||
csmemset(vertex_constants, 0, sizeof(vertex_constants));
|
||||
for (stage = 0; stage < 4; stage++)
|
||||
{
|
||||
rasterizer_set_target_as_texture(stage, source_target, FALSE);
|
||||
vertex_constants[stage * 2][0] = 1.0f;
|
||||
vertex_constants[stage * 2 + 1][1] = 1.0f;
|
||||
}
|
||||
/* the Xbox pass's corners, in its order */
|
||||
vertex_constants[0][3] = -offset;
|
||||
vertex_constants[1][3] = -offset;
|
||||
vertex_constants[2][3] = offset;
|
||||
vertex_constants[3][3] = offset;
|
||||
vertex_constants[4][3] = -offset;
|
||||
vertex_constants[5][3] = offset;
|
||||
vertex_constants[6][3] = offset;
|
||||
vertex_constants[7][3] = -offset;
|
||||
IDirect3DDevice8_SetVertexShaderConstant(
|
||||
global_d3d_device,
|
||||
-81,
|
||||
vertex_constants,
|
||||
8);
|
||||
|
||||
rasterizer_set_target(destination_target, 0, 0, FALSE, FALSE);
|
||||
|
||||
IDirect3DDevice8_Begin(global_d3d_device, D3DPT_TRIANGLEFAN);
|
||||
IDirect3DDevice8_SetVertexData2s(global_d3d_device, 4, 0, 0);
|
||||
IDirect3DDevice8_SetVertexData2f(global_d3d_device, 0, -1.0078125f, 1.0078125f);
|
||||
IDirect3DDevice8_SetVertexData2s(global_d3d_device, 4, 1, 0);
|
||||
IDirect3DDevice8_SetVertexData2f(global_d3d_device, 0, 0.9921875f, 1.0078125f);
|
||||
IDirect3DDevice8_SetVertexData2s(global_d3d_device, 4, 1, 1);
|
||||
IDirect3DDevice8_SetVertexData2f(global_d3d_device, 0, 0.9921875f, -0.9921875f);
|
||||
IDirect3DDevice8_SetVertexData2s(global_d3d_device, 4, 0, 1);
|
||||
IDirect3DDevice8_SetVertexData2f(global_d3d_device, 0, -1.0078125f, -0.9921875f);
|
||||
IDirect3DDevice8_End(global_d3d_device);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -181,6 +181,18 @@ boolean rasterizer_vertex_shaders_initialize(
|
||||
if (!success)
|
||||
error(2, "### ERROR rasterizer_vertex_shaders_initialize failed");
|
||||
|
||||
/* port: the model shader's lighting programs, whose draws can be lit for
|
||||
each pixel (d3d8_gl.c): rasterizer_set_vertex_shader_permutation's for
|
||||
models, 17 (planar fog), 10 (point lights), 9 (reflection) and 27
|
||||
(reflection, one node) */
|
||||
if (success)
|
||||
{
|
||||
halo_vertex_shader_lighting(vertex_shader_table[9].handle);
|
||||
halo_vertex_shader_lighting(vertex_shader_table[10].handle);
|
||||
halo_vertex_shader_lighting(vertex_shader_table[17].handle);
|
||||
halo_vertex_shader_lighting(vertex_shader_table[27].handle);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -442,12 +442,23 @@ static void render_window(
|
||||
#ifdef HALO_ANDROID
|
||||
if (render_vr_eye_pass)
|
||||
{
|
||||
/* the HUD and the menus go on the overlay (render_vr_overlay) */
|
||||
/* the HUD and the menus go on the overlay (render_vr_overlay);
|
||||
no antialiasing pass on the eyes' pictures */
|
||||
rasterizer_screen_flash();
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
/* port: the 3D view antialiased (display.anti_aliasing), before the
|
||||
HUD and menus are drawn over it */
|
||||
if (rasterizer_target == _render_target_primary)
|
||||
{
|
||||
halo_screen_anti_alias(
|
||||
rasterizer_camera->viewport_bounds.x0,
|
||||
rasterizer_camera->viewport_bounds.y0,
|
||||
rasterizer_camera->viewport_bounds.x1,
|
||||
rasterizer_camera->viewport_bounds.y1);
|
||||
}
|
||||
interface_draw_screen();
|
||||
rasterizer_screen_flash();
|
||||
halo_screen_ui_offset(TRUE);
|
||||
|
||||
@@ -120,6 +120,7 @@ symbols in this file:
|
||||
#include "data.h"
|
||||
#include "lruv_cache.h"
|
||||
#include "memory_pool.h"
|
||||
#include "cluster_partitions.h"
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
@@ -180,6 +181,9 @@ static game_state_before_save_proc before_save_procs[] =
|
||||
static game_state_before_load_proc before_load_procs[] =
|
||||
{
|
||||
game_sound_clear,
|
||||
/* port: where the cluster lists' references are, which the game state
|
||||
being loaded does not hold */
|
||||
cluster_partitions_port_forget,
|
||||
};
|
||||
|
||||
static game_state_after_load_proc after_load_procs[] =
|
||||
|
||||
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user