Track the Android Gradle build scripts excluded by the broad build ignore rule. Bump the Android version code and identify the Quest release in the version name.
Ship normal menu startup with cheats, test-map loading, weapon grants, and diagnostics disabled. Preserve and document the dynamic-lighting workaround in the external config.
The group is hidden now. The off hand's X brings it up for vr_hudTime seconds
and then it fades over 600ms. Almost everything it carries - how much ammo is
left, which way the objective is - is only wanted at the moment it is asked
for, and one that is always there is always in the way of a game about looking
at things.
The press is a timestamp rather than a toggle. The VR layer writes
Sys_Milliseconds into vr_hudShow on the button's down edge and cgame reads it
against cgi.Milliseconds, which is the same function, so there is no state to
latch and no edge to clear on the far side - and a second press during the fade
simply restarts it. Only the down edge counts: holding the button should not
keep restarting the timer, and should not hold the group open either, because a
button held down is a button the player has forgotten about. vr_hud 2 pins it
up, which is what to use while arguing with the layout.
X used to jump, and jump was also on the right hand's A, so it was a button
spent twice. The HUD is worth one of them and jump keeps A. Left Y is the
better candidate and is not used here: it still sends +scores for an objectives
overlay that no longer appears on its own, since the objectives moved into this
group and vr_objectivesAlways forces them on regardless. Moving the HUD there
would cost nothing, and would give X back to jump.
The compass has the two layers it was missing. The damage ring is
stats[STAT_DAMAGEDIR] drawn through compasspain, fading over 1500ms from
whenever the value last changed - it is a headingspinner, which unlike the
objective is the variant that does subtract the view angles, so the direction
arrives in world terms and has to be brought round to the head, and the +180 in
that is because the stat points at the attacker while the ring marks where the
damage came in. The objective is compassobjarrow turned to its angle rather
than the pip that stood in for it while nothing here could draw anything
rotated.
vr_hudGroupY is 0.92, which is 0.63 of the eye. 0.68 is as low as this can
reach at all - vr_hudScale compresses everything towards the middle of the
display, so past that the cvar has nothing left to give and the size has to come
down instead.
And a guard where the font bug actually lives. glGenTextures never legitimately
hands back 0 - it is the reserved name - so a zero means the call did not write,
and nothing downstream notices: Upload32 uploads into texture zero, GL_Bind
binds it, and the surface samples opaque black, which is what every string in
the game looked like. R_CreateImage now asks a second time and says so if it
still comes back empty.
That is a guard and not the cure, because the cause is still unknown. What is
known is that three of the four fonts recover and verdana-14 does not, that it
is the first one created, and that its image is alive and correctly named with
no texture attached - so it was never freed, and R_FreeImage and R_DeleteTextures
both memset, which is how that was ruled out. The trace prints every font atlas
as it is created, with its slot and its texnum, because from the outside an
image that was never created and one created without a texture are
indistinguishable by the time anything draws with it, and telling those apart is
the next step. Until it is found, CG_HudDrawElements still draws its text as
boxes.
The retail HUD is five menus from ui/hud_*.urc, each aligned to a corner of a
4:3 screen. On a monitor those corners are a glance away; through a lens they
are the far edge, present and unreadable. cg_vrhud.cpp draws the same
information again as one group below the centre of the view.
Nothing is read back out of the widgets, because there is nothing in them to
read: every one is a formatter over cl.snap.ps.stats[], thirty-two integers,
and the .urc files are pure layout - hud_health.urc is "playerstat 0,
maxplayerstat 1, draw it as a vertical bar". cgame has the same array as
cg.snap->ps.stats[]. The .urc files were still worth reading, as the spec for
which stat index and which shader each element uses, and the decode of the two
compass angles is taken from the widgets that already do it rather than guessed:
the heading is the eye yaw less stats[STAT_COMPASSNORTH]/182, a sixteen bit
angle, and the objective is stat/10 - 180 and is already relative to the view,
because hud_compass.urc draws it as a plain spinner and not the headingspinner
variant that subtracts the view angles itself.
It is drawn from cgame rather than as another widget for a reason beyond taste.
cgame's 2D goes through the one full screen window UIView3D::set2D sets up, and
that window is converged as a whole. A widget would arrive with its own window,
which is the first bug below.
Convergence: the correction belongs to the display, not to the window.
R_VRAdjust2DOrtho bent whatever ortho window it was handed, and in this engine
that window is usually one widget - UIWidget::set2D cuts a viewport and an ortho
window to each widget's own frame. The correction was therefore scaled to the
widget, and a 150 pixel compass in a 1680 pixel eye got about a twelfth of the
shift it needed while the objectives, drawn through the full screen window, got
all of it. Every element ended up with a disparity in proportion to its own
width, so each sat at a different apparent depth, and no vergence resolves that
set - which is what "the HUD will not converge" was.
It is R_VRAdjust2DRect now, applied to the viewport and scissor rectangle in
eye buffer pixels, which every 2D window shares. The scissor has to move with
it or a widget the shift has displaced is clipped away at where it used to be.
vr_hudOffset goes back to 1 in autoexec.cfg: 0 was set on a measurement of the
old bug, and it is the one value that cannot fuse at all, because the centre of
each eye's buffer is about seven degrees off its own axis in opposite
directions, so it asks the eyes to diverge by fourteen degrees.
Text: the font atlases had no texture.
Every string drew as a flat black rectangle - 0,0,0 with no glyph structure,
measured off an eye dump rather than a compositor screenshot. The atlas is a 32
bit TGA with a real alpha channel and the shader is correct, so neither of the
obvious causes held. The shader's own stage reported texnum 0, and GL_Bind binds
image->texnum straight, so it was binding texture zero, which samples opaque
black.
The image is alive and correctly named, so it was never freed - R_FreeImage and
R_DeleteTextures both memset, and the name survived. texnum was zero from
creation. R_LoadFontShader now notices and reloads, but it has to free the image
first: R_FindImageFileOld looks in the hash before it looks on disk, so asking
by name returns the very image that has no texture. Three of the four fonts come
back with a texture; verdana-14, the first one created, still comes back as zero
on a fresh load from disk, which means qglGenTextures is not writing to it and
the real bug is upstream in image creation. That is not fixed here, and
CG_HudDrawElements still draws its text as boxes because of it.
Also in here: R_DrawTrianglePic added to the cgame imports, which the UI has
always had as re.DrawTrianglePic and is the only way to draw anything rotated,
so the compass card can turn under a fixed needle the way hud_compass.urc does
it. The retail corner HUD is hidden from the "Hide the HUD when necessary" block
in UI_Update rather than from the explicit Display calls above it, because that
branch returns early and is not the path taken - when it is not, UpdateViews
walks the whole tree and draws every shown menu regardless, which is how
hud_health and hud_ammo stayed on screen through the first attempt.
Bars are R_DrawBox rather than a stretched gfx/2d/blank. That shader is declared
alphaGen oneMinusVertex, which inverts the alpha: the bars asked for at 1 came
out at 0 and the backdrop asked for at 0.35 came out at 0.65.
vr_hudGroupY is 0.73 and does not mean what it looks like. It places the cluster
in the flat frame, which is then scaled to vr_hudScale about the middle of the
display and lifted by the eye's vertical frustum asymmetry, 170 pixels of 1760
here - so 0.73 is a little below the middle of what is actually seen and 0.62
was a little above it. The reachable range is about 0.13 to 0.68 of the eye.
Not done: the damage direction ring, the objective as compassobjarrow rather
than a pip, and the compass worth having, which is one the player takes out of a
pocket and holds.
Firing in the Bocage multiplies roughly a sixth of the screen down by about 4x
for as long as the trigger is held. It is the ground plane, with hard polygon
edges, and it is not black - the texture is still there underneath. r_worldDlights
0 in autoexec.cfg stops the world responding to dynamic lights at all, which
loses flashes lighting the scenery and is the smaller loss.
The cause is not found. Seven theories were tried and all seven were wrong, so
what this mostly commits is the evidence, in docs/markdown/04-coding/05-dlights.md
next to the gl4es notes, so that picking it up again does not start from nothing:
the cadence table and the frame difference that measured the 4x, a table of every
dead theory with what killed it, the worked texcoord arithmetic showing the faces
land correctly, and the one hard contradiction to resume on - every component of
the dynamic lightmap path measures correct end to end, and switching the path off
makes the symptom stop.
Two things in there are recorded specifically because they look damning and are
not. "texels 9275" against "rows 28" cannot fit, but R_RealDlightFace counts
c_dlightTexels before it knows whether any light landed and then bails at
!added without committing to dli.allocated, so failed surfaces are counted and
take no space. And R_UploadDlights' guard on tr.pc.c_dlightSurfaces reads like it
must always be zero from the back end, because R_IssueRenderCommands clears the
counters before running the command list - it is not what happens, measured at 35
surfaces. That guard is gone anyway: the height below it is the same test done
against the allocator's own state rather than against a debug counter.
The workaround is its own cvar rather than a level of vr_traceDlight, which is
what it was while it was being tested. A diagnostic switch that also changes
behaviour means turning tracing off silently brings the bug back, and this port
has already lost a day to a cvar that did not do what its name said.
The diagnostics stay, off. 1 reports what each upload contained and where every
dlit face landed in the atlas; 2 stains the whole atlas white and paints the lit
texels magenta, which is read off the walls rather than out of the log; 3 skips
ProjectDlightTexture, which was measured never to run here and is kept because
proving that cost a run. The first stain was dark green and was useless in a
level made of hedgerows - white can only brighten, so it cannot be mistaken for
foliage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stats screen the game raises when the player dies, and again at the end of
a mission, went into both eye buffers as screen space 2D - at the near plane, a
hand's width away, with no disparity to fuse and each eye warping it through its
own asymmetric frustum. That is the same fault 013b2f5b fixed for the mission
briefing, and the fix there did not cover this one for two separate reasons.
cg_view.c raises it with UI_ShowMenu, which sends the widget a showmenu event
directly instead of going through MenuManager. The menu never reaches either
menu stack, so CurrentMenu() does not return it and UI_FullscreenMenuUp() cannot
see it however fullscreen it is.
And it is not fullscreen: statsscreen_failed.urc has its "fullscreen 1" line
commented out. It is a 640x480 overlay the flat game draws over the live view,
so even as the current menu it would still answer no.
So neither test could have caught it, and asking a third question about menus
would have been guessing at the next one. It now asks about intermission, which
is the same condition CG_DrawActiveFrame itself uses to decide to put the screen
up - if the game thinks it is showing a stats screen, the panel is where it
goes. That also covers mission success, the MOHTA mission_* screens and the
multiplayer scoreboard, which are the same kind of content with the same
problem.
Confirmed on the device: the screen is now a quad in the room. Its text renders
as black boxes with a sliver of the font atlas showing through, which is a
separate defect and untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The throw cvars are set here as well as defaulted in code, and that is the point
rather than duplication. They are CVAR_ARCHIVE, so a device that has run before
keeps whatever omconfig.cfg holds and a changed default in the source does
nothing at all. That cost a build: the floor stayed at its old 0.45 while the
curve moved under it, and every throw came out clamped to the same 45% however
hard it was thrown. autoexec runs after omconfig and wins.
Measured values, not guesses - vr_throwFull 340 is a hard overhand, against
gentle lobs at 145 to 192.
Also on: vr_traceUse, vr_traceHolster and vr_traceThrow, and the test harness -
cheats, give all, and m4l1 - because weapon work is still in progress. None of
that should ship; see 6e86e55c for turning it back off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four interactions that all needed the same thing: the hands' positions and
velocities on the other side of the game module boundary. They share the
plumbing, so they share a commit.
use moves to B on the weapon hand, where the reference puts activate. The
objectives list keeps the left hand's button; it had been bound to both, which
was a button spent twice on one rarely opened screen. The trace also starts from
the head's real position now rather than the body's notional eye - the two come
apart exactly when leaning through a doorway to reach the thing on the far side.
Weapons live on the body, the way Half-Life: Alyx does it: pistol at the right
hip, grenades at the left, rifle and smg over the shoulders. Reaching back to
where a weapon came from puts it away, reaching anywhere else swaps - holstering
on any zone would have cost two gestures for every swap. Zones are measured from
the head and rotated by the head's heading, so they stay on the body rather than
wandering as the player turns.
Ladders are climbed by pulling on them. MOHAA moves a climbing player by the
root motion of the climb animation, with pmove skipped entirely, so this runs
after the animation rather than instead of it: the animation still plays and the
distance travelled is the distance the hand pulled. Both grips, averaged when
both are held, and the grips stop meaning reload and rifle-steady while on a
ladder.
Grenades leave along the arm. Which instant of a swing to read is the whole
problem and took several passes: averaging across the release blends the wind up
with the follow through; the peak of the whole swing is usually the *backswing*,
which threw grenades over the player's shoulder. What works is the natural
gesture's own shape - take the direction the hand had as it let go, then find
the fastest sample that agrees with it, which excludes the wind up by
construction. Strength is a fraction of the game's own throw so all of MOHAA's
grenade tuning survives.
The sideways damping is the one deliberate lie. An underhand swing crosses the
body, so the hand really is moving sideways at release - measured, lobs drifted
left by up to 0.73, more sideways than forward. Damped in proportion to how
gentle the throw is, so hard throws keep their true direction.
A weapon wheel is included and is not finished: its input works and a tap of the
same stick holsters, but the ring never appeared on screen and the drawing was
not chased down. It is harmless - a hold selects nothing and does nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two changes to what the first person model is for.
Being unarmed is not a reason to have no hands. Flat, that model exists only to
hold a weapon, so putting the gun away hides the arms with it and nobody
notices; in a headset the player is holding two controllers whether or not there
is anything in them, and hands that vanish read as tracking having dropped out.
No mesh work was needed - triggerhand and lefthand are already separate
surfaces, so the unarmed case shows those two and hides garandhand, a left hand
posed around a rifle that is not there. Zoom, death and cg_drawviewmodel still
hide, being about something else entirely.
The weapon hand's copy now draws triggerhand alone. viewsleeves is a single mesh
carrying *both* forearms - weighted 27% to Bip01 R Forearm and 27% to
Bip01 L Forearm, with both elbows, shoulders and clavicles - so drawing it on a
model anchored to the right controller hung a disembodied left sleeve in the air
beside it. What is left is a whole hand rather than a stump: triggerhand is
weighted to Bip01 R Hand, every R finger bone, and helper Rwrist. The wrist is
in, the forearm is not, and there is no way to get part of one without
re-authoring - the forearm exists only inside the two-armed sleeve.
Those weights were read out of the retail USarmyplyr.skd rather than guessed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Use worked about half the time, and the half it worked was luck.
cg.refdefViewAngles is not only the view. CG_OffsetFirstPersonView overwrites it
with the player model's animated *head bone* orientation on the paths that place
the view from a world position, and CG_EyeAngles samples whatever is left there
when the packet is written. Those angles travel up in the usercmd as usereyes_t
and become Player::m_vViewAng, which is what getUseableEntities aims with.
Measured on device, that left a pitch pinned between 73 and 86 degrees. Every
press traced about 77 units into the floor and stopped there - the trace
fractions were 0.87 to 0.93 - and the box at the end of it caught the player's
own entity plus whatever trigger happened to be underfoot. Standing on a
TriggerUse was what "working" meant.
In VR the eye angles now come from the player state's view angles, which are the
headset's and only the headset's: CL_VRMove writes the head's yaw and pitch into
them every frame and nothing animated gets a say. No recoil kick or lean in them
either, which is right for reaching at a door handle.
Flat play keeps the old source. There the two agree except for kick and lean,
and changing where a desktop player's use is aimed is not what this is for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two spots in the status went stale with the change above. The device
configuration section said the harness ships on and to clear it to play
properly, which was true when written and is now the reverse. The hands section
said both hands need a roll of 180 in the config, which is now the compiled
default.
Both replaced with the rule they are instances of: nothing the game needs to be
correct lives only in autoexec.cfg, because that file is not in the APK.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vr_weaponAdjust and vr_offHandAdjust both defaulted to a zero roll, and the 180
that makes the hands come out the right way up existed only in
misc/android/autoexec.cfg. That file is deliberately not packaged into the APK
- it lives next to the game data, so that the config survives reinstalling the
build - which means anyone installing the APK on its own got upside-down hands
and no way to guess why.
Both defaults are now 180, which is the value measured on device rather than a
guess. The config still sets them, so it stays the place to tune them from;
it is no longer the only place the right answer exists.
The comment explains why the number is not arbitrary: anchoring forces the
bone to take the controller's orientation, and a wrist bone's local axes do not
run the way a controller is held, so the mesh inherits that mismatch as a
constant half turn about the aim axis. The two hands are mirrored and were
still measured to need the same 180, which is why they remain two cvars.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The harness was switched back on to tune the hands and left on: cheats, every
weapon handed over a second after spawn, and m4l1 loaded straight past the
menu. That is how you tune weapon alignment without replaying a level on every
build, and it is not how anyone should be handed the game.
cheats, vr_testStart and vr_startMap are empty again, with the working values
kept in the comment beside the explanation of why each needs its own timing.
The commented example said vr_startMap "" - which is the off value, not the
harness value, so the comment did not actually record how to turn it back on.
It says m4l1 now.
The reasoning for why m4l1 is the right test map stays where it is. It was
worked out from the game's own scripts rather than from reputation, and it will
be wanted again the next time the hands need looking at.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Section 4 said rendering at 36% of the pixels was the largest cheap win left. It
has been taken, and it was not cheap - it was a trade.
Measured in the Bocage at 1680x1760 per eye: 90 fps standing at spawn with an
8-9 ms eye pass, 77-85 fps moving with 11-12 ms against an 11.1 ms display
period, so most frames while moving are reprojected. Kept on how it plays rather
than on the number, which is a legitimate call and now a recorded one instead of
something for the next reader to rediscover and wonder about.
Two consequences replace the old next steps. MSAA is out - not because it is
expensive, since it resolves in tile memory, but because there is nothing spare
on the CPU side and the GPU was never the constraint. And the frame split cannot
be trusted: it reports world 1 ms against hud 5 ms, which is Set2DWindow's flush
charging the world's deferred draws to the first 2D call. gpuwait is still 3 ms,
so the GPU idles and the missing time is CPU issuing draw calls.
Fixing that attribution is now step 1, because half the frame is presently
charged to the wrong thing and every later decision about the budget rests on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vr_resolutionScale has been at 0.6 since the days when a frame cost 50-60 ms per
eye, which meant the headset was being given 1008x1056 against a native
1680x1760 - 36% of the pixels it has.
The measurement that justified it has since answered the opposite question.
Rendering at 36% of the pixels bought only 23% of the frame time back, which is
what proved the cost was never fill rate: it was CPU in rend2's back end binding
a GLSL program per surface per eye. Swapping to renderergl1 on gl4es took the eye
from 50-60 ms to 4-6, and nothing has claimed that budget since.
So it goes back to 1. The cvar stays rather than being deleted - it is still the
first question worth asking of any frame time regression, and it is the cheapest
thing to trade if a later feature wants budget it has not got.
Measured on the device afterwards, in the Bocage:
standing at spawn 90.0 fps eyes 8-9 ms
moving in the level 77-85 fps eyes 11-12 ms, most frames over the 11 ms period
So this does not hold 90 Hz while moving and the compositor is reprojecting a
good share of frames. Kept anyway, on the judgement of how it actually plays -
the picture is much sharper and the reprojection is not felt. The number is
recorded here so the trade is a known one rather than a surprise to whoever reads
the frame times next.
Two things follow. There is now no headroom for MSAA. And the frame split cannot
be taken at face value: it reports world 1 ms against hud 5 ms, which is the
Set2DWindow flush attributing the world's deferred draw calls to the first 2D
call that forces them out - the trap in QUEST_PORTING_GUIDE.md section 7.1. The
one figure that is trustworthy is gpuwait, still 3 ms, so the GPU remains idle
and whatever the 11 ms is, it is CPU side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It had drifted a long way. It still described renderergl2 as the renderer, said
head aim was the next real piece of work, listed the weapon handling as not
begun, and called the redundant renderer restart harmless when it was the entire
black menu bug. All four are false, and it is the first file a new session reads.
Rewritten against what is actually true and verified, with the phase table, the
build commands and the outstanding list all matched to the tree.
What is new rather than corrected:
- the frame composition rule, which is the thing most likely to be tripped over
next: cg.refdef.vieworg is not the camera, both routes to fixing that, and
why half of each amplifies instead;
- the hands - separate surfaces, tag anchoring, and the measurement that
corrected the guess about tag_weapon_left;
- the colour space, and that the sRGB format alone looks like the whole fix;
- the world being 52.5 units to the metre and how the game's own art says so;
- starting the logcat capture before handing over the headset.
Section 4 now says plainly what has *not* been confirmed on the device - the two
handed hold, the reload tap, off hand steering - rather than leaving them to read
as working. And it names the largest cheap win left, which is that
vr_resolutionScale is still at the 0.6 that was set when a frame cost ten times
what it now costs, so the game is rendering 36% of the pixels the headset has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything here was paid for on the device over one session, and none of it was
in the guide because none of it had happened yet.
New material:
4.8.1 The engine's camera variable is probably not the camera. Once the head
pose is composed in the renderer, refdef.vieworg is short of the real
camera by the head's play space offset, and the game module never sees
it. Gives the three symptoms that identify it - drifts on head turns,
lags snap turns, moves at double rate - and both correct routes, with
the warning that taking half of each is what produces the third.
4.8 World scale is the engine's own number, not a feel value. Where to find
it (an asset pipeline comment, the player bounding box) and why getting
it wrong reads as oversized hands rather than as a scale error.
6.4.1 Anchor on the hand, not the model origin: the tag composition inverted,
which anchor suits which hand, and the instruction to measure every
candidate rather than trust that it resolved. Records that the guess
this replaced was wrong.
6.4 The adjustment string's first field is the model's scale.
9.1 A map-loading harness has to wait for idle, and the menu is
CA_CINEMATIC rather than CA_DISCONNECTED.
9.4 Verifying the change landed, extended to the case where a refactor
changes what an argument *means* and the compiler cannot help.
9.4.1 Make silent paths speak - print on every exit, naming the state that
decided it.
9.4.2 On-device logs rotate, so start the capture before handing the headset
over.
The trap index is reordered to put the frame, the meaning-changing refactor, the
world scale and the anchor near the top, which is where their cost puts them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported as the hand and weapon models rendering too large. Two causes, one of
them the same knob failing to do what its name says.
**vr_worldscale was 32.** That is roughly the id Tech number for a game built in
inches and is simply the wrong engine's. MOHAA's art is authored at 16 units per
foot, which its own tik files state where they convert from centimetres:
models/weapons/bar.tik:
scale 0.52 // 16/30.5 since world is in 16 units per foot
16 / 0.3048 = 52.5 units to the metre, and the player dimensions agree - MAXS_Z
94 and DEFAULT_VIEWHEIGHT 82 come to 1.79 m standing and 1.56 m to the eye, where
32 would have made the player over two and a half metres tall.
Being out by a factor of 1.64 does not present as a scale error. Everything
measured in metres and handed to the game was too small, so a hand held half a
metre out was placed at 16 units rather than 26 - while the hand *model* is
authored at the game's true scale. A correctly sized model drawn far too close
reads as oversized, and there is nothing in the world to compare it against. Your
own hands are the only reference object, which is why this surfaced now and not
when the world first rendered.
Set in autoexec.cfg as well as changed in code, because vr_worldscale is
CVAR_ARCHIVE and the value already stored on the device beats any new default.
That trap is documented two sections above the one this was found in.
**The adjustment string's scale field did nothing to the model.** It is
scale,right,up,forward,pitch,yaw,roll, and the reference uses the first field as
the entity scale - VrInputWeaponAlign.c reads it into test_scale and cg_weapons.c
returns it as the model's scale. Here it only ever scaled the offset, so it was a
knob that appeared inert on a model that was the wrong size. Now applied to
model->scale, and applied before anything asks for a tag: TIKI_Orientation
multiplies the tag origin by the entity scale, so anchoring first and scaling
afterwards would slide the hand off the controller by whatever the scale changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Written after three of them rather than before, and aimed at the next one. The
Quake lineage is where the advice is most exact - id Tech 2/3 and everything
descended from it - but sections 3 to 10 transfer to any C or C++ engine of that
era.
Draws on this port, the Homeworld: Unbound port (1999 engine, GL 1.x on gl4es,
meson) and the openblack VR port (modern C++, bgfx, no gl4es), plus RTCWQuest as
the finished reference all three lean on. Where the three disagree the guide says
so, and where something is believed rather than measured it says that too.
The parts worth having are the ones that cost days: the renderer choice and the
numbers behind it, the gl4es boundary in full, the adb data directory ownership
rules, the sRGB swapchain finding, the off centre frustum sign and why either eye
alone still looks correct, and section 9 on debugging a device with no console -
prove the renderer can draw before believing a negative, show a probe reaches
what it measures, verify the change landed, and never change code and
configuration in the same step.
Section 4.4 carries a correction to the Homeworld notes it came from, which
claimed an sRGB swapchain without write control would over darken. It does not;
it washes out identically to a linear one, and believing otherwise is what let
this port sit in exactly that state for months while reading as already fixed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vr_startMap was still pointing at m1l1, which opens on a long scripted sequence -
a poor place to look at your hands. m4l1 was chosen off the game's own data
rather than by reputation:
- its script calls itself THE BOCAGE and opens on the Normandy hedgerows with
Manon, spawned with her AI disabled and set nodamage, so nothing is shooting
while you look at your hands;
- 2 spawn references in 538 lines of script, against m1l2a's 59 in 3660.
Almost nothing is scripted to happen at the start;
- global/loadout.scr hands the player a colt45 and an m1_garand and selects the
rifle, so there is a weapon in hand at spawn.
That last point is why it suits the hands specifically: the Garand is the weapon
that swaps the left hand surface to garandhand, so the level exercises both hand
meshes rather than only the common one.
vr_offHandAdjust is new, in the same seven fields as vr_weaponAdjust. Both carry
a roll of 180 and no offsets. The offsets are zero because each hand is now
anchored on its own bone, so there is no built in offset left to cancel - the
-10 right and -6 up the weapon used to need were correcting an anchor that no
longer exists, and keeping them would push the gun the wrong way. The roll is
not a fudge: anchoring forces the bone to take the controller's orientation, and
a wrist bone's local axes do not run the way a controller is held, so the mesh
inherits a constant twist. Both files say which field to reach for if the model
comes out on its side rather than upside down, and that the two hands are
mirrored so their corrections need not match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first person model already has the hands as separate surfaces - triggerhand,
lefthand, garandhand and viewsleeves on USarmyplyr.skd - so putting one on each
controller needs no mesh work at all. The weapon hand's copy drops the left hand
surface and a second copy is drawn on the off hand carrying only that, using the
same Surface_NameToNum and MDL_SURFACE_NODRAW the game already uses to swap
lefthand for garandhand on rifles.
Two things had to be right first, and both were wrong in ways that looked like
tuning problems rather than faults.
**The frame.** cg.refdef.vieworg is not the camera. R_VRComposeView adds the
head's play space offset to it, rotated into the body's frame, after cgame is
finished (tr_vr.c). So anything anchored on vieworg alone is left behind by
exactly the head's offset within the play space - and because the head swings
about the neck, every look around moved it, and a snap turn left it behind
entirely since a stick turn moves the body frame that a head relative offset
knows nothing about.
Both hands are now reported measured from the tracking origin, the frame
VR_PoseToView puts the eyes in, and composed the way the renderer composes the
camera:
bodyAngles = (0, viewYaw - baseYaw, 0)
handWorld = vieworg + bodyAxis * trackingOffset
RTCWQuest reaches the same place by a different road - convertFromVR rotates a
head relative offset by viewangles[YAW] - hmdorientation[YAW] and the -64 plus
hmdposition[1] * worldScale rebase then replaces its height, which comes to the
same thing. Either route works. Taking half of each does not, and did not: a
head relative offset composed as though it were play space leaves the hand at
vieworg + R*(hand - head) while the camera is at vieworg + R*head, so the head's
movement is counted twice and every hand movement is amplified. That is what
"the right hand is accelerated" was, and it happened because VR_GetWeaponPose
was left reporting the old frame while its caller had moved to the new one - the
signature is identical either way, so nothing complained. The header now says so
at length.
**The anchor.** A hand drawn at the model origin sits wherever the animation put
it, which for a rifle is a long way out in front. The model is now moved so the
hand's own bone lands on the controller instead, inverting the composition the
engine itself uses in Entity::GetTagPositionAndOrientation. The off hand anchors
on Bip01 L Hand and the weapon hand on tag_weapon_right, because what the player
is holding there is the gun and the controller is its grip.
Every candidate anchor is measured and logged rather than trusted, which earned
itself immediately: tag_weapon_left was assumed to be an unused slot parked at
the origin and is nothing of the sort (23.5 units out), while the weapon hand's
lookup was returning -1 in silence and anchoring nothing. Both now report what
they found and how far out it is, and the early return says which precondition
failed.
Tracking is confirmed 1:1 on both controllers on the device. What is left is the
wrist bones' own rotation convention, which is a constant and lives in
vr_weaponAdjust and vr_offHandAdjust.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vr_startMap waits for the client to sit idle before loading, because a map
started from autoexec.cfg runs during Com_Init and the game's own startup tears
it straight back down again. It counted only CA_DISCONNECTED as idle.
MOHAA's main menu is drawn over a static pic, and the enum says what that means:
CA_CINEMATIC // playing a cinematic or a static pic, not connected to a server
So the client is CA_CINEMATIC at the menu and never CA_DISCONNECTED. idleSince
was cleared every frame the menu was up, the four second wait could never
accumulate, and the map simply never loaded. Eight seconds sitting at the menu
was plenty; the timer just kept resetting. Confirmed by the state it now prints:
9, which is CA_CINEMATIC.
The reason this took a round trip to find is worth more than the fix. The
vr_testStart branch three lines below prints either way, and says why:
// Printed either way. A hook that silently does nothing is indistinguishable
// from one that never ran, and telling those apart has cost enough already.
vr_startMap printed only on success, so its silence could not be told from "ran
with an empty cvar". It now prints unconditionally and names the state that let
it through, so the next time this misbehaves the log says which.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported as flat lighting - the whole scene lit from everywhere, shadowed faces
pulled up towards lit ones. That reads as a lighting fault and is not one: the
shading is untouched, and a defect that moves the entire scene by one curve is
the pipeline rather than any model's normals.
The swapchains have always been GL_SRGB8_ALPHA8, which is the right declaration.
This engine's output is display referred - the same values a monitor would have
been handed in 2002 - so telling the compositor the images are sRGB is what makes
it pass them through instead of encoding them again.
What the format does not do on its own is stop GL encoding on the way *in*.
ES 3.0 converts linear to sRGB on every write into an sRGB attachment, blits
included, and core ES has no state to turn that off - GL_EXT_sRGB_write_control
is what adds the switch, and it defaults to on. So the driver was encoding
already encoded colour and the compositor was faithfully passing it through:
stored = encode(V), displayed = encode(V)
which is the same lifted mid tones as declaring the images linear, reached from
the other side. The format line looks like the fix and is half of it, which is
why this survived so long - a port that has done only that half reads as one
that cannot possibly have the washed out bug.
Measured on the Homeworld port, where the same defect arrived by the linear
route: a mid tone the data puts at 0.19 came out at 0.45, against 0.473 for a
straight sRGB encode of it, and the ratio between the brightest and darkest
tenth of a hull collapsed from about 5:1 to 1.76:1.
Not routed through gl4es. gl4es keeps no copy of this enable and never sets it
itself, so disabling it once holds for the whole run, the engine's own rendering
through gl4es included.
Confirmed on the device: "sRGB write conversion off" in the log, and the picture
is correct - which means considerably darker. That is how the game is meant to
look.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The config that goes out had cheats enabled, every weapon handed to the player a
second after spawn, and a map loaded straight past the menu. That is a harness
for tuning weapon alignment without replaying a level each build, and it has no
business being the way anyone is handed the game.
cheats, vr_testStart and vr_startMap are now empty, with the working values kept
in a comment next to the explanation of why each one needs its own timing -
autoexec runs before there is a map, and a map loaded from autoexec is torn back
down by the game's own startup.
vr_captureEye off and vr_traceFrame back on, which are the normal values for
both.
vr_weaponAdjust keeps the value being tuned; it is a correction the weapon
genuinely needs, not a testing shortcut.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things, all found by running it.
The mission briefing was drawn onto the player's face. VR_UseScreenLayer only
asked whether clc.state was CA_ACTIVE, so a fullscreen menu that comes up while
the player is still in the world skipped the quad layer and was drawn as screen
space 2D into both eye buffers - at the near plane, a hand's width away, with no
disparity to fuse and each eye warping it through its own asymmetric frustum.
That is exactly what the panel exists to prevent; nothing in-game was reaching
for it. It now checks UI_FullscreenMenuUp as well.
The log said so before the headset did: "view3d 0" with the eye passes still
running means the 3D scene is not being drawn at all, which only happens when a
fullscreen menu is covering it.
map in autoexec.cfg never had a chance. That file is exec'd during Com_Init and
the game's own startup - the intro stages, then the menu - runs afterwards and
tears down anything loaded there. vr_startMap now waits until the client is
sitting idle at the menu and loads it then. vr_testStart also prints
unconditionally: a hook that silently does nothing is indistinguishable from one
that never ran, and telling those apart has cost enough already.
vr_captureEye is a plain archived cvar rather than a VR_TuningCvar. Those force
their default at every start, which is right for a value still being argued with
and wrong for a switch - it made the config's own setting impossible to honour
and cost a device run to notice. The handoff warns about this exact trap.
vr_traceFrame is off in the shipped config: three lines a second rolls anything
that prints once out of logcat within a minute.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The view model is authored to sit at the camera with the arms reaching forward,
so anchoring its origin at the controller leaves that built-in offset in place
and the gun lands near the player's elbow. Rotation was already correct; only
the position was wrong, which is exactly what this fixes and exactly why no
amount of getting the hand pose right removes the need for it.
Same mechanism as the reference's vr_weapon_adjustment_<id> (rtcw
cg_weapons.c, VrInputWeaponAlign.c), as one tunable cvar:
vr_weaponAdjust "scale,right,up,forward,pitch,yaw,roll"
Two details from theirs that are easy to get wrong and are kept here. The
rotation correction is composed in the weapon's own frame rather than added to
the angles, which is only right for one axis at a time. And the offset is
applied along the corrected axes, after the rotation - that ordering is why a
model can rotate correctly and still sit in the wrong place. It is a cvar so it
can be dialled in by pushing a config and relaunching, with no rebuild in the
loop.
CL_RunStartupCommand runs vr_testStart once, a second and a half after the
player is in the world. Setting up a weapon test otherwise means playing through
to wherever the game hands one over on every single build; there is no console
in a headset to type "give" into, and autoexec runs before there is a map, let
alone a player to give anything to. The delay is because CA_ACTIVE arrives
before the player has finished spawning and a give aimed at nothing is dropped.
It re-arms on every map load rather than firing only once per run.
The reference keeps an adjustment per weapon. This is a single global one until
the numbers for each are known - which is what the harness is for.
Not yet run: the headset dropped its connection before this could be installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reference's gesture (VrInputDefault.c): press and release the dominant grip
inside vr_reloadtimeoutms and it sends +reload, then -reload a frame later
because the game wants to see the key held for a tick before it is let go. A
long hold is their weapon wheel, which this port does not have, so holding
simply does nothing rather than reloading late.
The weapon hand grip was a second binding for use. Use stays on the right stick
click, which is where the reference has it too - +activate on ovrButton_Joystick
- so nothing is lost.
vr_reloadTapMs is their vr_reloadtimeoutms under this port's naming.
Built, not yet run: the headset has been unreachable since the previous commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four pieces, each adapted from RTCWQuest rather than invented.
Aim. Weapon::GetMuzzlePosition derives a shot from player->m_vViewAng, which is
MOHAA's analogue of rtcw g_weapon.c:1904. It now takes the controller's pitch
and the yaw the hand leads the head by, added to the view yaw the game already
has - the controller's absolute yaw means nothing to the game, since the player
can snap turn on the stick and the play space has an arbitrary forward. Only the
trace moves; the camera stays on the head.
The angles reach fgame through gameImport_t rather than the usercmd, which has
no spare field - angles[3] is the view direction and buttons is packed. A
dedicated server answers false and keeps the aim it had.
Placement. The first person model is MOHAA's FPS view model, positioned by
CG_OffsetFirstPersonView against the camera and given the walk bob. Both are
wrong in a headset: the hand has its own position, and shaking the view model
when the camera is the player's head is a way to make people ill. It now goes
where the hand is - RTCWQuest's CG_CalculateVRWeaponPosition, including
dropping the origin to the feet and adding the measured head height back so the
weapon hangs at the height the player is really holding it, rather than off the
camera where crouching would drag it about.
Selection. Nothing was bound to it at all, and there is no button left - the
triggers fire, both face buttons on each hand are objectives and jump, and the
sticks and grips are duck and use. The reference does not spend a button either:
the dominant thumbstick up is weapprev and down is weapnext, gated on the
sideways component so it cannot be read as a turn and edge triggered so a held
stick does not run the inventory. This port's turn already ignores that stick's
Y axis, so nothing was displaced.
Two handed hold. Off hand grip squeezed with the hands close together points
the weapon along the line between them instead of along the weapon hand alone -
how a rifle is actually held, and steadier, because a wobble at one hand is
damped by the distance to the other. Roll is halved rather than taken from the
line, which carries none. The off hand grip was a duplicate of use; the weapon
hand's still opens doors.
The suggested binding array was 16 with 14 in use. Now 32.
Aim and placement are built but not yet confirmed on the device.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
r_traceSurf and the r_noCull/r_noDepth overrides were removed with the bugs they
were built to find. The technique the doc is actually teaching - ask the driver,
not gl4es - stands on its own and is four lines to put back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removes r_flatColor, r_traceSurf, r_forceGenericStage, r_noCull, r_noDepth,
r_noFog, r_invertCull, vr_debugPanel and its magenta panel clear, along with
R_TraceSurface, R_DriverGet, R_FlatColorPush/Pop and the GetFloatv/IsEnabled
entries added to qgl.h for them. Around 570 lines.
Everything they were built to find is fixed, and the fixes are all still here:
the cull face forced past gl4es's cache, RB_SurfaceFace's index type, the window
resize vid_restart, the frustum built from the runtime's tangents, the terrain
cone sized to the headset, and the VR layer's state calls routed through gl4es.
Verified rendering on the device after the strip, with no overrides at all.
vr_captureEye stays, defaulted off. It writes the eye buffer to a TGA and it is
the one probe that consistently earned its keep - a picture settled in one look
what rounds of description could not. Inert unless switched on.
misc/android/autoexec.cfg is down to that single line. Nothing in it is load
bearing any more.
Also records a third instance of the session's recurring mistake in the gl4es
doc: r_noDepth was declared, registered and tested for several device runs while
the edit meant to read it had silently failed to apply. Every result attributed
to it was a no-op, and it sat in the "known good" configuration taking credit for
what disabling culling was actually doing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three bugs on this port presented identically - geometry submitted, vertex data
correct, indices in range, matrices correct, on screen, unclipped, no GL error,
nothing drawn - and all three were gl4es state calls being dropped. The
framebuffer binding, the viewport, and finally the cull face, which cost most of
a session on its own.
The underlying fact is that gl4es keeps its own copy of GL state. It answers
glGetIntegerv for GL_CULL_FACE_MODE, GL_FRONT_FACE and GL_MODELVIEW_MATRIX from
its own tables rather than the driver, so reading state back through qgl and
comparing it against the engine compares the engine with a copy of itself and
agrees by construction. And it returns early from glCullFace, glDepthMask,
glDepthFunc, glBlendFunc, glColorMask, glViewport, glScissor and several
glEnable caps whenever the request matches that copy - so once it drifts, the
engine can never correct it. It asks for the right value, gl4es calls it
redundant, the driver never hears, and nothing raises an error.
docs/markdown/04-coding/04-gl4es.md records what to do about it: resolve
glGetIntegerv out of libGLESv3 to ask the hardware; route the VR layer's state
calls through gl4es, with the three exceptions that must not be; and where a
value has to land regardless, name a throwaway first so the real one cannot
match a stale cache.
It also records the two rules that are not about gl4es and cost as much: a probe
must be shown to reach the thing it measures - three of ours had blind spots
exactly where the fault was - and look at the frame, because one capture settled
what six rounds of description could not.
Linked from the Android build doc, since that is where someone hitting this will
be.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gl4es keeps its own copy of the cull face and returns early from glCullFace
whenever the requested mode matches it (src/gl/face.c:14). Measured on the
device, past gl4es and straight out of libGLESv3, its copy said GL_FRONT while
the driver was culling GL_BACK. So the engine asked for GL_FRONT every frame,
gl4es judged that redundant against a copy that had drifted, and the hardware
went on removing exactly the faces Quake wants kept - its world polygons are
back facing in GL's terms, which is why CT_FRONT_SIDED culls GL_FRONT at all.
The symptom was the entire world invisible while models rendered perfectly,
because model shaders are CT_TWO_SIDED and never enable culling. Inverting the
culled face did not help either, because asking for GL_BACK matched what the
driver was already doing. Only disabling culling outright brought the world
back, which is what made this look like a winding fault for so long.
GL_Cull now names GL_FRONT_AND_BACK first, so the mode it actually wants cannot
be mistaken for a no-op. Nothing is drawn between the two calls.
With this, and with the vid_restart and RB_SurfaceFace index fixes, the world
renders with no overrides at all: terrain, BSP, models, lightmaps, fog and sky,
at 89-90 fps and 6 ms per eye against rend2's 50-60.
Handoff section 9 is rewritten around what this cost to find: gl4es answers
glGetIntegerv from its own tables, so every "GL and the engine agree"
measurement taken through qgl compared the engine against a copy of itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gl4es answers glGetIntegerv for GL_CULL_FACE_MODE, GL_FRONT_FACE and
GL_MODELVIEW_MATRIX out of its own tables (src/gl/getter.c), the same way it
answers glGetString from constants - the trap section 5.5 already records. Two
measurements in this session read back "GL" state through qgl, compared it
against the engine, found it identical, and concluded the state was correct.
They were comparing the engine against gl4es's copy of the engine. That agrees
by construction and proves nothing, and both conclusions drawn from it were
worthless.
It matters because gl4es also drops calls its cache thinks are redundant -
glCullFace returns early when the mode already matches (src/gl/face.c:14), as do
glDepthMask, glDepthFunc, glBlendFunc, glColorMask, glScissor and glViewport. So
the driver can hold something the engine never asked for while gl4es reports the
engine's own value straight back at it.
r_traceSurf now resolves glGetIntegerv and glIsEnabled out of libGLESv3 directly
and prints the driver's answer next to gl4es's. The open question it is aimed at:
world geometry disappears with backface culling enabled whichever face is named,
while CT_TWO_SIDED models are unaffected. No winding fault does that, but a
driver cull mode of GL_FRONT_AND_BACK would, and gl4es would still report
GL_FRONT.
Not tested on the device.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MOHAA decides which terrain patches are visible with a cone test:
R_MarkTerrainPatch rejects anything further off the view axis than
g_fClipDotSquared allows, and that value is a table lookup indexed by
tr.refdef.fov_x. At the game's 80 degrees the half angle is about 46.
In VR nothing else reads fov_x any more. The projection is built from the
runtime's four tangents in R_SetupProjection, and the culling frustum from the
same in R_SetupFrustum. So refdef.fov_x stayed at whatever the game asked for
while a Quest 3 eye sees closer to 100 degrees, and terrain that was inside the
headset's view but outside an 80 degree cone was never marked visible and never
drawn - with nothing anywhere reporting it, because that cvar has no other
consumer left in this path.
The fov now comes from the widest horizontal tangent when a headset view is
active. Widest rather than the sum: the per-eye frustum is asymmetric and the
cone is symmetric about the view axis, so it has to cover the wider side.
The index is also clamped away from zero. g_fDistanceTable[0] is
443.5 / sqrt(1 - 1) - infinity - which poisons g_vClipOrigin and culls every
patch in the level. Not reachable today, but nothing checked, and the failure
would be total and silent.
MOHAA specific: RTCWQuest has no terrain, so the reference could not have caught
this one.
Not yet tested on the device.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
misc/android/autoexec.cfg is the only way to set a cvar on a headset - there is
no console, and the file is exec'd after omconfig.cfg and before R_Init, so it
beats both the stored config and the renderer's own defaults, latched and
archived cvars included.
It currently carries r_noCull 1 and r_noDepth 1, which the world does not draw
without. Those are workarounds, not fixes, and the file says so at the point
someone will read it. It lives next to the game data rather than in the APK, so
reflashing the data loses it - which is the reason to keep a copy here.
Section 10 also records the traps that cost round trips: adb install -r leaves
the old process running so a run can test the previous build; a cvar left out of
the config is whatever omconfig.cfg last archived, which is how a shadows test
came to test nothing; and pulling the data directory for a screenshot moves 1.6
GB of pk3s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs, both found on the device, both with the evidence recorded in
VR_RENDERER_HANDOFF.md sections 8 and 9.
The menu was a renderer restart. SDL reports a window resize; the handler
compares it against glconfig.vidWidth/Height, which in VR is the eye buffer and
never the Android surface, so it can never match. It set r_customwidth,
r_customheight and r_mode -1 - cvars the renderer then ignores, because the VR
resolution comes from GetVRRenderResolution - and scheduled a vid_restart that
destroyed and rebuilt the GL context and the OpenXR session under a running
frame loop. The panel drew a full 256/256 samples up to CL_Vid_Restart_f and
nothing ever after. VR_PORT_STATUS called that restart harmless; it was the
whole bug.
RB_SurfaceFace declared its output pointer as unsigned* and assigned
tess.indexes to it, which is glIndex_t* - unsigned short under gl4es. Every
index went in four bytes apart in a two byte array, so each triangle picked up
whichever vertex the zero high halves pointed at. That was the world smeared
into streaks that dragged its textures with it; models never come through that
function, which is why they were always correct. ProjectDlightTexture had the
same fault in hitIndexes. The compiler had been reporting it all along, under
sixty other warnings in that file.
Also: renderergl1 culled against the game's symmetric fov rather than the
runtime's asymmetric per-eye frustum; the VR layer set viewport, cull, depth,
blend, colour mask and scissor state natively while gl4es caches all of them and
drops the engine's next request for a value its stale copy already claims; and
GfxInfo_f reported a different draw route than R_DrawElements took.
The remaining diagnostics are CVAR_TEMP and default off. vr_captureEye is worth
keeping: it writes the eye buffer to a TGA, and one image settled questions that
six rounds of verbal description could not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
renderergl1 on gl4es runs on the headset at 90fps and draws the engine's own
geometry - the intro logo screen renders under a flat colour override. The main
menu still draws nothing, and that is the one open problem.
The largest blocker turned out to be the gl4es build itself: with CMake's flags
it cleared framebuffers perfectly and rendered no geometry whatsoever, measured
as 144 red from a clear and 0 green from a quad into the same buffer moments
later. Matching RTCWQuest's Android.mk fixed it outright with no engine-side
change. gl4es's own CMakeLists adds -DANDROID under the NDK toolchain and
ndk-build never does, so the reference has only been exercised without it, and
it gates the code that decides what the fixed function pipeline may emit.
Recorded at length because it invalidated most of a session's work: while gl4es
could not draw, every experiment ending "and nothing was drawn" returned the
same answer whatever it was testing. Four changes were adopted on the strength
of such results and are now marked for re-testing rather than trusted. The
document says plainly which findings are measured and which are not.
Also commits the framebuffer helpers the VR layer needs: gl4es has to own the
GL_FRAMEBUFFER binding or it renders from its own table into a 16x16 pbuffer,
while the swapchain texture attachment and the blit's read and draw bindings
must stay with the driver - routing those through gl4es empties the blit and
takes the cutscenes with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
renderergl1 on gl4es reaches the headset, clears the eye and panel buffers
correctly, and renders no geometry at all - immediate mode and vertex arrays
alike, with correct geometry, correct matrices, a complete framebuffer both
layers agree on, and no GL error on any draw. Measured as 144 red from a clear
and 0 green from a quad drawn immediately afterwards into the same buffer.
Since RTCWQuest runs this same stack on this same device, this makes our gl4es
identical to theirs rather than to CMake's defaults. From their Android.mk:
-DBCMHOST -DNOX11 -DNO_GBM -DDEFAULT_ES=2 -DNO_INIT_CONSTRUCTOR
-O3 -fcommon -fvisibility=hidden -funwind-tables
ANDROID is deliberately not among them. gl4es's own CMakeLists adds it whenever
the NDK toolchain is in use and ndk-build never does, so the reference has only
ever been exercised without it - and it is not cosmetic: it selects different
code in the loader, in glx, and in hardext, which is where gl4es compiles a
probe shader to decide what its fixed function pipeline may emit. It comes from
add_definitions(), a directory property, so it is removed where it was set
rather than overridden on the target.
-fcommon matters for a different reason: gl4es is old C that expects tentative
definitions to merge, clang has defaulted to -fno-common since 15, and this NDK
is well past that while the reference's toolchain is not.
BCMHOST is the Raspberry Pi switch and looks wrong here; everything it gates is
additionally guarded by !defined(ANDROID). Carried because matching a build that
demonstrably works matters more than tidiness.
This is convergence on a known-good configuration, not a diagnosis. If it
renders, bisect the flags to find which one mattered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three findings that each cost a device run: gl4es cannot bind a framebuffer it
did not create and fails silently when asked, so the renderer must bind nothing
and only flush; gl4es answers glGetString from constants, which defeats the
engine's only check that a context is live; and SDL unbinds the context and
reports success when the window has no EGL surface, which in a headset is always.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The headset was black while the frame loop ran at a contented 90 fps and the
audio played. Everything was being drawn into the window nobody looks at.
Routing the renderer's glBindFramebuffer through gl4es - so that gl4es would see
every call the renderer makes - is right in general and wrong for this call in
particular. gl4es can only bind framebuffers it created itself: its
glBindFramebuffer looks the name up in its own table, and every framebuffer the
VR layer owns comes from the driver's glGenFramebuffers. The lookup misses, it
raises GL_INVALID_VALUE, returns without binding, and goes on believing
framebuffer 0 is current. So the eye redirect quietly did nothing.
RTCWQuest's renderer never binds a framebuffer at all - Quake 3's fixed function
path predates them - and TBXR_Common binds the eye directly, leaving gl4es to
draw into whatever the driver has. renderergl1 is the same renderer and this now
does the same thing.
What the renderer is still told about is the flush. gl4es batches geometry and
issues it lazily, so work built for one eye would otherwise arrive in whichever
target is bound when it finally goes out. RE_SetDefaultFramebuffer flushes while
the old target is still current and binds nothing, which is also why the VR
layer calls it before its own bind rather than after.
readfboBegin and readfboEnd early out while gl4es's read and draw framebuffers
are the same, which they always are here, so gl4es does not rebind behind this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The OpenXR session could not be created: eglGetCurrentContext returned nothing
while SDL was certain a context was current.
SDL binds the GL context against the window's EGL surface. In a headset there is
no window being presented - every pixel reaches the display through the
swapchains - so that surface never arrives. What SDL does then is the trap.
SDL_EGL_MakeCurrent, handed no surface and without gl_allow_no_surface, calls
eglMakeCurrent(EGL_NO_SURFACE, EGL_NO_CONTEXT) to unbind everything and returns
success; SDL_GL_MakeCurrent then records the context as current. So SDL's
bookkeeping and EGL's actual state disagree permanently, and asking SDL to bind
it again does nothing, because SDL_GL_MakeCurrent sees its own record agree and
returns early.
RTCWQuest never meets this because it never depends on a window surface: it
makes its context current against a 16x16 pbuffer (TBXR_Common.c,
egl->TinySurface). Same here, except the context is the one SDL already made,
since the engine renders in it. The EGLConfig comes from eglQueryContext, which
VR_CreateSession was already recovering for the graphics binding.
The pbuffer outlives the session deliberately - the session is torn down and
rebuilt around renderer restarts, and unbinding the context in the middle of
that is the thing this prevents. It is released in VR_Shutdown.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems the first device run found, one hiding the other.
GLimp_SetMode's fixedFunction branch offers exactly one context: desktop OpenGL
1.1. There is no such thing on Android, so window creation failed three times
over and R_Init gave up with "Couldn't get a visual". renderergl2 never met this
because it takes the other branch, which already knows how to ask for ES.
gl4es is what makes the request answerable, and it wants the opposite of what
the renderer believes it is getting: desktop GL 1.x presented upwards, OpenGL ES
underneath. So the fixed function branch now asks for ES 3.0 and then ES 2.0,
and the renderer is never told - it reads its version through gl4es, which
reports desktop GL, and takes the desktop fixed function path on the strength of
it.
That got the renderer up, and revealed the second problem: the OpenXR session
found no current EGL context. Which is where gl4es having its own opinion about
glGetString stops being convenient. The engine's proof that a context is live is
GL_VERSION coming back non-NULL, and gl4es answers VERSION, VENDOR and RENDERER
out of constants without asking the driver - so a context that was created but
never bound passes that test and is only noticed much later, somewhere else
entirely. GLimp_SetMode now asks SDL directly whether the context it just made
is current, restoring the check gl4es defeated.
The session no longer assumes one is bound either. Being current is per thread,
and a window that has not been shown yet has no surface to be current against,
so VR_CreateSession asks for the binding through GLimp_MakeCurrent rather than
requiring it to already be there, and reports the EGL handles when it cannot get
one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Section 4 claimed renderergl1 compiled clean. It had never compiled at all -
android.cmake forced the renderer choice into the cache with CACHE INTERNAL,
which implies FORCE, so the command line was overwritten every configure.
Section 5.1 asked whether the gl4es target was real. It was, and it was
building; the artifact went into the engine's source tree because gl4es points
CMAKE_LIBRARY_OUTPUT_DIRECTORY at ${CMAKE_SOURCE_DIR}/lib and FetchContent makes
that the top-level project. RENDERER_LIBRARIES reached the client the whole
time.
Both of those turned out to be the wrong question anyway, which is the part
worth keeping: renderergl1 does not call GL by name. Every entry point is a
function pointer filled in from SDL_GL_GetProcAddress, and on EGL 1.5 SDL asks
eglGetProcAddress first - so linking gl4es correctly would still have left the
driver answering for every name GL 1.x and ES share.
Records what is now settled about gl4es alongside the VR layer, and what is
still only suspected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The VR layer bound each eye, the UI buffer and the wrist panel directly and
then told the renderer afterwards. That order was harmless while the renderer
issued every call as it made it. It is not harmless under gl4es, which batches
geometry and flushes lazily: it only learns to flush when something it knows
about changes the target, so a batch left over from one pass would arrive in
whatever was bound next - the previous eye's contents showing up in this one.
Routing the transition through re.SetDefaultFramebuffer first gives gl4es the
chance to flush into the target the geometry was drawn for. The direct bind
stays where it was needed, since the renderer only binds framebuffers when its
own framebuffer support is on; it is now a no-op against the same target.
Also points the Android docs at renderergl1 with gl4es under it, and records
that libgl4es.so rides into the APK as a dependency of libopenmohaa.so rather
than being loaded by name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything the VR work had built so far lived in renderergl2. This carries it
across to renderergl1, which is the renderer the port is moving to.
Most of it transfers unchanged: the head pose composed onto whatever camera the
game asked for (R_ApplyVRView, in RE_RenderScene so it follows cutscenes and
ladders as well as the player), the asymmetric per-eye frustum and vr_fovZoom in
R_SetupProjection, the eye render resolution asked for through
GetVRRenderResolution rather than r_mode, and the sprite paths that face the
viewer's position rather than the view plane.
Two places came out simpler than rend2. Everything 2D in this renderer goes
through Set2DWindow - RB_SetGL2D is a one line call to it - so the HUD's
convergence has a single place to be applied rather than two that had to be
kept in agreement. And RE_SetDefaultFramebuffer is nearly nothing: renderergl1
predates framebuffer objects, has none of its own and never touches the
binding, so redirecting an eye is one bind with no FBO_Bind to intercept and
nothing to restore.
glBindFramebuffer is resolved on first use rather than declared with the rest
of qgl, because renderergl1 has no extension probe of its own - it goes through
GLimp_GetProcAddress so it reaches gl4es like everything else the renderer
calls, and gl4es stays aware of which framebuffer is bound.
renderergl2 still builds and is unchanged in behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gl4es work was in the tree but nothing had exercised it. Three things were
wrong, each hiding the next.
cmake/platforms/android.cmake forced BUILD_RENDERER_GL1 OFF and GL2 ON into the
cache. CACHE INTERNAL implies FORCE, so the -D on the command line was
overwritten on every configure and renderergl1 had never once compiled - the
build that appeared to succeed was a renderergl2 build with gl4es sitting unused
beside it. The renderer choice goes back to the top level options, which already
default the way this port wants.
gl4es's own CMakeLists points CMAKE_LIBRARY_OUTPUT_DIRECTORY at
${CMAKE_SOURCE_DIR}/lib, which under FetchContent is this project's source root,
so the artifact was written into the engine's source tree. It also builds itself
as libGL.so.1 after the fashion of a system OpenGL, and Android has no versioned
sonames - the package manager extracts only lib*.so out of an APK. Redirected
into the build tree as libgl4es.so and staged with the other APK libraries.
With that fixed renderergl1 does compile for arm64, but it would not have
rendered. Its entry points come from GLimp_GetProcAddresses via
SDL_GL_GetProcAddress, and on EGL 1.5 - which the Quest is - SDL asks
eglGetProcAddress first. The driver answers for every name OpenGL 1.x and ES
have in common, so glEnable, glBindTexture and eighty more would have bound to
Adreno while glBegin and glMatrixMode bound to gl4es. gl4es would then have been
batching geometry and tracking a matrix stack against calls the driver was never
told about. The renderer now resolves against the gl4es handle directly, and
calls initialize_gl4es since NO_INIT_CONSTRUCTOR is set. The extension string
had the same problem and the same fix: read through SDL it is the driver's ES
list, which names none of the desktop extensions the renderer asks after, so
GL_ARB_multitexture read as absent and the renderer would have quietly dropped
to one texture unit.
The VR layer keeps its own direct GLES linkage; only the renderer is moved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A companion to VR_PORT_PLAN.md for picking the work up cold: what is
built, the deployment traps, how the VR layer fits together, and what to
do next.
It also records where the plan turned out to be wrong, which is the part
worth reading first. The renderer did not in fact start on any OpenGL ES
context; framebuffer objects were switched off on the ES path despite
being core there; there is no SDL audio path to fall back on; and static
linking the game modules is not possible. Each of those shaped the port
differently to how the plan assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The game now knows where the player is looking. Until now head tracking
only moved the camera the renderer drew from, so anything consulting the
player's view angles - objectives that ask them to look at something, aim,
hit detection - never saw it. CL_VRMove writes the head's heading and
pitch into cl.viewangles, which is what all of that reads.
Walking follows the off hand rather than the gaze, after the approach in
Team Beef's RTCWQuest. Tying travel to where the player is looking means
they cannot glance sideways without veering, which is what makes room
scale movement feel like being dragged; steering with the free hand lets
them look around and still walk a straight line. cl_vrWalkWithView
restores gaze-directed movement.
Turning snaps by default. A stick that rotates the world smoothly while
the inner ear insists otherwise is the usual way to make people ill, and
one flick gives one step rather than one per frame.
cl_vrSnapAngle 45 degrees per flick
cl_vrSmoothTurn 0 1 for smooth turning instead
cl_vrTurnSpeed 90 degrees/second when smooth
cl_vrDeadzone 0.2
cl_vrWalkWithView 0 1 to walk where the player looks
All archived, so they are tunable on the headset rather than at build
time, which is what section 5 of the porting plan asks for.
Thumbsticks are bound on the Touch profile only: the simple controller
has none, and offering a binding a runtime does not recognise makes it
reject the whole set, which would cost the fallback its pointer too.
The dominant hand's aim angles are carried through vrInput_t but not yet
used. Aim is still on the head, which section 5 is right to call wrong
for a shooter - that belongs with weapon handling, and looking at things
had to work first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Quake's autosprites and MOH:AA's sprite imposters both orient themselves
by adopting the camera's own axes, so every one of them sits parallel to
the view plane. On a monitor that is indistinguishable from facing the
viewer, because the screen is the view plane. In a headset it is not:
turning the head swings every billboard in the level round in unison,
and they visibly spin in place.
They now take the direction from their own position to where the viewer
actually is, built against world up so they stay upright instead of
inheriting the head's roll. Head rotation no longer affects them; only
moving does.
Covers AutospriteDeform, and SPRITE_PARALLEL and SPRITE_PARALLEL_UPRIGHT
in the sprite path - the latter being the foliage imposters that stand in
for static models at a distance, which is why near foliage looked correct
while distant foliage did not.
Still outstanding: some foliage continues to rotate, so at least one more
path orients this way. Left for when movement makes it easier to tell
which effects are misbehaving and why. Autosprite2Deform is untouched: it
pivots around a fixed axis for beams and rails, so it is far less
sensitive to this, and changing it blind would risk breaking effects that
currently look right.
All of this is conditional on a VR view being active, so the flat
renderer is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Runs on a Meta Quest 3 in stereo at the headset's native 1680x1760 per
eye, with the world tracking head movement in six degrees of freedom.
The session binds to the EGL context SDL already created rather than a
private one, so there is a single GL context and the renderer needs no
thread or context juggling. Each eye is rendered into an OpenXR swapchain
image by substituting the framebuffer the renderer treats as the screen,
which leaves the whole render path untouched. Exactly two eye passes per
frame; there is deliberately no third pass to produce a flat image.
The session and its swapchains belong to the GL context they were made
with, so they are torn down and rebuilt around the renderer restarts the
engine performs.
Menus, the loading screen and cinematics go on a quad layer fixed in the
room, placed once in front of wherever the player is facing. Drawing
screen space content into both eyes instead gives it no stereo disparity
while each eye still warps it through its own asymmetric frustum, and the
result cannot be fused. The panel is drawn into one persistent buffer
that is then copied into the swapchain image: a swapchain rotates through
several images, and the engine's 2D path expects the screen to still hold
what it drew last frame.
Head tracking:
- The per-eye asymmetric frustum from xrLocateViews feeds
R_SetupProjection directly. A symmetric approximation misaligns the two
images by a few degrees, which the eyes cannot fuse.
- The pose is applied in RE_RenderScene, so it follows every camera the
game has - the player's, cutscenes, ladders, death animations - without
each needing to know about VR.
- Orientation is carried as axes rather than pitch/yaw/roll, and composed
with the game's heading as a rotation. Adding Euler angles only works
while pitch and roll are near zero and turns every head movement into a
twist once they are not.
- Position is measured from where the head was when tracking started, so
the game keeps supplying eye height and the headset contributes only
leaning, ducking and stepping. The initial heading is subtracted too,
or the play space's arbitrary forward is added to the character's and
the player spawns facing the wrong way.
The controllers point at the panel and click: the aim ray is intersected
with the quad and the hit becomes the engine's cursor position, so the
menus behave as they always did.
The renderer's own framebuffer objects stay off on Android. Each eye is
already a swapchain image, so routing through tr.renderFbo first would
cost a second full resolution pass and resolve per eye for nothing.
Known gaps: head orientation does not yet reach the player's view angles,
so the game does not know where the player is looking; and there is no
controller model or pointer ray.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Builds an APK for arm64-v8a, brought up on a Meta Quest 3 but not
specific to it. The engine reaches the main menu with retail data
side-loaded and renders through renderergl2 on an OpenGL ES 3.2 context.
Build system: a new cmake/platforms/android.cmake, driven by the NDK's
android.toolchain.cmake. renderergl1 is not built (it is fixed function
and would need a GL-over-GLES translation layer), the renderer is linked
into the client rather than dlopen'd, and SDL2 and OpenAL Soft are built
from source since neither ships an Android binary. OpenAL is not
optional here: it is the engine's only sound driver, so USE_OPENAL=OFF
does not build. Native libraries are staged per-ABI for gradle, which
packages them in misc/android.
Portability fixes for bionic:
- pthread_cancel does not exist; GameSpy's async DNS thread detaches
instead.
- <execinfo.h> does not exist. sys_android_new.c walks the stack with
_Unwind_Backtrace and resolves it through dladdr, writing straight to
logcat. This matters because tombstoned does not reliably report a
backtrace, leaving no account of where the engine died.
- librt does not exist; those symbols live in libc.
- libmumblelink.c needs shm_open and was being compiled even with
USE_MUMBLE off. It now builds only when Mumble is enabled.
con_android.c sends the console to logcat and pumps stdout and stderr
there too, so output from the C library and from third party code is not
silently discarded.
The fatal signal handlers move ahead of Com_Init. Startup is exactly
when renderer and filesystem bring-up crash on a new platform, and a
fault there previously died with no backtrace at all.
Game modules ship inside the APK as libgame.so and libcgame.so and are
loaded off the linker's search path. Static linking is not an option:
cgame and fgame both compile q_shared, q_math and the script system, so
linking them into one binary collides. Android extracts only lib*.so
from an APK, hence DLL_PREFIX in q_platform.h.
Paths point at the app's external files directory, which is where the
user side-loads the game data and the only place the engine can write.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GLimp_GetProcAddresses() failed on every OpenGL ES context, so the GL2
renderer could not start on any ES target. Two independent causes:
QGL_1_1_PROCS carried five entry points that exist in no version of
OpenGL ES - glCallList, glDrawPixels, glFogi, glLineStipple and
glPixelZoom. That list is shared by the desktop and ES paths, so
requiring them there failed the whole load. Upstream keeps ES-incompatible
functions in QGL_DESKTOP_1_1_PROCS; these are moved there to match. Only
renderergl1 calls them - every use in renderergl2 sits inside an
"#if 0 // FIXME: unimplemented (GL2)" - so nothing loses a function it
actually invokes.
The ES 3.0 context was also only ever requested under __EMSCRIPTEN__.
Everywhere else the highest ES context asked for was 2.0, and since EGL
returns a genuine ES 2.0 context when that is what was requested, ES 3
was never reached even on hardware that supports it. It is now requested
first on all platforms.
Verified on a Meta Quest 3 (Adreno 740): renderergl2 now comes up on an
ES 3.2 context and compiles all 115 GLSL shaders. Desktop builds of both
renderers are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Survey of the tree plus a staged plan: Android build first, then flat
playable on the headset, then OpenXR stereo, then the VR interaction
design.
The finding that shapes the whole approach is that renderergl2 already
speaks OpenGL ES, inherited from upstream ioquake3 - qglesMajorVersion
is a real runtime variable, tr_glsl.c emits "#version 300 es" and
injects precision qualifiers, tr_extensions.c branches on it, and
sdl_glimp.c has an r_preferOpenGLES cvar that requests an ES context.
That is a much better starting position than it might have been. A
comparable Quest port of Homeworld needed gl4es to emulate
fixed-function GL 1.x over GLES2, and every later constraint followed
from that. Here renderergl2 can be driven at GLES 3 directly: no
translation layer, real shaders, and FBOs already first class in
tr_fbo.c - which matters for stereo, since rendering each eye into its
own framebuffer is exactly the piece the Homeworld port never built and
whose absence blocked MSAA there.
renderergl1 is treated as out of scope for the same reason: it is fixed
function and would need the emulation layer we are avoiding.
Also records the deployment traps from that earlier port - the silent
2770 directory ownership that locks the app out of its own data, the
chmod-after-push ordering, never launching over adb, and verifying that
crash logging works before trusting it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This is the default behavior in upstream (ioquake3).
This avoids issues with the current directory varying depending on how the program is launched (shortcut, services...) and improves portability when the binary is moved.
It doesn't require users to cd into a specific directory before launching.
The game directory can be overridden by setting `fs_basepath` variable.
This fixes links set the markdown ID style to GITHUB so the documentation can be read both using Markdown or from the documentation website.
It also updates doxygen to version 1.16.1.
Co-authored-by: laurensdepaepe <laurens@encima.com>
Co-authored-by: smallmodel <15067410+smallmodel@users.noreply.github.com>
This prevents expansions from inheriting the default crosshair texture on first launch, avoiding the crosshair turning green when hovering over a teammate.
The name of bots are game-specific settings, so it must be prefixed with `g_` rather than `sv_`, plus the setting name should properly separated by an underscore for clarity.
Some allocation variables like `sv_numbots` and `sv_maxbots` start with `sv_` even though they are read by game. It's because they interfere with the server's allocation of clients.
Resolves an issue where the macOS application's dylibs could not be
loaded because there were too many maps; on macOS the default maximum
number of open files is 256.
It is now possible for servers to send large vote configuration files to clients without causing disconnections due to packets being larger than 50 KB.
The issue also occurs on vanilla servers.
This fixes animation flickering due to floating-point precision differences between SSE2/x64 and x86 FP87. The animation time could drift slightly and wrap earlier or later than it should.
Q_stricmp uses uppercase comparison and the original game (win32 builds) uses lowercase comparison.
This can be an issue when sorting animations containing characters between lowercase/uppercase letters (like the '_').
Animations sorted using Q_stricmp won't have the same index as when using _stricmp, causing a few animations mismatch with the original game.
Following changes to the console code, it turned out that at very high
resolutions, the large number of glyphs rendered would cause render
commands to be dropped; avoid this by simply raising the maximum number
of render commands.
- .min files are now created for maps that don't have one.
- Saving .min files no longer results in a crash.
- The loading bar is now always cleared when no .min file is present.
Caching all FPS models would create too many unnecessary entries. On some maps, this would cause clients to reach the MAX_MOD_KNOWN limit, causing crashes.
R_LoadImage now tries .dds as a final fallback when compressed textures are disabled.
This allows assets to ship as DDS-only without requiring duplicate PNG/TGA for every texture.
Fixes#583.
- Drop carryable turrets, even if they are items.
- Don’t drop weapons when dead.
- In single-player mode, only allow dropping when realism mode is enabled.
Com_Filter uses a limited sort of proto-regex syntax including a
character class like construct, but prior to this change these did not
work if preceded by a * glob.
On restart, this will spawn bots before scripts are notified, so subsequent rounds can start immediately.
Some scripts rely on the match time: the issue would cause some of them to fall behind the match time, as the round would start at the next server frame, which would cause the teamwin event to be sent too late (after the match has ended due to the clock).
This PR replaces `NULL` with `false` for a boolean member in an `extensions_table_t` initializer to fix C++ type-mismatch build error on modern C++ compilers.
Co-authored-by: smallmodel <15067410+smallmodel@users.noreply.github.com>
This PR replaces `NULL` with `0` for integer file handles to fix C++ type-mismatch build errors on modern C++ compilers.
---------
Co-authored-by: smallmodel <15067410+smallmodel@users.noreply.github.com>
Sys_BinaryPathRelative takes a parameter which is path concatenated with
Sys_BinaryPath, and resolved to a canonical path. The intended use case
is to facilitate the situation where you want the game data directory to
exist outside the same directory in which the binary lives, but relative
to it. More specifically, if you want to distriute multiple binaries for
different architectures, in the same tree, this allows for a means of
having said binaries in architecture subdirectories, with a shared data
directory, e.g.:
ioq3/x86/ioquake3.exe etc.
ioq3/x86_64/ioquake3.exe etc.
ioq3/baseq3/pak0.pk3 etc.
Here, when building you would define DEFAULT_RELATIVE_BASEDIR=".." by
appending it to the build system CFLAGS, and then the executables will
by default look in their parent directory for the data.
Before commit a7d33b1c "(bug #3639) BoxOnPlaneSide rewrite" there
was more code here, but since then the only thing left is the
Invert24To16 function, to which I couldn't find any references since
the beginning of ioquake3's git history.
Resolves: https://github.com/ioquake/ioq3/issues/774
Signed-off-by: Simon McVittie <smcv@debian.org>
client: Remove i386 GNU assembler version of S_WriteLinearBlastStereo16()
This was the last remaining GNU assembler code that was in its own
independent source file, as opposed to being embedded in C code with
GNU `__asm__ volatile` or MSVC `__asm` (which tends to be much easier
to deal with in a modern build system).
When the Quake 3 engine was originally written, this might have been
an "expensive" function when written in portable C, but after 25 years
of CPU development it's less likely to matter (and modern compilers
might produce faster results from the C code anyway).
Resolves: https://github.com/ioquake/ioq3/issues/778
Signed-off-by: Simon McVittie <smcv@debian.org>
Bot names must be set using `sv_botXname` where X is the bot ID, because bots can be spawned automatically using `sv_minPlayers`. `addbot` and `addbotnamed` commands shouldn't be used, they only exist for testing purposes.
ARM uses unsigned char by default when the sign is unspecified, this would cause some issues with messages containing accents, players would lose user control.
FARPROC is defined with an empty parameter list, which newer versions of GCC consider equivalent to (void). Compilation fails when qSHGetFolderPath gets called with arguments.
The VAO surface cache uses glBufferSubData and triggers a very slow
path in some GLES implementations. Specifically I have observed 10x
frame times under Emscripten with ANGLE/Metal on macOS and with Mali
on Android.
Co-authored-by: Zack Middleton <zack@cloemail.com>
Co-authored-by: WofWca <wofwca@protonmail.com>
# Conflicts:
# code/renderergl2/tr_local.h
- Fix tiny mistakes related to distance calculation
- Fix bot going back and forth when opening a door in stairs (mohdm4)
- More precise collision detection
- Add a way to climb over obstacles like in Brest (getting outside the house spawn area)
- Bots will try to break window objects in their way
- Fix some ladders not being usable as a path for bots
- Fix bots acting dumb when trying to climb a ladder
- Improve the way bots jump on obstacles overall
- Improve bot collision detection
This prevents bots from navigating through minefields and getting killed. Unfortunately this has the side-effect of filtering a large zone like the west axis spawn in obj_team3
This adds simple support for unit testing, starting with the LZ77 compressor.
- Unit tests run on a separate GitHub Actions workflow
- Remove CTest commands from shared build workflow
This fixes an issue where very tiny values would fail in comparison operators, for example the comparison `2.00000620 > 2` succeeds in the original game but fails in openmohaa.
- Obstacles with a radius smaller than 100 units (sphere) will be ignored
- An obstacle that would cover polygons more than twice the radius of the obstacle will be ignored
Currently, this only works for big obstacles that affect small areas
- Use `allow` rather than `filter`, as it checks for allowed commands
rather than prohibited commands.
- Allow userinfo variables to be created by the server.
Userinfo variables are now removed when disconnecting. Some servers use
userinfo variables for custom settings or to store temporary
information, which makes them irrelevant on other servers and uses space.
MoveTo() was incorrectly setting the script goal to the actor's origin rather than the waypoint's origin, causing the actor to stand still instead of moving to the specified location.
The number must match the entity number parameter used in G_GetEntity().
This fixes a crash caused by gclient struct being reset when a client reconnects to the same player slot
In the terrain tessellation code, patches sometimes rely on neighbors, which if are hidden can issues with invalid tri. Collision doesn't rely on shader contents anyway
- Paths are generated automatically during map loading, for any map
- Bots can take path that requires climbing ladders (can climb ladders)
- Bots can take path that requires jump (like the ledge on Stalingrad)
- Bots will try to avoid obstacles in front of them by navigating on the
side
The old navigation system with pathnodes can still be used with `g_navigation_legacy
1`
This fixes issues where some tools fail to handle multiple print packets. On a 64-player server, the status command can generate around 5000 bytes of output.
The buffer size in the original game is 49152 bytes, but that's excessively large and can lead to excessive IPv4 fragmentation.
- Avoids conflicts with third-party DLLs (like SDL, OpenAL) which have no suffix.
- Easier to build an installer and document.
- More clarity as the architecture is already indicated in the archive name.
- Simplifies launchers & automation (no need to guess the architecture).
- The architecture is still visible when typing `version` in the console or when the executable starts
WiX is used to generate an MSI file per architecture for easy installation. GitHub Actions distributes the resulting MSI packages as artifacts and when releasing a new tag.
The heartbeat implementation now supports an optional delay between sending heartbeat packets to multiple master servers. This is a precaution in case there are issues when sending multiple heartbeat packets simultaneously. If it causes any problems, the delay between packets can be adjusted.
Rework the version string.
- Make the original game version appear first in the `version` variable.
- Use `com_target_shortversion` for the short version number.
- Use `com_target_version` for the original game version.
Some software, such as HLSW, as well as certain mods, check if the version string begins with "Medal of Honor ....". This commit makes these changes to ensure most software properly handles the game version.
In windowed mode, the mouse is no longer grabbed when one of the UI/console catcher is active. When a UI menu is active, the system's hardware cursor is shown, using the game's custom cursor texture, instead of simulating and rendering the cursor in-game.
* Add exports to load a raw image from the renderer
* Set the hardware cursor to the game's custom mouse cursor texture
* When mouse is not grabbed, the mouse gets updated with absolute mouse position, relative to the window position
Implement multi-master server list query and multi-master heartbeats. The server list is fetched from multiple masters to improve redundancy, reliability and performance. This improves multiplayer availability by implementing support for using multiple masters.
- The server sends an heartbeat to all masters at once
- The client can query up to 4 masters in parallel depending on their rate
20 is the bare minimum for players, any value below makes movements look jittery on a 20 fps server. 30 is the optimal value based on network variation and fps variations
The server list UI refresh the server list for the first time when drawing. In mohaa (not Spearhead nor Breakthrough), refreshserverlist is also called by clicking "browse internet servers" which causes the list to be queried twice
Makes it easier to identify the currently running OPM version from the server list, useful for troubleshooting and checking if the server is up to date
This adds a feature that makes it easy to join a populated low-ping server (matching criteria from configuration file). This feature has been available since MOHAA Breakthrough 2.30.
* Add a cvar to enable/disable update checking
* Refactor update checker
This splits the thread part from the main part to clarify the code
* Move and update the configuration documentation, for a more general approach
The default c++ allocator was be used to allocate an array of pointers, but then when destroying the array, Z_Free was called which would throw an error
Unregister() was mistakenly being called on the DM_Manager object when it should have been called on the level object, to unregister events like axiswin, allieswin, draw
As the control is activated before, the connection should be done after the activation because ActivateControl() sends W_Activated to the UIFloatingWindow, which will cause ActivateControl() to be called again to activate the dialog's title bar button (and would deactivate the gamespy dialog)
This fixes an issue where configstrings or game state chars would overflow when many models (such as actors with random headmodels/headskins) are spawned
The name was ambiguous. The delegate is triggered when the player gets hit (getting damaged), and not when the player damages someone else. The name properly matches the action.
If the player model or the player german model is not valid, they will be reset to the default value. If the server doesn't have one or more of these models, force model feature will not work
Some player models are missing from precache scripts. This caches all models inside the player directory to avoid a short hitch when a player spawns with a non-cached model
This prevented players with low connection speed unable to connect due to UDP packets being lost. The rate of connecting clients will be properly taken into account
Previously, 64 aliases could be chosen for random search, it was incremented to 128 since 2.0, however it should have been increased in Breakthrough as well, because there are aliases with more than 128 elements
renderercommon is now used, which will appropriately use opengl1 when building static libraries, or compile both opengl1/opengl2 modules when building shared libraries
This was using an uninitialized buffer to find the cached TIKI, in most configurations it always returned false, but when compiled with GCC it would return true
There are some situations where EV_MoveDone is posted again through a call to G_PushMove(), this avoids the mover unexpectedly and immediately moving to the new location
Since the expected time is in milliseconds, it's best to use an integer instead. Floating-point numbers lose precision as they get larger, especially when the level's current time is long (e.g., over 24 hours). This loss of precision could cause scripts to wait unsuccessfully with small numbers, causing unexpected behaviors
This removes coupling between the renderer and UI/client functions. An option USE_RENDERER_DLOPEN was added to specify whether a renderer module should be compiled and loaded, instead of integrating the renderer into the executable directly. This opens the door for a new renderer
Each target will produce a single binary having multiple architectures, instead of having a binary per architecture. The suffix for each binary will be `multiarch`. This makes it easier for the end-user to install and use. This also helps the transition between Intel and Apple Silicon hardware
Large lightmap can be disabled for very low-end hardware. A BSP file with a smaller lightmap (_sml.bsp) will be used instead of the standard one if the map supports it
Some distros install to lib64 instead of lib, respect the lib extension
otherwise some {f,c}game gets installed to lib64 and other libs get
installed to lib.
Sounds such as sea waves (mohaa m3l1a) and subpen (mohaas flughafen) will be played accordingly. This uses OpenAL extensions such as AL_EXT_IMA4 and SOFT features such as AL_SOFT_block_alignment
This can occur when starting a new sound without playing it, which can happen when reloading the sound system while all sounds are paused (game paused)
This fixes an issue where turrets would Think() before the base they are mounted on (like tanks), which means the turret would set its origin before the base (tank) does. This issue also occurs on the original game
On Win32:
- Binaries and libraries will be installed by default in <PREFIX>/bin
- The prefix is set to `Program Files/openmohaa` by default
On Unix:
- Binaries and libraries will be installed by default in <PREFIX>/lib/openmohaa
- Shared data will be installed in <PREFIX>/share/applications, <PREFIX>/share/metainfo and <PREFIX>/share/icons
- The prefix is set to `/usr/local` by default
This removes the use of the DESTINATION parameter from install() command and instead rely on default values.
The shared build action file was modified to package installed files in a single folder
Add a .desktop file, this is used by application launchers in order to
find and execute applications, this should be installed to
`/usr/share/applications/org.openmoh.openmohaa.desktop`.
Add appstream metainfo, this provides metainfo that is used by Software
Centers for discoverability, this should be installed to
`/usr/share/metainfo/org.openmoh.openmohaa.metainfo.xml`.
This will open the door to getting OpenMoHAA packaged up by various
linux distros.
Instead, prevent sentients from picking up weapons if they get no ammo from it and they already have that weapon. Instead, just let the weapon be picked up as a new weapon
Before 2.0, the tracer frequency is set on almost all weapons. This has some artifacts on multiplayer, the tracer doesn't look adjusted in first-person view
This fixes terrain rendering incorrectly on big-endian architectures by using bit operation. The `flags` field in the union always matched the first byte of the variance float value instead of the least significant byte
There was a very rare occurrence where it would hang trying to find a free mark poly, when the number of specified polys was more than the number of allocated polys. This bug also occurs in the original game
Profiling can be enabled with `cl_netprofile` and `sv_netprofile`, the overlay can be shown using `cl_netprofileoverlay` and `sv_netprofileoverlay` and the server can dump network profile using the `netprofiledump` command
Useful for scripts that are using fadeout during cinematics and then transitioning to the nextmap, like in e2l2 where vehicles explode at the end of the mission
It was only checking if the channel was playing. This was an issue because, in the main menu the music is paused, which means saving from main menu would not save the music filename
This makes TriggerEntity respond to any object that is a subclass of Entity. This is useful for scripts wanting script_model to touch triggers for cinematic purposes (for example, the bike crash gag at the end of e2l2)
- On 1.11 and below, the ubersound folder is parsed either only in dedicated server mode or in client-only mode
- On 2.0 and above, alias are cleared and the ubersound folder is always parsed in multiplayer on both client and server
This fixes poses being updated even if they don't need update (no frame changed), which resulted in some performance loss and some rare accuracy issues when AI fire weapons
This was missing and this allows the torso state to the modheight to set the bounding box height. Before that, it did not recognize the turret so the modheight was still set to stand. It caused issues when trying to get out a vehicle turret, like the flak88 in e1l1 (the player wasn't able to get out of the turret)
This fixes an issue when loading from a save. A variable referencing target list container would have random entities in it, which would cause errors and crashes when trying to do an action on the array of entities
Effects that are using commanddelay were not working at all, this fixes the processing of delayed events so they get processed sequentially and at the correct time
This fixes an issue that also occur in OG, where some weapons would constantly replay the same sounds (often reload/rechamber) when the weapon entity is removed then added to the client snapshot
This fixes the issue where the game could crash (especially in optimized release) due to a memory corruption, because the event command list was deconstructed earlier
This fixes an issue where shared libraries would use functions from in the executable because they have the same name, which would cause memory corruption
- Add a sv_sharedbots to specify if real client slots should be used
- Removed m_bSpawnBot from level, bots are spawned directly in G_BotBegin instead
- sv_numbots can be used to directly configure the number of bots to spawn
- Fix the way bots spawn/unspawn on-demand
- Documented g_bot functions
In 64-bit, some windings are off by 1 bit (float values) compared to 32-bit, which causes issue on some maps like `dm/mp_stadt_dm` where the player would get stuck climbing up because of the blocking patch at the top
- The bot will immediately move upon spawning
- If blocked, the block will move randomly to get out
- There is now only 1/5 chance the bot will go to the location where they died
The non-dedicated server version of MOH: Breakthrough (standalone executable) rejects clients that have a version of 2.30 or below, or a version of 2.50 or above (0.1 difference), which prevents playing LAN sessions of Breakthrough. To solve this issue, the client now sends a version value that is within the allowed version range for the target game
This fixes the issue where there the client (in spearhead protocol mode) would read 2 bits instead of the 1 bit the server sent, which would cause a parse error
This fixes an issue where some part of the loading screen would not be drawn correctly, due to the screen being updated recursively through UI_Update()
Same error as commit c76dda1523. This fixes an issue where AI would run too slowly, like the crate guy in the intro of e1l1, the crate guy was too slow that it ran through the intro vehicle
This fixes an issue where some scripts wouldn't be initialized in time, like those that set AI animations. For example this caused an issue with the plane passenger in e2l1, where the animation would get reset due to a very specific chain of events
This fixes:
- Losing FPS in a match with a lot of players, due to a massive amount of emitters getting spawned every time a weapon fires
- Memory leaks due to emitters staying in memory forever after being spawned
+seta, +sets, and +setu were ignored because Com_AddStartupCommands
thought Com_StartupVariable handled it.
+set didn't allow value to be multiple tokens which due to Unix shell
unintuitively removing quotes causes the variable to only be set to
the first token. This could be worked around by escaping quotes
ioq3ded +set g_motd \"hello world\"
but it doesn't match behavior of other start up commands (which now
includes seta, sets, and setu) that use all tokens.
The legs state won't be stuck anymore waiting for an empty animation. This could happen under special circumstances, when the torso evaluation sets the movecontrol to non-legs (clears the legs anim) and then back to legs in subsequent iterations.
Fixes#105, the issue cannot be exploited anymore for land-sharking
This is problematic when doing different logic when a widget doesn't exist, for example when loading a map, if the continue button doesn't exist then the game should continue automatically
This prevented actors from attacking correctly because the AI cover target state expects the AI to 100% stare at the enemy and the vehicle would keep changing the AI angles
The end anim script should be executed below a certain speed (1/4 below normal speed).
This fixes a bug in e3l4 where the game would never continue past the first bunker. The actor would get stuck after collecting supplies for the first bunker at the beginning because it stopped too far away from the target origin
Original game checks for the NUL character using the line buffer (`c_str()`) directly instead of the str object's operator[], as this would cause an assertion failure because the str's length ignore the NUL character
This fixes#330 where `s_khz` below 44 (the usual sample rate of mohaa wav files) would cause an infinite loop due to improper usage of the `ii` variable in the loop where another variable would be accidentally incremented instead
ReregisterMaterial() must register the material if it was registered before, and GetMaterial() must register the material if it hasn't been registered and return it
This commit implements code to get/set sample offset in an OpenAL channel and saves the music filename/offset into the file. This only works by using the experimental sound system, so it partially fixes#327
This caused cvars like `paused` being wrongly restored. If the `paused` value is 1 or above, like when saving from the menu, the game would get stuck loading the level because it's paused, no frame could be executed.
It also fixes cvars like `sv_fps`, `g_playermodel` not being archived when they should.
Fixes#326
This new version introduces patches against crashes and exploits. CMake is also fully supported and platforms are properly detected so architecture specific optimizations are used.
To use it, USE_SOUND_NEW must be non-zero when configuring the project
This fixes#311 where a memory corruption that occurred in ARM was caused because of insufficient ScriptVM stack size. The stack size was incorrectly calculated because the stack offset was using unsigned values instead of signed in ARM.
This fixes some entities being invisible when `r_fastentlight` is set to 1, due to the alpha color being 0 (cast from int to char for each color value). Fixes#299
Some script events don't check if it's an actual entity, it would cause crashes.
E3L3 would crash after the player got out of the AB41: the AB41 was targetting a VehiclePoint and vehicles_thinkers.scr script set the `self.collisionent` to `self.target` which caused a crash as `VehiclePoint` doesn't inherit from `Entity`
This fixes the gunfire incorrectly damaging things it shouldn't damage.
For example, the sniper3 balcony AI on e3l1:
The bomber caused this AI to fall before initialization (by damaging them when it shouldn't), the AI became nonsolid. After initialization, the AI had enemy enabled, however it remained nonsolid because of the balcony fall. It was constantly shooting at the player and it was unkillable because it was nonsolid.
The mission wasn't able to be completed because of this issue.
This fixes the script slave moving infinitely and not sending movedone notifications as it would never advance to the next path. e3l4's outro is now fixed because of that (was stuck).
This fixes the issue where vehicles would not be moving precisely where it should be moving (incorrect clipping of the velocity). For example, the minesweeper tank in e1l2 kept getting stuck because it didn't move appropriately (fixes#261)
- Don't damage if health is already 0 or below
- Check for immunity
- Error if attacker or inflictor is NULL
- Don't apply velocity for vehicles
- Set the minimum mass to 20 and momentum to 1000
- Still process pain when in godmode
Implemented SV_WorldTrace(), SV_ClientIsVisibleTrace(), EntityDistCheck() and SV_ClientIsVisible()
Entities also will be hidden when:
- Their parent are not sent to clients
- They don't have a model
- They are behind t he farplane
- Clients can't see eachother with sv_netoptimize
Fixed wrong fog flag being used (must be GLS_FOG) in RB_SetupFog().
R_SetupFrustum():
- Use skybox farplane and farplane bias
- RB_SetupFog() is now the only function who handle fog activation
RE_RenderScene():
- Set renderTerrain, skybox_farplane and farplane_bias parameters
- Set color overrides
- Use farclip when necessary
RB_StageIteratorLightmappedMultitextureUnfogged(): Fixed issue with fog being reset
When the number of shaders reached >1023, some surfaces started to be have random shaders started on them
The number of sorted shaders in a single game can now be up to 2048
Fixed respawn time after death
Fixed Sentient not switching to the primary weapon when spawning
Fixed weapon select not showing
Initialize all Player variables
Remove projectiles when switching teams
Set viewmodelanim to disabled when reaching the weapon's max fire movement
DoUse: Call RemoveFromVehiclesAndTurretsInternal() directly
Use G_PMDrawTrace if sv_drawtrace is above 1
Set allow lean while moving
Disable movement prediction if player is glued onto something
Multiply the speed by the weapon movement speed
Added Teamkill warn and kick
Added support for invulnerability, sprint and team spawn
Added support for heal rate
Added talk icon
Set the player view accordingly when choosing a spawn point
Don't set m_fDamageMultipliers in realism mode
Score isn't processed in single-player anymore
For **OpenMoHAA VR** changes, see the [VR release notes](https://github.com/bearstonem/openmohaa-vr/releases)
and [port status](VR_PORT_STATUS.md). Quest is the current release target;
Steam Frame integration is in development.
The changelog below is inherited from upstream OpenMoHAA and records engine
changes. Its release links and version numbers belong to the upstream project.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
#### Server
- Feature to name bots (most requested bot feature). See the [bot documentation](https://github.com/openmoh/openmohaa/blob/main/docs/markdown/03-configuration/01-configuration.md#bots). This may change in the future.
- More settings for bots. See the [documentation](https://github.com/openmoh/openmohaa/blob/main/docs/markdown/03-configuration/03-configuration-bots.md) for settings.
- Reason will be sent to clients getting kicked automatically by the game for teamkill or inactivity.
- Reflective team damage feature. See the [documentation](https://github.com/openmoh/openmohaa/blob/main/docs/markdown/03-configuration/02-configuration-server.md#g_teamdamage) for settings and explanations.
- Store the map file name in the `mapfilename` variable.
##### Scripting
- Add a third optional parameter for `removeclass` to specify if it should include subclasses (defaults to true).
- The owner of a projectile is now accessible from scripts.
#### General
- Add the bot count (if there are any) in the server browser. This will only work for servers running at least the version **0.83.0** of OpenMoHAA. The bot count will also show on the [333networks](https://master.333networks.com/) master server website.
#### Client
- Add support for one-shot animated shaders in the UI (#838) (not supported in the original game).
#### Platform
- Allow separation of config, data, and state directories within the home path (XDG directory support) (#588).
### Changed
- Older saves are incompatible due to changes from commit 5df5735965298b26b9bd4b90034843c743ff7158.
#### Server
- Prevent dropping the binoculars (this change was introduced in Spearhead v2.11).
- Servers now read and write into a configuration file `omconfig_server.cfg`, separate from the client configuration for convenience.
- Fix Spearhead getting medals from Allied Assault when the config file doesn't exist (also occurs in the original game) (#818).
#### Server
- Allow `g_teamkillkick` to be set to 0. Players would get kicked when this variable was set to 0 (also occurs in the original game).
- Allow spectators and dead players to vote (#843).
- Fix a bug where players who switched to spectator mode immediately after dying would have 0 health and drop DM items (also occurs in the original game).
- Fix a crash that occurs when the server receives a gamespy cdkey authorize request.
- Fix a crash that could occur when starting a `jvbot` game.
- Fix a possible crash caused by scripts using `angles_pointat` (also occurs in the original game).
- Fix a rare glitch causing the player to be teleported to (0,0,0) and stuck (also occurs in the original game).
- Fix Actors not doing anything without client #0 (also occurs in the original game).
- Fix Actors squad formation.
- Fix an issue where clients could crash because of too many models being loaded from the server.
- Fix bad animation blending with portable turret.
- Fix bots being counted for voting.
- Fix death animation movement for bots.
- Fix holstered weapon attachment position (#847).
- Fix improper weapon falling rotation.
- Fix mismatched animation issues between the original clients and the server (#842).
- Fix players not having binoculars on Allied Assault.
- Fix players temporarily losing user input control on ARM-based OpenMoHAA servers when a message contained a special ASCII character.
- Fix some death animations flickering for original clients playing on an OpenMoHAA servers (#857).
- Fix some RCON programs not working with `dumpuser`.
- Fix the carryable turret disappearing when being dropped (#812).
- Fix the round limit being always `15` on Tug-of-War.
- Fix votes being messed up after calling a vote the second time during the match.
- Fix `wait` command not working on dedicated server binaries. Further commands would not be able to be executed (also occurs in the original game).
- Fix weapon select mod not working (#849).
- Fix wrong portable turret placement.
- Large vote configurations will no longer cause clients to disconnect (also occurs in the original game) (#858).
- The gamespy port `net_gamespy_port` now reflects the real port being used by the server (the issue also occurs in the original game).
- The player would spawn with no weapons on single-player when `sv_maxbots` was set (#783).
#### Clients
- Fix a crash caused when firing bullets to specific entities with no surface type (#831).
- Fix entities not being able to emit a light when attached to another entity (also occurs on the original Allied Assault but was fixed since Spearhead 2.0) (#824).
- Fix mapinfo (.min) files never being saved, resulting in the loading bar never showing on some maps (#832).
- Fix not being able to view sniper zoom when spectating.
- Fix sky being partially culled out on some single-player maps (#817).
- Fix the issue where the network would no longer work after hosting a multiplayer game on Allied Assault using the client.
- Fix wrong alignment for UI elements using `centery` (#848).
### Changed
#### Server
- Free-For-All is now the default gametype on dedicated servers (`g_gametype 1`) when unspecified, rather than single-player (the issue also occurs in the original game).
- Freezeplayer now freezes player animation/state, because frozen players would still emit footsteps sound when frozen.
- Nocliping when dead no longer makes the player animation sketchy.
#### Clients
- Slightly increase the brightness of red/green chat messages.
#### Misc
- Completely refactor CMake build system (from [ioquake3's](<https://github.com/ioquake/ioq3>) new CMake build system).
## [0.82.1] - 2025-08-05
### Fixed
#### Crashes
- Fix a crash that would occur when loading custom maps in some map packs (due to a UI incorrectly declaring a widget: loadingbar, twice) (#764)
- Fix an issue where the server could crash MOHAA base (1.11) clients in certain situations (#778)
- The game no longer terminate with error `FS_ReadFile with empty name`. (#772)
#### Drawing & rendering
- Fix force model always using an allied skin for dead bodies.
- Fix HUD being shown during briefings (#767)
- Fix in-game mouse cursor disappearing on some platforms (#765)
- Set a more distant fog for d-day by default (`set g_ddayfog 0`). This fixes an issue with sky artifacts in m3l1a. (#770)
#### Multiplayer
- Fix frequent telefrags on servers with a lot of players, like 25 players.
- Fix taunts being played as team rather than global (#775)
#### Miscellaneous
- Update third-party libraries like jpeg and zlib.
## [0.82.0] - 2025-08-05
### Added
#### General
- Commands `add`, `subtract`, `scale`, `append` and `bitset`. `bitset`, which comes from mohaab, is particulary useful to set flags for flag-based variables like `dmflags`.
- Logfile timestamping. It's enabled by default with the `com_logfile_timestamps` cvar. The current time is displayed at the beginning of each log entry.
- Setup files (.msi packages) are now distributed for Windows.
- Update check. The game will verify the latest version in the background (by querying the latest GitHub release of the repository) and print a
- Add the **Instant Action** feature from Breakthrough. This feature, although only usable through the Breakthrough UI, allows anyone to quickly find and join popular low-ping multiplayer servers.
#### Multiplayer
- Add `(all)` prefix in text message when a player is talking to all
- A message will be printed into console/log when a client changes their name.
- In Spearhead and Breakthrough, axis players can now select the kar98 sniper by choosing a german panzer skin (this was one of the feature of Daven's patch).
- The client number and their IP address will be printed next to each action (text message, joined the game...)
##### Bots
- [Recast Navigation](https://recastnav.com/) is now used by default to generate the navigation for bots. It's all automatic and works out of the box for any level, there is no need for `mp-navigation.pk3`. It provides a better and more accurate ways for bots to navigate through the maps.
- Bots can now take path that requires jumping, falling or climbing ladders.
- Bots will now try to get around small obstacles in front of them.
*Note: bigger obstacles are minimally supported, but may cause some issues in some area, like the West axis spawn in Omaha Beach.*
It takes less than 2 seconds to generate the navigation mesh on large maps like Berlin and Druckkammern.
If you have issues, executing `set g_navigation_legacy 1` command and then reloading the map will fallback to the basic pathnode-based navigation system.
### Fixed
#### Audio
- Updated OpenAL to version 1.24.3
- The sound quality has improved a little bit, the new version of OpenAL uses another sound resampler by default.
- Fix enhanced audio spatialization being forcefully enabled when it shouldn't for audio devices detected as headphones that aren't headphones.
#### General
- Fix `LOCALIZATION ERROR` spam.
- Fix an infinite loop that would occur during name sanitization.
- Fix a crash that would occur when using special menus (like admin menus).
- Fix memory leaks after the process has been running for a long time.
- Fix player name being cutoff at the first negative character
- Fix player being able to shoot bullets from under the LOD terrain (also occurs on the original game)
- Fix the `version` string, ensuring most software such as HLSW and mods work as intended depending on the target game version.
#### Client
- Fix bullet impacts having multiple effects sometimes.
- Fix cinematic not stopping on map change.
- Fix delayed SFX (such as smoke sound) being played on unrelated entities
- Fix `Illegible server message` error when firing some type of bullets on Spearhead and Breakthrough.
- Fix lean angle slightly not matching the original game.
- Fix localization issues.
- Fix Waffen skins not being usable on servers
- Player models (selected skins) will be reset to their default value in-game if it doesn't exist, and force models will not work if the model doesn't exist on the server.
- Reallow client `name` to be changed by the server. Names are often changed by administrators to fix duplicate names.
#### Multiplayer
- Correctly print the text of instant messages in the console/logfile.
- Fix AllocEdict when some Breakthrough maps are started in multiplayer mode (for co-op).
- Fix a crash related to spawnpoints.
- Fix a crash that would occur when playing a sound with subtitle without player #0.
- Fix a crash that occured when a player reconnects to the same slot (after timing out) and then displays the scoreboard
- Fix an issue preventing spawning next to spawn points with an offset. This lead to frequent telefrags with lot of players.
- Fix a crash that would occur when the player sets a first-person model as a skin, causing an infinite state loop (also occurs in the original game).
- Fix clients joining during intermission and having a corrupt `Time` value in the scoreboard (also occurs in the original game).
- Fix fog sometimes not updating on maps such as Gewitter.
- Fix incorrect number of wins shown for allies and axis in the scoreboard.
- Fix spawnpoints metrics.
- Fix missing player starts on some maps.
- Fix players temporarily having the name of another player when rejoining after losing connection (also occurs in original game)
- Fix projectiles with no owner getting removed when a player joins a team (also occurs in Spearhead and Breakthrough)
- Fix score not being able to have negative values in the result returned by the `status` server command.
- Fix scores being incorrectly sorted in round-based matches (they must be sorted by total kills).
- Fix some issues with laser mods, like the `Freeze-Tag` mode where the laser would not trace from the player sight.
- Fix `sv_precache` being ignored.
- Fix the issue where the text message gets empty when exceeding character limit.
- Fix the round ending incorrectly in mods like beach soccer.
- Fix weapon being dropped sometimes when switching weapons too fast (also occurs in original game)
- Increase text message character limit to 400 characters (taking the name of the player into account).
#### Game module
- Bazooka was never showing as the preferred weapon in singleplayer.
- Fix AI not shooting when squadmates are next to eachother in Spearhead and Breakthrough (also occurs in original game).
- Fix AI not moving to specific location when ordered.
- Fix AI misbehaving when curious.
- Fix AI moving too fast on Allied Assault.
- Fix AI having a notepad.
- Fix ammo type not properly localized.
- Fix an infinite loop that would occur on some maps when a vehicle start driving to a path.
- Fix an issue where legs animations (like idle pistol stand) would not loop.
- Fix an undefined behavior issue in some mods that could lead to infinite loop, like, `while (self.variable)` (where `self` is `NULL`).
- Fix a crash (MAX_GAMESTATE_CHARS) and a memory leak in long matches after many models are loaded. This would occur mostly in ToW maps during long matches, or in maps spawning many `Actor`.
- Fix a crash related to VehicleTurretGun having no vehicle owner (also occurs in original mohaab).
- Fix a crash that would occur when a turret shoots projectiles.
- Fix a crash when a player quits the server while in a vehicle (also occurs in original game).
- Fix damage blend not working in Spearhead and Breakthrough.
- Fix leaning not making a difference when trying to use.
- Fix mods like Elgbot server-side not working properly.
- Fix inconsistent cross-blending of torso animations.
- Fix "silent walk", where rapidly switching weapons would prevent emitting footstep sounds (also occurs in original game).
- Fix weapon banning not working properly when banning rifles.
#### Rendering
- Fix a crash that would occur when there are insufficient triangles for LOD terrain tessellation (also occurs in original game).
- Fix a possible crash when loading a map with lightmaps and then loading a map that has no lightmaps.
- Fix a rare crash that would occur when toggling the scoreboard (also occurs in original game, but was fixed in Breakthrough 2.30).
- Fix blurry text.
- Fix disconnected water lakes
- Fix console prompt being hidden when there are too many characters
- Fix line breaks occurring mid-word
- Fix long messages being cut off in the console
- Fix omaha beach shorelines being incorrectly rendered
- Fix the server list being requested twice on Allied Assault when browsing internet servers.
#### Server
- Fix `CM_AdjustAreaPortalState` error
- Fix clients rejected for too low/high ping not being able to reconnect again.
- Fix some programs (like gamedig) not being able to retrieve gamespy data from the server.
- Fix some programs not being able to retrieve the complete list of players
- Fix `SV_FindIndex: overflow` errors that would occur more often than the original game during long matches.
- Fix server crashing all clients under rare occasions.
### Changed
- Removed the architecture suffix from executables. They're now simply named `openmohaa` and `omohaaded`, for multiple reasons:
- Avoids conflicts with shipped third-party DLLs (like SDL, OpenAL) which have no suffix.
- Easier to build an installer and document.
- Makes launchers and automation scripts simpler (universal name for executable files).
- The architecture is already indicated in the archive name, on startup and in the `version` variable.*
- Older saves are incompatible due to changes from commit 4e8ddf3c358baef7cbfe2c8ff66c2426338cd2c7.
#### Client
- A random number is now added for the default name, like `UnnamedSoldier#1234`, or `*** Blank Name #1234 ***`.
#### Multiplayer
- All player models inside the `models/player/` folder will be cached in multiplayer. This avoids short lags/hitches when a player spawns with an un-cached model.
##### Master server
- Updated the master server to use [333networks](https://333networks.com/).
- Add support for multiple master servers. The server browser queries them all at once, and servers send updates to each. They are powered by the **333networks** community, managed independently. Even if one master goes down, others are used, ensuring constant availability of multiplayer.
- Iccups will occur less when fetching the server list if the network is really slow, or if the master server is unreachable.
#### Rendering
- On windowed mode, the hardware cursor is now used to navigate in the UI. This quickly allow users to get out of the window when bringing the console or the main menu up, instead of doing ALT+TAB.
#### Server
- The server is now showing the project version number in the server list.
## [0.81.1] - 2025-01-04
### Fixed
- Fix the text of pulldown elements being invisible on some menus
- Fix the server not taking the client rate into account and transmitting network fragments too fast, this may be the reason why some clients were unable to connect
## [0.81.0] - 2025-01-02
### Added
#### Audio
- Add support for IMA-ADPCM Wave sound files (4-bit compressed sound). Sounds such as sea waves (in the d-day landing mission) and subpen are now played accordingly
#### Client
- Add support for smaller lightmaps (BSP files having `_sml` suffix). Smaller lightmaps are enabled at startup when the texture detail is set to `Low` and if the map supports it (single-player levels of Spearhead and Breakthrough), useful for old low-end hardware.
- Add a new variable to customize the FOV client-side `cg_fov` (defaults to 80), the FOV is no longer an ephemeral server-side setting.
- The FOV is automatically adjusted depending on the aspect ratio
#### UI
- Automatically scale UI and HUD for resolutions above 1920x1080 (support for 2K and above). UI and HUD elements no longer look tiny on 4K resolutions
### Fixed
- The OpenAL library has been updated to version [1.24.1](https://github.com/kcat/openal-soft/releases/tag/1.24.1).
#### Client
- Fix the client getting stuck in the loading screen when disconnected due to an error
- Fix the client loading the map twice when the server restarts and change map immediately
- Fix the first person model not matching the player model
#### General
- Fix letters with diacritic not being parsed nor displayed correctly in Spearhead and Breakthrough.
- Fix a crash that would occur on some custom maps with a lot of grid patches (increased `MAX_PATCH_PLANES`).
- Fix a rare crash that would occur when rendering static models under certain configurations.
- Fix a stack overflow crash that would occur with a name of more than 30 characters.
#### AI
- Fix singleplayer AI not aiming at the enemy while taking cover
- Fix singleplayer AI not dropping weapons if forcedropweapon is set on the sentient and weapon drops are disabled in the level (should be better for Breakthrough single-player maps)
- Fix a random crash that would occur when using multiplayer bots
- Improved multiplayer bot logic
- Fix the match not ending after the clock reached 0:00 in round-based games
#### Game
- Fix doors closing immediately when blocked (this bug also occurs in the original game)
- Fix smoke grenades not reapparing when loading from save
- Fix vehicle tank tandem causing a crash when a single turret was attached
- Make players non-solid when they're a passenger in a vehicle (fixes players getting hit/damaged by the moving vehicle)
- Prevent the player from going out of the maximum world bounds in noclip mode (as in mohaas/mohaab)
- Prevent the player from shooting silently by firing and dropping the weapon at the exact same time (this is trick is possible in the original game)
- When a door is blocked (voluntarily by a player or not), it will now try to open again in the other direction, to make the door not impossible to open (this is also an issue in the original game)
#### Multiplayer
- Fix game messages not having a newline
- Fix letters with accents not displaying in the chat box
- Fix a bug that would make the player die or spawn with no inventory. This occurs when getting killed and respawning immediately (this bug also occurs in the original game)
- Fix an issue where remote clients would lose track of other clients team that have changed their name
- Fix players getting the score from previous maps if they reconnect at the right moment (this bug also occurs in the original game)
- Fix players deaths (or total kills) being added twice in round-based matches
- Fix spawn metrics being terrible in FFA (like players respawning right behind other players)
- Fix teamkill kick not displayed in Spearhead and Breakthrough
#### Server
- Fix the result of the `status` command which prevented tools like `Advanced Security` from getting IP addresses (note: `Advanced Security` doesn't support IPv6 addresses)
- Fix an issue where connecting clients would disconnect with the error `tried to parse cg message without cgame loaded`. This issue occurs in original 1.11, but was fixed in original 2.0 (Spearhead and Breakthrough)
#### UI
- Fix an issue when resizing an in-game window, the window would stop being resized when the mouse was rapidly moving
- Fix huddraw elements being wrongly positioned on wide screen when elements have virtualscale enabled
- Fix pulldown menus being improperly focused and activated at the same time
- Fix the crosshair being too small or hidden when a lower `Texture Detail` value (`r_picmip`) is set
- Properly handle long strings in the chat message box and in the game message box
### Changed
#### General
- Don't execute newconfig.cfg anymore as it overrides graphical settings.
- Some errors that show in console will only print in developer mode now (`developer` set to 1), as those are not relevant for the end-user.
- The `unnamedsoldier.cfg` configuration file will be used as a template for creating the `omconfig.cfg` configuration file if the latter doesn't exist
## [0.80.0] - 2024-11-24
### Added
#### Audio
- Add "tmvolume", so single-player scripts can adjust the music volume.
#### Client
- Add fuse stopwatch (Medal of Honor: Spearhead and Breakthrough feature).
- Add testemitter support for creating emitters.
- Briefings videos are now played in mohaas/mohaab.
- The game intro is now played when the game starts.
#### Game module
- Add a server feature to tweak in-game text messages and instant messages.
- Add a server feature to prevent unbalancing teams.
- Add Breakthrough liberation instant messages.
#### Rendering
- Add dynamic light support for terrains
- Add lighting for smoke FX
- Add support for sunflares and lensflares
- Improved the overall lighting. Objects should now be lit correctly (some issues could still occur)
- JPEG screenshots are now supported using the `screenshotJPEG` command
### Fixed
#### Audio
- Ambient sounds should now play correctly on all maps.
- Fix a crash when the sound quality was set to a value lower to `High` (which means 44 kHz).
- Fix an occurence where some sounds would loop forever. This would occur mostly when loading a saved game.
- Fix background ambient sounds being muted when restarting after the intermission (this is a bug in the original game).
- The ambient sound is now completely reset on each map, which means ambient sounds from previous maps won't be heard (this is a bug in the original game).
#### Client
- Better FX
- Fix a rare crash that would occur when loading a map.
- Fix an issue where delayed effects wouldn't spawn, like the K5 cannon in MOH: Breakthrough, e3l3.
- Fix animations issue when the player dies (weird shaking, etc).
- Fix barrel water/oil leaks not showing.
- Fix bullet holes disappearing on doors.
- Fix emitters leaking, this would cause a massive lag in a server with a lot of players.
- Fix issues with shaky animation in a multiplayer game, like when dying, the animation would repeat and look buggy.
- Fix the client not being able to play on standalone non-dedicated MOH: Breakthrough servers (which would prevent awesome LAN sessions).
- Fix the quick message mode window being shown as a full chat window.
- In mohaas/mohaab mode, team radar icon should now blink when the teammate is talking.
- It's now possible to navigate and play voices from the in-game voice menu.
#### Filesystem
- Fix pk3 ordering: for example, files starting with `zzzzz_name` must take precedence over `zz_name`. This should fix the issue where some mods would unexpectedly take precedence over other mods
#### General
- Fix a crash that could occur when no valid animation exist for a model.
- Fix long subtitles not being displayed.
- Lowered log verbosity to avoid spamming console with useless stuff. Set `developer` to 1 for more verbosity.
- Missing animations are no longer missing.
#### Rendering
- Dynamic lights are now rendered properly and they no longer cause rendering artifacts all over the place
- Fix a crash that would occur in terrain code on some platforms
- Fix static models being incorrectly culled/hidden
- Fix water and oceans being strangely deformed
- Improved the overall lighting. Objects should now be lit correctly (some issues could still occur)
- Sky seams and similar graphical artifacts should no longer be seen on most configurations
#### Server
- Don't send pak names to clients if download (`sv_allowDownload`) is disabled
- Fix animations not playing (like in e2l1) when they are played while the server is starting
- Fix an hang (infinite loop) that would occur when sv_fps is above 1000 (this bug also occurs in the original game)
- Fix entities being sent through skyportals
- Fix `status` returning too long columns for some programs
- Fix velocity being incorrect on ARM systems due to a signed integer overflow
- Reject demo clients on full servers
- Reject non-Breakthrough clients when targeting Breakthrough
#### UI
- Fix a bug where the game would get stuck in the loading screen if the loading screen doesn't have a "continue" button
- Fix bugs in the UI map rotation list when adding/applying
- Improved in-game Notepad support
- The UI should no longer lock up when refreshing/canceling the server list (this bug also occurs in the original game)
### Changed
#### Client
- As a protection, stufftext commands (commands that the server want clients to execute) are now filtered out, only some stufftext commands are allowed to prevent any alteration from a remote server.
- Animations now play smoothly, a lower timescale won't result in a poorly laggy animation anymore (this is an issue in the original game).
#### Game
- Don't allow lean in single-player in the base game.
- Don't count telefrags as teamkills (this is an issue in the original game).
- Fix a bug where the map would not end when the remaining time left reached 0:00 and the fraglimit was set
- Fix a crash that would occur when loading a saved game
- Fix a memory leak issue that can be seen after a long play time, some of the memory leaks also occur in the original game
- Fix AI animations
- Fix AI incorrectly holding a turret too far away
- Fix AI moving incorrectly after loading from save
- Fix AI not turning away from walls
- Fix AI not being able to see through a window frame
- Fix AI running too slowly, and a rare occurence where AIs would get stuck when going down the stairs
- Fix an issue where bullets would not hit anything behind triggers in Allied Assault, like in m2l3 from inside the vent
- Fix an issue where the player would be invulnerable when using heavy weapons, like the Granatwerfer
- Fix an issue where some AIs would not have a speech animation when speaking
- Fix barrel jittering
- Fix errors related to the script lexer & grammar
- Fix Flak88 and Nebelwerfer destroyed models in Spearhead and Breakthrough
- Fix jeep vehicle position on m3l2
- Fix ladder positioning (the issue would only occur on 64-bit systems)
- Fix player position on a vehicle turret gun (like the AA gun, or the Granatwerfer)
- Fix sentients still dropping their weapons even if the level has disabled weapon drops
- Fix some rare mods not loading properly due to an error in the script lexer
- Fix some script models not having any animation after loading from save
- Fix the barrel jittering
- Fix the roof top door on e1l3 (with Gino Corelli) opening when closing the handle (due to an incorrect returned result of a script command)
- Fix vehicle turret gun not making any sounds
- Fix vehicle turret gun being out of sync with the vehicle after loading from save (this issue also occurs in the original game)
- Fix weapon pickups where picking up a weapon with only 1 ammo remaining (like a grenade) would not pick the ammo up (this is a bug in the original game)
- Fix weapprev/weappnext/weapdrop switching to inventory item like binoculars
- Navigation fixes
#### Server
- Clients who are not sent to others (invisible + not sent to snapshots) won't be able to use anything like doors, and won't be able to send instant/voice messages
## [0.70.0] - 2024-09-02
### Added
#### Binaries
- This new version ships with binaries for PowerPC, PowerPC64 and PowerPC64 Little-Endian. Currently experimental and the client may not work correctly.
- For linux binaries, AARCH64 was renamed to ARM64
#### General
- Add a default idle animation for TIKI files not having any animation
#### Rendering
- Add support for DDS textures
- Add morphs: dynamic facial expressions for characters
- Implemented sky portal
#### Sound
- Add OpenAL support:
- Ambient sound (soundtrack)
- HRTF can be used on OpenAL by setting the speaker type to `Headphones`
- Loopsounds
- Music support (triggered music)
- Proper sound entity spatialization
- Streamed sounds - audio files (such as music) are streamed from disk without them being entirely loaded in memory
- Volume/Pitch variation
#### UI
- Add in-game Notepad (thanks to @pryon)
- Add screenshots for saved games
### Fixed
#### General
- Animation fixes
- Fix a crash related to localization (due to some strings containing a backtick)
- Fix some models having surfaces without a shader
- Fix entity collision
- Fix bad terrain/patch collision with grenades
- Fix improper collision on fences - it's now possible to target flak88 behind barbwires with binoculars
- Fix load/save game crashes
- Networking fixes
- Improved support for big-endian architectures (like PowerPC 32/64 bits and ARM in big-endian mode)
#### Client
- Fix entities being incorrectly attached sometimes
##### CGame module
- Bullet hit effects like snow, water and glass are fixed, as well as bullet tracers and flesh impacts
- Corrected Gewehrgranate viewmodel animations
- Fix occasional crashes due to volumetric smoke
- Fix marks not showing on entities like doors
- Invisible models fix
- Rain/snow should now be properly working
- Various fixes for special FX
#### Server
- Fix configstrings overflow on big maps, like t1l2 on MOH: Spearhead
##### Game module
- Door fixes, like door opening in the wrong direction
- Physics fixes, like grenade bouncing
- Fix AIs: they're less dumb than on previous versions
- Fix the artillery system (ProjectileGenerator) - fixes for t2l3, t2l4...
- Fix game loading/saving having wrong datas
- Fix the scoreboard on FFA and for spectators
- Fix the script engine - all scripts must be working now
- Fix an issue with spawnpoints where the player wouldn't spawn at the correct spot
- The mine detector correctly detect mines, in single-player
- The mission date (such as "JUNE 6, 1944") is now displayed
- The vehicle system is now fully working properly: no more vehicles getting stuck
- Singleplayer maps are all completable now (although some rare bugs may occur)
#### Rendering
- Fix crashes related to terrain
- Fix crashes related to `vid_restart` and `snd_restart`
- Fix strange lighting when `Full entity lighting` is disabled
- Lighting related fixes
- Most shaders should correctly load
#### UI
- Fix crashes related to UI
- Fix `vid_restart` breaking the UI
- Fix the multiplayer map picker displaying the same maps twice
- Player model picker should now be displaying characters properly
- Properly show the "disconnected" menu when the client get disconnected from a server
## [0.61.0] - 2024-01-08
### Added
#### Game Module
- Landmines are now fully working (a **Medal of Honor: Allied Assault Breakthrough** feature)
- Script command documentation can now be exported properly using `dumpallclasses` command
#### Client
- Add single-player map list and multiplayer map list
- Add map rotation list for multiplayer
- Add player model selection list
- Add LAN game searcher
- Add subtitles, centerprint, and locationprint
- Add the miniconsole
- Binds can now be set
### Fixed
#### Game module
- Crash fixes
- Fix callvote with invalid strings
- Fix grenade crash
- Fix weapons being attached to the wrong player's hand under some circumstances
- Players should now get the correct loadout in multiplayer
-`sv_fps` with a value higher than 20 shouldn't cause issues anymore with animations
#### Client
- 3D player models are now shown in the main menu screen
- Configstring can now be handled in multiple chunks
- Fix mohaas server version mismatch in the server list when using mohaab target game
- Fix vote menu not working
- Fix UI sounds not working
- Fix locationprint and centerprint not working
## [0.60.2] - 2023-11-29
### Fixed
#### Game Module
- Fix pain animations
- Fix portable turret animations
- Fix game crashing when saving and/or loading from save
#### ClientGame Module
- Fix for the client game module not hiding the stats screen
#### Server
- Fix level file archive
## [0.60.1] - 2023-11-15
### Changed
- The carryable turret (packed MG42 turret) can now be placed
### Fixed
- Fix bullet tracers, they should now be working correctly
- Fix a bug where player could get stuck in turret mode after the turret was removed
## [0.60.0] - 2023-11-15
### Added
- Add non-pvs feature (hide players from other clients who cannot see them)
- Add more SH and BT features
- Add ioq3 server features
- Mostly implemented AI Actor (still buggy)
- Implemented Vehicle and TurretGun from BT 2.40
- Implemented voting system
### Changed
- Configs are now stored in the homepath
- Binaries are now stored in the root folder (no need to replace existing MOH binaries anymore)
- Crashes fixes
- Rendering fixes, client fixes, game fixes and script engine fixes
## [0.58.1] - 2023-08-21
### Fixed
- Fix demo recordings not working with different protocols
- Publish `sv_fps` in server info variables
## [0.58.0] - 2023-08-20
### Added
- Multiplayer bot support
- Server browser
#### Spearhead and Breakthrough features
- Animated farplanes
- Bullets through wood/metal
- Smokes
- Sprint
- Compass radar (icon of teammates on the compass)
- Landmines
- Tug-of-War and Liberation game types
- ...
### Fixed
- Fix bugs, and crashes
- Fix training map not working
- The script engine should now be fully compatible with mohaa scripts
## [0.57.0] - 2023-07-25
### Added
#### Renderer
- Sphere lights for entities
- Light grid entity lighting
- Light grid support from Spearhead and Breakthrough
#### Game
- Allow/disallow leaning while moving (Spearhead and Breakthrough)
## [0.56.0] - 2023-07-23
### Added
- Add bullets, bullet trails, emitters, explosions, marks, model attachment, special effects, viewmodel anim
- Add fog, font, dynamic lights, shader fade, sprite, trees
### Changed
- Use a single executable to play on mohaa, moh_spearhead or moh_breakthrough servers, by starting openmohaa with `+set com_target_game x` commandline (check README)
### Fixed
- Fix disconnect menu not working
- Fix compass not working
- Fix crashes
## [0.55.3] - 2023-06-27
## [0.55.2] - 2023-06-27
### Added
- Add Breakthrough-compatible executable
## [0.55.1] - 2023-06-24
## [0.54.0] - 2023-05-30
### Added
- ioquake3 backport
- SDL2 support, which means input, rendering and sound improvements
- Better console support (using OS console host, rather than a custom Windows 95 console)
- Rendering improvement: sprite support, light glow support (light coronas), more shaders support, minor fixes, and beam fixes
- Sound improvement: MP3 support + better sound/format support
OpenMoHAA VR is an independent VR port of [OpenMoHAA](https://github.com/openmoh/openmohaa), with standalone Meta Quest releases and Steam Frame as a development target. There is no Steam Frame binary yet. These guidelines apply to [bearstonem/openmohaa-vr](https://github.com/bearstonem/openmohaa-vr); upstream maintains its own contribution policies and release process.
Start with the [port status](VR_PORT_STATUS.md), [repository layout](LAYOUT.md), and [renderer handoff](VR_RENDERER_HANDOFF.md). The status document separates device-tested behavior from changes that still need headset validation.
All contributions, including **issues**, **discussions**, **code comments**, **commit messages**, and **documentation**, must be written in *English*. This ensures clarity and consistency for all contributors.
There are many ways to contribute. Contributions are welcome in the following areas:
- Fixing [bugs and crashes](#issues).
- VR rendering, tracking, controller interaction, and HUD improvements.
- Performance improvements backed by measurements.
- Headset testing and reproducible regression reports.
- Documentation and build improvements.
- Steam Frame development and testing, with support claims kept explicit.
- Helping users and proposing enhancements through this repository's issues.
Discuss major features or architecture changes in a [port issue](https://github.com/bearstonem/openmohaa-vr/issues) before investing in a large implementation. Focused fixes, tests, and documentation improvements can be submitted directly.
---
## Issues
Before opening a new issue:
1. Check the [port releases](https://github.com/bearstonem/openmohaa-vr/releases) and [known issues](VR_PORT_STATUS.md). Upstream desktop releases do not include this port's VR changes.
2. Search [existing port issues](https://github.com/bearstonem/openmohaa-vr/issues).
Include the following details to help reproduce and diagnose an issue:
- A clear description and reproducible steps, including the mission/map and game edition.
- Headset model, headset OS/firmware, controllers, and OpenXR runtime/version when known.
- Port release and asset name, or the source commit and build configuration. Include the engine version from the startup log or `version` command as well.
- Relevant VR/render settings, mods, and whether the release's external `autoexec.cfg` was installed.
- Startup and failure logs, including `qconsole.log`, Android logcat, or runtime logs as appropriate. Include a backtrace for crashes.
- For visual or performance issues, whether one or both eyes are affected, and any captures or frame-time measurements.
For Quest logs, begin capture before reproducing the issue: `adb logcat -v threadtime -s openmohaa:V AndroidRuntime:E > openmohaa-vr.log`. See the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) for the full checklist.
Report port problems here first. If a problem also reproduces in upstream OpenMoHAA, provide that evidence and follow upstream's policies when reporting or contributing there.
## Tools and contribution responsibility
AI-assisted contributions are welcome in this fork. Contributors remain responsible for reviewing, understanding, and validating every submitted change, including generated code and documentation. Explain the problem, the resulting behavior, and the checks performed; distinguish source review, automated tests, and actual headset testing.
Keep changes focused and reviewable. Preserve license headers, copyright notices, and authorship attribution, and identify the source and license of imported code or assets. Do not submit proprietary game data. Tool use does not change these responsibilities.
This policy applies only to OpenMoHAA VR. It does not override upstream OpenMoHAA's independently maintained policies or authorize sending AI-assisted work upstream.
## Coding
### Compatibility
Preserve the engine's compatibility with the original game while adapting presentation and input for VR:
- Assets must be loaded correctly.
- Changes related to networking must be compatible with existing MOHAA clients and servers, these changes must not cause an error/disconnect.
- Scripts/mods must remain functional.
- Preserve single-player campaign behavior; report which missions and expansions were actually tested.
- Keep desktop and dedicated-server builds working when changing shared code.
- Do not change existing package IDs, config/save paths, network identifiers, or binary/module names solely for display branding.
### Coding style & Guidelines
- Match the existing code style in any files you modify.
- Avoid personal tags or author comments (e.g., `// @Name: note`). Git already tracks authorship.
- Build the affected targets and run relevant checks. State when a target or headset was unavailable.
- Follow the surrounding naming conventions, including engine subsystem prefixes such as `CG_`, `CL_`, `R_`, and `VR_`.
- Format changed code using the repository's `clang-format` configuration; avoid unrelated whole-file formatting.
- In VSCode from the command palette: **Format document**
### Event declaration
When declaring a new Event, use the following structure:
```cpp
EventEV_YourEventName// Pascal Case naming convention
(
"name",// Each parameter on a new line
flags,
"format specifiers...",
"argument names...",
"description"
);
```
### Code annotations
When modifying existing code from the original game or adding new classes, annotate your changes using one of the following patterns:
#### Additions
```cpp
// Added in OPM
// Description
```
#### Changes
```cpp
// Changed in OPM
// Description
```
#### Fixes
```cpp
// Fixed in OPM
// Description
```
#### Removal
```cpp
// Removed in OPM
// Description
```
If referencing a specific game version, replace OPM with that version number. Example:
```cpp
// Added in 2.11
// Make sure to clean turret stuff up
// when the player is deleted
```
Known versions are `1.00`, `1.10`, `1.11`, `2.0`, `2.1`, `2.11`, `2.15`, `2.30`, `2.40`.
You can also group multiple related functions like this:
```cpp
// Added in OPM
//====
voidFunction1();
voidFunction2();
voidFunction3();
//====
```
For new source files, include the annotation after the license notice.
### Source files
- Always put the license notice as the header of the file (template [here](docs/markdown/05-contributing/01-license-header.md)).
- Only `#include` what's necessary.
- Each source file should contain related classes and functions.
---
## Pull Requests
Pull Requests are the preferred way of submitting code changes.
### Before you start
1.**Discuss major changes first** in this port's issue tracker.
2.**Create a branch** based on this repository's current default branch, rather than upstream's `main`.
3.**Keep PRs as small as possible and focused**: One clear purpose per PR.
### Submitting a PR
- Target this repository's current default branch, unless a maintainer requests a different integration branch.
- Add a short and clear description of what you changed and why.
- Reference matching issues (e.g. `Fixes #123`).
- Make sure your code builds and passes tests.
- Test locally to ensure your changes don't break functionality or compatibility.
- For VR changes, list the headset/runtime, build, and scenarios tested. A successful build alone does not establish device support or a performance improvement.
### Reviews
- Maintainers may ask for tweaks or commit changes directly to your PR, that's normal.
- Stay polite and technical.
- Maintainers decide whether and when a change is ready to merge.
### Additional Notes
- Contributions follow the same license as the project.
- Keep things consistent with existing code and docs.
- Use the [OpenMoHAA VR issue tracker](https://github.com/bearstonem/openmohaa-vr/issues) for port questions and proposals.
| docker | Docker files, for compiling on various linux versions |
| docs | Documentation stuff |
| misc/ | Project stuff, definition files and various resources stuff. |
This fork contains the OpenMoHAA engine and its VR port. Meta Quest is the current standalone release target; Steam Frame is a development target with no binary available yet. Existing engine names, package IDs, and data paths remain in place for compatibility.
Start with [VR_PORT_STATUS.md](VR_PORT_STATUS.md) for current support and known issues. The older plan and renderer handoff record design decisions and historical measurements; they are not release support claims.
| Path | Description |
| --- | --- |
| [VR_PORT_STATUS.md](VR_PORT_STATUS.md) | Port status, tested behavior, known issues, and build/deployment notes. |
| [VR_PORT_PLAN.md](VR_PORT_PLAN.md) | Original Quest port survey and implementation plan. |
| [VR_RENDERER_HANDOFF.md](VR_RENDERER_HANDOFF.md) | Renderer migration and gl4es integration history. |
| [QUEST_PORTING_GUIDE.md](QUEST_PORTING_GUIDE.md) | Lessons and reference workflows from the Quest port. |
| `code/vr/` | OpenXR session, frame submission, tracking, and controller input; `vr_common.h` exposes the engine-facing API. |
| `code/cgame/` | Client game presentation, view models, and the grouped VR HUD in `cg_vrhud.cpp`. |
| `.github/` | Build/release workflows and issue templates for this repository. |
| `docs/` | Engine documentation and Android/VR integration notes. |
Use [CONTRIBUTING.md](CONTRIBUTING.md) for development conventions and report port issues in [bearstonem/openmohaa-vr](https://github.com/bearstonem/openmohaa-vr/issues).
OpenMoHAA VR brings the original game into room-scale VR: head tracking, controller-aimed weapons, tracked hands, physical interactions, and menus and HUD adapted for a headset. The game runs on the headset itself. A PC is needed for the documented sideloading workflow, not for streaming gameplay.
This is the VR development fork of [OpenMoHAA](https://github.com/openmoh/openmohaa). You must supply your own original game data; no Medal of Honor game assets are included.
| **Meta Quest** | Standalone Android/OpenXR prerelease available. Quest 3 is the headset tested so far; other Quest models are unverified. |
| **Steam Frame** | Standalone VR development target. Platform integration and device validation are still pending; there is no Steam Frame release yet. |
OpenMoHAA is still in its early days. The singleplayer part is not working correctly, the current usage is multiplayer.
The current VR backend uses Android, EGL, and OpenGL ES. The inherited desktop engine build is useful for development, but it does not establish Steam Frame VR support.
## Running
**This is a port in progress.** The latest code review passed desktop and Android builds and targeted regression checks. Its visual changes and performance impact have not yet been measured in a headset. Upstream campaign, expansion, mod, and multiplayer compatibility should not be read as a claim that each has been validated in this VR port.
Backup existing mohaa files, **cgamex86.dll** and **gamex86.dll** (set their extension to **.bak**)
## Download and install on Quest
a) extract archive to your MOHAA installation directory.
Get the APK, `autoexec.cfg`, and `SHA256SUMS` from the [VR releases page](https://github.com/bearstonem/openmohaa-vr/releases). The current release is [0.2.0](https://github.com/bearstonem/openmohaa-vr/releases/tag/vr-quest3-0.2.0).
-or-
1. Install the APK:
b) Copy all pak*.pk3 files and the sound/ directory from your **MOHAA/main** directory into openmohaa's 'main' directory.
```sh
adb install -r openmohaa-vr-quest3-0.2.0.apk
adb shell am force-stop org.openmoh.openmohaa
```
If you have problems running the game :
2. On a fresh installation, launch once from inside the headset to initialize the app's files directory, then close it. Supply the retail game's `main/` data under `/sdcard/Android/data/org.openmoh.openmohaa/files/main/`. See the [Quest build and installation guide](docs/markdown/04-coding/03-android.md) for the data-copy steps.
3. Copy the release configuration:
You may need to install Microsoft Visual C++ 2015/2017/2019/2022 Redistributable from
4. Launch the game from inside the headset, under Unknown Sources. Release 0.2.0 appears as **OpenMoHAA**; subsequent source builds use **OpenMoHAA VR**. Do not start it with `adb shell am start`.
You can now start a local OpenMOHAA server or play on a server.
**When upgrading, update the configuration too.** Reinstalling the APK keeps the old `autoexec.cfg`; back up personal changes before replacing it. The supplied configuration starts at the normal menu, disables the development cheats and test map, and includes the current lighting workaround. Let the app create its own `main/save` and `main/configs` directories.
To play expansions, append to the executable `+set com_target_game 1` for Spearhead, and `+set com_target_game 2` for Breakthrough. On windows you can make a shortcut.
The APK is debug-signed with optimized Release native libraries. Version 0.2.0 uses the same signing certificate as 0.1.0 for upgrades. Verify downloaded assets with `sha256sum -c SHA256SUMS`.
## Builds
## VR features
### Server
- Stereo OpenXR rendering and 6DoF head tracking.
- Controller-aimed weapons, tracked hands, stick locomotion, and snap turning.
- Body holsters, grip-driven ladder climbing, and physical grenade throws.
- Menus and mission/death screens displayed on a panel in the room.
- An on-demand HUD on the off-hand **X** button: compass, objectives, health, and ammo.
- Controller-based use interaction and a weapon-selection wheel.
The server version can successfully be built.
Some stuff are not working as intended, such as Actor or Vehicle. These systems are both almost fully implemented, but not really in stable state.
These systems are implemented; controller interactions, weapon alignment, and individual missions still need broader device testing.
Overall the server part and the game module are almost fully implemented, but the stability may vary, there may be some minor bugs and crashes.
## Known issues
### Client
- Some fonts can render as black boxes because of an unresolved font-atlas texture issue.
- Dynamic lights can darken the scenery. The supplied `autoexec.cfg` keeps `r_worldDlights 0`; the APK's defaults alone do not apply that workaround.
- Per-weapon grip alignment and remaining foliage behavior need further work.
- Fresh headset performance measurements are pending. Earlier Quest 3 measurements are historical, not benchmarks of the latest release.
- Steam Frame integration and validation remain to be done.
The client version of OpenMoHAA is partially implemented and not stable currently. The cgame module is partially implemented as well, but is compatible with the original MOH:AA game. Credits to **Heavy Metal: F.A.K.K. 2**'s SDK for the cgame module.
See [port status](VR_PORT_STATUS.md) for implementation details and the historical Quest measurements.
Here is the current working state for each part of the engine:
## Development
| Part name | Full | Almost | Half | Early | Bad | Not working | Comment |
| Server | | x | | | | | Probably a few bugs remaining |
| Server module (fgame) | | x | | | | | Actor, vehicle, and few gameplay bugs |
| UI | | | | x | | | Has core features only (menu, widgets, console, hud, urc loading) |
The active development branch is `vr-quest3`; the project targets both Quest and Steam Frame.
**Medal of Honor: Allied Assault Spearhead** and **Medal of Honor: Allied Assault Breakthrough** are supported in OpenMoHAA with the `com_target_game` variable. See the **Running** section for the usage.
## Compiling
OpenMoHAA supports both x86 and x64 architectures, and although the target platform is Windows, it still can be compiled for other platforms, and even other architectures.
These are the tools required for all platforms :
- CMake >= 3.5
- Flex and Bison (currently not used)
- A C++11 compiler is required.
The installation directory can be set to MOHAA directory.
The client build can be built by adding `-DWITH_CLIENT=1` to the CMake command line. This will compile the cgame module and the full app with rendering, ui and sound suppport, alongside the dedicated server version.
### Linux
These are the tools required on Unix :
- Clang >= 3.3 or GCC >= 4.8.1
**clang-3.5** and **gcc-4.8.5** should work (tested on Ubuntu 16.04). Use the latest possible version of those compilers.
SDL2 library is required (libsdl2-dev).
1 line install command with latest clang version : `sudo apt-get install -y ninja-build cmake make clang-15 lld-15 flex bison libsdl2-dev`
Example with **CMake**, **clang-15** and **ninja-build** installed:
Visual Studio (2019 or 2022) is generally preferred.
## Feedback and credits
## Screenshots
Report port bugs in [this repository's issue tracker](https://github.com/bearstonem/openmohaa-vr/issues). Include your headset, release or commit, map, steps to reproduce, and relevant logs. Steam Frame development reports should include the runtime and build configuration.
|  |  |
|  |  |
Built on the work of the [OpenMoHAA project](https://github.com/openmoh/openmohaa), [ioquake3](https://github.com/ioquake/ioq3), and the F.A.K.K SDK. [Team Beef's RTCWQuest](https://github.com/DrBeef/RTCWQuest) is the reference for much of the VR integration. The original engine documentation, assets, and attribution remain in the tree.
Engine source is licensed under [GPL-2.0-or-later](COPYING.txt). Medal of Honor game data is separately owned and is not distributed here. This is an independent community port, not an official Electronic Arts, Meta, or Valve release.
#elif ( defined(__APPLE__) && (defined(__arm64__) || defined(__aarch64__)) ) /* this might work on other ARM targets, but this is a known quantity... */
Loaded 100 of 2239 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.