Skip to content

WC3 Cinematic / Cutscene System

Developer cinematic cheats

In-map Warcraft III cutscenes are ordinary map JASS triggers. With sv_cheats 1, OpenRealm exposes a thin debugging layer over the same trigger/event machinery:

cinematic list [filter]
cinematic play <trigger-index> [selected]
cinematic stop

cinematic list is discovery only. It reports registered triggers whose action function names contain strong cinematic markers (cinematic, cutscene, intro, outro, ending, or interlude). It rejects obvious skip, time_stop/timestop, and cheat helpers. Generic victory and defeat names are intentionally not cinematic evidence because maps commonly use them for ordinary mission progression. The map does not carry a canonical cinematic-trigger table, so this heuristic may still miss unusually named cutscenes; use trigger list [filter] to inspect every registered trigger when that happens.

cinematic play deliberately shares trigger fire semantics: run the chosen trigger's authored actions directly, even if the trigger is disabled and without evaluating conditions. This preserves the real camera natives, transmissions, unit orders, waits, cinefilters, quest/result changes, and any later map logic. selected supplies the current selected unit as event subject when the authored actions require GetTriggerUnit() / unit-derived GetTriggerPlayer() context. Other event-specific response fields are not synthesized.

cinematic stop follows the existing Escape lifecycle instead of directly changing client_ui_state, camera state, or user control. It publishes EVENT_PLAYER_END_CINEMATIC for human players so the map's registered TriggerRegisterPlayerEventEndCinematic actions perform their own skip flag and cleanup. This is important because forcibly hiding the cinematic panel would not stop the running JASS coroutine or undo map-authored unit/camera state.

These commands cover scripted cutscenes inside the loaded map. The JASS PlayCinematic(movieName) path for prerendered movie files is separate and still depends on movie playback support.

Architecture

Cutscenes in Warcraft III are driven entirely by the map's JASS script (war3map.j). The engine provides JASS native bindings; the script orchestrates timing, camera, dialogue, and unit movement.

Pre-rendered campaign movies are a separate client media path driven by PlayCinematic; see Pre-rendered Movies. They do not use the in-engine cinematic HUD/camera pipeline described below.

Cinematic portrait and filter transport

The cinematic dialogue path sends the portrait model through the existing 16-bit player stat UI_PLAYERSTAT_CINEMATIC_PORTRAIT_MODEL (slot 29), so model indices above 255 survive; cinematic_portrait is only a presence flag. This reuses an existing stats slot and does not add player-state delta bits.

Cinematic filters use five player-state fields: cinefilter_image, cinefilter_color, cinefilter_blendmode, cinefilter_uv, and cinefilter_texmapflags, appended to playerStateFields so existing delta bit assignments stay stable. Every CinematicFadeBJ and CinematicFilterGenericBJ call sets a mask texture (White_mask, Black_mask, DreamFilter_Mask, ...), so all WC3 fades go through this path; WC3 no longer writes cinefade. G_RunClients() interpolates the authored RGBA. Plain fade masks (White_mask, Black_mask have no alpha channel) keep the authored alpha, so a fade to black reaches opaque. SetCineFilterTexture reads the mask's image header; masks with their own alpha (DreamFilter_Mask) draw at half the authored alpha, which matches the lighter retail vignette in footage of the Gul'dan glyph scenes. Alpha 0 means hidden (not displayed, or cutscene skipped). SetCineFilterBlendMode maps the JASS blendmode constants onto BLEND_MODE with G_BlendModeFromJass(): JASS has no ADDALPHA entry, so its MODULATE and MODULATE_2X values are one below the engine's. The start/end UV boxes are interpolated like the color (an empty box, from natives that never set UVs, means full UVs) and sent as NFT_BOX2; SetCineFilterTexMapFlags selects which axes repeat instead of clamping, so scrolling haze filters tile.

The client turns the filter (or a game's plain cinefade) into the generic viewDef overlay in SCR_SetupViewBlend(). Image 0 or an unknown image draws the tint untextured. The renderer draws it at the end of R_RenderView() while the scene viewport and scissor are still bound, so it covers exactly the 3D view, stays below every HUD layer, and is unaffected by r_hud. Mask textures keep their own alpha: DreamFilter_Mask is a JPEG BLP whose alpha is 0 at the centre and about 38% on average, which gives the Gul'dan vignette its clear centre.

This is a network protocol change; protocol 23 rejects older peers. The portrait meaning change is part of the serialized player contract, so the WC3 save version is 78; version 77 saves are rejected rather than migrated. Cinematic filter state is transient and is not persisted.

Flow

  1. Enter cinematic mode: JASS calls CinematicModeBJ(true, player) → ShowInterface(false) → sets client_ui_state = CLIENT_UI_CINEMATIC.
  2. Dialogue: TransmissionFromUnitWithNameBJ(...) → SetCinematicSceneBJ(...) → SetCinematicScene(...) → sets speaker/dialogue text on currentplayer.
  3. Unit movement: IssuePointOrderLocBJ(unit, "move", location) → unit_issueorder → order_move → unit_setmove(self, &move_move_walk).
  4. Per-frame update: monster_think → M_MoveFrame advances self->s.frame each game tick. ai_move_walk moves the unit.

ESC / Skip Mechanism

  • ESC is bound to cmd cancel (games/warcraft-3/share/config.cfg line 9).
  • CMD_Cancel (g_commands.c:199) publishes EVENT_PLAYER_END_CINEMATIC for the canceling player and all other human players.
  • The map registers a trigger via TriggerRegisterPlayerEventEndCinematic(trigger, player). When the event fires, the trigger sets a skip flag (e.g. udg_IntroSkipped = true) and calls cleanup: CinematicModeBJ(false, ...), SetUserControlForceOn(...), ResetToGameCameraForPlayer(...).
  • The main cinematic coroutine checks the skip flag after each TriggerSleepAction and returns early.

Skip Cutscene Cvar

The skip_cutscene cvar provides an engine-level fast-forward. When set to 1: - SetCinematicScene returns early (no dialogue) - TriggerSleepAction sleeps only 1ms - Camera durations forced to 0 - Cinefilter forced off

ShowInterface and EnableUserControl still honor the JASS booleans while the script fast-forwards. Keeping cinematic UI/input state prevents edge-scroll camera messages from racing the script: the final camera setup must land before cleanup restores input.

This is separate from the JASS-level ESC skip mechanism.

Key Files

File Role
games/warcraft-3/game/api/api_misc.h SetCinematicScene, EndCinematicScene, ShowInterface, EnableUserControl
games/warcraft-3/game/api/api_camera.h Camera control natives (all check G_SkipCutscene)
games/warcraft-3/game/api/api_cinefilter.h Cinefilter overlay (fades, masks)
games/warcraft-3/game/api/api_trigger.h TriggerSleepAction, TriggerWaitForSound
games/warcraft-3/game/api/api_unit.h IssuePointOrderLoc, SetUnitAnimation, SetUnitPosition
games/warcraft-3/game/g_commands.c CMD_Cancel — publishes EVENT_PLAYER_END_CINEMATIC
games/warcraft-3/game/g_main.c G_SkipCutscene() (cvar check), G_CineFilterColor(), G_RunClients() (camera lerp, filter state)
games/warcraft-3/game/g_ai.c unit_setmove(), unit_changeangle(), unit_moveindirection()
games/warcraft-3/game/g_monster.c M_MoveFrame() (animation clock), monster_think()
games/warcraft-3/game/skills/s_move.c order_move(), ai_move_walk()
games/warcraft-3/game/g_events.c G_ExecuteEvent() — dispatches JASS triggers
games/warcraft-3/jass/jdo.c jass_calltrigger(), jass_evaluatetrigger() — coroutine execution
games/warcraft-3/game/hud/hud_cinematic.c Loads FDF cinematic/message frames, binds runtime data, serializes HUD layers
games/warcraft-3/game/hud/hud_write.c C-constructed FT_FRAME+FT_TEXTAREA message overlay (size, font, inset, anchor)

Debugging

For the complete retail-map instrumentation, legacy repacking workflow, Wine launch command, failure modes, and low-noise sampling options, see Retail Warcraft III camera tracing.

Console Commands

  • skip_cutscene 1 — fast-forward all cinematic timing
  • skip_cutscene 0 — restore normal timing

Camera and transmission natives should not grow permanent investigative tracing; use the focused JASS/network tests below or a bounded campaign run to inspect their state. The pre-existing CLIENTCOMMAND(Cancel) path still prints its ESC/end-cinematic event publication, but this cinematic parity work adds no runtime debug logging.

Common Issues

Opening cinematic runs too fast after a mission/map load: The authored JASS waits and camera durations can be correct while the whole opening still fast-forwards if the generic server scheduler carries wall-clock loading time forward as simulation debt. SV_Frame advances at most one 100 ms game step per outer-loop call; without a backlog clamp, a multi-second synchronous map/resource load can leave sv.next_frame_msec seconds behind svs.realtime. At a high render rate the following outer-loop iterations then each advance 100 ms of JASS/camera time only ~15–16 ms apart until the debt drains. This was confirmed on Prologue01 on both an initial load and a same-process restart; skip_cutscene remained disabled and TriggerSleepAction retained the authored durations.

The fix belongs in the generic server scheduler, not the WC3 cinematic natives: SV_ClampSimulationDeadline rebases deadlines that are more than one fixed tick overdue before SV_RunGameFrame, discarding loading latency while preserving ordinary sub-tick lateness. See Server Architecture and server_net.scheduler_clamps_multi_tick_wall_clock_backlog.

A same-process restart also resets map/server time while the client renderer survives. V_AdvanceSceneTime therefore treats a backwards scene timestamp as a new render epoch and forces deltaTime = 0 for that frame; otherwise unsigned DWORD subtraction can produce a near-2^32 ms effect delta. This is a separate one-frame restart hazard, covered by net.scene_time_rewind_starts_a_new_render_epoch and documented in Client Architecture.

Mismatched player numbers in SetCinematicScene/EndCinematicScene: currentplayer is the GetLocalPlayer() presentation selector, not the owner of the unit that fired an event. Event ownership belongs in JASSCONTEXT.playerState for GetTriggerPlayer(); local-player execution lives in JASSCONTEXT.localPlayerState. TriggerExecute() must inherit those values independently.

Human02 provides a concrete regression case: the victory chain begins when the Blademaster (map player 4) dies, then executes nested victory/cinematic triggers whose local UI guards target the connected Human player (map player 1). If the dying unit owner is copied into currentplayer, the trigger and waits still run but the player-1 camera/UI/dialogue branches are skipped, producing an invisible victory cinematic before RemovePlayer(Player(1), VICTORY). The wc3_api.enemy_event_keeps_trigger_player_separate_from_local_player_context test covers this event-owner/local-player split through a nested TriggerExecute() chain.

ESC moves units but leaves both the cinematic camera and cinematic HUD active: After the entity/player contract trim (#162), an incremental build could leave libjass compiled against the old edict_t/GAMECLIENT layout. jass.h includes game/g_local.h; jass_eventplayer() dereferences unit->client->ps, but the original JASS make rule tracked only JASS sources and libshared, not game/common headers. The trigger's global actions still ran, while GetLocalPlayer() guards failed for camera/UI cleanup.

Confirmed on ROC Maps\Campaign\Human02.w3m: connection edict 0 represents map player 1. Before rebuilding the VM, ESC at 4200 ms left server and decoded client client_ui_state=2, cinematic quaternion and FOV 45; camera prediction was inactive. Rebuilding JASS without changing its behavior restored the same skip path: ResetToGameCamera and ShowInterface(true) ran for player 1, both sides returned to state 0, FOV 50, distance 1650, gameplay quaternion approximately (-0.292,0,0,0.956). This was a stale module ABI, not a missing snapshot field.

JASS_HEADERS in games/warcraft-3/game.mk now tracks the shared/game header dependency closure. Do not widen the network structs or force the client UI to game mode to mask this failure.

Regression checks:

make test-jass-build
make test-wc3-engine WC3_PATTERN='wc3_api.escape*'
make test

The VM test sends the real cancel command through the event queue, uses a nonmatching connection/map slot, checks an unrelated player's cancel does nothing, and asserts gameplay UI flags, control, target, FOV, distance and quaternion. net.cinematic_cleanup_restores_camera_and_ui_samples separately checks serialization through CL_ParseServerMessage. The build test uses make -n -W <header>; removing JASS_HEADERS makes it fail on common/shared.h.

For bounded campaign verification, create a config containing 150 wait lines, cmd cancel, 80 more wait lines, then screenshot. Launch once without -tft and once with it:

build/bin/openwarcraft3 -data 'data/Warcraft III' +map 'Maps\Campaign\Human02.w3m' +exec /tmp/escape.cfg +com_frame_limit 280

Use the ESC binding's command (cmd cancel), not skip_cutscene, which exercises a different lifecycle. See build/platform contracts for incremental-build and pixel-format constraints.

TransmissionFromUnitWithNameBJ not showing dialogue: ForceEnumPlayers must populate the force for IsPlayerInForce(GetLocalPlayer(), ...) guards in Blizzard.j. If empty, all transmissions are skipped silently.

Transmissions flash too fast: TriggerWaitForSound must sleep the full millisecond duration, not a fraction.

Cinematic HUD layers hidden: CLIENT_UI_CINEMATIC hides portrait, console, command bar, info panel, inventory via UI_LayoutShouldSkipLayoutLayer in client/cl_unit_layout.c.

Cinematic chrome/text visible but talking-head portraits missing: Warcraft unit portraits use a companion model (Foo_Portrait.mdx) stored in cl.portraits[] beside the world model in cl.models[]. Initial CS_MODELS configstrings must remain registration data until CL_PrepRefresh(), because that pass loads both files. Do not eagerly populate cl.models[] from CL_ParseConfigString() before cl.refresh_prepped: CL_PrepRefresh() skips non-null model slots, which otherwise prevents every companion portrait from loading. Once refresh is prepared, a dynamic model configstring must release and reload both the world-model and portrait-model slots together. SetCinematicScene transports the normal model configstring index; FT_PORTRAIT prefers cl.portraits[index] on the client.

Fast-forward ends at the last cinematic camera position: Log server playerState.vieworigin, client camera prediction, client_ui_state, and no_control together. If gameplay input becomes active before the final camera native, edge scrolling can send clc_camera_position during the accelerated script and overwrite the authoritative snap. skip_cutscene must shorten timing only; the JASS cleanup owns the UI and input transition.

DisplayText Message Overlay

DisplayTextToPlayer and its timed variants pass message content and (x,y) screen position to UI_ShowText. The server constructs the message overlay as a static C FRAMEDEF pair (FT_FRAME root with FT_TEXTAREA child) in hud_write.c. The C code owns the full-screen parent, text-area size, font, inset, and default anchor as inline float values. A valid JASS position overrides only the copied text frame's anchor for that serialized message. Missing or out-of-range coordinates retain the default anchor.

Branch Maintenance

WC3 gameplay features live on main as wc3: commits. Feature branches that diverge from main will miss these fixes. Before testing WC3 campaign maps, always check that the branch is up to date with main:

git log --oneline main..HEAD | wc -l   # commits behind main
git log --oneline HEAD..main | wc -l   # commits ahead of main

Key wc3: commits to watch: - 6cd01ebd — cinematic dialogs/portraits (ForceEnumPlayers fix) - 55724517 — collision & pathfinding parity - 76b701a4 — JASS natives (camera, events) - 4a56a651 — gradual unit turning - dcac4868 — authentic collisionSize

Implementation Notes

Unit Movement During Cutscenes

Units move using the same system as normal gameplay: JASS scripts issue move orders via IssuePointOrder/IssuePointOrderLoc. The pathfinding and collision system handles the rest. For cutscenes with many simultaneous movers (e.g. 8 footmen), the destination-keyed heatmap cache (16 slots) and unreachable-cell skipping prevent performance issues.

Camera Control

Camera follows units via SetCameraTargetController. The camera interpolation runs in G_RunClients() each frame, lerping position/FOV and other scalar fields between camera.start_time and camera.end_time. Euler angle components use CL_GameLerpDegrees() with WC3 shortest-periodic-arc interpolation rather than raw numeric lerp. This is required because Warcraft-authored angle-of-attack values can represent the same orientation with numbers separated by whole revolutions: for example CameraSetupSetField(..., 304) stores pitch -394, while ResetToGameCamera uses 326; those differ by 720 degrees but are equivalent modulo 360. Raw lerp makes the camera rotate under the terrain and back over it during cinematic cleanup. The snapshot carries Euler viewangles; the client converts those to quaternions and slerps between 10 Hz samples. The G_UpdateCameraTarget function follows target_controller unit's position plus offset. When inheritOrientation is true, the authoritative camera also follows the unit facing. WC3 entity state stores unit facing in radians, while camera rotation is authored in degrees, so the runtime first converts with RAD2DEG and then applies the same authored-camera convention (viewangles.z = 90 - facingDegrees). The controller's behaviour is one cameraTargetMode_t in camera.target_mode: CAMERA_TARGET_FOLLOW, CAMERA_TARGET_FOLLOW_FACING (inheritOrientation), or CAMERA_TARGET_ORIENT (SetCameraOrientController). Clearing or replacing the target controller resets it to CAMERA_TARGET_FOLLOW.

PanCameraToWithZ and PanCameraToTimedWithZ author a camera-target Z offset in addition to X/Y. G_RunClients() interpolates that offset and the near/far clip planes alongside the other camera values, and the WC3 player snapshot transports all three to the client. The server camera sample remains exact; during map registration the renderer builds a separate blurred terrain source, and the client applies that source to the current camera XY while preserving the server-authored terrain-relative Z offset, interpolated between snapshots. Terrain-derived camera Z is not temporally smoothed. Prediction rebases each sample before changing its XY; see camera samples. Camera setups support CAMERA_FIELD_NEARZ, CAMERA_FIELD_FARZ, and CAMERA_FIELD_ZOFFSET; the ...WithZ apply variants override the setup Z offset with their explicit argument. CameraSetupApply(..., doPan=false, ...) and CameraSetupApplyForceDuration(..., doPan=false, ...) preserve the current target position while still applying camera fields, matching Warsmash's separation between setup fields and destination panning.

CameraSetTargetNoise, CameraSetSourceNoise, and their ...NoiseEx variants are game logic, not client logic. The natives store magnitude, velocity, and vertOnly in gameClient_t.camera.noise[] (saved with the client record). G_RunClients() evaluates the oscillator once per 10 Hz server frame in G_CameraNoiseOffset() and writes the result to the generic playerState_t.viewoffset (target noise) and playerState_t.eyeoffset (source noise). The shared client knows nothing about noise: it interpolates the two most recent offset samples and adds them to the look-at point and derived eye while building the view matrix. This is the Quake 2 p_view.c split for view kicks; see network.md.

The offsets never touch vieworigin, viewangles, or distance, so the scripted camera state, interpolation endpoints, controllers, StopCamera, camera getters, input focus movement, and target_height all keep reading a steady camera. Passing zero magnitude or velocity clears the corresponding noise. vertOnly constrains the noise to world Z, as requested by Blizzard's earthquake helper. Retail's exact waveform is not documented, so OpenRealm uses a deterministic oscillator (constants in g_main.c, marked TODO) while preserving magnitude/rate inputs and source-versus-target independence. Because the waveform is sampled at the server frame rate and interpolated linearly, the shake cannot contain motion faster than 5 Hz; Blizzard.j earthquake velocities are far above that and therefore read as per-frame pseudo-random jitter. Do not move the evaluation into client/ to gain frequency; if a retail trace shows a faster shake is required, raise it with the developer as a contract question.

ResetToGameCamera clears both source and target noise channels as part of returning to ordinary gameplay camera state. Map scripts may call the reset without an explicit CameraClearNoiseForPlayer; leaving either channel active would keep the shake running after the reset. The regression wc3_api.reset_to_game_camera_clears_source_and_target_noise covers both channels and their stored flags.

SetCameraOrientController uses the same game-owned controller slot as SetCameraTargetController, but with distinct ownership semantics. Activating it samples the realized in-flight camera, captures that camera's source/eye, and then keeps the source fixed while the unit plus X/Y offset determines the look direction. The authored target distance is preserved: OpenRealm moves the virtual orbit target along the look ray instead of moving the camera source to the unit. Manual camera movement, ResetToGameCamera, a replacement target controller, or removal of the controlled entity clears the controller. This is deliberately not implemented as SetCameraTargetController(..., inheritOrientation=...), because that native moves the camera target with the unit rather than keeping the source stationary.

SetCinematicCamera remains separate work because it requires playback of an authored MDX camera track. The renderer parses and samples MDX cameras for model/portrait views, but retail sequence selection, end-of-track behavior, multiple-camera selection, and interaction with subsequent scalar camera natives are not established enough to make the world-camera handoff authoritative. Keep that limitation explicit rather than inventing model-camera timing semantics.

SetCameraField and AdjustCameraField reuse the authoritative camera-state interpolation: a new field transition starts from the sampled in-flight camera state, and StopCamera freezes an active timed transition at that sampled state instead of snapping to either endpoint. Setter/adjuster angular and FOV values remain in Warcraft's authored degree units; the already-implemented runtime GetCameraField keeps its observed retail contract and reports angular/FOV values in radians from the realized player camera. Local pitch/yaw/roll camera fields remain unsupported, matching the camera-setup path.

SetCameraQuickPosition is not a camera movement native. It records Warcraft's spacebar/quick-position recall point for the local player. Assigning that point must leave camera.state.position unchanged; treating it like SetCameraPosition makes quest discovery and cinematic scripts unexpectedly jump the view when they only intend to set the later spacebar target.

The generic minimap client consumes Space while recent alert positions exist. When history is empty, the normal WC3 SPACE binding sends the game-owned quickcamera command; the server reads the authoritative scripted quick position and applies it through normal camera bounds/state handling. See alerts-and-minimap-pings.md.

Camera target bounds are map-global. G_SpawnEntities() initializes level.camera_bounds from the four W3I camera-bound points; SetCameraBounds replaces that one rectangle for every client. GetCameraBoundMinX/Y and GetCameraBoundMaxX/Y read it. Do not mutate level.mapinfo->cameraBounds: W3I remains immutable initial map metadata. Do not put the rectangle on playerState_t.

W3I stores its four integer camera complements in left, right, bottom, top order, while the JASS CAMERA_MARGIN_* selector constants are left, right, top, bottom. mapCameraBounds_t.complement deliberately follows the W3I on-disk order because CM_ReadInfoInto() reads the structure directly. The complements crop the complete W3E terrain to the playable rectangle; they are not camera margins themselves. GetCameraMargin() first reconstructs that playable rectangle from CM_GetWorldBounds() plus the complements, derives the axis-aligned default camera rectangle from the eight W3I camera-bound floats, and returns the gap between those two rectangles on the requested side. This matches Warsmash's GetCameraMargin: playable-map area and default camera bounds are separate inputs.

Do not implement GetCameraMargin() as complement * TILE_SIZE. World Editor generated main functions already start from playable-area edge constants and add/subtract GetCameraMargin() before calling SetCameraBounds; using the complement width there applies the unplayable terrain border a second time. Once SetCameraBounds became authoritative in OpenRealm, that old approximation manifested as the camera stopping roughly a viewport too early at the map edge. Warsmash's GameCameraManager.applyVelocity() also clamps its camera target directly to the rectangle supplied by SetCameraBounds, so the correct fix is to reproduce the native margin calculation rather than expanding the runtime clamp by a guessed viewport offset.

Ordinary server camera target writers use G_ClampCameraPosition() against level.camera_bounds. SetCameraOrientController is the deliberate exception: its camera.state.position is a virtual orbit target placed on the look ray so the captured camera source stays fixed, and clamping that virtual point would translate the source instead of merely constraining player panning. The client clamps predicted user-movement targets with CM_GetWorldBounds() — the loaded map, not a per-player snapshot. CL_ParsePlayerInfo() reclamps pending prediction to those world bounds before comparing it with the authoritative server origin.

Scripted target panning is independent from scalar camera-field interpolation. PanCameraTo and PanCameraToWithZ read Warcraft's UI\MiscData.txt [CameraRates] Strafe and Forward values and move X/Y independently at those authored world-unit rates, matching Warsmash's GameCameraManager.panTo(). PanCameraToTimed* derives one X rate and one Y rate from the requested duration so both axes arrive at the destination at that duration. CameraSetupApply(setup, doPan=true, panTimed=true) and CameraSetupApplyWithZ use the same normal-rate pan path; a non-timed setup apply snaps its destination, while the ForceDuration variants retain their explicit duration interpolation. The pan track owns target XY only, so a timed Z offset or other scalar field can advance concurrently without forcing all camera properties through one shared movement rate. StopCamera, direct player movement, reset, and target/orient controllers sample or cancel the active pan rather than leaving stale movement behind.

The [CameraRates] values are Warcraft data, not OpenRealm tuning constants. Production reads them through the existing merged MiscData cache; the WC3 test fixture authors small deterministic Strafe=100 / Forward=200 values so JASS tests can assert per-axis movement without depending on a retail MPQ installation.

The camera bounds constrain the target, not the visible frustum. Do not widen SetCameraBounds to compensate for a view that appears to stop too early. WC3's world projection must instead use the actual gameplay viewport above the command console. V_RenderView() assigns {0, 0.22, 1, 0.76} to both viewDef.viewport and viewDef.scissor; Matrix4_getCameraMatrix() derives aspect ratio from that viewport rather than the full window. Renderer screen rays (R_LineForScreenPoint) and drag-selection normalization use the same viewport, so ground picking, middle-dragging, minimap camera-footprint projection, and the visible world all agree. A full-window projection followed only by scissoring narrows the horizontal ground footprint and leaves the camera target vertically off-center inside the visible WC3 world area; near map edges this can look like an over-tight camera clamp even when the JASS bounds themselves are correct.

The August 30, 2026 viewport change (9c22952, wc3: match retail camera viewport and bounds semantics) also established a presentation contract for post-world overlays. R_RenderView() calls R_RevertSettings() before drawing 2D overhead bars, and V_RenderView() draws the drag-selection marquee after re.RenderFrame(), so both paths are back in full-window GL state even though their content belongs to the world. Those overlays must temporarily re-apply viewDef.scissor and then restore the full-window scissor before SCR_DrawLayout() draws the HUD. Do not move the WC3 HUD boundary into renderer constants or rely on opaque console art to hide world overlays; the client-authored viewDef.scissor remains the authoritative rectangular world boundary.

The classic console is not itself rectangular: the retained info/status panel, portrait/minimap region, build queue, and command-card buttons can protrude above the flat world-scissor bottom while still being authored HUD. Drag selection therefore has one additional client-side rule. SCR_LayoutClampSelectionRect() examines the visible retained layout frames that extend into the bottom console and constrains the marquee's moving corner before the rectangle is used for both drawing and EntitiesInRect(). This keeps the selection box out of transparent status/command-card art without changing the camera viewport or teaching the shared renderer Warcraft-specific button coordinates. Full-screen world games naturally skip this rule because their world scissor already reaches the bottom of the UI canvas. See economy-and-unit-presentation.md for overhead-bar presentation details.

Regression coverage:

make test-wc3-engine WC3_PATTERN='wc3_api.camera*'
make test

The focused JASS cases cover camera bounds, shortest-arc Euler interpolation (including equivalent multi-turn pitch values), timed WithZ interpolation, normal-rate untimed panning from authored CameraRates, panTimed camera-setup panning, target-controller orientation inheritance, setup clip/Z fields, and doPan destination ownership. The full test target additionally covers net.camera_clamp_uses_world_bounds, net.playerstate_camera_render_fields_roundtrip, net.playerinfo_copies_server_clip_planes, and net.camera_prediction_reconciles_to_server_clamped_bound. The server composes the camera look-at into playerState.vieworigin (XY focus plus the retained retail target reference and JASS Z-offset) and always sends znear/zfar with distance, including spawn and ResetToGameCamera defaults (WC3_CAMERA_DEFAULT_FOV/DISTANCE/NEAR_Z/FAR_Z in games/warcraft-3/common/ui_constants.h). CL_GameDefaultCamera uses that same set. The client copies that vec3 into the camera sample; it does not keep a previous clip or invent 100/5000. It does not rebuild Z from the heightmap. viewangles travel on the player snapshot; camera target bounds do not. Unused WoW map metadata is carried by one map-info configstring. Do not add a WC3-only camera_render field. Retain the existing cinematic cleanup tests because all of this camera state is client-visible. Camera JASS regression tests run against the deliberately minimal games/warcraft-3/tests/resources-src/Scripts/common.j; when a test uses a common.j constant or native, add its real declaration/value to that fixture. In particular, GetCameraMargin requires the integer CAMERA_MARGIN_LEFT/RIGHT/TOP/BOTTOM selectors (0/1/2/3), while clip/Z tests require the real CAMERA_FIELD_FARZ, CAMERA_FIELD_ZOFFSET, and CAMERA_FIELD_NEARZ converted handles. Leaving those globals undefined passes the wrong JASS value type to the native before the assertion can run.

Visual viewport-overlay verification must cover both archive modes because the HUD art can differ while the world boundary contract must not:

build/bin/openwarcraft3 -data 'data/Warcraft III' -roc +map 'Maps\Campaign\Human02.w3m' +com_frame_limit 1200
build/bin/openwarcraft3 -data 'data/Warcraft III' -tft +map 'Maps\Campaign\Human02.w3m' +com_frame_limit 1200

During each bounded run, drag a world selection marquee downward through the command console and select a living unit near the lower world edge. Overhead health/mana bars must stop at viewDef.scissor. The marquee must stop earlier wherever retained bottom-console frames protrude above that rectangular boundary, including the status/info panel and command-card/build-button area; no console pixel may reveal the marquee.

Widescreen cinematic chrome

The ordinary gameplay HUD remains authored inside the centered 0.8 x 0.6 4:3 safe area. Cinematic letterbox chrome is different: CinematicTopBorder and CinematicBottomBorder must cover the complete widened UI canvas while the portrait, speaker text, and dialogue retain their original safe-area coordinates. The server marks only those two backdrops with UIFLAG_EXTEND_WIDESCREEN_X; SCR_LayoutRect() then replaces the marked frame's horizontal rect with x=0 and w=SCR_UICanvasWidth() after normal FDF anchor resolution. At 4:3 this is still exactly 0.8, so the behavior is a no-op.

A 2880x1620 trace demonstrated the failure mode: canvas width was 1.06667, HUD root was x=0.13333,w=0.8, and both cinematic borders resolved to x=0.13333,w=0.8 with valid human-options-menu-background.blp and human-cinematic-border.blp textures. Therefore missing left/right cinematic fill is a geometry-policy issue, not missing art. Do not widen CinematicPanel itself: doing so would move safe-area portrait/dialogue anchors.

Actor Presentation

SetUnitScale(unit, x, y, z) follows Warsmash's current compatibility behavior: the WC3 XYZ API is rendered as a uniform scale using the X component. Y/Z are consumed for JASS signature compatibility but are not independent axes in the current entity renderer. Do not use the Z argument as the uniform scale; campaign scripts commonly pass equal values, which can hide that mistake until custom cinematic scaling uses distinct components.

Cinefilter

Full-screen overlay effects (fades, blurs) use SetCineFilterTexture/SetCineFilterStartColor/SetCineFilterEndColor/SetCineFilterDuration/DisplayCineFilter. G_RunClients() publishes the filter image, interpolated RGBA and blend mode in player state; the renderer draws them as the view overlay over the 3D viewport, below the HUD (see Cinematic portrait and filter transport above).

The client-side fade is physical-screen presentation, not centered 4:3 HUD content. SCR_DrawLayout() must cover { x=0, y=0, w=SCR_UICanvasWidth(), h=UI_BASE_HEIGHT }, the full canvas scene; under the widened (1.30+ data) policy that is wider than the centered HUD root returned by SCR_LayoutSceneRect(), under the classic stretched policy both are the authored 0.8 width. Renderer projection and input use that same canvas (ui-canvas.md); the world camera retains the physical viewport aspect.

Human02 Victory-Cinematic Runtime Findings

A September 2026 trace of the Human02 victory sequence showed the cinematic trigger chain itself continuing correctly through camera moves, sleeps, unit orders, dialogue, cleanup, and level transition. The highest-confidence functional gaps were narrower:

  • IssueImmediateOrder(..., "mirrorimage") returned false, leaving the campaign's later illusion target null. AOmi now runs through the ordinary no-target spell pipeline, creates timed illusion copies, and publishes summon events so the campaign trigger can capture GetSummonedUnit().
  • scripted holdposition now routes through the same Hold Position state transition as the command card instead of returning false from IssueImmediateOrder.
  • owner-only cinematic sounds and minimap pings must resolve an explicitly supplied client edict before comparing Warcraft player numbers to engine connection slots. The old slot-only lookup caused owner unavailable / no client failures even for the connected local player.
  • SetSoundVolume, SetSoundPosition, and AttachSoundToUnit now feed the existing one-shot sound packet. Campaign MP3 dialogue uses the generic one-shot decoder; stop/fade, volume groups, and thematic music remain separate audio gaps.

The same trace reported authored .mdl effects and .tga cinefilter masks as missing at the diagnostic lookup layer. Do not special-case those names in cinematic code: the renderer already retries .mdl as .mdx and missing .tga textures as .blp. A diagnostic that probes only the authored name can therefore report a false missing asset even when render-time fallback succeeds.