Warcraft III Pre-Rendered Movies
Contract
Classic Warcraft III campaign movies are client presentation, not simulation state. JASS chooses a logical movie with
PlayCinematic("HumanEd"); games/warcraft-3/game/api/api_misc.h converts that to the Warcraft asset convention
Movies\\HumanEd.mpq and calls the generic game_import.QueueMovie callback. The game module must not link FFmpeg or
decode video itself.
QueueMovie is a deferred-session interposer. If the same simulation frame later requests ChangeLevel, EndGame, or
another gi.MenuAction, client/cl_main.c freezes the outgoing server simulation, plays the queued movie, then performs
the preserved map/menu action after EOF or Escape. This avoids replacing the world while JASS is on the C stack and also
avoids advancing the outgoing map behind a full-screen movie.
SetCinematicScene / triggered dialogue are unrelated in-engine presentation. They do not use this movie path.
Optional FFmpeg Backend
The default build has no FFmpeg dependency. Enable the movie decoder with:
The Warcraft III build then requires only these pkg-config packages:
client/cl_movie.c owns demux/decode, A/V scheduling, Escape-to-skip, archive extraction, and full-screen presentation.
The renderer exposes a Quake-II-style DrawCinematicFrame operation. The client supplies decoded RGBA pixels and the destination rectangle; the renderer owns the persistent upload texture and draws it. Passing NULL releases that renderer-owned texture. The sound mixer exposes a generic 44.1-kHz stereo S16 stream slot, following the same ownership split as Quake-style cinematic audio. Movie and music streams are independent; see music.md.
The decoder first asks FS_ResolveLoosePath() for a real disk path. This lets libavformat stream a normal loose
Movies\\*.mpq file without loading the whole movie into memory. If the movie is found only inside an archive,
FS_ExtractFile() copies it to the per-user openrealm-movie.tmp file for the duration of playback and removes that file
when playback ends. Do not hold FS_OpenFile() open for a movie: the archive API's global file lock is intentionally
short-lived and would block unrelated asset reads.
The backend is format-driven. Do not special-case the .mpq filename extension in the decoder; libavformat probes the
actual media container. Warcraft's movie files use the historical movie naming convention even though the payload is
media rather than a normal game-data MPQ archive.
For direct diagnosis from the console:
The first form expands to Movies\\HumanEd.mpq. A build without FFMPEG=1 keeps the command but reports that movie
playback is disabled.
Renderer And Audio Flow
PlayCinematic("HumanEd")
-> gi.QueueMovie("Movies\\HumanEd.mpq")
-> client defers pending MenuAction
-> CL_PlayMovie
-> libavformat
-> libavcodec
-> swscale -> RGBA -> renderer dynamic texture
-> swresample -> stereo S16/44100 -> S_StreamSamples(S_STREAM_MOVIE)
-> EOF or Escape
-> resume preserved MenuAction
Movie rendering uses the renderer's actual UI scene rectangle and letterboxes/pillarboxes to preserve the source aspect
ratio. While active, movie input consumes keyboard/mouse interaction so hidden gameplay/menu controls cannot be
activated. Escape terminates playback. The system cursor is hidden while the movie is drawn. If background music is active, movie startup suspends it before the movie PCM stream starts; EOF/Escape restores the prior music pause state. The separate S_STREAM_MOVIE and S_STREAM_MUSIC buffers prevent either decoder from resetting the other.
Campaign Selection Metadata
Warcraft campaign data has three movie-like fields in each campaign section:
The Warsmash reference parser (CampaignMenuData) reads each field with exactly the same three-column shape as a mission:
and stores them separately from the numbered Mission0, Mission1, ... list. Current Warsmash parses these three fields
but its current mission-list construction iterates only getMissions(), so the reference code does not itself provide a
finished movie-selection implementation.
OpenRealm's games/warcraft-3/menu/screens/single_player.c now parses those three records into separate campaign cinematic
slots. As a first integration step, builds compiled with FFMPEG=1 add each authored cinematic to the existing mission
MapListBox. The temporary rows follow the campaign structure rather than the declaration order in the data: IntroCinematic
and OpenCinematic precede the numbered missions, while EndCinematic follows the final mission. The temporary diagnostic
presentation is deliberately plain text:
The menu normalizes logical movie names such as HumanEd to Movies\HumanEd.mpq and invokes the existing typed
menuImport.PlayMovie(path) callback when that row is selected. If a cinematic filename already contains a directory it
is preserved; a bare filename with an extension is placed under Movies\.
These rows are compiled out of the mission list when BZ_FFMPEG is absent, so a normal build made without FFMPEG=1
continues to show the unchanged mission list. Parsing the metadata is still harmless in that build and keeps campaign data
handling independent from the decoder capability.
This first-pass list intentionally does not claim retail availability semantics: all three authored cinematic records are shown when FFmpeg is enabled, regardless of mission-play filtering. That is useful for validating data parsing and playback before progression persistence exists. The retail-compatible follow-up remains:
- Replace the temporary
Cinematic:rows with the Warcraft camera-button FDF art (CampaignListBox/StandardCampaignCameraButton*). - Gate Open/End rows with the state written by
SetOpCinematicAvailable/SetEdCinematicAvailable; those natives are still stubs, so unlock persistence must be implemented before claiming retail-compatible availability. - Determine whether Intro availability is always data-authored/default-open or has its own progression rule in the original client before hiding it behind a guessed state bit.
- Keep campaign/mission availability (
wc3_campaign_visibility=unlocked) separate from cinematic availability. Campaign progress records authoredSetCampaignAvailable/SetMissionAvailablestate; an unlocked cinematic is a different progression fact.
Known Pitfalls
PlayCinematicis notSetCinematicScene; one is pre-rendered movie playback, the other is in-engine dialogue/cinematic UI.- Do not synchronously play or change maps from the JASS native. Both operations cross client/session boundaries only
after
SV_Framereturns. - Do not load an entire movie with
FS_ReadFile()on memory-constrained targets. Prefer a loose disk path or temporary extraction so FFmpeg can seek/stream normally. - Do not expose FFmpeg types through
server/game.h,client/menu.h, or renderer public structs. FFmpeg remains a client implementation detail underBZ_FFMPEG. - The camera-selection rows are not safe to expose as "unlocked" until
SetOpCinematicAvailableandSetEdCinematicAvailablehave persistent state.
Verification
The implementation is designed to be tested by the developer with original game data; automated tests should not depend on local Warcraft movies. Useful manual cases are:
Then verify:
- aspect ratio is preserved with black bars where necessary;
- audio is synchronized closely enough to dialogue/video and does not underrun;
- Escape skips immediately and the hidden menu/game does not receive the key;
- a campaign
PlayCinematicfollowed byChangeLevelplays the movie before loading the next map; - a campaign
PlayCinematicfollowed byEndGameplays the movie before rebuilding the frontend; - a build made without
FFMPEG=1has no FFmpeg link dependency and continues the deferred map/menu transition if movie playback is unavailable.