diff --git a/docs/engine/cd-player.md b/docs/engine/cd-player.md index f23db095..69ac290d 100644 --- a/docs/engine/cd-player.md +++ b/docs/engine/cd-player.md @@ -24,19 +24,18 @@ player, and the driver runs its loop (`Driver.listen` in **Improvements:** - RANDOM TRACK MODE draws the next piece from `std.Random`, among the page's other pieces. The game - draws `rand() % 12` up to 20 times for one other than the piece that ended, and plays that piece - again where none is. + draws `rand() % 12` up to 20 times to find a piece other than the one that ended, and plays the + same piece again if it finds none. - Mods can change the lists: a load script sets which piece each row plays and its name, and adds tracks after the last, up to 96 in all ([The CD player's tracks](../guide/scripting.md#the-cd-players-tracks)). The game's are fixed. -- The lists are pages of the records' tracks, twelve to a page: the Reliant's, the Yamato's, then - the tracks mods add, the last page holding what's left. A menu script can turn the page - ([The rooms](../guide/scripting.md#the-rooms)). The rows are then numbered as the records number - the tracks, 13 to 24 on the Yamato's page and on from 25 after it, and a page's rows without a - piece hold nothing to choose. The player keeps the piece chosen - by its place among all the tracks rather than by its row, so the pieces that follow it come from - its own page, and it shows lit only there. The game keeps the row alone, and numbers each list - 01 to 12. +- The lists are pages of the records' tracks, twelve per page: the Reliant's, the Yamato's, then + the tracks that mods add, with the rest on the last page. A menu script can turn the page + ([The rooms](../guide/scripting.md#the-rooms)). The rows are numbered as in the records: 13 to 24 + on the Yamato's page, and from 25 on after it. Rows without a piece can't be chosen. The player + remembers the chosen piece by its position among all the tracks rather than by its row, so the + pieces that follow it come from its own page, and it is lit only on that page. The game only + remembers the row, and numbers each list 01 to 12. - The pointer is drawn after what scripts draw over the player, so that it stays on top. ## The screen diff --git a/docs/engine/radio.md b/docs/engine/radio.md index 409d4a75..c2882638 100644 --- a/docs/engine/radio.md +++ b/docs/engine/radio.md @@ -348,11 +348,11 @@ instructor speaks in the reminders and the launch's words, and answers PERMISSIO A line plays through the speech sample ([Sound](sound.md#speech)), its peaks rounded off and in the cockpit's cabin unless `--original`. -**Improvement:** a mod's recording of a line, named after it in any of the formats that -[Lines](../guide/modding.md#lines) lists, plays in its place as recorded, at its own rate and in its -own channels, in the same room and matched to the same loudness. The radio looks for a recording -before a line in the game's codec (`recordings.Source.read`), and FFmpeg's decoders decode one that -isn't a WAV file, as they decode the crew's lines. +**Improvement:** a mod's recording of a line, named after the line and in any of the formats that +[Lines](../guide/modding.md#lines) lists, replaces it and plays as recorded: at its own rate and in +its own channels, with the same reverb, and matched to the same loudness. The radio looks for a +recording before it reads a line in the game's codec (`recordings.Source.read`), and FFmpeg decodes +a recording that isn't a WAV file, as it decodes the crew's lines. Without a speech sample, as with `--no-sound`, a line still lasts as long as it would play, so its face shows and a mission's script that waits for it waits as long ([Speech](sound.md#speech)). diff --git a/docs/engine/rooms.md b/docs/engine/rooms.md index a435ce5f..151b65df 100644 --- a/docs/engine/rooms.md +++ b/docs/engine/rooms.md @@ -6,7 +6,7 @@ The hub a single-player campaign comes back to between missions: the Reliant's r [`game/interface/rooms.zig`](../../src/engine/game/interface/rooms.zig) holds the rooms and the news report, [`rooms/views.zig`](../../src/engine/game/interface/rooms/views.zig) their views, which `tablegen rooms` reads out of the executable (`make room-tables`), and [`rooms/crew.zig`](../../src/engine/game/interface/rooms/crew.zig) the crew ([The crew](#the-crew)). [`induction.zig`](../../src/engine/game/interface/induction.zig) holds the induction, [`in_game_options.zig`](../../src/engine/game/interface/in_game_options.zig) the in-game options, [`restart.zig`](../../src/engine/game/interface/restart.zig) the restart screen, [`game/winmain.zig`](../../src/engine/game/winmain.zig) what `WinMain` does as a campaign starts (`CampaignStart`) and after each mission (`afterMission`), and [`game/gameflow.zig`](../../src/engine/game/gameflow.zig) the campaign it carries from one mission to the next. The driver runs each in a loop of its own ([`openreliant/rooms.zig`](../../src/openreliant/rooms.zig)), and plays the movies between them as the game plays them ([Movies](movies.md)). -The pilot roster's START GAME leads into the rooms, through the induction for mission 1; its LOAD GAME, into the rooms before the loaded game's mission, as START GAME would, through the induction where that is mission 1 ([Front end](front-end.md#the-saved-games)). The briefing room's door leads to the briefing ([Briefing](briefing.md)), and then to the mission, after the hangar's movie. Use ITAC opens the ITAC ([The ITAC](itac.md)), Enter Simulator Pod the pod ([The simulator pod](simulator-pod.md)), Open Locker the locker ([The locker](locker.md)), and Use CD player the CD player ([The CD player](cd-player.md)). The in-game options' MAIN MENU leads back to the main menu, and QUIT quits. After the mission the campaign goes on through the rooms to the next briefing, or through the restart screen to the same mission ([After a mission](#after-a-mission)). While the window is away, the rooms pause the music and the voices, as the message pump does. The game's pump also waits for the window to come back, but the rooms keep running, and the paused music counts as playing, so the CD player doesn't take it for the end of a track ([Output](../port/sound.md#output)). +The pilot roster's START GAME leads into the rooms, through the induction for mission 1; its LOAD GAME, into the rooms before the loaded game's mission, as START GAME would, through the induction where that is mission 1 ([Front end](front-end.md#the-saved-games)). The briefing room's door leads to the briefing ([Briefing](briefing.md)), and then to the mission, after the hangar's movie. Use ITAC opens the ITAC ([The ITAC](itac.md)), Enter Simulator Pod the pod ([The simulator pod](simulator-pod.md)), Open Locker the locker ([The locker](locker.md)), and Use CD player the CD player ([The CD player](cd-player.md)). The in-game options' MAIN MENU leads back to the main menu, and QUIT quits. After the mission the campaign goes on through the rooms to the next briefing, or through the restart screen to the same mission ([After a mission](#after-a-mission)). While the window is inactive, the rooms pause the music and the voices, as the message pump does. The game's pump also blocks until the window is active again. In OpenReliant the rooms keep running, and the paused music counts as playing, so the CD player doesn't take the pause for the end of a track ([Output](../port/sound.md#output)). As the rooms open, they write the pilot's profile with the call sign ([The pilot's profile](../formats/profile.md)). @@ -26,7 +26,7 @@ As the rooms open, they write the pilot's profile with the call sign ([The pilot - The loudest peaks of Enriquez's scenes are rounded off, as the radio's lines' are ([Radio](radio.md)). `--original` cuts them flat. - The rooms' sounds, the hum, the steps, the doors and the crew's lines, ring subtly in a small room of the ship, and Enriquez's scenes on the television and the monitors as the radio's voices do, in the cockpit's cabin, as over a speaker ([Sound](../port/sound.md#openal-soft)); the game plays them dry. `--no-reverb` and `--original` leave the rooms out. - Mods can change the views: a load script sets their movies, labels, exits, hotspots, actions and sounds ([The rooms' views](../guide/scripting.md#the-rooms-views)), which the rooms read from the records (`rooms.install`). The game's are fixed. -- Mods' scripts can tell which view the player is in and which place's screen shows, and draw over the rooms and the places' screens ([The rooms](../guide/scripting.md#the-rooms)). The pointer of the rooms, the ITAC, the simulator pod, the locker and the CD player is drawn after what scripts draw, so that it stays on top. +- Mods' scripts can read which view the player is in and which place's screen is showing, and draw over the rooms and the places' screens ([The rooms](../guide/scripting.md#the-rooms)). The pointer of the rooms, the ITAC, the simulator pod, the locker and the CD player is drawn after what scripts draw, so that it stays on top. ## The carrier diff --git a/docs/engine/sound.md b/docs/engine/sound.md index 8fdb506f..b13bfb48 100644 --- a/docs/engine/sound.md +++ b/docs/engine/sound.md @@ -149,12 +149,12 @@ it; it keeps its full volume half as far again (`sound3d.MissileSound.follows`). **Improvement:** the game's rates are made for its 3D sounds, which are recorded at 22,050 Hz, so a mod's sound at another rate would play at another pitch: an octave low at 44,100 Hz. OpenReliant -scales the rates a mod's sound plays at by its own rate over 22,050 (`sound3d.rateScale`), whether -it replaces one of the bank's sounds or is a ship type's or a gun's own. The game's own sounds play -at the game's rates, including `CLOAK01`'s, which is recorded at 44,100 Hz and plays an octave low. -A voice is freed once its sound has played at the rate it plays at: its frames over 22,050, scaled -for a mod's sound, so that a mod's sound at another rate or in stereo isn't stopped early or held -on (`hog_snd.Sound.update3D`). +scales the rates a mod's sound plays at by the sound's own rate divided by 22,050 +(`sound3d.rateScale`), whether it replaces one of the bank's sounds or belongs to a ship type or a +gun. The game's own sounds play at the game's rates, including `CLOAK01`, which is recorded at +44,100 Hz and plays an octave low. A voice is freed once its sound has finished at the rate it +plays at: its frames divided by 22,050, scaled for a mod's sound, so a mod's sound at another rate +or in stereo isn't cut short or kept too long (`hog_snd.Sound.update3D`). ### The player's engine @@ -216,7 +216,7 @@ the pieces of its lists, and turns their level ([The CD player](cd-player.md)). ## Script sounds -Not the game's: player and menu scripts play sounds on the game's voices +Added by OpenReliant: player and menu scripts play sounds on the game's voices ([Sound](../guide/scripting.md#sound)). Each script sound gets an ID, which its voice stores (`hog_snd.Starter`). A game sound that takes the voice clears the ID. diff --git a/docs/formats/fat.md b/docs/formats/fat.md index d019bcfd..6f436789 100644 --- a/docs/formats/fat.md +++ b/docs/formats/fat.md @@ -66,22 +66,22 @@ The payload loads these banks by name: | Bank | What it holds | |---|---| -| `stdsmp.fat` | The menus' and the flight display's sounds, which scripts play by number too, and the sounds of a frame placed around the camera ([Sound](../engine/sound.md#positional-sounds-of-a-frame)) | +| `stdsmp.fat` | The sounds of the menus and the flight display, which scripts can also play by number, and the sounds of a frame placed around the camera ([Sound](../engine/sound.md#positional-sounds-of-a-frame)) | | `smp3d.fat` | The 3D sounds: guns, missiles, explosions, engines, jumps and more, which `sltool fat ls` names ([3D sounds](../engine/sound.md#3d-sounds)) | | `betty.fat` | Betty's warnings in the cockpit | -| `ldsmp.fat` | The loadout's | -| `inter\itac\itacsnd.fat` | The ITAC's | -| `vrsnd.fat`, `wlksmp.fat` | A carrier's rooms': the ship's hum and the ways into some views; the player's steps and the doors | -| `vrsfx.fat`, `waitloop.fat` | The briefing's: the way in's doors and the room's chatter; the sound that plays while it loads | -| `newsloop.fat`, `rlanda.fat` to `rlande.fat`, `ylanda.fat` to `ylande.fat` | The landings': the news reports' loop, and a sound for each outcome of a landing on the Reliant or the Yamato | +| `ldsmp.fat` | The loadout's sounds | +| `inter\itac\itacsnd.fat` | The ITAC's sounds | +| `vrsnd.fat`, `wlksmp.fat` | A carrier's rooms: the ship's hum and the ways into some views; the player's steps and the doors | +| `vrsfx.fat`, `waitloop.fat` | The briefing: the doors on the way in and the room's chatter; the sound that plays while it loads | +| `newsloop.fat`, `rlanda.fat` to `rlande.fat`, `ylanda.fat` to `ylande.fat` | The landings: the news reports' loop, and a sound for each outcome of a landing on the Reliant or the Yamato | ## Mods' sounds A mod's sound replaces one of a bank's sounds. It's named after the bank and the sound's number, as `sltool fat extract` saves it (`fat.soundName`), and is a WAVE, FLAC, MP3, Ogg Vorbis or Opus file -([Sounds](../guide/modding.md#sounds)). As OpenReliant reads a bank, it makes a new one in memory -with the mods' sounds in place of the bank's own, each entry keeping its priority (`fat.rebuild`, -`hog_snd.BankFile.of`), so the game plays them as it plays its own. A sound the drivers can't play -stays the bank's own, and the log says so. +([Sounds](../guide/modding.md#sounds)). When OpenReliant reads a bank, it builds a new one in +memory with the mods' sounds instead of the bank's own, each entry keeping its priority +(`fat.rebuild`, `hog_snd.BankFile.of`), so the game plays them like its own. If the drivers can't +play a mod's sound, the bank's own sound stays, and the log says so. **Improvement:** the original plays only its banks' own sounds. diff --git a/docs/formats/flac.md b/docs/formats/flac.md index ab2950d3..db7241aa 100644 --- a/docs/formats/flac.md +++ b/docs/formats/flac.md @@ -27,8 +27,8 @@ The first block is always the STREAMINFO, 34 bytes long: file's rate and channels, which a voice or the music plays (`bink.AudioDecoding`). The whole file goes to the decoder as one packet: the decoder reads the marker and the metadata, then decodes one frame at a time. Samples of more than 16 bits keep their top 16 bits, as FFmpeg's own conversion -does. A line of speech decodes all at once, and a piece of music a few seconds of music each frame -of the game, starting once it's decoded ([The driver](../port/sound.md#the-driver)). +does. A line of speech is decoded all at once, and a piece of music a few seconds at a time, once a +frame, starting once it is fully decoded ([The driver](../port/sound.md#the-driver)). -Where a mod has a recording in more than one format, OpenReliant takes the first in the order that +If a mod has a recording in more than one format, OpenReliant uses the first in the order that [Music](../guide/modding.md#music) gives (`recordings.Source.find`). diff --git a/docs/formats/mp3.md b/docs/formats/mp3.md index 8e5ee3a7..b42b26bb 100644 --- a/docs/formats/mp3.md +++ b/docs/formats/mp3.md @@ -11,9 +11,9 @@ file the discs hold is MPEG-2 audio, Layer III, at 22,050 Hz in joint stereo, 64 [`formats/mp3.zig`](../../src/formats/mp3.zig) reads a file's frames, and FFmpeg's MP3 decoder ([Platform](../port/platform.md#movies)) decodes them one at a time into a WAVE file of 16-bit PCM at the first frame's rate and channels, which a voice or the music plays (`bink.AudioDecoding`). A -crew's line decodes all at once (`bink.Codec.decodeMp3`), and so does a mod's line of speech in MP3 -([Lines](../guide/modding.md#lines)). A mod's piece of music in MP3 decodes a few seconds of music -each frame of the game, and starts once it's decoded ([Music](../guide/modding.md#music)). +crew's line is decoded all at once (`bink.Codec.decodeMp3`), and so is a mod's line of speech in MP3 +([Lines](../guide/modding.md#lines)). A mod's piece of music in MP3 is decoded a few seconds at a +time, once a frame, and starts once it is fully decoded ([Music](../guide/modding.md#music)). ## Frames diff --git a/docs/formats/ogg.md b/docs/formats/ogg.md index 300aa475..4278d3d6 100644 --- a/docs/formats/ogg.md +++ b/docs/formats/ogg.md @@ -23,7 +23,7 @@ A file is a series of pages (RFC 3533), each with a header of 27 bytes: | 26 | 1 | Number of segments | The lengths of the segments follow, one byte each, and then the segments. A segment shorter than -255 bytes ends its packet, so a packet runs over as many segments, and pages, as it needs. The +255 bytes ends its packet, so a packet spans as many segments, and pages, as it needs. The checksum is a CRC-32 with the polynomial `0x04C11DB7`, starting at 0, neither reflected nor inverted, over the whole page with the checksum's own bytes as 0. @@ -48,15 +48,15 @@ samples that were encoded. ## In OpenReliant -[`formats/ogg.zig`](../../src/formats/ogg.zig) reads the pages and checks their checksums, puts -together the packets of the file's first stream, and reads the stream's headers. It passes over -bytes that start no page, pages that are cut short or damaged, and any packet that a missing page -cuts. FFmpeg's Vorbis and Opus decoders ([Platform](../port/platform.md#movies)) then decode the +[`formats/ogg.zig`](../../src/formats/ogg.zig) reads the pages and checks their checksums, +reassembles the packets of the file's first stream, and reads the stream's headers. It skips bytes +that don't start a page, pages that are truncated or damaged, and any packet that a missing page +cuts off. FFmpeg's Vorbis and Opus decoders ([Platform](../port/platform.md#movies)) then decode the packets into a WAVE file of 16-bit PCM at the stream's rate and channels (`bink.AudioDecoding`). The Vorbis decoder takes the three headers laced together: the byte 2, the lengths of the first two headers in the way a page gives its segments' lengths, then all three headers. The Opus decoder takes the identification header, drops the pre-skip and applies the output gain. OpenReliant then -cuts the samples past the stream's end, the last page's granule position less the pre-skip, so that -a sound or a piece of music loops without a gap. A line of speech or a sound decodes all at once, -and a piece of music a few seconds of music each frame of the game, starting once it's decoded +cuts the samples past the stream's end, the last page's granule position minus the pre-skip, so +that a sound or a piece of music loops without a gap. A line of speech or a sound is decoded all at +once, and a piece of music a few seconds at a time, once a frame, starting once it is fully decoded ([The driver](../port/sound.md#the-driver)). diff --git a/docs/guide/modding.md b/docs/guide/modding.md index c04a7d2b..6e1d66c5 100644 --- a/docs/guide/modding.md +++ b/docs/guide/modding.md @@ -374,13 +374,13 @@ Movies in modern formats are planned ([#1179](https://github.com/OpenReliant/ope A piece of music is a WAV file (16-bit PCM or IMA ADPCM), or a FLAC, MP3, Ogg Vorbis or Opus file, at any rate, named after the game's piece: `New_Pensive.wav`, `New_Pensive.flac`, `New_Pensive.mp3`, `New_Pensive.ogg` or `New_Pensive.opus` replaces the game's `music\New_Pensive.wav`. A `.ogg` or -`.opus` file can hold either Vorbis or Opus. If a mod has more than one, OpenReliant takes the first -in that order: WAV, FLAC, MP3, Ogg, then Opus. FLAC keeps the music exactly as recorded in about two +`.opus` file can hold either Vorbis or Opus. If a mod has more than one, OpenReliant uses the first +in this order: WAV, FLAC, MP3, Ogg, then Opus. FLAC keeps the music exactly as recorded in about two thirds of the space of 16-bit WAV. MP3, Vorbis and Opus take less space still, but lose some of the -sound. A piece in any format but WAV decodes over the next few frames, so that the game doesn't -stall, and starts once it's decoded, a fraction of a second later. If the game's piece loops back to -a point partway through, the mod's piece loops back to the same moment in the music, whatever its -format. Make it the same length as the game's piece, or at least as long as its loop point, to keep +sound. A piece in any format other than WAV is decoded over the next few frames, so the game +doesn't stall, and starts once it is decoded, a fraction of a second later. If the game's piece +loops back to a point partway through, the mod's piece loops back to the same point in time, +whatever its format. Make it the same length as the game's piece, or at least as long as its loop point, to keep the loop where the game has it. If the game's piece can't be read, the mod's piece loops back to the game file's byte offset instead, which may be the wrong moment, and the log says so. [`examples/mods/music`](../../examples/mods/music) replaces the main menu's music with a track made @@ -402,14 +402,14 @@ smp3d.fat` names the 3D sounds that play each of that bank's sounds. If a mod ha sound, the bank's own sound plays, so a mod can replace just a few. A sound is a WAV file (16-bit or 8-bit PCM, or IMA ADPCM), or a FLAC, MP3, Ogg Vorbis or Opus file, -at any rate, mono or stereo. If a mod has more than one for a sound, OpenReliant takes the first in +at any rate, mono or stereo. If a mod has more than one for a sound, OpenReliant uses the first in the same order as for music ([Music](#music)). MP3 adds a little silence at the start and the end, which you hear when a sound loops, such as an engine's, so the other formats suit sounds better. A sound plays wherever the game plays the bank's, with the bank's priority, and at the volume, pitch -and place the game gives it. A 3D sound plays at its own pitch whatever its rate. In stereo, it -keeps its width as far as what it comes from has a size: a big ship's engine close by sounds wide, -and narrows to a point as the ship moves away, while a shot, a missile or an explosion is a point. -With `--original`, which mixes the sound plainly, a 3D sound in stereo plays in mono so that it can +and place the game gives it. A 3D sound plays at its own pitch, whatever its rate. A stereo 3D +sound is only as wide as the thing it comes from: a big ship's engine sounds wide close by and +narrows to a point as the ship moves away, while a shot, a missile or an explosion is a point. With +`--original`, which uses the plain software mixer, a stereo 3D sound plays in mono so that it can still be placed. Scripts can also play a sound file from their mod by name, in any of these formats, in 2D or 3D ([Sound](scripting.md#sound)). @@ -706,9 +706,9 @@ The game keeps its lines in `ms_speech\msspeech.hog`, under names without an ext `ABRT_001`. A mod's line replaces the game's line with the same name. `sltool speech extract ms_speech/msspeech.hog lines` saves every line as a WAV file, to find the one to replace. -A mod's line is best a recording, a WAV file or a FLAC, MP3, Ogg Vorbis or Opus file named after the -line: `abrt_001.wav` or `abrt_001.opus`, say, replaces `ABRT_001`. If a mod has more than one, -OpenReliant takes the first in the same order as for music ([Music](#music)). The line plays as +The best way to replace a line is with a recording: a WAV, FLAC, MP3, Ogg Vorbis or Opus file +named after the line. For example, `abrt_001.wav` or `abrt_001.opus` replaces `ABRT_001`. If a mod +has more than one, OpenReliant uses the first in the same order as for music ([Music](#music)). The line plays as recorded, at its own rate, mono or stereo, with the same effects as the game's lines: the radio's sound for a radio line, and a briefing's last word at the narration's loudness. WAV files can hold 16-bit or 8-bit PCM or IMA ADPCM, which audio editors write by default. FLAC is about half the size @@ -873,8 +873,8 @@ When the player flies it, it can also give: picture at two or four times that keeps it sharp. A picture left out keeps the base's shape. - `WingIcon`, the name of the picture the wing's window shows the ship as, `teapoticon_000.png`, drawn over the base's icon's rectangle. -- `EngineSound`, a WAV file, PCM or IMA ADPCM at any rate, that loops as the engine's sound, - pitched and loudened by the throttle as the base's is. Give it whole cycles of each tone, so that +- `EngineSound`, a WAV file, PCM or IMA ADPCM at any rate, that loops as the engine's sound. The + throttle changes its pitch and volume as it does for the base type's sound. Give it whole cycles of each tone, so that its loop doesn't click. - `BlindFire` and `SpectralShields`, `yes` or `no`, whether it carries blind fire and spectral shields. Without them, it carries what its base carries. The loadout lists them with its specials. @@ -924,8 +924,9 @@ A gun uses its base's shots, flashes and sounds, except for the ones it gives it extension, so a PNG, DDS or KTX2 file works. Give it a transparent background. - `ShotSize` is how far the picture reaches from the shot's middle in each direction. The default is 60, the size of the Pulse Cannon's flare. -- `Sound` is a WAV file in the mod, PCM or IMA ADPCM at any rate, that each shot makes in place of - its base's. It is heard as its base's sound is: as far, as loud, and following the shot. +- `Sound` is a WAV file in the mod, PCM or IMA ADPCM at any rate, that each shot makes instead of + the base gun's sound. It is heard like the base gun's sound: as far, as loud, and following the + shot. - `Flash` is a picture in the mod, which the muzzle flash is drawn with as each shot leaves the gun, in place of its base's flares: across the muzzle and in three blades along the flash, added to what's behind it, shrinking away as its base's does. It is found as `Shot` is. Black is @@ -1269,7 +1270,7 @@ most: | Build a model | `sltool shp from-obj `, `sltool shp from-gltf ` | | Save a face film's frames, or make a film | `sltool fm8 extract `, `sltool fm8 encode ` | | Save lines as WAV files, or make a line | `sltool speech extract `, `sltool speech encode ` | -| List a sound bank's sounds, or save them as WAV files named as a mod's sounds are | `sltool fat ls `, `sltool fat extract ` | +| List a sound bank's sounds, or save them as WAV files with the names a mod's sounds use | `sltool fat ls `, `sltool fat extract ` | | List a mission's ships, triggers and script parts | `sltool dte ships `, `sltool dte triggers `, `sltool dte parts ` | | List a stats table | `sltool stats list ` | @@ -1326,8 +1327,8 @@ problem](https://github.com/OpenReliant/openreliant/issues/new?template=mod_prob report one. As it starts, it lists each mod it loads, in order, by its manifest's name if it has one, and then what each of the mod's files does: which game file, texture, shape, picture, font, line or sound it replaces, which earlier mod's file it replaces, or which file it adds. A recording -in another format than the game file it replaces, such as `New_Pensive.flac`, is listed as replacing -that file, `New_Pensive.wav`. A mod that the mods screen has turned off is listed as off, and none +in a different format from the game file it replaces, such as `New_Pensive.flac`, is listed as +replacing `New_Pensive.wav`. A mod that the mods screen has turned off is listed as off, and none of it is used. When a font is loaded, the log says which outline font draws it. ```text diff --git a/docs/guide/openreliant.d.luau b/docs/guide/openreliant.d.luau index 2cf016a7..ad2aa3a9 100644 --- a/docs/guide/openreliant.d.luau +++ b/docs/guide/openreliant.d.luau @@ -727,7 +727,7 @@ export type Records = { planets: { [number]: Planet }, --- The CD player's tracks, twelve to a page: the Reliant's, 1 to 12, then the Yamato's, 13 to 24. Load scripts change their fields, and add tracks after the last, up to 96 in all, on pages of their own. cd_player: { [number]: CdPlayerTrack }, - --- The rooms' views, the Reliant's and the Yamato's, 1 to 125 in the order they lie in the executable. Load scripts change their fields. + --- The rooms' views, the Reliant's and the Yamato's, numbered 1 to 125 in the order they are stored in the executable. Load scripts can change their fields. room_views: { [number]: RoomView }, } @@ -877,7 +877,7 @@ export type Hud = { picture: (at: vector, file: string, size: vector?, style: FillStyle?) -> (), --- Draws the power window's ball, turning with the player's power setting as the window's does, whether or not the window is open, with its top left corner at `at` and `size` in window pixels (nil for its size on the game's display), tinted by `style`. Unlike the window's, it doesn't shake when the ship is hit. Only during a mission. power_ball: (at: vector, size: vector?, style: FillStyle?) -> (), - --- Draws shape `index` of the game's sprite set for this layer (the flight display's, or the front end screen's), or of the set the style's `sprites` names, with its anchor at `at`, in window pixels. The style's `scale` multiplies its size in the game's pixels. + --- Draws shape `index` with its anchor at `at`, in window pixels. The shape comes from the game's sprite set for this layer (the flight display's, or the front end screen's), or from the set that the style's `sprites` names. The style's `scale` multiplies its size in the game's pixels. shape: (at: vector, index: number, style: ShapeStyle?) -> (), --- Draws `text` at `at`, in pixels from the window's top left corner, in the game's font, as `style` says. text: (at: vector, text: string, style: TextStyle?) -> (), @@ -913,15 +913,15 @@ export type Ui = { play_movie: (name: string) -> (), --- Asks the front end to quit the game. Only menu scripts can use it. quit: () -> (), - --- Where the point `at` of the game's 640 by 480 screen lies in the window, in pixels, as the front end and the rooms fit that screen to the window, in its middle: for drawing over a place on one of their pictures. + --- Converts the point `at` on the game's 640 by 480 screen to window pixels, as the front end and the rooms centre that screen in the window. Use it to draw over a spot on one of their pictures. from_game_screen: (at: vector) -> vector, - --- Where the window's pixel `at`, such as the pointer's, lies on the game's 640 by 480 screen, as `from_game_screen` places that screen. + --- Converts the window pixel `at`, such as the pointer's position, to a point on the game's 640 by 480 screen. The inverse of `from_game_screen`. to_game_screen: (at: vector) -> vector, --- Shows the screen `name`: the calling mod's by its own name, or any mod's by the qualified one. Nil closes the screen shown. Returns whether it's registered. show_screen: (name: string?) -> boolean, --- Draws a PNG from the calling mod at `at`, with `size` in window pixels (nil uses its native size), tinted by `style`. Files are cached for the script context; one that hasn't been drawn for 2 frames makes room for others when the cache is full. picture: (at: vector, file: string, size: vector?, style: FillStyle?) -> (), - --- Draws shape `index` of the game's sprite set for this layer (the flight display's, or the front end screen's), or of the set the style's `sprites` names, with its anchor at `at`, in window pixels. The style's `scale` multiplies its size in the game's pixels. + --- Draws shape `index` with its anchor at `at`, in window pixels. The shape comes from the game's sprite set for this layer (the flight display's, or the front end screen's), or from the set that the style's `sprites` names. The style's `scale` multiplies its size in the game's pixels. shape: (at: vector, index: number, style: ShapeStyle?) -> (), --- Draws `text` at `at`, in pixels from the window's top left corner, in the game's font, as `style` says. text: (at: vector, text: string, style: TextStyle?) -> (), diff --git a/docs/guide/reference.md b/docs/guide/reference.md index 14c09194..94d794a0 100644 --- a/docs/guide/reference.md +++ b/docs/guide/reference.md @@ -158,13 +158,13 @@ Each of the rooms' views, `room_views[n]`, is a `RoomView` with these fields: | Field | Type | What it is | |---|---|---| -| `movie` | string, or nil | The movie on the way into the view, a Bink file of the disc's archive or a mod's, such as `rel_l2cd.bik`, at most 64 characters; nil for none. | -| `loop` | string, or nil | The movie the view loops once the player is there, as `movie` is given; nil for none, which leaves the way in's last frame on the screen. | -| `label` | string | The words that name the way into the view, which show at the foot of the screen while the pointer rests on its hotspot. | -| `exits` | list of number | The views it leads on to, at most 4, by their numbers. | +| `movie` | string, or nil | The movie that plays on the way into the view: a Bink file from the disc's archive or from a mod, such as `rel_l2cd.bik`, at most 64 characters; nil for none. | +| `loop` | string, or nil | The movie that loops while the player is in the view, given like `movie`; nil for none, which leaves the last frame of the way in on the screen. | +| `label` | string | The text that names the way into the view. It shows at the bottom of the screen while the pointer is over the view's hotspot. | +| `exits` | list of number | The numbers of the views this view leads to, at most 4. | | `action` | [RoomAction](#roomaction) | What happens once the player is in the view, such as `cd_player`, which opens the CD player. | -| `hotspot` | [ScreenRect](#screenrect) | Where the pointer finds the way into the view, on each view it's an exit of, on the game's 640 by 480 screen. | -| `sound` | number, or nil | The sound of the rooms' `vrsnd.fat` the way into the view plays, by its number; nil for none. | +| `hotspot` | [ScreenRect](#screenrect) | The area of the game's 640 by 480 screen where the pointer selects the way into this view, in each view that has it as an exit. | +| `sound` | number, or nil | The number of the sound from the rooms' `vrsnd.fat` that plays on the way into the view; nil for none. | ### `openreliant.hooks` @@ -264,7 +264,7 @@ Drawing over the flight display, while it's shown: text, lines and rectangles, i | `window_state(instrument: HudInstrument)` | [HudWindowState](#hudwindowstate), or nil | How far the game's window that holds `instrument` has opened, and whether it's opening or closing, as the display last moved it on, which it does in every view; for the comms, the further open of its two windows. Nil for an instrument outside the windows, and outside a mission. | | `picture(at: vector, file: string, size: vector?, style: FillStyle?)` | nothing | Draws a PNG from the calling mod at `at`, with `size` in window pixels (nil uses its native size), tinted by `style`. Files are cached for the script context; one that hasn't been drawn for 2 frames makes room for others when the cache is full. | | `power_ball(at: vector, size: vector?, style: FillStyle?)` | nothing | Draws the power window's ball, turning with the player's power setting as the window's does, whether or not the window is open, with its top left corner at `at` and `size` in window pixels (nil for its size on the game's display), tinted by `style`. Unlike the window's, it doesn't shake when the ship is hit. Only during a mission. | -| `shape(at: vector, index: number, style: ShapeStyle?)` | nothing | Draws shape `index` of the game's sprite set for this layer (the flight display's, or the front end screen's), or of the set the style's `sprites` names, with its anchor at `at`, in window pixels. The style's `scale` multiplies its size in the game's pixels. | +| `shape(at: vector, index: number, style: ShapeStyle?)` | nothing | Draws shape `index` with its anchor at `at`, in window pixels. The shape comes from the game's sprite set for this layer (the flight display's, or the front end screen's), or from the set that the style's `sprites` names. The style's `scale` multiplies its size in the game's pixels. | | `text(at: vector, text: string, style: TextStyle?)` | nothing | Draws `text` at `at`, in pixels from the window's top left corner, in the game's font, as `style` says. | | `line(from: vector, to: vector, style: LineStyle?)` | nothing | Draws a line from `from` to `to`, in pixels, as `style` says. | | `rectangle(from: vector, to: vector, style: FillStyle?)` | nothing | Fills the rectangle between the corners `from` and `to`, in pixels, as `style` says. | @@ -287,11 +287,11 @@ Drawing over the menus, the front end's screens and the pause menu, while they'r | `launch_mission()` | nothing | From a game mode's briefing screen, asks the front end to fly the mode's next mission (`core.game_mode_mission`). Only menu scripts can use it. | | `play_movie(name: string)` | nothing | Plays the movie `name`, a Bink file of the game folder's or a mod's such as `"intro.bik"`, on a cleared screen, as the front end shows its next frame. Escape or the pointer's right button ends it. Only menu scripts can use it. | | `quit()` | nothing | Asks the front end to quit the game. Only menu scripts can use it. | -| `from_game_screen(at: vector)` | vector | Where the point `at` of the game's 640 by 480 screen lies in the window, in pixels, as the front end and the rooms fit that screen to the window, in its middle: for drawing over a place on one of their pictures. | -| `to_game_screen(at: vector)` | vector | Where the window's pixel `at`, such as the pointer's, lies on the game's 640 by 480 screen, as `from_game_screen` places that screen. | +| `from_game_screen(at: vector)` | vector | Converts the point `at` on the game's 640 by 480 screen to window pixels, as the front end and the rooms centre that screen in the window. Use it to draw over a spot on one of their pictures. | +| `to_game_screen(at: vector)` | vector | Converts the window pixel `at`, such as the pointer's position, to a point on the game's 640 by 480 screen. The inverse of `from_game_screen`. | | `show_screen(name: string?)` | boolean | Shows the screen `name`: the calling mod's by its own name, or any mod's by the qualified one. Nil closes the screen shown. Returns whether it's registered. | | `picture(at: vector, file: string, size: vector?, style: FillStyle?)` | nothing | Draws a PNG from the calling mod at `at`, with `size` in window pixels (nil uses its native size), tinted by `style`. Files are cached for the script context; one that hasn't been drawn for 2 frames makes room for others when the cache is full. | -| `shape(at: vector, index: number, style: ShapeStyle?)` | nothing | Draws shape `index` of the game's sprite set for this layer (the flight display's, or the front end screen's), or of the set the style's `sprites` names, with its anchor at `at`, in window pixels. The style's `scale` multiplies its size in the game's pixels. | +| `shape(at: vector, index: number, style: ShapeStyle?)` | nothing | Draws shape `index` with its anchor at `at`, in window pixels. The shape comes from the game's sprite set for this layer (the flight display's, or the front end screen's), or from the set that the style's `sprites` names. The style's `scale` multiplies its size in the game's pixels. | | `text(at: vector, text: string, style: TextStyle?)` | nothing | Draws `text` at `at`, in pixels from the window's top left corner, in the game's font, as `style` says. | | `line(from: vector, to: vector, style: LineStyle?)` | nothing | Draws a line from `from` to `to`, in pixels, as `style` says. | | `rectangle(from: vector, to: vector, style: FillStyle?)` | nothing | Fills the rectangle between the corners `from` and `to`, in pixels, as `style` says. | @@ -640,7 +640,7 @@ turret, and a turret's own scripts on their turret. | `window_state(instrument: HudInstrument)` | [HudWindowState](#hudwindowstate), or nil | How far the game's window that holds `instrument` has opened, and whether it's opening or closing, as the display last moved it on, which it does in every view; for the comms, the further open of its two windows. Nil for an instrument outside the windows, and outside a mission. | | `picture(at: vector, file: string, size: vector?, style: FillStyle?)` | nothing | Draws a PNG from the calling mod at `at`, with `size` in window pixels (nil uses its native size), tinted by `style`. Files are cached for the script context; one that hasn't been drawn for 2 frames makes room for others when the cache is full. | | `power_ball(at: vector, size: vector?, style: FillStyle?)` | nothing | Draws the power window's ball, turning with the player's power setting as the window's does, whether or not the window is open, with its top left corner at `at` and `size` in window pixels (nil for its size on the game's display), tinted by `style`. Unlike the window's, it doesn't shake when the ship is hit. Only during a mission. | -| `shape(at: vector, index: number, style: ShapeStyle?)` | nothing | Draws shape `index` of the game's sprite set for this layer (the flight display's, or the front end screen's), or of the set the style's `sprites` names, with its anchor at `at`, in window pixels. The style's `scale` multiplies its size in the game's pixels. | +| `shape(at: vector, index: number, style: ShapeStyle?)` | nothing | Draws shape `index` with its anchor at `at`, in window pixels. The shape comes from the game's sprite set for this layer (the flight display's, or the front end screen's), or from the set that the style's `sprites` names. The style's `scale` multiplies its size in the game's pixels. | | `text(at: vector, text: string, style: TextStyle?)` | nothing | Draws `text` at `at`, in pixels from the window's top left corner, in the game's font, as `style` says. | | `line(from: vector, to: vector, style: LineStyle?)` | nothing | Draws a line from `from` to `to`, in pixels, as `style` says. | | `rectangle(from: vector, to: vector, style: FillStyle?)` | nothing | Fills the rectangle between the corners `from` and `to`, in pixels, as `style` says. | @@ -693,11 +693,11 @@ turret, and a turret's own scripts on their turret. | `launch_mission()` | nothing | From a game mode's briefing screen, asks the front end to fly the mode's next mission (`core.game_mode_mission`). Only menu scripts can use it. | | `play_movie(name: string)` | nothing | Plays the movie `name`, a Bink file of the game folder's or a mod's such as `"intro.bik"`, on a cleared screen, as the front end shows its next frame. Escape or the pointer's right button ends it. Only menu scripts can use it. | | `quit()` | nothing | Asks the front end to quit the game. Only menu scripts can use it. | -| `from_game_screen(at: vector)` | vector | Where the point `at` of the game's 640 by 480 screen lies in the window, in pixels, as the front end and the rooms fit that screen to the window, in its middle: for drawing over a place on one of their pictures. | -| `to_game_screen(at: vector)` | vector | Where the window's pixel `at`, such as the pointer's, lies on the game's 640 by 480 screen, as `from_game_screen` places that screen. | +| `from_game_screen(at: vector)` | vector | Converts the point `at` on the game's 640 by 480 screen to window pixels, as the front end and the rooms centre that screen in the window. Use it to draw over a spot on one of their pictures. | +| `to_game_screen(at: vector)` | vector | Converts the window pixel `at`, such as the pointer's position, to a point on the game's 640 by 480 screen. The inverse of `from_game_screen`. | | `show_screen(name: string?)` | boolean | Shows the screen `name`: the calling mod's by its own name, or any mod's by the qualified one. Nil closes the screen shown. Returns whether it's registered. | | `picture(at: vector, file: string, size: vector?, style: FillStyle?)` | nothing | Draws a PNG from the calling mod at `at`, with `size` in window pixels (nil uses its native size), tinted by `style`. Files are cached for the script context; one that hasn't been drawn for 2 frames makes room for others when the cache is full. | -| `shape(at: vector, index: number, style: ShapeStyle?)` | nothing | Draws shape `index` of the game's sprite set for this layer (the flight display's, or the front end screen's), or of the set the style's `sprites` names, with its anchor at `at`, in window pixels. The style's `scale` multiplies its size in the game's pixels. | +| `shape(at: vector, index: number, style: ShapeStyle?)` | nothing | Draws shape `index` with its anchor at `at`, in window pixels. The shape comes from the game's sprite set for this layer (the flight display's, or the front end screen's), or from the set that the style's `sprites` names. The style's `scale` multiplies its size in the game's pixels. | | `text(at: vector, text: string, style: TextStyle?)` | nothing | Draws `text` at `at`, in pixels from the window's top left corner, in the game's font, as `style` says. | | `line(from: vector, to: vector, style: LineStyle?)` | nothing | Draws a line from `from` to `to`, in pixels, as `style` says. | | `rectangle(from: vector, to: vector, style: FillStyle?)` | nothing | Fills the rectangle between the corners `from` and `to`, in pixels, as `style` says. | diff --git a/docs/guide/scripting.md b/docs/guide/scripting.md index 9cc9e32c..15930c51 100644 --- a/docs/guide/scripting.md +++ b/docs/guide/scripting.md @@ -97,7 +97,7 @@ Each example mod shows one part of the scripting, with comments in its files: | [`dvd`](../../examples/mods/dvd) | A menu script that [draws](#drawing) over the menus | | [`interceptor`](../../examples/mods/interceptor) | A mod's ship type, flown in a [game mode](#game-modes) | | [`main-menu`](../../examples/mods/main-menu) | [Replacing a screen](#replacing-a-screen) of the front end | -| [`music`](../../examples/mods/music) | A piece of music in place of the game's, which keeps its [loop point](modding.md#music), and a track of [the CD player](#the-cd-players-tracks) that plays it, from a load script | +| [`music`](../../examples/mods/music) | A piece of music that replaces the game's and keeps its [loop point](modding.md#music), and a track of [the CD player](#the-cd-players-tracks) that plays it, from a load script | | [`rules`](../../examples/mods/rules) | [Hooks](#hooks) on the game's functions | | [`strafe-run`](../../examples/mods/strafe-run) | A custom order, a [HUD display](#hud-displays), a [camera view](#camera-views), a [screen](#screens) and [actions](#keys-and-actions), through the [built-in interfaces](#built-in-interfaces) | | [`tally`](../../examples/mods/tally) | [Saved games](#saved-games), [storage](#storage) and [timers](#timers) | @@ -1490,10 +1490,10 @@ records.planet_light = vector.create(1, 1, 1) `records.cd_player` holds the tracks of the CD player in the rooms: the Reliant's twelve, Spirit of War to The Catalyst, numbered 1 to 12, then the Yamato's, Final Word to The Rise, numbered 13 to 24 -([The CD player](../engine/cd-player.md#the-lists)). Each has its `piece`, the music it plays, as -the name of its file in the `music` folder without the `.wav`, and its `name` in the list. A load -script changes them, so that a mod's music can have a row of the player, and adds tracks after the -last one, which the player shows on pages of their own: +([The CD player](../engine/cd-player.md#the-lists)). Each has a `piece`, the music it plays, given +as the name of its file in the `music` folder without `.wav`, and a `name` shown in the list. A +load script can change them, so a mod's music can have a row in the player, and can add tracks after +the last one, which the player shows on extra pages: ```lua local records = require("openreliant.records") @@ -1506,11 +1506,11 @@ records.cd_player[24] = { piece = "New_Pensive", name = "OpenReliant Theme" } records.cd_player[#records.cd_player + 1] = { piece = "Foster_Theme", name = "Foster's Theme" } ``` -- The player shows twelve tracks to a page: the Reliant's list, the Yamato's, then the tracks mods - add, up to 96 tracks in all. A script adds a track by assigning to the number after the last, and +- The player shows twelve tracks per page: the Reliant's list, the Yamato's, then the tracks that + mods add, up to 96 tracks in all. A script adds a track by assigning to the number after the last, and can't remove one. -- A piece is found by the name of its file, like the game's music: a file with the same name in a - mod plays in place of the game's, and a mod can also add a piece with a new name. +- A piece is looked up by its file name, like the game's music: a file with the same name in a mod + replaces the game's, and a mod can also add a piece with a new name. - Names are read from the game's text and can be changed to any text, in the game's code page. - The CD player opens on the list of the carrier the player is on. A menu script can turn to the other pages ([The rooms](#the-rooms)). @@ -1519,20 +1519,21 @@ records.cd_player[#records.cd_player + 1] = { piece = "Foster_Theme", name = "Fo `records.room_views` holds the views of the carrier's rooms, the places the player stands in between missions ([The views](../engine/rooms.md#the-views)): the Reliant's and the Yamato's, 125 -in all, numbered from 1 in the order they lie in the executable. A view has: +in all, numbered from 1 in the order they are stored in the executable. A view has: -- `movie`, the movie on the way into it, and `loop`, the movie it loops once the player is there: - Bink files from the disc's archive, or a mod's of the same name. Without a loop, the way in's - last frame stays on the screen. Either can be nil. -- `label`, the words that show at the foot of the screen while the pointer rests on the way into +- `movie`, the movie that plays on the way into it, and `loop`, the movie that loops while the + player is there: Bink files from the disc's archive, or a mod's file with the same name. Without a + loop, the last frame of the way in stays on the screen. Either can be nil. +- `label`, the text that shows at the bottom of the screen while the pointer is over the way into it. -- `exits`, the views it leads on to, at most 4, by their numbers, and `hotspot`, where the pointer - finds the way into it on each view it's an exit of, on the game's 640 by 480 screen. +- `exits`, the numbers of the views it leads to, at most 4, and `hotspot`, the area of the game's + 640 by 480 screen where the pointer selects the way into it, in each view that has it as an + exit. - `action`, what happens once the player is in it, such as `"cd_player"`, which opens the CD player ([RoomAction](reference.md#roomaction)). -- `sound`, the number of the sound the way into it plays, or nil. +- `sound`, the number of the sound that plays on the way into it, or nil. -A load script changes them, and the rooms play what they say: +A load script can change them, and the rooms follow the changes: ```lua local records = require("openreliant.records") @@ -1727,15 +1728,15 @@ return { - **Shapes.** A shape number picks a shape from the game's current sprite set: the flight display's set for `hud`, or the current menu screen's set for `ui` ([Shapes](modding.md#shapes)). The style's `sprites` names another of the game's sets instead, by its name in `resource.hog`, such - as `"frntend5.spr"`, the front end's arrows and boxes: it's read the first time a script draws - from it, and a mod's copy of it, or its shapes, stand in for the game's as they do on the game's + as `"frntend5.spr"`, the front end's arrows and boxes. It's read the first time a script draws + from it, and a mod's copy of the set, or of its shapes, replaces the game's as on the game's own screens. Each shape keeps its own anchor point, and is drawn at the game's scale times the style's `scale`. A set that isn't loaded or found, or a shape it doesn't have, is an error. -- **The game's screen.** The front end's screens and the rooms are 640 by 480 pictures, fitted to - the window in its middle. `ui.from_game_screen(at)` gives the window's pixel at a point of that - screen, and `ui.to_game_screen(at)` the point of the screen at a window's pixel, such as the - pointer's, so that a script can draw over a place on one of those pictures, and find the pointer - over it. +- **The game's screen.** The front end's screens and the rooms are 640 by 480 pictures, scaled to + fit the window and centred in it. `ui.from_game_screen(at)` converts a point on that screen to + window pixels, and `ui.to_game_screen(at)` converts a window pixel, such as the pointer's + position, to a point on the screen. With them, a script can draw over a spot on one of those + pictures, and tell whether the pointer is over it. - **Fonts.** A text style's `font` is one of the game's, `"default"`, `"hud"`, `"menu_small"` or `"menu_large"`, or a font file in the mod. Text is always drawn in the style's colour. - A `.ttf` or `.otf` file is drawn at the window's resolution. It takes the spacing of the game's @@ -2086,25 +2087,27 @@ player's target. ### The rooms -`openreliant.rooms` says where the player is in the carrier's rooms, while they're on screen: +`openreliant.rooms` tells where the player is in the carrier's rooms, while they're on screen: - `carrier`, `"reliant"` or `"yamato"`. -- `view`, the view the player stands in, or walks into, by its number in +- `view`, the number of the view the player is in, or is walking into, in [`records.room_views`](#the-rooms-views). -- `place`, the place whose screen shows: `"itac"`, `"simulator"`, `"locker"` or `"cd_player"`, or - `"news"` while the television's report plays. It's nil while the player walks the rooms. +- `place`, the place whose screen is showing: `"itac"`, `"simulator"`, `"locker"` or + `"cd_player"`, or `"news"` while the television's report plays. It's nil while the player walks + through the rooms. All three are nil while the rooms aren't on screen: in flight, in the front end, in the in-game -options, the briefing and the movies. `on_room_changed(view, place)` runs as either changes: as the -player sets off for another view, opens or leaves a place, or the rooms come on screen or leave it. - -The CD player shows [`records.cd_player`](#the-cd-players-tracks)'s tracks a page at a time, -twelve to a page: the Reliant's list on page 1, the Yamato's on page 2, and the tracks mods add on -the pages after them. `rooms.cd_player_pages` is how many pages there are. While it's open, -`rooms.cd_player_page` is the page it shows, and `rooms.set_cd_player_page(page)` turns to another. -It opens on its carrier's page, numbered 1 to 12 as the game numbers each list. Once a script turns -a page, the rows are numbered as the records number the tracks, from 13 on page 2. A piece chosen -plays on, and the pieces that follow it come from its own page, whichever page shows. +options, the briefing and the movies. `on_room_changed(view, place)` runs when either changes: +when the player sets off for another view, opens or leaves a place, or the rooms appear on screen +or leave it. + +The CD player shows the tracks of [`records.cd_player`](#the-cd-players-tracks) a page at a time, +twelve per page: the Reliant's list on page 1, the Yamato's on page 2, and the tracks that mods add +on the pages after them. `rooms.cd_player_pages` is the number of pages. While the CD player is +open, `rooms.cd_player_page` is the page it shows, and `rooms.set_cd_player_page(page)` turns to +another. It opens on its carrier's page, with the rows numbered 1 to 12 as in the game. Once a +script turns a page, the rows are numbered as in the records, from 13 on page 2. A chosen piece +keeps playing, and the pieces that follow it come from its own page, whichever page is showing. ```lua local rooms = require("openreliant.rooms") diff --git a/docs/guide/what-mods-can-do.md b/docs/guide/what-mods-can-do.md index 027ce4a5..d171c6f2 100644 --- a/docs/guide/what-mods-can-do.md +++ b/docs/guide/what-mods-can-do.md @@ -91,7 +91,7 @@ the mods screen ([The mods screen](modding.md#the-mods-screen)). | A mod can | Read | Example | |---|---|---| | Draw over the flight display and the menus: text, lines, rectangles, the mod's pictures and fonts, and the game's shapes from any of its sprite sets | [Drawing](scripting.md#drawing), [Pictures, shapes and fonts](scripting.md#pictures-shapes-and-fonts) | [`dvd`](../../examples/mods/dvd), [`drawing-assets`](../../examples/mods/drawing-assets), [`cd-player-pages`](../../examples/mods/cd-player-pages) | -| Tell where the player is in the carrier's rooms, hear when that changes, and draw over the rooms and their screens | [The rooms](scripting.md#the-rooms) | [`cd-player-pages`](../../examples/mods/cd-player-pages) | +| Find out where the player is in the carrier's rooms, get notified when that changes, and draw over the rooms and their screens | [The rooms](scripting.md#the-rooms) | [`cd-player-pages`](../../examples/mods/cd-player-pages) | | Turn the CD player's pages, to show both carriers' lists on either | [The rooms](scripting.md#the-rooms) | [`cd-player-pages`](../../examples/mods/cd-player-pages) | | Add displays to the flight display | [HUD displays](scripting.md#hud-displays) | [`arena`](../../examples/mods/arena), [`strafe-run`](../../examples/mods/strafe-run) | | Replace, move and scale the game's HUD instruments, and read what they show | [The game's instruments](scripting.md#the-games-instruments) | [`hud-layout`](../../examples/mods/hud-layout) | diff --git a/docs/port/platform.md b/docs/port/platform.md index 12967b9f..a8db8e0a 100644 --- a/docs/port/platform.md +++ b/docs/port/platform.md @@ -20,9 +20,9 @@ SDL comes from the [castholm/SDL](https://github.com/castholm/SDL) package, whic ## Movies -The game's movies ([Movies](../engine/movies.md)) are decoded by [FFmpeg](https://ffmpeg.org)'s Bink decoders, in place of RAD's library, and the crew's lines in the rooms by its MP3 decoder, in place of Miles's ([The crew](../engine/rooms.md#the-crew)). Its FLAC, Vorbis and Opus decoders handle mods' music, lines and sounds in those formats ([FLAC files](../formats/flac.md), [Ogg files](../formats/ogg.md)). [`deps/ffmpeg`](../../deps/ffmpeg/build.zig) builds FFmpeg 9.0.2, pinned in its manifest by the release's tag and commit, as a static library of the Bink video and audio decoders, the MP3, FLAC, Vorbis and Opus decoders, and the part of `libavcodec`, `libavutil` and `libswresample` they reach (the Opus decoder resamples with `libswresample`): the files a program using the decoding calls links, as FFmpeg's `configure --disable-everything --enable-decoder=bink,binkaudio_rdft,binkaudio_dct,mp3float,flac,vorbis,opus --disable-asm --disable-pthreads --disable-autodetect` builds them, and the settings those files read. It is plain C for any processor, without assembly or threads, and built optimized whatever the game is built as, as OpenAL Soft is. None of FFmpeg's GPL parts is built. +The game's movies ([Movies](../engine/movies.md)) are decoded by [FFmpeg](https://ffmpeg.org)'s Bink decoders, in place of RAD's library, and the crew's lines in the rooms by its MP3 decoder, in place of Miles's ([The crew](../engine/rooms.md#the-crew)). Its FLAC, Vorbis and Opus decoders handle mods' music, lines and sounds in those formats ([FLAC files](../formats/flac.md), [Ogg files](../formats/ogg.md)). [`deps/ffmpeg`](../../deps/ffmpeg/build.zig) builds FFmpeg 9.0.2, pinned in its manifest by the release's tag and commit, as a static library of the Bink video and audio decoders, the MP3, FLAC, Vorbis and Opus decoders, and the parts of `libavcodec`, `libavutil` and `libswresample` they need (the Opus decoder resamples with `libswresample`). It compiles the files that FFmpeg's `configure --disable-everything --enable-decoder=bink,binkaudio_rdft,binkaudio_dct,mp3float,flac,vorbis,opus --disable-asm --disable-pthreads --disable-autodetect` would build for a program that uses the decoding calls, with the settings those files read. It is plain C for any processor, without assembly or threads, and like OpenAL Soft it is always built optimized, whatever the game is built as. None of FFmpeg's GPL parts are built. -[`platform/video.zig`](../../src/platform/video.zig) sets each decoder up as FFmpeg's reader of the container would, and hands the engine the pictures, 4:2:0 in BT.601's limited range, and the sound, as 16-bit samples rounded as FFmpeg's conversion rounds them. The pictures and the sound come out as the `ffmpeg` tool decodes the same files with its plain C code (`-cpuflags 0`), to within the last bit of a sample. Built without threads, FFmpeg is called from the game's thread alone. +[`platform/video.zig`](../../src/platform/video.zig) sets each decoder up as FFmpeg's demuxer would, and gives the engine the pictures, 4:2:0 in BT.601's limited range, and the sound, as 16-bit samples rounded as FFmpeg's conversion rounds them. The pictures and the sound match what the `ffmpeg` tool decodes from the same files with its plain C code (`-cpuflags 0`), to within the last bit of a sample. Since it is built without threads, FFmpeg is only called from the game's thread. ## Fonts diff --git a/docs/port/scripting.md b/docs/port/scripting.md index 82c95ff5..0858c7c1 100644 --- a/docs/port/scripting.md +++ b/docs/port/scripting.md @@ -15,11 +15,12 @@ explains how to write them; this page explains how they run. **Improvement:** scripted HUD/UI drawing uses mod PNG pictures, the current game's sprite set or one a script names, and selected bitmap or outline fonts (#590). `drawing.Assets` caches the mods' -pictures and fonts by script context and file name, and the game's sprite sets the scripts name by -their name in `resource.hog` (`ShapeStyle.sprites`), read with the mods' copies and pictures as the -front end reads its own, while `drawing.View` borrows the current game's fonts, sprite set and -rasterizer. A named set's shapes are drawn from it directly; the layer's own set is taken as the -layer draws, since its screen can change it between the scripts' frame and the drawing. Custom outline fonts reuse `hud.outline.Fit` and its atlas path. Measurement uses +pictures and fonts by script context and file name, and the game's sprite sets that scripts name, by +their name in `resource.hog` (`ShapeStyle.sprites`). The sets are read with the mods' copies and +pictures, as the front end reads its own, while `drawing.View` borrows the current game's fonts, +sprite set and rasterizer. A named set's shapes are drawn from it directly. The layer's own set is +looked up when the layer draws, since its screen can change the set between the scripts' frame and +the drawing. Custom outline fonts reuse `hud.outline.Fit` and its atlas path. Measurement uses the same bitmap layout as drawing. When a new picture or font needs room, the pictures drawn longest ago are taken out of the cache, but never one drawn in the last two frames, and freed at the next draw, which hands their textures back to the device. Fonts, and the rest at shutdown, @@ -497,7 +498,7 @@ starts, calls and stops them ([`running.zig`](../../src/scripting/running.zig)). Then it calls `on_frame`. - Each loop of the rooms' driver says what it shows as it runs the scripts' frame (`Driver.pump`): the rooms, a place's screen, or a screen outside them, such as the in-game - options. `openreliant.rooms` reads the last frame's (`rooms.zig`). The host holds values alone, + options. `openreliant.rooms` reads the last frame's (`rooms.zig`). The host holds only values, never a screen: `rooms.set_cd_player_page` leaves a request that the CD player's loop takes after the scripts' frame (`Presentation.takeCdPlayerPage`), so a script called between frames can't reach a CD player that has closed. diff --git a/docs/port/sound.md b/docs/port/sound.md index 4ca5eadb..b8eb7891 100644 --- a/docs/port/sound.md +++ b/docs/port/sound.md @@ -14,7 +14,7 @@ The game's sound code ([Sound](../engine/sound.md)) calls the Miles Sound System ## The driver -`mss.Driver` has a call for each `AIL_` function the game makes, named for it, on handles to samples, 3D samples and streams. A handle's status is Miles's: done once finished or never started, playing, or stopped part of the way. Samples, 3D samples and streams play a WAVE sound from memory the caller keeps, at a rate of their own, as many times as their loop count says (0 for ever). The game's decompression of its 3D sounds into PCM (`AIL_decompress_ADPCM`) has nothing to do in OpenReliant: both players decode IMA ADPCM themselves. A stream's loop block and position, byte offsets into its data, fall on the start of their ADPCM block. The music's loop table gives its loop points as bytes into the game's own files; for a mod's piece in place of one, `hog_snd.modLoopStart` takes the moment of the game's piece that the table's bytes fall on, and the bytes at that moment of the mod's, whatever its format and rate. A mod's piece in a format other than WAV decodes about three seconds of music each frame (`Sound.decodeMusic`, called from `updateMusic` or `runTimer`, whichever the loop runs), because decoding a long piece at once would stall the game for a few frames. Meanwhile it counts as playing and keeps a pause for when it starts. It starts once it's decoded, unless the music is closed or faded out first. A 3D sample in stereo, such as a mod's sound, is placed as one point: the software mixer mixes its channels to one (`wave.Decoder.downmix`), and OpenAL Soft places both channels at the source, spread only as far as its radius. In place of a 3D sample's length in bytes of 16-bit PCM (`AIL_3D_sample_length`), which count each channel, the drivers give its frames (`sample3DFrames`), from which `hog_snd.Sound.update3D` times its voice. +`mss.Driver` has a call for each `AIL_` function the game makes, named for it, on handles to samples, 3D samples and streams. A handle's status is Miles's: done once finished or never started, playing, or stopped part of the way. Samples, 3D samples and streams play a WAVE sound from memory the caller keeps, at a rate of their own, as many times as their loop count says (0 loops forever). The game's decompression of its 3D sounds into PCM (`AIL_decompress_ADPCM`) has nothing to do in OpenReliant: both players decode IMA ADPCM themselves. A stream's loop block and position, byte offsets into its data, fall on the start of their ADPCM block. The music's loop table gives its loop points as byte offsets into the game's own files. For a mod's piece that replaces one, `hog_snd.modLoopStart` converts the offset to a time in the game's piece, and then to the byte offset at that time in the mod's piece, whatever its format and rate. A mod's piece in a format other than WAV is decoded about three seconds at a time, once a frame (`Sound.decodeMusic`, called from `updateMusic` or `runTimer`, depending on the loop), because decoding a long piece at once would stall the game for a few frames. Until it is fully decoded, the piece counts as playing, and a pause during the decoding applies when it starts. It starts once it is decoded, unless the music is closed or faded out first. A stereo 3D sample, such as a mod's sound, is placed as a single point: the software mixer mixes its channels to mono (`wave.Decoder.downmix`), and OpenAL Soft places both channels at the source, spread only over its radius. Instead of a 3D sample's length in bytes of 16-bit PCM (`AIL_3D_sample_length`), which counts each channel, the drivers give its length in frames (`sample3DFrames`), which `hog_snd.Sound.update3D` uses to time its voice. What Miles made of a volume or a pan, and how its providers placed a sound, is not known here; OpenReliant takes: @@ -66,7 +66,7 @@ The game opens Miles's driver at 22,050 Hz in 16-bit stereo, or at 11,025 Hz whe [`platform/audio.zig`](../../src/platform/audio.zig) opens the default playback device as an SDL audio stream, in 32-bit float at the device's own rate: in as many channels as the device has with OpenAL Soft, in stereo with the software mixer. SDL asks for more from its own thread; the player renders it and the master bus passes it through. Where OpenAL Soft cannot start, the software mixer plays instead; where no device opens, the game runs silent. -As the window goes inactive, the message pump's part in [`game/winmain.zig`](../../src/engine/game/winmain.zig) pauses the music, and with a mission loaded the game pauses into its [pause menu](../engine/pause-menu.md), which pauses the 3D voices, the voices and the clock. Active again, the music goes on; the rest waits for the menu's CONTINUE ([Sound](../engine/sound.md#start-up)). The game's pump waits for the window to come back, so nothing else runs meanwhile. OpenReliant's loops keep running, so the music paused for the window counts as playing, and a piece that starts meanwhile starts paused (`Sound.holdMusic`). That way nothing takes the paused piece for one that has ended, such as the CD player, which would play its next track. **Improvement:** the game pauses the mission for the window only in multiplayer, and in single player lets the timer's ticks pile up while the window is away. +When the window becomes inactive, the message pump's part in [`game/winmain.zig`](../../src/engine/game/winmain.zig) pauses the music, and with a mission loaded the game pauses into its [pause menu](../engine/pause-menu.md), which pauses the 3D voices, the voices and the clock. When the window is active again, the music resumes, and the rest waits for the menu's CONTINUE ([Sound](../engine/sound.md#start-up)). The game's pump blocks until the window is active again, so nothing else runs in between. OpenReliant's loops keep running, so the music paused for the window counts as playing, and a piece that starts in between starts paused (`Sound.holdMusic`). Otherwise the CD player would take the paused piece for one that has ended and play its next track. **Improvement:** the game pauses the mission for the window only in multiplayer. In single player it lets the timer's ticks pile up while the window is inactive. `openreliant` sets the sound up as `WinMain` does, with 10 voices, the volumes of `[Sound]` in `starlancer.ini`, `bank_stdsmp` and `smp3d.fat`, and runs the frame's sound once the camera is placed. The fades step then too, where `tick_timer` steps them on a timer of its own every five ticks, which comes to the same while frames come faster. The player's engine starts sounding as its launch does, and a mission's script plays its music (`PlayMusic`); `--music` plays a piece from `music` from the start, until the script plays its own, and `--no-sound` runs silent ([Platform](platform.md#running)), with every line of speech lasting as long as it would be heard ([Speech](../engine/sound.md#speech)). diff --git a/src/engine/bink.zig b/src/engine/bink.zig index b2465fcc..b4b6c00c 100644 --- a/src/engine/bink.zig +++ b/src/engine/bink.zig @@ -81,35 +81,37 @@ pub const Codec = struct { codec.vtable.close(codec.context, stream); } - /// Decodes the MP3 file `bytes` (`formats/mp3.zig`) all at once into a WAVE file of 16-bit PCM - /// in `gpa`, at the first frame's rate and channels, for the rooms' crew's lines - /// (`AudioDecoding`). + /// Decodes the MP3 file `bytes` (`formats/mp3.zig`) all at once into a WAVE file of 16-bit PCM, + /// at the first frame's rate and channels (`AudioDecoding`). Used for the crew's lines in the + /// rooms. pub fn decodeMp3(codec: Codec, gpa: Allocator, bytes: []const u8) (Error || error{NoFrames})![]u8 { var decoding: AudioDecoding = try .beginMp3(codec, bytes); return decoding.complete(gpa); } }; -/// How an audio file's audio is coded, which its decoder is set up for (`Codec.openFile`). +/// How an audio file is encoded, which sets up its decoder (`Codec.openFile`). pub const Encoding = union(enum) { - /// An MP3 file's frames (`formats/mp3.zig`), sent one at a time: the rooms' crew's lines, - /// which Miles's MP3 decoder (`MP3DEC.ASI`) plays in the game, and mods' recordings. + /// An MP3 file's frames (`formats/mp3.zig`), sent one at a time. Used for the crew's lines in + /// the rooms, which Miles's MP3 decoder (`MP3DEC.ASI`) plays in the original, and for mods' + /// recordings. mp3, - /// A FLAC file (`formats/flac.zig`), sent whole as one packet: mods' recordings. + /// A FLAC file (`formats/flac.zig`), sent whole as one packet. Used for mods' recordings. flac, - /// An Ogg stream's packets of Vorbis or Opus (`formats/ogg.zig`), sent one at a time, with the - /// stream's headers as the decoder takes them (`ogg.Stream.setup`): mods' recordings. + /// The packets of an Ogg stream of Vorbis or Opus (`formats/ogg.zig`), sent one at a time. The + /// payload is the stream's headers in the form the decoder takes them (`ogg.Stream.setup`). + /// Used for mods' recordings. vorbis: []const u8, opus: []const u8, }; -/// An audio file decoded a few frames at a time (`step`) into the WAVE file `finish` makes, so that -/// a long piece of music can decode over several of the game's frames: an MP3, FLAC or Ogg file. -/// The file must last until the decoding is finished. +/// Decodes an MP3, FLAC or Ogg file a few frames at a time (`step`) into a WAVE file (`finish`), so +/// a long piece of music can be decoded over several of the game's frames. The file must stay +/// valid until the decoding is finished. pub const AudioDecoding = struct { codec: Codec, stream: Stream, - /// The file's rate and channels, which the WAVE file takes. + /// The file's sample rate and channel count, which the WAVE file gets. rate: u32, channels: u8, /// The samples decoded so far, interleaved. @@ -119,8 +121,8 @@ pub const AudioDecoding = struct { /// frame at a time. frames: union(enum) { mp3: mp3.Frames, flac, ogg: OggStream }, - /// An Ogg stream's packets, and the samples of each channel that the decoder drops at the start - /// (`ogg.Stream.pre_skip`). + /// An Ogg stream's packets, and how many samples of each channel the decoder drops at the + /// start (`ogg.Stream.pre_skip`). const OggStream = struct { packets: ogg.Packets, pre_skip: u16, @@ -129,7 +131,7 @@ pub const AudioDecoding = struct { /// Starts decoding the MP3 file `bytes`, at its first frame's rate and channels. pub fn beginMp3(codec: Codec, bytes: []const u8) (Error || error{NoFrames})!AudioDecoding { const frames: mp3.Frames = .init(bytes); - // A copy finds the first frame, which the decoder still gets first. + // Read the first frame from a copy of the iterator, so the decoder still gets it first. var ahead = frames; const first = ahead.next() orelse return error.NoFrames; return .{ .codec = codec, .stream = try codec.openFile(.mp3), .rate = first.header.rate, .channels = first.header.channels, .frames = .{ .mp3 = frames } }; @@ -182,16 +184,16 @@ pub const AudioDecoding = struct { return false; } - /// The WAVE file of the samples decoded, made in `gpa`. It lets go of the decoder, whether or - /// not it succeeds. + /// Makes a WAVE file of the samples decoded so far. Frees the decoder, whether or not it + /// succeeds. pub fn finish(decoding: *AudioDecoding, gpa: Allocator) Allocator.Error![]u8 { defer decoding.deinit(gpa); return wave.pcm16(gpa, decoding.rate, decoding.channels, decoding.kept()); } - /// The samples decoded, without those past an Ogg stream's end: the decoder decodes whole - /// packets, and the stream's last page says where in the last one it ends, so that a piece - /// loops without a gap. + /// The samples decoded, without those past the end of an Ogg stream. The decoder decodes whole + /// packets, and the stream's last page says where in the last packet the stream ends. Cutting + /// there lets a piece loop without a gap. fn kept(decoding: AudioDecoding) []const i16 { const all = decoding.pcm.items; const ogg_stream = switch (decoding.frames) { @@ -203,8 +205,8 @@ pub const AudioDecoding = struct { return all[0..@min(all.len, length)]; } - /// Decodes the rest of the file and makes its WAVE file (`finish`). It lets go of the decoder, - /// whether or not it succeeds. + /// Decodes the rest of the file and makes its WAVE file (`finish`). Frees the decoder, whether + /// or not it succeeds. pub fn complete(decoding: *AudioDecoding, gpa: Allocator) Error![]u8 { _ = decoding.step(gpa, std.math.maxInt(usize)) catch |err| { decoding.deinit(gpa); @@ -233,7 +235,7 @@ test AudioDecoding { const file = mp3.testing.silence(3); var decoding: AudioDecoding = try .beginMp3(codec, &file); try std.testing.expectEqual(22050, decoding.rate); - // One sample of each channel takes a frame, and 103 more take the other two. + // One sample per channel needs one frame, and 103 more need the other two. try std.testing.expect(!try decoding.step(gpa, 1)); try std.testing.expectEqual(204, decoding.pcm.items.len); try std.testing.expect(!try decoding.step(gpa, 103)); @@ -245,7 +247,7 @@ test AudioDecoding { const parsed = try wave.Wave.parse(made); try std.testing.expectEqual(2, parsed.channels); try std.testing.expectEqual(612 * 2, parsed.data.len); - // A FLAC file goes to the decoder whole, here decoded all at once. + // A FLAC file is sent to the decoder whole. Here it is decoded all at once. const lossless = try flac.testing.constant(gpa, 44100, &.{ 1, 2 }, 8); defer gpa.free(lossless); var whole: AudioDecoding = try .beginFlac(codec, lossless); @@ -253,9 +255,9 @@ test AudioDecoding { defer gpa.free(decoded); try std.testing.expectEqual(44100, (try wave.Wave.parse(decoded)).rate); try std.testing.expectEqual(0, decoders.streams); - // An Ogg stream's packets go to the decoder one at a time, and the samples past where its last - // page says it ends are cut. Here the test codec gives each packet's byte as a sample: 3 of - // them, where the stream ends 2 samples in, after a pre-skip of 1. + // An Ogg stream's packets are sent to the decoder one at a time, and samples past the end + // that the last page gives are cut. Here the test codec gives each packet's byte as a sample: + // 3 samples, and the stream ends after 2, with a pre-skip of 1. const silent = try ogg.testing.opus(gpa, 1, 1, 3, 3); defer gpa.free(silent); var from_ogg: AudioDecoding = try .beginOgg(codec, gpa, silent); @@ -266,7 +268,7 @@ test AudioDecoding { const cut = try from_ogg.finish(gpa); defer gpa.free(cut); try std.testing.expectEqual(2 * @sizeOf(i16), (try wave.Wave.parse(cut)).data.len); - // A file of none of the kinds is refused before a decoder opens. + // A file in the wrong format is rejected before a decoder opens. try std.testing.expectError(error.NoFrames, AudioDecoding.beginMp3(codec, "no frames")); try std.testing.expectError(error.NotFlac, AudioDecoding.beginFlac(codec, "RIFF")); try std.testing.expectError(error.NoStream, AudioDecoding.beginOgg(codec, gpa, "RIFF")); diff --git a/src/engine/files.zig b/src/engine/files.zig index b2fa725b..7a355fa8 100644 --- a/src/engine/files.zig +++ b/src/engine/files.zig @@ -10,7 +10,7 @@ const Io = std.Io; pub const max_path = 260; /// The most OpenReliant reads of one of the game's files into memory, far past the largest. -pub const max_file_size = 256 << 20; +pub const max_file_size = std.fmt.parseIntSizeSuffix("256MiB", 10) catch unreachable; /// The characters that split the folders of a game path. Windows takes both. pub const separators = "\\/"; diff --git a/src/engine/game/bigfile/catalogue.zig b/src/engine/game/bigfile/catalogue.zig index d35ac193..40970ed9 100644 --- a/src/engine/game/bigfile/catalogue.zig +++ b/src/engine/game/bigfile/catalogue.zig @@ -110,9 +110,9 @@ pub const Repositories = struct { pub const format = 1; /// The size limit of an index, in bytes. -pub const most_index_bytes = 4 * 1024 * 1024; +pub const most_index_bytes = std.fmt.parseIntSizeSuffix("4MiB", 10) catch unreachable; /// The size limit of a thumbnail, in bytes. -pub const most_thumbnail_bytes = 2 * 1024 * 1024; +pub const most_thumbnail_bytes = std.fmt.parseIntSizeSuffix("2MiB", 10) catch unreachable; /// The largest archive an entry may have: a HOG archive's offsets and sizes are 32-bit (`hog`), so /// it can't be larger. A larger size skips the entry, as a missing one does. diff --git a/src/engine/game/bigfile/mods.zig b/src/engine/game/bigfile/mods.zig index fe841c9b..a6abf3b8 100644 --- a/src/engine/game/bigfile/mods.zig +++ b/src/engine/game/bigfile/mods.zig @@ -997,10 +997,8 @@ const GameFiles = struct { /// The file `name` without the extension of a mod's recording (`recordings.Kind`); null if it /// has another. fn recordingStem(name: []const u8) ?[]const u8 { - for (std.enums.values(recordings.Kind)) |kind| { - if (withoutSuffix(name, kind.extension())) |stem| return stem; - } - return null; + const kind = recordings.Kind.of(name) orelse return null; + return withoutSuffix(name, kind.extension()); } /// The file `name` without the picture extension; null if it has a different extension. diff --git a/src/engine/game/gameflow.zig b/src/engine/game/gameflow.zig index 5d469418..b4cf897e 100644 --- a/src/engine/game/gameflow.zig +++ b/src/engine/game/gameflow.zig @@ -861,8 +861,8 @@ test "the canon campaign's missions keep the rules OpenReliant 0.8 applied by nu 12, 13, 17, 22 => 1, else => number, }; - try std.testing.expectEqualStrings(try std.fmt.bufPrint(&movie, "new_m{d:0>2}.bik", .{shown}), mission.briefing.hologram.?); - try std.testing.expectEqualStrings(try std.fmt.bufPrint(&tag, "ms_speech\\enrbr_tag{d:0>2}.ut", .{number}), mission.briefing.last_word.?); + try std.testing.expectEqualStrings(try std.mem.print(&movie, "new_m{d:0>2}.bik", .{shown}), mission.briefing.hologram.?); + try std.testing.expectEqualStrings(try std.mem.print(&tag, "ms_speech\\enrbr_tag{d:0>2}.ut", .{number}), mission.briefing.last_word.?); try std.testing.expectEqual(null, mission.briefing.speech); } } diff --git a/src/engine/game/hog_snd.zig b/src/engine/game/hog_snd.zig index 47903d14..269a9769 100644 --- a/src/engine/game/hog_snd.zig +++ b/src/engine/game/hog_snd.zig @@ -37,10 +37,10 @@ const log = std.log.scoped(.sound); pub const betty = @import("hog_snd/betty.zig"); -/// What `sound_play` takes: a volume from nothing to `loudest`; a pan from the left, 0, to -/// `rightmost`, `centre` between; a count of plays, `forever` for one that goes on until it is -/// ended; and `own_pitch`, a sound's own pitch, which the quarter tones up or down are counted -/// from. +/// The values `sound_play` takes. The volume runs from 0 to `loudest`. The pan runs from 0 (left) +/// to `rightmost` (right), with `centre` in the middle. A play count of `forever` repeats the +/// sound until it is stopped. The pitch is in quarter tones up or down from `own_pitch`, the +/// sound's own pitch. pub const loudest = mss.max_level; pub const rightmost = mss.max_level; pub const centre = mss.centre_pan; @@ -70,7 +70,7 @@ const queued_fade_step = 5; /// immediate in `sound_play`). const priority_ceiling = 9999; -/// Not the game's: the priority of a script sound, the lowest priority in the banks. A script sound +/// Added by OpenReliant: the priority of a script sound, the lowest in the banks. A script sound /// never takes a voice from a game sound, and a game sound with a higher priority takes a script /// sound's voice when no voice is free. pub const script_priority = 1; @@ -102,8 +102,8 @@ const rollover_after = 58; pub const distance_scale: f32 = 0.0004; pub const velocity_scale: f32 = 1e-5; -/// Not the game's: who started the sound on a voice: the game, or a script sound, identified by the -/// ID OpenReliant gives it (`openreliant.audio`). +/// Added by OpenReliant: who started the sound on a voice. Either the game, or a script sound with +/// the ID OpenReliant gave it (`openreliant.audio`). pub const Starter = enum(u32) { game = 0, _, @@ -180,11 +180,11 @@ pub const Voice3D = extern struct { /// leaves the voice free while it plays, for any other sound to take. pub const no_owner: i32 = -1; - /// Not the game's: `owner` of a script sound at a point (`sound3d.playOwn`). Unlike a game - /// sound at a point, it keeps its voice until it ends. + /// Added by OpenReliant: the `owner` of a script sound at a point (`sound3d.playOwn`). Unlike + /// a game sound at a point, it keeps its voice until it ends. pub const script_owner: i32 = -2; - /// Not the game's: `sound` of a script sound, which is not one of the game's sounds. + /// Added by OpenReliant: the `sound` of a script sound, which is not one of the game's sounds. pub const script_sound: i32 = -1; /// Whether the voice is free: its `owner` is `no_owner`. @@ -260,18 +260,18 @@ pub const Volumes = struct { } }; -/// A bank's file read whole, as `hog_load` and `hog_read_file` read one, whose sounds play from its -/// bytes, and the bank it holds. +/// A bank file read into memory, as `hog_load` and `hog_read_file` read it. Its sounds play +/// straight from `bytes`. pub const BankFile = struct { bytes: []u8, bank: fat.Bank, - /// Added by OpenReliant: which of its sounds mods give (`of`); empty where none does. + /// Added by OpenReliant: for each sound, whether a mod replaces it (`of`). Empty when no mod + /// replaces any. modded: []const bool = &.{}, - /// `bytes`, the file of the bank `name`, which it keeps, with the sounds mods give in place of - /// its own from `source`; null where it is not a bank, which the log says, the bytes then - /// freed. Each of a mod's sounds is named after the bank and the sound's number - /// (`fat.soundName`). + /// Parses `bytes` as the bank `name` and takes ownership of them, then swaps in the sounds + /// that mods in `source` replace. A mod's sound is named after the bank and the sound's number + /// (`fat.soundName`). Returns null and frees `bytes`, with a warning, if they are not a bank. /// /// **Improvement:** the original plays only its banks' own sounds. pub fn of(gpa: Allocator, bytes: []u8, name: []const u8, source: recordings.Source) ?BankFile { @@ -283,8 +283,9 @@ pub const BankFile = struct { return withModSounds(.{ .bytes = bytes, .bank = bank }, gpa, name, source); } - /// The bank `name` of `archive` (`hog_load`), with the sounds its mods give in place of its - /// own, which `codec` decodes (`of`); null where it is left out, which the log says. + /// Reads the bank `name` from `archive` (`hog_load`) and swaps in the sounds that its mods + /// replace, decoded with `codec` (`of`). Returns null, with a warning, if the bank can't be + /// read. pub fn read(gpa: Allocator, archive: *const bigfile.Hog, name: []const u8, codec: ?bink.Codec) ?BankFile { const bytes = archive.readFile(gpa, name) catch |err| { log.warn("the bank {s} is left out: {t}", .{ name, err }); @@ -298,9 +299,10 @@ pub const BankFile = struct { gpa.free(file.bytes); } - /// `file`, the bank `name`, which it takes, with the sounds mods give in place of its own - /// (`fat.rebuild`), made in `gpa`. A sound that can't be played stays the bank's own, and so - /// does every sound where the bank can't be made, which the log says. + /// Rebuilds `file`, the bank `name`, with the sounds that mods replace (`fat.rebuild`), and + /// takes ownership of `file`. A mod's sound that a voice can't play is skipped, so the bank's + /// own sound stays. If the bank can't be rebuilt, the log says so and `file` comes back + /// unchanged. fn withModSounds(file: BankFile, gpa: Allocator, name: []const u8, source: recordings.Source) BankFile { if (source.mods.list.len == 0) return file; const count = file.bank.entries.len; @@ -332,8 +334,8 @@ pub const BankFile = struct { return .{ .bytes = bytes, .bank = bank, .modded = modded }; } - /// A mod's sound `name` from `source` as a WAVE file in `gpa` that a voice can play; null - /// where no mod gives one, and, with a warning, where it can't be played. + /// Reads a mod's sound `name` from `source` as a WAVE file that a voice can play. Returns null + /// if no mod has the sound, or, with a warning, if a voice can't play it. fn playable(gpa: Allocator, source: recordings.Source, name: []const u8) ?[]u8 { const bytes = source.read(gpa, name) orelse return null; _ = mss.voice.Voice.init(bytes) catch |err| { @@ -368,11 +370,11 @@ pub const Music = struct { fade_step: i32 = 0, /// A piece to play once this one has faded (`0x0056533A` on). queued: ?Piece = null, - /// Added by OpenReliant: a mod's piece in a format other than WAV on its way, which decodes a - /// few seconds of music each pass (`Sound.decodeMusic`) and plays once it's decoded. The music counts as - /// playing meanwhile. + /// Added by OpenReliant: a mod's piece in a format other than WAV that is still being decoded, + /// a few seconds each pass (`Sound.decodeMusic`). It plays once it is fully decoded, and the + /// music counts as playing until then. decoding: ?Decoding = null, - /// Added by OpenReliant: whether the music is paused because the game's window is away + /// Added by OpenReliant: whether the music is paused because the game's window is inactive /// (`Sound.holdMusic`). held: bool = false, /// Added by OpenReliant: the piece the stream plays (`Sound.pieceOn`). @@ -412,8 +414,8 @@ pub const Music = struct { } }; - /// A mod's piece in a format other than WAV on its way: the recording being decoded, the - /// piece, and whether the music was paused meanwhile. + /// A mod's piece in a format other than WAV that is still being decoded, and whether the + /// music was paused during the decoding. pub const Decoding = struct { recording: recordings.Decoding, piece: Piece, @@ -421,8 +423,8 @@ pub const Music = struct { }; }; -/// How many seconds of a mod's piece in a format other than WAV decode each pass -/// (`Sound.decodeMusic`): a few milliseconds of work, so that the game doesn't stall. +/// How many seconds of a mod's piece are decoded each pass (`Sound.decodeMusic`). That takes a +/// few milliseconds, so the game doesn't stall. const seconds_decoded_a_pass = 3; /// Where a piece of music loops back to once it has played through: a byte offset into its data @@ -497,7 +499,7 @@ pub const Sound = struct { voice_count: u8 = 0, /// The voices `pauseAll` stopped, for `resumeAll` (`0x00563A20`). paused: [max_voices]bool = @splat(false), - /// Not the game's: who started the sound on each voice and each 3D voice. + /// Added by OpenReliant: who started the sound on each voice and each 3D voice. starters: [max_voices]Starter = @splat(.game), starters_3d: [max_voices_3d]Starter = @splat(.game), /// Each positional sound's level in the left ear and the right, gathered over a frame. @@ -585,9 +587,9 @@ pub const Sound = struct { return chosen; } - /// Not the game's: plays the WAVE file `file` as a script sound (`openreliant.audio`) with the - /// settings in `how`, choosing a voice as `play` does. `file` must stay valid while it plays. - /// Returns the voice, or null if none is free. + /// Added by OpenReliant: plays the WAVE file `file` as a script sound (`openreliant.audio`) + /// with the settings in `how`, choosing a voice as `play` does. `file` must stay valid while it + /// plays. Returns the voice, or null if none is free. pub fn playFile(sound: *Sound, file: []const u8, how: Starting) ?u8 { const driver = sound.driver orelse return null; const chosen = sound.takeVoice(driver, how.priority) orelse return null; @@ -635,8 +637,8 @@ pub const Sound = struct { /// Plays the WAVE file `file`, a line of speech, once on voice `v`, panned to the middle. It /// ends the voice's current sound and holds the voice, so that no other sound takes it. The /// rooms play the crew's lines this way (`0x0043B598` on). `file` must stay valid while it - /// plays. Not the game's: the line gets the reverb of the camera's surroundings, like the - /// rooms' other sounds (`inScene`). + /// plays. Added by OpenReliant: the line gets the reverb of the camera's surroundings, like + /// the rooms' other sounds (`inScene`). /// /// **Fix:** it plays at the speech volume, scaled by the master volume. The game sets the /// sample to full volume itself, whatever the settings. @@ -667,7 +669,7 @@ pub const Sound = struct { const file = bank.sound(index) orelse return; const started = sound.startFile(v, file, .{ .priority = @intCast(bank.entries[index].priority), - // Not the game's: the cockpit's warnings play in the cockpit's cabin. + // Added by OpenReliant: the cockpit's warnings get the cockpit's reverb. .room = if (sound.fromCockpit(bank)) .cockpit else .none, .volume = volume, .rate_factor = if (pitch != 0) pitchFactor(pitch) else null, @@ -717,23 +719,24 @@ pub const Sound = struct { return true; } - /// Not the game's: the sample on voice `v` rings in what surrounds the camera, as the 3D - /// sounds do (`mss.Room.scene`): for a sound of the scene that the game plays as a sample. + /// Added by OpenReliant: gives the sample on voice `v` the reverb of the camera's surroundings + /// (`mss.Room.scene`), as the 3D sounds have. For sounds in the scene that the game plays as + /// plain samples. pub fn inScene(sound: *Sound, v: u8) void { const driver = sound.driver orelse return; if (v < sound.voice_count) driver.setSampleRoom(sound.voices[v].sample, .scene); } - /// `play`, the sound then ringing in what surrounds the camera (`inScene`): the voice it plays - /// on, or null. + /// Plays a sound as `play` does, with the reverb of the camera's surroundings (`inScene`). + /// Returns the voice, or null. pub fn playInScene(sound: *Sound, bank: fat.Bank, index: usize, volume: i32, loops: u32, pan: i32, pitch: i32) ?u8 { const v = sound.play(bank, index, volume, loops, pan, pitch) orelse return null; sound.inScene(v); return v; } - /// Not the game's: what surrounds the camera, whose reverb the 3D sounds and the samples of the - /// scene ring in, told to the driver as it changes. + /// Added by OpenReliant: sets the camera's surroundings, whose reverb the 3D sounds and the + /// scene's samples get. The driver is told only when they change. pub fn surround(sound: *Sound, surroundings: mss.Surroundings) void { if (surroundings == sound.surroundings) return; sound.surroundings = surroundings; @@ -853,8 +856,8 @@ pub const Sound = struct { /// `tick_timer` (`0x004827C0`) 100 times a second on its own multimedia timer (`timer_start`, /// `0x004A70F0`, started by `sound_init`), so its fades continue while a movie plays. /// OpenReliant calls this once a frame from the rooms' loop, the movies' loop and the wait - /// before the launch. It then decodes more of a mod's piece on its way (`decodeMusic`), which - /// `updateMusic` does in the front end and in missions. + /// before the launch. It then decodes more of a mod's piece that is still being decoded + /// (`decodeMusic`). In the front end and in missions, `updateMusic` does that instead. pub fn runTimer(sound: *Sound, clock: *Clock, now: u64) void { clock.advanceTo(now); sound.timerTick(clock.game_ticks); @@ -929,9 +932,10 @@ pub const Sound = struct { // --- The 3D voices --------------------------------------------------------------------------- - /// `sound_3d_open` (`0x00481900`) on OpenReliant's one provider: as many 3D voices as it has, - /// up to 64, each free, then the effects set up on them (`sound3d.init`) with the bank `smp3d`, - /// of which mods give the sounds `modded` says (`BankFile.modded`). + /// `sound_3d_open` (`0x00481900`) on OpenReliant's one provider. Allocates as many free 3D + /// voices as the provider has, up to 64, then sets up the effects on them (`sound3d.init`) + /// with the bank `smp3d`. `modded` tells which of its sounds come from mods + /// (`BankFile.modded`). pub fn open3D(sound: *Sound, smp3d: fat.Bank, modded: []const bool) void { const driver = sound.driver orelse return; sound.close3D(); @@ -1002,7 +1006,7 @@ pub const Sound = struct { /// is freed, except the engine's and the afterburner's. The others are repositioned relative to /// the camera according to what they follow, and freed once out of range. /// - /// Not the game's: a script sound (`sound3d.playOwn`) keeps its voice until it ends, so a + /// Added by OpenReliant: a script sound (`sound3d.playOwn`) keeps its voice until it ends, so a /// looping one plays until it's stopped. It fades out toward the edge of its range /// (`sound3d.fadeToFar`), stays silent beyond it, and becomes audible again when it comes back /// in range. @@ -1010,8 +1014,8 @@ pub const Sound = struct { const driver = sound.driver orelse return; if (sound.voice_3d_count == 0) return; sound3d.engineUpdate(sound, scene); - // Not the game's, which opens no listener: the listener moves with the player's ship, for - // the Doppler shifts. The software mixer's stays still. + // Added by OpenReliant: the listener moves with the player's ship, for the Doppler + // shifts. The game opens no listener, and the software mixer's listener stays still. const player = &scene.objects.slots[scene.objects.player].object; driver.set3DListenerVelocity(miles(math.transformTransposed(scene.camera.orientation, player.velocity.vector()) * @as(Vector, @splat(velocity_scale)))); const frame_start = scene.clock.frame_start; @@ -1026,12 +1030,12 @@ pub const Sound = struct { continue; } } else if (!set_aside) { - // How long its sound plays, in ticks. The game takes the sound's bytes of 16-bit - // PCM over the 441 a tick plays at 22,050 Hz, multiplying by their rounded - // reciprocal (`0x004DC8B8`). **Improvement:** OpenReliant divides its frames by the - // rate it plays at, `sound3d.sample_rate` scaled for a mod's sound - // (`sound3d.rateScale`), so that a mod's sound at another rate or in stereo isn't - // stopped early or held on. The game's own sounds come out the same. + // How long the sound plays, in ticks. The game divides the sound's size in bytes + // of 16-bit PCM by the 441 bytes a tick plays at 22,050 Hz, by multiplying with a + // rounded reciprocal (`0x004DC8B8`). **Improvement:** OpenReliant divides the + // frames by the rate they play at, `sound3d.sample_rate` scaled for a mod's sound + // (`sound3d.rateScale`), so a mod's sound at another rate or in stereo isn't cut + // short or kept too long. The game's own sounds get the same length. const frames: f32 = @floatFromInt(driver.sample3DFrames(voice.sample)); const ticks = math.round(frames / (sound3d.sample_rate * sound.effects.rate_scales[index]) * main.ticks_per_second); if (ticks < frame_start - voice.started) { @@ -1177,10 +1181,10 @@ pub const Sound = struct { if (sound.music.held) driver.pauseStream(stream, true); } - /// Decodes `seconds_decoded_a_pass` more seconds of a mod's piece on its way, and plays it once - /// it's decoded, paused if the music was paused meanwhile. + /// Decodes `seconds_decoded_a_pass` more seconds of a mod's piece. Once the piece is fully + /// decoded, starts it, paused if the music was paused during the decoding. fn decodeMusic(sound: *Sound) void { - const decoding = if (sound.music.decoding) |*on_way| on_way else return; + const decoding = if (sound.music.decoding) |*pending| pending else return; const files = sound.files orelse return; const driver = sound.driver orelse return; const recording = &decoding.recording; @@ -1201,9 +1205,9 @@ pub const Sound = struct { if (decoded.paused) sound.pauseMusic(true); } - /// Drops a mod's piece on its way, which then never plays. + /// Cancels the decoding of a mod's piece, which then never plays. fn cancelDecoding(sound: *Sound) void { - const decoding = if (sound.music.decoding) |*on_way| on_way else return; + const decoding = if (sound.music.decoding) |*pending| pending else return; const gpa = (sound.files orelse return).gpa; decoding.recording.deinit(gpa); sound.music.decoding = null; @@ -1218,8 +1222,9 @@ pub const Sound = struct { sound.update3D(scene); } - /// `music_update` (`0x00482C30`), once a frame: the queued piece starts once the music has - /// stopped. OpenReliant first decodes more of a mod's piece on its way (`decodeMusic`). + /// `music_update` (`0x00482C30`), called once a frame: starts the queued piece once the music + /// has stopped. OpenReliant first decodes more of a mod's piece that is still being decoded + /// (`decodeMusic`). pub fn updateMusic(sound: *Sound) void { sound.decodeMusic(); const queued = sound.music.queued orelse return; @@ -1231,7 +1236,7 @@ pub const Sound = struct { /// `music_fade_out` (`0x00482960`): starts fading the music out by `step` every five ticks, /// counting from `game_ticks`, the timer's count now, until it stops. /// - /// A mod's piece on its way that hasn't started never plays. + /// A mod's piece that is still being decoded is cancelled and never plays. pub fn fadeMusic(sound: *Sound, step: i32, game_ticks: u32) void { if (sound.music.stream == null) return sound.cancelDecoding(); sound.music.fading = true; @@ -1239,8 +1244,8 @@ pub const Sound = struct { sound.faded_at = game_ticks; } - /// `music_playing` (`0x00482940`). A mod's piece on its way counts as playing, and so does a - /// piece paused while the window is away (`holdMusic`). + /// `music_playing` (`0x00482940`). A mod's piece that is still being decoded counts as + /// playing, and so does a piece paused because the window is inactive (`holdMusic`). pub fn musicPlaying(sound: *Sound) bool { const driver = sound.driver orelse return false; if (sound.music.decoding != null) return true; @@ -1256,15 +1261,15 @@ pub const Sound = struct { pub fn pauseMusic(sound: *Sound, paused: bool) void { const driver = sound.driver orelse return; if (sound.music.stream) |stream| driver.pauseStream(stream, paused); - if (sound.music.decoding) |*on_way| on_way.paused = paused; + if (sound.music.decoding) |*pending| pending.paused = paused; } - /// `music_pause` and `music_resume` as the message pump calls them while the game's window is - /// away (`winmain.followActivation`). The game's pump waits for the window to come back, so - /// nothing else runs meanwhile. OpenReliant's loops keep running, so the paused piece counts - /// as playing (`musicPlaying`), and a piece that starts meanwhile starts paused. That way - /// nothing takes the paused piece for one that has ended, such as the CD player, which would - /// play its next track. + /// `music_pause` and `music_resume` as the message pump calls them when the game's window + /// becomes inactive and active again (`winmain.followActivation`). The game's pump blocks + /// until the window is active again, so nothing else runs in between. OpenReliant's loops keep + /// running, so the paused piece counts as playing (`musicPlaying`), and a piece that starts in + /// between starts paused. Otherwise the CD player would take the paused piece for one that has + /// ended and play its next track. pub fn holdMusic(sound: *Sound, held: bool) void { sound.music.held = held; sound.pauseMusic(held); @@ -1288,11 +1293,11 @@ pub const Sound = struct { sound.music.queued = null; } - /// Not the game's: the path of the piece playing, or of a mod's piece still decoding. Returns - /// null while no music plays (`musicPlaying`). + /// Added by OpenReliant: the path of the piece playing, or of a mod's piece still being + /// decoded. Returns null while no music plays (`musicPlaying`). pub fn pieceOn(sound: *Sound) ?[]const u8 { if (!sound.musicPlaying()) return null; - if (sound.music.decoding) |*on_way| return on_way.piece.pathOf(); + if (sound.music.decoding) |*pending| return pending.piece.pathOf(); return if (sound.music.piece) |*playing| playing.pathOf() else null; } @@ -1310,8 +1315,9 @@ pub const Sound = struct { voice_3d: u8, }; - /// Not the game's: the voice the script sound `starter` plays on. Returns null once the sound - /// has ended, or a game sound has taken its voice. A paused sound still counts as playing. + /// Added by OpenReliant: the voice the script sound `starter` plays on. Returns null once the + /// sound has ended, or once a game sound has taken its voice. A paused sound still counts as + /// playing. pub fn playingOf(sound: *Sound, starter: Starter) ?Playing { const driver = sound.driver orelse return null; if (starter == .game) return null; @@ -1324,7 +1330,7 @@ pub const Sound = struct { return null; } - /// Not the game's: stops the script sound `starter` if it is still playing. + /// Added by OpenReliant: stops the script sound `starter` if it is still playing. pub fn endStarted(sound: *Sound, starter: Starter) void { switch (sound.playingOf(starter) orelse return) { .voice => |v| sound.endVoice(v), @@ -1357,16 +1363,16 @@ pub fn musicLoopStart(path: []const u8) i32 { return 0; } -/// A music file read: a WAVE file, and whether it's a mod's; or a mod's file of another format on -/// its way to one. +/// A music file that was read: a whole WAVE file and whether it came from a mod, or a mod's file +/// in another format that is being decoded to WAVE. const MusicRead = union(enum) { whole: struct { bytes: []u8, from_mod: bool }, decoding: recordings.Decoding, }; -/// Reads the music file at `path`, a game path with backslashes: a mod's recording with the same -/// name, in any of the formats a recording can be in (`recordings.Kind`), or else the game folder's -/// file, ignoring case as Windows does (`files.find`). +/// Reads the music file at `path`, a game path with backslashes. A mod's recording with the same +/// name, in any format a recording can have (`recordings.Kind`), takes priority. Otherwise it +/// reads the game folder's file, ignoring case as Windows does (`files.find`). /// /// **Improvement:** the original plays only its own WAVE files. fn readMusic(files: Files, path: []const u8) !MusicRead { @@ -1385,14 +1391,14 @@ fn readGameMusic(files: Files, path: []const u8) ![]u8 { return try paths.readFile(files.io, files.gpa, files.dir, path, .limited(paths.max_file_size)) orelse error.FileNotFound; } -/// Where a mod's piece of music `own` at `path` loops back to. The loop table's `loop_start` is a -/// byte offset into the game's file, so the mod's piece loops back to the same moment of the -/// music. When the game's file can't be read or isn't a WAVE file, it loops back to `loop_start` -/// as it is, and the log says so. +/// Where a mod's piece of music `own`, which replaces the game's piece at `path`, loops back to. +/// The loop table's `loop_start` is a byte offset into the game's file, so it is converted to the +/// same point in time in the mod's piece. If the game's file can't be read or isn't a WAVE file, +/// the log says so and `loop_start` is used unchanged. /// -/// **Improvement:** the original reads no mods. A mod's piece in another format than the game's, -/// such as 16-bit PCM at 44,100 Hz for the game's IMA ADPCM at 22,050, would otherwise loop back -/// to another moment of the music. +/// **Improvement:** the original reads no mods. Without the conversion, a mod's piece in another +/// format than the game's, such as 16-bit PCM at 44,100 Hz instead of IMA ADPCM at 22,050 Hz, +/// would loop back to the wrong point. fn modLoopStart(files: Files, path: []const u8, own: []const u8, loop_start: i32) i32 { if (loop_start <= 0) return loop_start; return carriedModLoop(files, path, own, @intCast(loop_start)) catch |err| { @@ -1560,14 +1566,14 @@ test "Sound.speakOn holds its voice" { const sound = &speaker.sound; const bytes = comptime testing.bank(1); const bank = try fat.Bank.parse(&bytes); - // What the voice played ends, and the line plays on it, held. + // The voice's sound ends, and the line plays on it with the voice held. sound.playOn(2, bank, 0, loudest, forever, centre, own_pitch); sound.speakOn(2, testing.sound_file); try std.testing.expect(sound.voicePlaying(2)); try std.testing.expectEqual(1, sound.voices[2].held); - // No other sound takes it over. + // No other sound takes the voice. for (0..3) |_| try std.testing.expect(sound.play(bank, 0, loudest, once, centre, own_pitch) != 2); - // Ended, it is let go. + // Ending the voice releases it. sound.endVoice(2); try std.testing.expectEqual(0, sound.voices[2].held); } @@ -1617,7 +1623,7 @@ test BankFile { const file = BankFile.of(gpa, try gpa.dupe(u8, &bytes), "two.fat", .none).?; defer file.deinit(gpa); try std.testing.expectEqual(2, file.bank.entries.len); - // Not a bank, it is left out, and its bytes let go. + // Bytes that are not a bank are rejected and freed. try std.testing.expectEqual(null, BankFile.of(gpa, try gpa.dupe(u8, "x"), "x.fat", .none)); } @@ -1939,7 +1945,7 @@ test "Sound.playMusic queues a piece until the music has stopped" { try std.testing.expectEqual(100, sound.music.level); } -test "Sound.holdMusic pauses the music while the window is away, and it counts as playing" { +test "Sound.holdMusic pauses the music while the window is inactive, and it counts as playing" { const gpa = std.testing.allocator; const io = std.testing.io; var tmp = std.testing.tmpDir(.{ .iterate = true }); @@ -1954,7 +1960,7 @@ test "Sound.holdMusic pauses the music while the window is away, and it counts a sound.files = .{ .gpa = gpa, .io = io, .dir = tmp.dir }; defer sound.closeMusic(); - // Paused for the window, the music counts as playing, so a queued piece waits for it. + // While paused for the window, the music counts as playing, so a queued piece waits. sound.playMusic("music\\one.wav", once, loudest, .now); sound.holdMusic(true); try std.testing.expectEqual(mss.Status.stopped, driver.streamStatus(sound.music.stream.?)); @@ -1962,14 +1968,14 @@ test "Sound.holdMusic pauses the music while the window is away, and it counts a sound.music.queued = .of("music\\two.wav", once, 100); sound.updateMusic(); try std.testing.expect(sound.music.queued != null); - // A piece that starts meanwhile starts paused. + // A piece that starts while the window is inactive starts paused. sound.playMusic("music\\two.wav", once, loudest, .now); try std.testing.expectEqual(mss.Status.stopped, driver.streamStatus(sound.music.stream.?)); try std.testing.expect(sound.musicPlaying()); - // With the window back, the music goes on. + // When the window is active again, the music resumes. sound.holdMusic(false); try std.testing.expectEqual(mss.Status.playing, driver.streamStatus(sound.music.stream.?)); - // A pause of the game's own still counts as stopped, as in the game. + // A pause by the game itself still counts as stopped, as in the original. sound.pauseMusic(true); try std.testing.expect(!sound.musicPlaying()); } diff --git a/src/engine/game/hudoptions/menu.zig b/src/engine/game/hudoptions/menu.zig index a416b9d7..6da358bc 100644 --- a/src/engine/game/hudoptions/menu.zig +++ b/src/engine/game/hudoptions/menu.zig @@ -224,9 +224,9 @@ pub const Pointer = struct { /// DirectInput's movements from where its pointer last stood, and puts the system's back /// near the window's corner after each. pub fn update(pointer: *Pointer, mouse: input.Mouse, screen: [2]u32) void { - if (mouse.at) |share| for (&pointer.at, share, screen) |*at, fraction, size| { + if (mouse.pixelsIn(screen)) |pixels| for (&pointer.at, @as([2]f32, pixels), screen) |*at, pixel, size| { const last: i32 = @intCast(size -| 1); - at.* = std.math.clamp(round(fraction * @as(f32, @floatFromInt(size))), 0, last); + at.* = std.math.clamp(round(pixel), 0, last); }; const down = mouse.buttons.any(); pointer.pressed = down and !pointer.down; diff --git a/src/engine/game/interface/canvas.zig b/src/engine/game/interface/canvas.zig index 22fae16e..02fdb25c 100644 --- a/src/engine/game/interface/canvas.zig +++ b/src/engine/game/interface/canvas.zig @@ -556,8 +556,7 @@ pub const Pointer = struct { /// /// `share` is how large the screen is drawn, as the canvas has it (`Canvas.share`). pub fn update(pointer: *Pointer, mouse: *input.Mouse, window: [2]u32, share: f32, elapsed: i32) void { - if (mouse.at) |fractions| { - const pixels = @as(@Vector(2, f32), fractions) * @as(@Vector(2, f32), @floatFromInt(@as(@Vector(2, u32), window))); + if (mouse.pixelsIn(window)) |pixels| { const point = Placement.of(window, share).onScreen(pixels); for (&pointer.at, point, size) |*at, axis, across| at.* = std.math.clamp(hud.round(axis), 0, @as(i32, @intCast(across)) - 1); } diff --git a/src/engine/game/interface/cd_player.zig b/src/engine/game/interface/cd_player.zig index ff54d9fa..2557d0fd 100644 --- a/src/engine/game/interface/cd_player.zig +++ b/src/engine/game/interface/cd_player.zig @@ -69,7 +69,7 @@ pub const Track = struct { /// `longest_piece`. pub fn path(listed: Track, buffer: *[files.max_path]u8) ?[]const u8 { if (listed.piece.len == 0) return null; - return std.fmt.bufPrint(buffer, music_path, .{listed.piece}) catch null; + return std.mem.print(buffer, music_path, .{listed.piece}) catch null; } }; @@ -80,9 +80,11 @@ fn track(piece: []const u8, name: u16) Track { /// The pieces of each list, which the player shows a page at a time. pub const track_count = 12; -/// The most tracks there can be: eight pages, so that every track's number has the two digits the -/// game writes (`%02d`). -pub const most = 8 * track_count; +/// The highest number with the two digits the game writes for a row (`%02d`). +const highest_number = 99; + +/// The most tracks there can be: as many whole pages as keep every track's number to two digits. +pub const most = highest_number / track_count * track_count; /// The page of `carrier`'s list. fn pageOf(carrier: rooms.Carrier) usize { @@ -242,7 +244,7 @@ const first_line = 0xAB; /// The rows' numbers, `%02d` of each from 1 (`0x004E8CF0`), on to the last page's: two digits each. const numbers = blk: { - assert(most < 100); + assert(most <= highest_number); var all: [most][2]u8 = undefined; for (&all, 1..) |*number, n| number.* = .{ '0' + n / 10, '0' + n % 10 }; break :blk all; diff --git a/src/engine/game/interface/mod_manager.zig b/src/engine/game/interface/mod_manager.zig index 6f2f59e2..25b7e61a 100644 --- a/src/engine/game/interface/mod_manager.zig +++ b/src/engine/game/interface/mod_manager.zig @@ -116,7 +116,7 @@ pub const description_lines: Canvas.Lines = .{ .width = details_lines.width, .he pub const thumbnail_box: [2]i32 = .{ details_lines.width, 70 }; pub const thumbnail_gap = 6; /// The most pixels' bytes a thumbnail is read to, far more than the panel shows. -const most_thumbnail_bytes = 16 * 1024 * 1024; +const most_thumbnail_bytes = std.fmt.parseIntSizeSuffix("16MiB", 10) catch unreachable; /// The arrows that move the chosen mod up or down the order: a gold box of up and down arrows, unlike /// the lists' arrows that scroll the list, in the middle of the gap between the frames and diff --git a/src/engine/game/recordings.zig b/src/engine/game/recordings.zig index f9fb120d..7f7dc02b 100644 --- a/src/engine/game/recordings.zig +++ b/src/engine/game/recordings.zig @@ -19,8 +19,8 @@ const wave = @import("../../formats/wave.zig"); const log = std.log.scoped(.mods); -/// The formats of a mod's recording, in the order they're looked for. An Ogg file can hold Vorbis or -/// Opus, with either extension. +/// The formats of a mod's recording, in the order they are looked for. An Ogg file can hold Vorbis +/// or Opus, with either extension. pub const Kind = enum { wave, flac, @@ -85,7 +85,7 @@ pub fn decode(codec: ?bink.Codec, gpa: Allocator, recording: Recording, most: us gpa.free(bytes); return error.TooLarge; }, - .decoding => |on_way| on_way, + .decoding => |pending| pending, }; while (true) { const done = decoding.step(gpa, decoding.audio.rate) catch |err| { @@ -108,9 +108,9 @@ pub fn problem(err: anyerror) []const u8 { }; } -/// The extensions of the game's files that a mod's recording replaces, besides the lines of speech, -/// which have none: the music's WAVE files (`hog_snd.Sound.playMusic`) and Enriquez's scenes -/// (`interface.rooms.speak`). +/// The extensions of the game files that a mod's recording can replace: the music's WAVE files +/// (`hog_snd.Sound.playMusic`) and Enriquez's scenes (`interface.rooms.speak`). The lines of +/// speech have no extension. pub const replaced_extensions = [_][]const u8{ wave.extension, ".box" }; /// A mod's recording, as the mod's file holds it. @@ -119,8 +119,8 @@ pub const Recording = struct { kind: Kind, }; -/// A recording in a format other than WAVE, decoded into a WAVE file a few frames at a time, so that -/// a long piece of music can decode over several of the game's frames (`bink.AudioDecoding`). +/// A recording in a format other than WAVE, decoded into a WAVE file a few frames at a time, so a +/// long piece of music can be decoded over several of the game's frames (`bink.AudioDecoding`). pub const Decoding = struct { audio: bink.AudioDecoding, /// The recording's file, which the decoder reads until it's done. @@ -132,8 +132,8 @@ pub const Decoding = struct { return decoding.audio.step(gpa, samples); } - /// The WAVE file of what was decoded, made in `gpa`. It frees the decoder and the recording's - /// file, whether or not it succeeds. + /// Makes a WAVE file of what was decoded. Frees the decoder and the recording's file, whether + /// or not it succeeds. pub fn finish(decoding: *Decoding, gpa: Allocator) Allocator.Error![]u8 { defer gpa.free(decoding.file); return decoding.audio.finish(gpa); @@ -145,15 +145,15 @@ pub const Decoding = struct { } }; -/// A mod's recording ready to play, or on its way. +/// A mod's recording, either ready to play or still being decoded. pub const Opened = union(enum) { /// A WAVE file, as it is. wave: []u8, decoding: Decoding, }; -/// Where a mod's recordings come from: the mods that hold them, and what decodes those that aren't -/// WAVE files, if anything. +/// Where mods' recordings come from: the mods that hold them, and the decoder for those that aren't +/// WAVE files, if there is one. pub const Source = struct { mods: *const bigfile.Mods, codec: ?bink.Codec, @@ -161,14 +161,13 @@ pub const Source = struct { /// No mods, and nothing to decode with. pub const none: Source = .{ .mods = &bigfile.Mods.none, .codec = null }; - /// Reads a mod's recording `name`, a name without an extension, into `gpa` as it is: the first + /// Reads a mod's recording `name`, given without an extension, without decoding it: the first /// of `name.wav`, `name.flac`, `name.mp3`, `name.ogg` and `name.opus` that a mod has (`Kind`). - /// Returns null if no mod has any of them, and also, with a warning, if that file can't be - /// read. + /// Returns null if no mod has any of them, or, with a warning, if the file can't be read. pub fn find(source: Source, gpa: Allocator, name: []const u8) ?Recording { var buffer: [bigfile.member_name_room + Kind.longest]u8 = undefined; for (std.enums.values(Kind)) |kind| { - // A name too long for a mod's file is in no mod. + // A name too long for a mod's file can't be in any mod. const named = std.mem.print(&buffer, "{s}{s}", .{ name, kind.extension() }) catch return null; const found = source.mods.readFile(gpa, named) catch |err| { log.warn("can't read the recording {s}: {t}", .{ named, err }); @@ -183,7 +182,7 @@ pub const Source = struct { /// Logs a warning and returns null if it can't be decoded. pub fn open(source: Source, gpa: Allocator, recording: Recording, name: []const u8) ?Opened { return begin(source.codec, gpa, recording) catch |err| { - log.warn("the recording {s}{s} is left out: {s}", .{ name, recording.kind.extension(), problem(err) }); + warnLeftOut(name, recording.kind, err); return null; }; } @@ -194,10 +193,14 @@ pub const Source = struct { pub fn read(source: Source, gpa: Allocator, name: []const u8) ?[]u8 { const recording = source.find(gpa, name) orelse return null; return decode(source.codec, gpa, recording, std.math.maxInt(usize)) catch |err| { - log.warn("the recording {s}{s} is left out: {s}", .{ name, recording.kind.extension(), problem(err) }); + warnLeftOut(name, recording.kind, err); return null; }; } + + fn warnLeftOut(name: []const u8, kind: Kind, err: anyerror) void { + log.warn("the recording {s}{s} is left out: {s}", .{ name, kind.extension(), problem(err) }); + } }; test "Kind.of" { @@ -243,8 +246,8 @@ test "Source.open" { defer gpa.free(piece); try std.testing.expectEqual(22050, (try wave.Wave.parse(piece)).rate); try std.testing.expectEqual(0, decoders.streams); - // A file that doesn't start decoding, and one without a decoder, are left out, their bytes - // freed. + // A file that fails to start decoding, and a file without a decoder, give null, and their + // bytes are freed. try std.testing.expectEqual(null, source.open(gpa, .{ .bytes = try gpa.dupe(u8, "not MP3"), .kind = .mp3 }, "broken")); try std.testing.expectEqual(null, Source.none.open(gpa, .{ .bytes = try gpa.dupe(u8, &frames), .kind = .mp3 }, "piece")); } diff --git a/src/engine/game/sound3d.zig b/src/engine/game/sound3d.zig index 208166af..af4a6f53 100644 --- a/src/engine/game/sound3d.zig +++ b/src/engine/game/sound3d.zig @@ -160,8 +160,8 @@ pub const Effects = struct { classes: [hog_snd.max_voices_3d]Class = @splat(.not_reserved), /// Added by OpenReliant: what each 3D voice's rates are scaled by (`rateScale`). rate_scales: [hog_snd.max_voices_3d]f32 = @splat(1), - /// Added by OpenReliant: the starting volume and the minimum distance, in Miles's units, of each - /// 3D voice's sound, for fading out script sounds (`heardLevel`). + /// Added by OpenReliant: the starting volume and the minimum distance, in Miles's units, of + /// each 3D voice's sound, for fading out script sounds (`heardLevel`). levels: [hog_snd.max_voices_3d]i32 = @splat(0), nears: [hog_snd.max_voices_3d]f32 = @splat(0), /// The player's engine sound (`0x0058CB04`), and when its afterburner last came on or went off @@ -182,9 +182,9 @@ pub const Effects = struct { } }; -/// Not the game's: the volume factor for a script sound at `distance`, so that it fades out at -/// `far` instead of cutting off there. Beyond `near`, the drivers apply the inverse distance law, -/// `near / distance`. This factor makes the total gain the smaller of that and +/// Added by OpenReliant: the volume factor for a script sound at `distance`, so that it fades out +/// at `far` instead of cutting off there. Beyond `near`, the drivers apply the inverse distance +/// law, `near / distance`. This factor makes the total gain the smaller of that and /// `(1 - (distance - near) / (far - near))` squared, which reaches 0 at `far`. FMOD calls this the /// inverse tapered roll-off. pub fn fadeToFar(near: f32, far: f32, distance: f32) f32 { @@ -304,13 +304,14 @@ pub fn play(sound: *Sound, scene: Scene, at: ?Vector, facing: ?Vector, owner: ?u return playFile(sound, scene, at, facing, owner, which, null, volume, class); } -/// `play`, with the WAV file `file`, if given, in place of the bank's sound for `which`: a mod's own -/// sound, played like `which`. **Improvement:** the game only plays its banks' sounds. +/// `play`, but with the WAV file `file`, if given, instead of the bank's sound for `which`: a +/// mod's own sound, played like `which`. **Improvement:** the original only plays its banks' +/// sounds. pub fn playFile(sound: *Sound, scene: Scene, at: ?Vector, facing: ?Vector, owner: ?u16, which: sounds.Sound, file: ?[]const u8, volume: f32, class: Class) ?u8 { return begin(sound, scene, &sounds.definitions[@backingInt(which)], .{ .game = which }, at, facing, owner, file, volume, class); } -/// Not the game's: the settings of a 3D script sound (`playOwn`). +/// Added by OpenReliant: the settings of a 3D script sound (`playOwn`). pub const Own = struct { /// Where it plays: at an object, given by its slot, which it follows, or at a fixed point. at: union(enum) { object: u16, point: Vector }, @@ -335,10 +336,10 @@ pub const default_far = sounds.definitions[@backingInt(sounds.Sound.gun01)].max_ /// The direction a script sound at a point faces. It doesn't matter: the sound has no cone. const facing_anywhere: Vector = .{ 0, 0, 1 }; -/// Not the game's: plays the WAVE file `file` as a 3D script sound (`openreliant.audio`) with the -/// settings in `own`, on a free voice, with no cone. `file` must stay valid while it plays. The -/// sound doesn't start beyond `own.far`. Once started, it plays until it ends, or until it's -/// stopped if it loops (`hog_snd.Sound.update3D`). Returns the voice, or null. +/// Added by OpenReliant: plays the WAVE file `file` as a 3D script sound (`openreliant.audio`) +/// with the settings in `own`, on a free voice, with no cone. `file` must stay valid while it +/// plays. The sound doesn't start beyond `own.far`. Once started, it plays until it ends, or +/// until it's stopped if it loops (`hog_snd.Sound.update3D`). Returns the voice, or null. pub fn playOwn(sound: *Sound, scene: Scene, file: []const u8, own: Own) ?u8 { const follows: Follows, const at: ?Vector, const owner: ?u16 = switch (own.at) { .object => |slot| .{ .object, null, slot }, @@ -474,8 +475,8 @@ fn begin(sound: *Sound, scene: Scene, definition: *const Definition, played: Pla driver.set3DVelocity(voice.sample, hog_snd.miles(moving)); driver.set3DSampleCone(voice.sample, definition.cone_inner, definition.cone_outer, std.math.lossyCast(i32, definition.cone_outer_volume)); driver.set3DSampleDistances(voice.sample, range, min_distance * hog_snd.distance_scale); - // Not the game's: how far the sound of what it follows spreads, its model's radius, which the - // software mixer leaves out. + // Added by OpenReliant: the sound spreads over the radius of the model it follows. The + // software mixer ignores the radius. driver.set3DSampleRadius(voice.sample, radius * hog_snd.distance_scale); const rate: u32 = switch (played) { .game => |which| switch (which) { @@ -557,18 +558,19 @@ pub fn engineSound(ship_type: gameobj.Type) sounds.Sound { return .pship01; } -/// The engine's sound a mod gives its ship type, a WAV file's bytes, which plays as its base's would, -/// pitched and loudened by the throttle the same way; null for its base's. +/// The engine sound that a mod gives its ship type, as the bytes of a WAV file. It plays like the +/// base type's sound, with the same pitch and volume changes from the throttle. Returns null if +/// the mod gives none, so the base type's sound plays. fn ownEngineSound(ship_type: gameobj.Type) ?[]const u8 { const mod = ship_type.added() orelse return null; return mod.extra.engine_sound; } -/// The player's engine started on the engine's voice: the sound for the player's ship type -/// (`engineSound`), or the one a mod gives the type (`ownEngineSound`). The game starts it in the -/// Reliant's and the Yamato's launches, as the ship comes out (`launch_reliant_run` at `0x0041B2AE`, -/// `launch_yamato_run` at `0x00419A50`), and as the afterburner ends where it has no voice of its -/// own (`engineUpdate`, `0x0049DE94`). +/// Starts the player's engine sound on the engine's voice: the sound for the player's ship type +/// (`engineSound`), or the one a mod gives the type (`ownEngineSound`). The game starts it during +/// the Reliant's and the Yamato's launches, as the ship comes out (`launch_reliant_run` at +/// `0x0041B2AE`, `launch_yamato_run` at `0x00419A50`), and when the afterburner ends, if the +/// afterburner has no voice of its own (`engineUpdate`, `0x0049DE94`). fn startEngine(sound: *Sound, scene: Scene) void { const all = scene.objects; const ship_type = all.slots[all.player].object.type; diff --git a/src/engine/game/tractor.zig b/src/engine/game/tractor.zig index db6af570..88b27b19 100644 --- a/src/engine/game/tractor.zig +++ b/src/engine/game/tractor.zig @@ -143,8 +143,8 @@ const beam_length: f32 = 400; const beam_span: [2][2]f32 = .{ .{ 0.04, 0.04 }, .{ 0.99, 0.99 } }; const beam_quads = 1 + beam_ribbons; -/// How far a beam's glow reaches either side of its axis, as a share of the beam's own width, and -/// how solid it is beside the beam. Not the game's. +/// Added by OpenReliant: how far a beam's glow reaches on either side of its axis, as a multiple +/// of the beam's width, and how opaque it is beside the beam. const glow_width: f32 = 3; const glow_share: f32 = 0.35; @@ -210,9 +210,9 @@ pub const Beam = struct { /// How it is turned in the part's frame, as it was last aimed. turn: math.Matrix = math.identity, - /// The game draws the square in one group of polygons and the ribbons in another, of the same - /// material; OpenReliant draws them in one, over `look`'s texture, with the glow's ribbons where - /// its `glow` has them. + /// The game draws the square in one group of polygons and the ribbons in another, with the + /// same material. OpenReliant draws them in one group, over `look`'s texture, with the glow's + /// ribbons where its `glow` has them. pub fn create(gpa: Allocator, look: Look, ship: u16, part: usize, point: Vector) Allocator.Error!*Beam { var corners: [most_corners]Vector = undefined; corners[0..guns.blade_corners].* = .{ diff --git a/src/engine/input.zig b/src/engine/input.zig index 4b8240c5..f37b13c7 100644 --- a/src/engine/input.zig +++ b/src/engine/input.zig @@ -782,6 +782,13 @@ pub const FreshPress = struct { } }; +test "Mouse.pixelsIn" { + var mouse: Mouse = .{}; + try std.testing.expectEqual(null, mouse.pixelsIn(.{ 800, 600 })); + mouse.at = .{ 0.5, 0.25 }; + try std.testing.expectEqual(@Vector(2, f32){ 400, 150 }, mouse.pixelsIn(.{ 800, 600 }).?); +} + test "Mouse.notches" { var mouse: Mouse = .{}; // A trackpad turns the wheel by parts of a notch, which count once they make a whole one. @@ -821,6 +828,13 @@ pub const Mouse = struct { /// up. Added by OpenReliant, as the game reads no wheel. wheel: f32 = 0, + /// Where the pointer is, in pixels of an area `size` pixels across, such as the window; null + /// until the pointer has been over the window. + pub fn pixelsIn(mouse: Mouse, size: [2]u32) ?@Vector(2, f32) { + const share: @Vector(2, f32) = mouse.at orelse return null; + return share * @as(@Vector(2, f32), @floatFromInt(@as(@Vector(2, u32), size))); + } + /// What `read_mouse` reads, the parts of `MouseState` the game uses: the movement since the /// read before, in whole counts, and the buttons down. pub const State = struct { diff --git a/src/engine/link.zig b/src/engine/link.zig index e7a3723e..fb15ecce 100644 --- a/src/engine/link.zig +++ b/src/engine/link.zig @@ -128,7 +128,7 @@ pub const Link = struct { session: ?u32 = null, /// How many bytes a read takes at most. - const read_size = 64 * 1024; + const read_size = std.fmt.parseIntSizeSuffix("64KiB", 10) catch unreachable; pub fn init(gpa: Allocator, transport: Transport, game: Game, version: []const u8) Link { return .{ .gpa = gpa, .transport = transport, .game = game, .version = version }; diff --git a/src/engine/mss.zig b/src/engine/mss.zig index 44c17b4d..4d0aaeb3 100644 --- a/src/engine/mss.zig +++ b/src/engine/mss.zig @@ -1,6 +1,6 @@ //! The Miles Sound System (`MSS32.DLL`), as far as the game uses it: a digital driver that mixes //! samples, 3D samples and streams of WAVE sounds. This is OpenReliant's own replacement, written -//! from what the calls do, not from Miles's code. Each function stands for the `AIL_` call it is +//! from what the calls do, not from Miles's code. Each function replaces the `AIL_` call it is //! named after, and the game's code in [`game/hog_snd.zig`](game/hog_snd.zig) calls them as it //! called Miles. //! @@ -106,7 +106,7 @@ pub fn gain(volume: i32) f32 { return @as(f32, @floatFromInt(volume)) / max_level; } -/// The gain of `db` decibels, as a `T`: ten to the power of a twentieth of them. +/// The linear gain of `db` decibels, as a `T`: 10 to the power of `db / 20`. pub fn decibelGain(comptime T: type, db: T) T { return std.math.pow(T, 10, db / 20); } @@ -371,8 +371,8 @@ pub const Driver = struct { } }; -/// OpenReliant's own digital driver, in software: the handles and a plain mix of them, as Miles's -/// own mixer most likely was. It is the reference the OpenAL renderer is compared with, and what +/// OpenReliant's software digital driver: the handles and a plain mix of them, as Miles's own mixer +/// most likely was. It is the reference that the OpenAL renderer is compared with, and what /// `--original` plays through. pub const Mixer = struct { /// The output's frames a second. @@ -576,8 +576,8 @@ pub const Mixer = struct { /// only, which is why the game decompresses its ADPCM first (`AIL_decompress_ADPCM`); /// OpenReliant's play either. /// - /// **Improvement:** a sound in stereo, as a mod's may be, plays mixed to mono so that it keeps - /// its place. Every 3D sound the game plays is mono. + /// **Improvement:** a stereo sound, which a mod may have, is mixed to mono so that it stays in + /// place. Every 3D sound the game plays is mono. pub fn set3DSampleFile(mixer: *Mixer, handle: Sample3D, file: []const u8) bool { mixer.lock.acquire(); defer mixer.lock.release(); @@ -691,8 +691,8 @@ pub const Mixer = struct { return statusOf(mixer.sample3D(handle).playing); } - /// Not Miles's: the frames its sound holds, in place of `AIL_3D_sample_length`'s bytes of - /// 16-bit PCM, which count each channel (`hog_snd.Sound.update3D`). + /// Not Miles's: the number of frames in its sound, instead of `AIL_3D_sample_length`'s size in + /// bytes of 16-bit PCM, which counts each channel (`hog_snd.Sound.update3D`). pub fn sample3DFrames(mixer: *Mixer, handle: Sample3D) u32 { mixer.lock.acquire(); defer mixer.lock.release(); diff --git a/src/engine/mss/positional.zig b/src/engine/mss/positional.zig index 6a9c6e3c..b5198a1a 100644 --- a/src/engine/mss/positional.zig +++ b/src/engine/mss/positional.zig @@ -18,7 +18,7 @@ pub const Vector = math.Vector; /// The speed of sound, a millisecond, in DirectSound3D's default metres. pub const speed_of_sound = 0.3433; -/// A cone's angle, in degrees, that takes in every direction: no cone at all. +/// A cone angle, in degrees, that covers every direction: no cone at all. pub const full_cone = 360; /// Up, for a 3D sample's orientation. @@ -32,8 +32,9 @@ pub const Placing = struct { face: Vector = .{ 0, 0, 1 }, min_distance: f32 = 1, max_distance: f32 = 1000, - /// The cone: inside half the inner angle full volume, outside half the outer angle - /// `outer_volume` over `mss.max_level`, in degrees; `full_cone` each for none. + /// The cone, with angles in degrees. Within half the inner angle the volume is full, and + /// beyond half the outer angle it is `outer_volume` over `mss.max_level`. `full_cone` for + /// both angles means no cone. inner_angle: f32 = full_cone, outer_angle: f32 = full_cone, outer_volume: f32 = mss.max_level, diff --git a/src/engine/mss/voice.zig b/src/engine/mss/voice.zig index 9a735eba..a6fc6574 100644 --- a/src/engine/mss/voice.zig +++ b/src/engine/mss/voice.zig @@ -42,7 +42,7 @@ pub const Voice = struct { phase: f32 = 0, /// The frames are used up and nothing is left to loop. ended: bool = false, - /// Whether it plays its channels mixed to one (`wave.Decoder.downmix`), as a 3D sample does. + /// Whether its channels are mixed to mono (`wave.Decoder.downmix`), as for a 3D sample. mono: bool = false, /// A voice for `file`, a WAVE sound, at its own rate, not yet playing. diff --git a/src/engine/web.zig b/src/engine/web.zig index fa260ed6..5aa5265f 100644 --- a/src/engine/web.zig +++ b/src/engine/web.zig @@ -35,10 +35,10 @@ const log = std.log.scoped(.web); pub const allocator = std.heap.smp_allocator; /// The size of the buffers a response is read through. -pub const chunk_size = 64 * 1024; +pub const chunk_size = std.fmt.parseIntSizeSuffix("64KiB", 10) catch unreachable; /// The longest redirect location accepted, as GitHub's release downloads redirect. RFC 9110 /// recommends at least 8000 bytes. -pub const redirect_size = 8 * 1024; +pub const redirect_size = std.fmt.parseIntSizeSuffix("8KiB", 10) catch unreachable; /// How many redirects a download follows at most. GitHub's release downloads redirect once, to /// another host. const most_redirects = 5; diff --git a/src/formats/cdimage/cdi.zig b/src/formats/cdimage/cdi.zig index a8620b1f..a429d99e 100644 --- a/src/formats/cdimage/cdi.zig +++ b/src/formats/cdimage/cdi.zig @@ -19,7 +19,7 @@ const mark_half = [_]u8{ 0, 0, 1, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF }; const track_mark = mark_half ++ mark_half; /// The longest table this reads. -pub const max_table = 64 * 1024; +pub const max_table = std.fmt.parseIntSizeSuffix("64KiB", 10) catch unreachable; pub const Track = struct { /// 0 for audio, 1 for Mode 1 data, 2 for Mode 2 data. diff --git a/src/formats/checksums.zig b/src/formats/checksums.zig index b492d547..a525cc2a 100644 --- a/src/formats/checksums.zig +++ b/src/formats/checksums.zig @@ -16,7 +16,7 @@ pub const Digest = [Sha256.digest_length]u8; const digits = Sha256.digest_length * 2; /// The bytes a file is read in as it is hashed. -const chunk_size = 1 << 20; +const chunk_size = std.fmt.parseIntSizeSuffix("1MiB", 10) catch unreachable; /// The digest of `bytes`. pub fn digest(bytes: []const u8) Digest { diff --git a/src/formats/dte/same.zig b/src/formats/dte/same.zig index b6b3977e..69a70d14 100644 --- a/src/formats/dte/same.zig +++ b/src/formats/dte/same.zig @@ -85,7 +85,7 @@ const Comparison = struct { b: Side, fn say(c: *Comparison, comptime message: []const u8, args: anytype) Error!?[]const u8 { - return try std.fmt.allocPrint(c.arena, message, args); + return try c.arena.print(message, args); } /// Objects compared by the records they stand for, for `statements.same`. diff --git a/src/formats/dte/source/compile.zig b/src/formats/dte/source/compile.zig index ebb3f659..67bdbecc 100644 --- a/src/formats/dte/source/compile.zig +++ b/src/formats/dte/source/compile.zig @@ -113,7 +113,7 @@ pub const Compiler = struct { /// Records why the compilation fails, at `node`, and gives `Invalid`. pub fn fail(c: *Compiler, node: Index, comptime message: []const u8, args: anytype) Error { - const text = std.fmt.allocPrint(c.arena, message, args) catch return error.OutOfMemory; + const text = c.arena.print(message, args) catch return error.OutOfMemory; c.failure.* = .{ .at = c.tree.at(node), .message = text }; return error.Invalid; } diff --git a/src/formats/dte/source/definitions.zig b/src/formats/dte/source/definitions.zig index f44e5504..b567748b 100644 --- a/src/formats/dte/source/definitions.zig +++ b/src/formats/dte/source/definitions.zig @@ -131,7 +131,7 @@ fn writeEventFields(arena: Allocator, w: *Writer) Error!void { var found: std.StringArrayHashMapUnmanaged(Checked) = .empty; for (conditions.table, 0..) |condition, number| { const event = try script.eventNames(arena, condition.values); - const condition_name = vocabulary.name(.condition, @intCast(number)) orelse try std.fmt.allocPrint(arena, "{d}", .{number}); + const condition_name = vocabulary.name(.condition, @intCast(number)) orelse try arena.print("{d}", .{number}); for (condition.values, event) |value, name| { if (!value.checked) continue; const slot = try found.getOrPut(arena, name); diff --git a/src/formats/dte/source/names.zig b/src/formats/dte/source/names.zig index b259ba19..8bee1823 100644 --- a/src/formats/dte/source/names.zig +++ b/src/formats/dte/source/names.zig @@ -129,16 +129,16 @@ pub const Namer = struct { } } const base: []const u8 = if (made.items.len == 0) - try std.fmt.allocPrint(namer.arena, "{s}_{d}", .{ kind, index }) + try namer.arena.print("{s}_{d}", .{ kind, index }) else if (std.ascii.isDigit(made.items[0])) - try std.fmt.allocPrint(namer.arena, "{s}_{s}", .{ kind, made.items }) + try namer.arena.print("{s}_{s}", .{ kind, made.items }) else if (isReserved(made.items)) - try std.fmt.allocPrint(namer.arena, "{s}_{s}", .{ made.items, kind }) + try namer.arena.print("{s}_{s}", .{ made.items, kind }) else made.items; var unique = base; var count: usize = 2; - while (namer.taken.contains(unique)) : (count += 1) unique = try std.fmt.allocPrint(namer.arena, "{s}_{d}", .{ base, count }); + while (namer.taken.contains(unique)) : (count += 1) unique = try namer.arena.print("{s}_{d}", .{ base, count }); try namer.taken.put(namer.arena, unique, {}); return unique; } diff --git a/src/formats/dte/source/reference.zig b/src/formats/dte/source/reference.zig index 389f9d55..a614a29a 100644 --- a/src/formats/dte/source/reference.zig +++ b/src/formats/dte/source/reference.zig @@ -142,14 +142,14 @@ fn writeCommand(arena: Allocator, w: *Writer, command: commands.Command, number: if (parameters.len == 0) return w.writeAll("Takes nothing.\n"); try w.writeAll("| Parameter | Type | Label |\n|---|---|---|\n"); for (parameters) |parameter| { - try w.print("| `{s}` | `{f}` | {f} |\n", .{ parameter.name, cell(try std.fmt.allocPrint(arena, "{f}", .{parameter.type})), cell(parameter.label) }); + try w.print("| `{s}` | `{f}` | {f} |\n", .{ parameter.name, cell(try arena.print("{f}", .{parameter.type})), cell(parameter.label) }); } } /// Writes `condition`, the catalogue's `number`th: what a trigger of it can watch, and the values /// its events carry. fn writeCondition(arena: Allocator, w: *Writer, condition: conditions.Condition, number: usize) Error!void { - const name = vocabulary.name(.condition, @intCast(number)) orelse try std.fmt.allocPrint(arena, "{d}", .{number}); + const name = vocabulary.name(.condition, @intCast(number)) orelse try arena.print("{d}", .{number}); try w.print("\n### `{s}`\n\nThe catalogue's `{s}`, condition {d}.", .{ name, condition.name, number }); var subjects: std.ArrayList([]const u8) = .empty; inline for (.{ .{ "ship", "a ship" }, .{ "flight_group", "a flight group" }, .{ "squad", "a squad" } }) |pair| { diff --git a/src/formats/dte/source/syntax.zig b/src/formats/dte/source/syntax.zig index 07365f46..d3c00e24 100644 --- a/src/formats/dte/source/syntax.zig +++ b/src/formats/dte/source/syntax.zig @@ -365,5 +365,5 @@ test Tree { test Position { var buffer: [32]u8 = undefined; - try std.testing.expectEqualStrings("12:5", try std.fmt.bufPrint(&buffer, "{f}", .{Position{ .line = 11, .column = 4 }})); + try std.testing.expectEqualStrings("12:5", try std.mem.print(&buffer, "{f}", .{Position{ .line = 11, .column = 4 }})); } diff --git a/src/formats/dte/source/write.zig b/src/formats/dte/source/write.zig index e8bd4bcf..bbd7c2b8 100644 --- a/src/formats/dte/source/write.zig +++ b/src/formats/dte/source/write.zig @@ -137,7 +137,7 @@ const Source = struct { if ((counts.get(start) orelse 0) < 2 or source.handlers.contains(start)) continue; const subject = if (owner) |object| source.record_names.objects[object] orelse "" else ""; const condition = vocabulary.name(.condition, @backingInt(trigger.condition)) orelse ""; - const display = try std.fmt.allocPrint(source.arena, "{s} {s}", .{ subject, condition }); + const display = try source.arena.print("{s} {s}", .{ subject, condition }); try source.handlers.put(source.arena, start, try source.namer.name(display, @tagName(Word.handler), index)); } } @@ -250,9 +250,9 @@ const Source = struct { for (try source.mission.membersOf(index)) |member| { const object = source.record_names.object(member.object_id); try members.append(source.arena, if (member.part()) |component| - try std.fmt.allocPrint(source.arena, "{t}({f}, {d})", .{ Word.component, object, component }) + try source.arena.print("{t}({f}, {d})", .{ Word.component, object, component }) else - try std.fmt.allocPrint(source.arena, "{f}", .{object})); + try source.arena.print("{f}", .{object})); } try writeList(w, members.items, try source.open(w, name, .squad, squad.name)); } @@ -262,7 +262,7 @@ const Source = struct { try heading(w, "Formations", source.formations.len); for (source.formations, source.formation_names, source.formation_points) |formation, name, list| { var points: std.ArrayList([]const u8) = .empty; - for (list) |point| try points.append(source.arena, try std.fmt.allocPrint(source.arena, "{f}", .{vector(source.points[point].offset)})); + for (list) |point| try points.append(source.arena, try source.arena.print("{f}", .{vector(source.points[point].offset)})); try writeList(w, points.items, try source.open(w, name, .formation, formation.name)); } } @@ -306,7 +306,7 @@ const Source = struct { try heading(w, "Triggers", source.triggers.len); for (source.triggers, source.owners) |trigger, owner| { const subject: script.Indexed = if (owner) |object| source.record_names.object(object) else .{ .name = "nil" }; - const head = try std.fmt.allocPrint(source.arena, "{t}({f}, {f}", .{ Word.on, subject, named(.condition, @backingInt(trigger.condition)) }); + const head = try source.arena.print("{t}({f}, {f}", .{ Word.on, subject, named(.condition, @backingInt(trigger.condition)) }); var fields: Fields = .init(source.arena); switch (trigger.repeat) { .once => {}, @@ -439,11 +439,11 @@ const Source = struct { const formation = source.points[point].formation; if (formation < source.formations.len) { if (std.mem.findScalar(u16, source.formation_points[formation], point)) |place| { - return std.fmt.allocPrint(source.arena, "{s}[{d}]", .{ source.formation_names[formation], place + 1 }); + return source.arena.print("{s}[{d}]", .{ source.formation_names[formation], place + 1 }); } } } - return std.fmt.allocPrint(source.arena, "{t}({d})", .{ Word.formation_point_index, point }); + return source.arena.print("{t}({d})", .{ Word.formation_point_index, point }); } }; @@ -532,7 +532,7 @@ const Fields = struct { } fn add(fields: *Fields, key: []const u8, comptime format: []const u8, args: anytype) Allocator.Error!void { - try fields.list.append(fields.arena, try std.fmt.allocPrint(fields.arena, "{s} = " ++ format, .{key} ++ args)); + try fields.list.append(fields.arena, try fields.arena.print("{s} = " ++ format, .{key} ++ args)); } /// Writes the fields as a table (`writeTable`). diff --git a/src/formats/games/bsg/level.zig b/src/formats/games/bsg/level.zig index 2de66a69..137e3606 100644 --- a/src/formats/games/bsg/level.zig +++ b/src/formats/games/bsg/level.zig @@ -192,7 +192,7 @@ pub const Level = struct { const kind = for (std.enums.values(PointKind)) |candidate| { if (isPoint(attribute.name, candidate)) break candidate; } else continue; - const size = if (kind == .jet) if (level.find(try std.fmt.allocPrint(arena, "{s}Size", .{attribute.name}))) |sized| sized.number() else null else null; + const size = if (kind == .jet) if (level.find(try arena.print("{s}Size", .{attribute.name}))) |sized| sized.number() else null else null; try found.append(arena, .{ .name = attribute.name, .kind = kind, .position = position, .size = size }); } return found.items; @@ -208,7 +208,7 @@ fn fieldOf(name: []const u8, prefix: []const u8) ?[]const u8 { /// The attribute name of `prefix`'s field `name`. fn fieldName(arena: Allocator, prefix: []const u8, name: []const u8) Allocator.Error![]const u8 { - return std.fmt.allocPrint(arena, "{s}_{s}", .{ prefix, name }); + return arena.print("{s}_{s}", .{ prefix, name }); } /// The matrix row a hardpoint's field gives, `xform0` to `xform3`, or null for another field. diff --git a/src/formats/games/bsg/model.zig b/src/formats/games/bsg/model.zig index d927eea1..2a3d6d70 100644 --- a/src/formats/games/bsg/model.zig +++ b/src/formats/games/bsg/model.zig @@ -46,7 +46,7 @@ pub const Part = struct { /// `.bmsh` for `.msh`, such as `models/shv2vi00/shv2vi00c1_viper2shape.bmsh`. pub fn meshPath(part: Part, arena: Allocator, model: []const u8) Allocator.Error![]const u8 { const stem = if (std.mem.endsWith(u8, part.mesh, source_extension)) part.mesh[0 .. part.mesh.len - source_extension.len] else part.mesh; - const path = try std.fmt.allocPrint(arena, "models/{s}/{s}.bmsh", .{ model, stem }); + const path = try arena.print("models/{s}/{s}.bmsh", .{ model, stem }); return std.ascii.lowerString(path, path); } }; diff --git a/src/formats/games/bsg/to_gltf.zig b/src/formats/games/bsg/to_gltf.zig index 21fe53b0..6a1183ee 100644 --- a/src/formats/games/bsg/to_gltf.zig +++ b/src/formats/games/bsg/to_gltf.zig @@ -114,9 +114,9 @@ const Making = struct { fn objectNode(made: *Making, name: []const u8, depth: usize) Error!u32 { const allocator = made.arena(); const lower = try std.ascii.allocLowerString(allocator, name); - const model_path = try std.fmt.allocPrint(allocator, "models/{s}/{s}.mdl", .{ lower, lower }); + const model_path = try allocator.print("models/{s}/{s}.mdl", .{ lower, lower }); const model: model_file.Model = try .parse(allocator, try made.files.read(allocator, model_path) orelse return error.MissingModel); - const level_path = try std.fmt.allocPrint(allocator, "levels/{s}.lvl", .{lower}); + const level_path = try allocator.print("levels/{s}.lvl", .{lower}); const level: ?level_file.Level = if (try made.files.read(allocator, level_path)) |bytes| try .parse(allocator, bytes) else null; var children: std.ArrayList(u32) = .empty; @@ -197,7 +197,7 @@ const Making = struct { if (depth >= max_mounts) break :mount; const node = made.objectNode(mounted, depth + 1) catch |err| switch (err) { error.MissingModel => { - try made.missing.append(allocator, try std.fmt.allocPrint(allocator, "models/{s}/", .{mounted})); + try made.missing.append(allocator, try allocator.print("models/{s}/", .{mounted})); break :mount; }, else => |e| return e, @@ -310,7 +310,7 @@ const Making = struct { fn textureOf(made: *Making, name: []const u8) Error!?Texture { const allocator = made.arena(); const stem = try std.ascii.allocLowerString(allocator, std.Io.Dir.path.stem(name)); - const source = try std.fmt.allocPrint(allocator, "models/textures/{s}.btga", .{stem}); + const source = try allocator.print("models/textures/{s}.btga", .{stem}); if (made.textures.get(source)) |known| return known; const bytes = try made.files.read(allocator, source) orelse { try made.missing.append(allocator, source); @@ -318,7 +318,7 @@ const Making = struct { return null; }; const read: texture_file.Texture = try .parse(allocator, bytes); - const file = try std.fmt.allocPrint(allocator, "{s}.png", .{stem}); + const file = try allocator.print("{s}.png", .{stem}); std.mem.replaceScalar(u8, file, ' ', '_'); try made.pictures.append(allocator, .{ .file = file, .texture = read }); const found: Texture = .{ .ref = try made.gltf.texture(file), .alpha = read.hasAlpha() }; @@ -364,7 +364,7 @@ fn hardpointName(arena: Allocator, defined: level_file.Hardpoint) Allocator.Erro /// What tells materials apart: the record and the textures' names. fn materialKey(arena: Allocator, material: mesh_file.Material) Allocator.Error![]const u8 { const textures = material.textures; - return std.fmt.allocPrint(arena, "{x}:{s}:{s}:{s}:{s}", .{ std.mem.asBytes(material.record), textures[0], textures[1], textures[2], textures[3] }); + return arena.print("{x}:{s}:{s}:{s}:{s}", .{ std.mem.asBytes(material.record), textures[0], textures[1], textures[2], textures[3] }); } /// A material's name: its texture's, such as `sh_v2_viper01`, and how it blends where it doesn't @@ -373,9 +373,9 @@ fn materialName(arena: Allocator, material: mesh_file.Material) Allocator.Error! const stem = if (material.colour()) |name| std.Io.Dir.path.stem(name) else "untextured"; return switch (material.record.blend) { .@"opaque" => stem, - .additive => std.fmt.allocPrint(arena, "{s}_additive", .{stem}), - .blended => std.fmt.allocPrint(arena, "{s}_blended", .{stem}), - _ => std.fmt.allocPrint(arena, "{s}_blend{d}", .{ stem, @backingInt(material.record.blend) }), + .additive => arena.print("{s}_additive", .{stem}), + .blended => arena.print("{s}_blended", .{stem}), + _ => arena.print("{s}_blend{d}", .{ stem, @backingInt(material.record.blend) }), }; } diff --git a/src/formats/games/bsg/wart.zig b/src/formats/games/bsg/wart.zig index 9d7273ba..73440e5c 100644 --- a/src/formats/games/bsg/wart.zig +++ b/src/formats/games/bsg/wart.zig @@ -60,7 +60,7 @@ pub const Entry = extern struct { }; /// The most a compressed member's chunk unpacks to. Each chunk's stream unpacks on its own. -pub const chunk_size = 128 * 1024; +pub const chunk_size = std.fmt.parseIntSizeSuffix("128KiB", 10) catch unreachable; /// A chunk's length, before its stream. const ChunkLength = u32; diff --git a/src/formats/games/trek/res.zig b/src/formats/games/trek/res.zig index 894d8609..bac9fee6 100644 --- a/src/formats/games/trek/res.zig +++ b/src/formats/games/trek/res.zig @@ -135,7 +135,7 @@ pub const File = struct { /// Where to write it under a folder, `/`-separated: its name with forward slashes, or /// `slot_N` for a file without one. pub fn path(item: File, arena: Allocator) Allocator.Error![]const u8 { - const name = item.name orelse return std.fmt.allocPrint(arena, "slot_{d}", .{item.slot}); + const name = item.name orelse return arena.print("slot_{d}", .{item.slot}); const relative = try arena.dupe(u8, std.mem.trimStart(u8, name, "\\")); std.mem.replaceScalar(u8, relative, '\\', '/'); return relative; diff --git a/src/formats/numbered.zig b/src/formats/numbered.zig index 7d101eb6..528eeedc 100644 --- a/src/formats/numbered.zig +++ b/src/formats/numbered.zig @@ -17,7 +17,8 @@ pub const Part = struct { index: usize, }; -/// `stem`, a part's name without an extension, split; null where `name` writes no such name. +/// Splits `stem`, a part's name without an extension, into the file's name and the part's number. +/// Returns null if `name` would never write `stem`. pub fn parse(stem: []const u8) ?Part { const mark = std.mem.findScalarLast(u8, stem, '_') orelse return null; const digits = stem[mark + 1 ..]; diff --git a/src/formats/ogg.zig b/src/formats/ogg.zig index 18ed9cdb..06973af8 100644 --- a/src/formats/ogg.zig +++ b/src/formats/ogg.zig @@ -21,7 +21,7 @@ pub const capture = "OggS"; /// A page's flags. pub const Flags = packed struct(u8) { - /// The page's first segments carry on the packet that the stream's previous page didn't end. + /// The page continues a packet that started on the stream's previous page. continued: bool = false, /// The stream's first page. first: bool = false, @@ -100,7 +100,7 @@ pub const Page = struct { return if (page.checksum(bytes) == header.crc) page else null; } - /// The checksum of the page, which `bytes` start with: its bytes, with its checksum's as 0. + /// The checksum of the page at the start of `bytes`, computed with the checksum field as 0. fn checksum(page: Page, bytes: []const u8) u32 { const crc_at = @offsetOf(PageHeader, "crc"); var crc: Crc = .init(); @@ -111,8 +111,8 @@ pub const Page = struct { } }; -/// A file's pages, in order. Bytes that start no page are passed over, and so are pages that are -/// cut short or have the wrong checksum, as a decoder finds its way back to the pages. +/// A file's pages, in order. It skips bytes that don't start a page, and pages that are cut short or +/// have a wrong checksum, the way a decoder resynchronises. pub const Pages = struct { bytes: []const u8, at: usize = 0, @@ -136,8 +136,8 @@ pub const Pages = struct { } }; -/// The packets of a file's first stream, in order, among the pages of any other streams. A packet -/// that runs over pages is put together. One that a missing or damaged page cuts is passed over. +/// The packets of a file's first stream, in order, skipping the pages of other streams. It joins a +/// packet that spans pages, and skips a packet that a missing or damaged page cuts. pub const Packets = struct { pages: Pages, /// The stream's serial number, from its first page. @@ -167,9 +167,10 @@ pub const Packets = struct { packets.* = undefined; } - /// The stream's next packet, which lasts until the next call; null at the stream's end. + /// Returns the stream's next packet, valid until the next call; null at the end of the + /// stream. pub fn next(packets: *Packets, gpa: Allocator) Allocator.Error!?[]const u8 { - // Done with the last packet handed out, which `joined` holds if it ran over pages. + // Drop the previous packet, which `joined` holds if it spanned pages. if (!packets.joining) packets.joined.clearRetainingCapacity(); while (true) { const page = packets.page orelse { @@ -203,9 +204,9 @@ pub const Packets = struct { return .{ .bytes = page.body[start..packets.at], .ends = ends }; } - /// The samples of each channel that the stream's packets decode to, as its last page read gives - /// them: its granule position, less `pre_skip`, the samples the decoder drops at the start - /// (`Stream.pre_skip`). Null if no page gives a granule position. + /// The number of samples per channel the stream's packets decode to: the granule position of + /// the last page read, less `pre_skip`, the samples the decoder drops at the start + /// (`Stream.pre_skip`). Null if no page gave a granule position. pub fn samples(packets: Packets, pre_skip: u16) ?u64 { return (packets.granule orelse return null) -| pre_skip; } @@ -224,8 +225,8 @@ pub const Packets = struct { packets.at = 0; if (page.granule()) |granule| packets.granule = granule; packets.ended = page.header.flags.last; - // The start of a packet whose page is missing, and the end of one whose start is, are - // passed over. + // Skip the start of a packet whose next page is missing, and the end of a packet whose + // start was missing. if (packets.joining and !(follows and page.header.flags.continued)) { packets.joining = false; packets.joined.clearRetainingCapacity(); @@ -237,8 +238,8 @@ pub const Packets = struct { } }; -/// Appends to `lacing` the lengths that lace a packet of `length` bytes: as many of the most a -/// segment holds as fit, then the rest, which may be 0. +/// Appends the segment lengths of a packet of `length` bytes to `lacing`: `segment_most` for each +/// full segment, then the remainder, which may be 0. fn lace(gpa: Allocator, lacing: *std.ArrayList(u8), length: usize) Allocator.Error!void { @memset(try lacing.addManyAsSlice(gpa, length / segment_most), segment_most); try lacing.append(gpa, @intCast(length % segment_most)); @@ -256,10 +257,10 @@ pub const Error = error{ BadHeaders, } || Allocator.Error; -/// What a stream's header packets say, which its decoder is set up with. +/// What a stream's header packets say, for setting up its decoder. pub const Stream = struct { codec: Codec, - /// Samples a second. + /// Samples per second. rate: u32, channels: u8, /// The samples of each channel that the decoder drops at the start: Opus's pre-skip, the @@ -269,7 +270,7 @@ pub const Stream = struct { /// header packets laced together (`xiph_lacing`). setup: []u8, - /// Reads the stream's header packets from `packets`, which then go on with its audio. + /// Reads the stream's header packets from `packets`. The packets that follow are audio. pub fn read(gpa: Allocator, packets: *Packets) Error!Stream { const first = try packets.next(gpa) orelse return error.NoStream; if (std.mem.startsWith(u8, first, opus_head)) return readOpus(gpa, packets, first); @@ -342,12 +343,11 @@ const opus_tags = "OpusTags"; /// 0, a table follows it of how the channels map to the Opus streams in each packet. pub const OpusHead = extern struct { signature: [opus_head.len]u8, - /// 1, and anything up to 15 for a later version that decoders of this one can read. + /// 1, or up to 15 for later versions that this decoder can still read. version: u8, channels: u8, pre_skip: u16 align(1), - /// The rate of the audio that the encoder was given, which the stream doesn't keep - /// (`opus_rate`). + /// The sample rate of the encoder's input. The stream always decodes at `opus_rate`. input_rate: u32 align(1), /// A gain for the decoder to apply, in 256ths of a decibel. FFmpeg's decoder applies it. output_gain: i16 align(1), @@ -394,8 +394,8 @@ pub const VorbisIdentification = extern struct { } }; -/// How many headers follow in Xiph lacing, less one: the first byte of Vorbis's three headers laced -/// together, which then give the first two headers' lengths in the way a page gives its segments'. +/// The first byte of Vorbis's three headers in Xiph lacing: the number of headers less one. The +/// lengths of the first two headers follow, coded like a page's segment lengths. const xiph_lacing = 2; /// Ogg files for the tests, built by hand. @@ -403,7 +403,7 @@ pub const testing = struct { /// The serial number of the streams that the tests make. pub const serial = 0x4F52; - /// What a page says of itself: its flags, its granule position, its stream and its number. + /// A page's header fields: flags, granule position, stream and page number. pub const Place = struct { flags: Flags = .{}, granule: i64, diff --git a/src/formats/shp.zig b/src/formats/shp.zig index 1b5484ca..2fa01d67 100644 --- a/src/formats/shp.zig +++ b/src/formats/shp.zig @@ -1145,7 +1145,7 @@ pub const PartData = struct { /// file, and `budget_base` more. A shipped model's records take less than twice its bytes, where an /// earlier exporter wrote shorter records than the engine keeps. const budget_per_byte = 16; -const budget_base = 1 << 20; +const budget_base = std.fmt.parseIntSizeSuffix("1MiB", 10) catch unreachable; /// A parsed model. Everything is allocated from the allocator passed to `parse`, which is expected /// to be an arena. diff --git a/src/formats/wave.zig b/src/formats/wave.zig index 3461f2b9..f3ce6892 100644 --- a/src/formats/wave.zig +++ b/src/formats/wave.zig @@ -13,8 +13,7 @@ const riff = @import("riff.zig"); pub const extension = ".wav"; -/// The level of a 16-bit sample at full scale: a sample as a float from -1 to 1 is the sample over -/// this. +/// The level of a 16-bit sample at full scale. A sample divided by this is a float from -1 to 1. pub const full_scale = -std.math.minInt(i16); /// What a sound's WAVE header says. @@ -246,8 +245,8 @@ test pcm16 { try std.testing.expectEqual(null, decoder.next()); } -/// Reads a wave's frames in order, as 16-bit samples, the one channel of a mono sound in both. IMA -/// ADPCM is decoded as it goes, a block at a time, with no buffer of its own. +/// Reads a wave's frames in order as 16-bit samples, with a mono sound's channel in both channels. +/// IMA ADPCM is decoded on the fly, a block at a time, without a buffer. pub const Decoder = struct { wave: Wave, /// How `wave` holds its samples, which `init` checked it can decode. @@ -264,13 +263,14 @@ pub const Decoder = struct { /// The sample formats it decodes. pub const Codec = enum { pcm8, pcm16, ima_adpcm }; - /// A frame of `next` with its two channels mixed to one: their average. A mono frame, which - /// `next` gives in both channels, stays as it is. + /// Mixes a frame from `next` to one channel, the average of its two. A mono frame, which `next` + /// gives in both channels, is unchanged. pub fn downmix(frame: [2]i16) i16 { return @intCast(@divTrunc(@as(i32, frame[0]) + frame[1], 2)); } - /// A decoder of `wave`. A sound without a rate is refused, as it would never play to its end. + /// A decoder for `wave`. A sound with a rate of 0 is rejected, since it would never finish + /// playing. pub fn init(wave: Wave) Error!Decoder { if (wave.channels != 1 and wave.channels != 2) return error.Unsupported; if (wave.block_align == 0 or wave.rate == 0) return error.Unsupported; @@ -290,8 +290,8 @@ pub const Decoder = struct { return .{ .wave = wave, .codec = codec, .frames = wave.frameCount() }; } - /// The rest of the frames, as 16-bit samples made in `gpa`, interleaved in the wave's own - /// channels. + /// Decodes the remaining frames into 16-bit samples, interleaved, with the wave's own number + /// of channels. pub fn rest(decoder: *Decoder, gpa: std.mem.Allocator) std.mem.Allocator.Error![]i16 { const channels = decoder.wave.channels; const samples = try gpa.alloc(i16, @as(usize, decoder.frames - decoder.frame) * channels); @@ -407,8 +407,8 @@ pub const testing = struct { /// A chunk of `body`, with its header and its padding. pub const chunk = riff.testing.chunk; - /// A WAVE file of `format`, its format chunk `fmt` (and the chunk's `fmt_extension`), then - /// `extra` chunks before the data. + /// A WAVE file with the format chunk `fmt` (followed by `fmt_extension`), then the `extra` + /// chunks, then the data. pub fn file(comptime fmt: Wave.FormatChunk, comptime fmt_extension: []const u8, comptime extra: []const u8, comptime data: []const u8) []const u8 { const body = Wave.form ++ chunk("fmt ", &std.mem.toBytes(fmt) ++ fmt_extension) ++ extra ++ chunk("data", data); return chunk(riff.Header.riff_id, body); @@ -494,7 +494,7 @@ test "Decoder reads PCM" { try std.testing.expectEqual([2]i16{ 127 << 8, 127 << 8 }, bytes.next().?); try std.testing.expectEqual([2]i16{ -128 << 8, -128 << 8 }, bytes.next().?); - // A sound without a rate is refused. + // A sound with a rate of 0 is rejected. var rateless = try Wave.parse(comptime testing.pcm("\x00\x00")); rateless.rate = 0; try std.testing.expectError(error.Unsupported, Decoder.init(rateless)); diff --git a/src/openreliant/debug.zig b/src/openreliant/debug.zig index ca0a9725..6137cdc2 100644 --- a/src/openreliant/debug.zig +++ b/src/openreliant/debug.zig @@ -603,7 +603,7 @@ const Debugger = struct { /// The key `format` makes with `number`, written in `buffer`; empty where it doesn't fit. fn keyOf(buffer: []u8, comptime format: []const u8, number: usize) []const u8 { - return std.fmt.bufPrint(buffer, format, .{number}) catch ""; + return std.mem.print(buffer, format, .{number}) catch ""; } fn unplacedText(err: (script.Unplaced || Allocator.Error)) []const u8 { @@ -665,7 +665,7 @@ test "the debugger's commands and the game's messages" { // The state names the thread's place, the globals and the variables that aren't 0. out.clearRetainingCapacity(); var state: [256]u8 = undefined; - const text = try std.fmt.bufPrint(&state, "clock=3\nhold=held\nstep=into\npaused=0\nthread.0.at={d}\nthread.0.wake=0\nglobal.1=7\nvariable.mission_over=0\nvariable.backup_available=1\n", .{second}); + const text = try std.mem.print(&state, "clock=3\nhold=held\nstep=into\npaused=0\nthread.0.at={d}\nthread.0.wake=0\nglobal.1=7\nvariable.mission_over=0\nvariable.backup_available=1\n", .{second}); debugger.handle(.{ .tag = @backingInt(link.Own.state), .payload = text }); try std.testing.expect(std.mem.find(u8, out.written(), "Clock 3 s. The script is held, stepping (into).") != null); try std.testing.expect(std.mem.find(u8, out.written(), " 0 at ") != null); diff --git a/src/openreliant/install.zig b/src/openreliant/install.zig index 236b5c71..3726eff7 100644 --- a/src/openreliant/install.zig +++ b/src/openreliant/install.zig @@ -523,6 +523,9 @@ fn cdDrives(arena: Allocator) Allocator.Error![]const []const u8 { return drives.items; } +/// The most of Linux's mount table that is read, far more than any system lists. +const max_mount_table = std.fmt.parseIntSizeSuffix("1MiB", 10) catch unreachable; + /// Linux's table of mounts, `/proc/self/mounts`. It's read as a stream: like every file in `/proc`, /// it gives its size as 0, which a reader that goes by the size takes as empty. fn mountTable(io: Io, arena: Allocator) ![]const u8 { @@ -530,7 +533,7 @@ fn mountTable(io: Io, arena: Allocator) ![]const u8 { defer file.close(io); var buffer: [4096]u8 = undefined; var reader = file.readerStreaming(io, &buffer); - return reader.interface.allocRemaining(arena, .limited(1 << 20)); + return reader.interface.allocRemaining(arena, .limited(max_mount_table)); } /// The mount points of the disc file systems, ISO 9660 and UDF, in a table of mounts as Linux diff --git a/src/openreliant/log_file.zig b/src/openreliant/log_file.zig index a766c8fb..ba02ca34 100644 --- a/src/openreliant/log_file.zig +++ b/src/openreliant/log_file.zig @@ -17,10 +17,10 @@ pub const name = "openreliant.log"; /// How large the file can grow. Each run starts it afresh, so it never takes more of the disk. /// Messages past it only go to the terminal, and a crash is still written. -pub const max_size = 8 * 1024 * 1024; +pub const max_size = std.fmt.parseIntSizeSuffix("8MiB", 10) catch unreachable; /// What the terminal, the scripting console and the log file itself say once the log is full. -pub const full_message = std.fmt.comptimePrint("the log file {s} is full at {d} MiB, so the rest of this run's messages only go to the terminal. A script that writes a message every frame can fill it.", .{ name, max_size >> 20 }); +pub const full_message = std.fmt.comptimePrint("the log file {s} is full at {Bi}, so the rest of this run's messages only go to the terminal. A script that writes a message every frame can fill it.", .{ name, max_size }); const full_line = "warning: " ++ full_message ++ "\n"; diff --git a/src/openreliant/movies.zig b/src/openreliant/movies.zig index caf5ef78..ec9a1b15 100644 --- a/src/openreliant/movies.zig +++ b/src/openreliant/movies.zig @@ -245,16 +245,15 @@ const Bank = struct { file: hog_snd.BankFile, voice: ?u8, - /// The bank `name` names, read from `resources` (`hog_load`) with the sounds mods give in place - /// of its own, which `codec` decodes, and its first sound played through `sound` at `volume`, - /// `loops` times, from the middle at its own pitch (`sound_play`); null where the bank is left - /// out. + /// Reads the bank `name` from `resources` (`hog_load`), with the sounds that mods replace + /// decoded by `codec`, and plays its first sound through `sound` at `volume`, `loops` times, + /// centred and at its own pitch (`sound_play`). Returns null if the bank can't be read. fn start(gpa: Allocator, resources: *const game.bigfile.Hog, codec: engine.bink.Codec, sound: *hog_snd.Sound, name: []const u8, volume: i32, loops: u32) ?Bank { const file = hog_snd.BankFile.read(gpa, resources, name, codec) orelse return null; return .{ .file = file, .voice = sound.play(file.bank, 0, volume, loops, hog_snd.centre, hog_snd.own_pitch) }; } - /// Its voice ended (`sound_voice_end`), then the bank freed. + /// Ends its voice (`sound_voice_end`), then frees the bank. fn stop(bank: Bank, gpa: Allocator, sound: *hog_snd.Sound) void { if (bank.voice) |v| sound.endVoice(v); bank.file.deinit(gpa); diff --git a/src/openreliant/updates.zig b/src/openreliant/updates.zig index 87025d58..6e5a5181 100644 --- a/src/openreliant/updates.zig +++ b/src/openreliant/updates.zig @@ -42,7 +42,7 @@ pub const time_limit: Io.Duration = .fromSeconds(5); /// How much of GitHub's answer is read at most. A release's description, with its notes and its /// files, is far smaller. -const max_answer = 1 << 20; +const max_answer = std.fmt.parseIntSizeSuffix("1MiB", 10) catch unreachable; /// The key in `[OpenReliant]` that keeps the newest release the main menu has shown. pub const seen_key = "UpdateSeen"; diff --git a/src/platform/audio.zig b/src/platform/audio.zig index 6ff0761d..7bcede61 100644 --- a/src/platform/audio.zig +++ b/src/platform/audio.zig @@ -27,12 +27,12 @@ const log = std.log.scoped(.sdl); /// The frames the callback renders at a time. const chunk = 512; -/// The rate the output runs at where the system says nothing of the device's, and the lowest it -/// runs at whatever the device says. +/// The output's rate when the system doesn't report the device's rate, and the lowest rate the +/// output runs at, whatever the device reports. const fallback_rate = 44100; const lowest_rate = 8000; -/// How often `Output.update` looks at what the output is, in milliseconds. +/// How often `Output.update` checks whether the output is a pair of headphones, in milliseconds. const look_interval = 1000; /// What plays Miles's calls. @@ -56,7 +56,7 @@ pub const Output = struct { channels: u8, source: Source, master: ?mss.master.Master, - /// When `update` last looked at what the output is, in SDL's milliseconds. + /// When `update` last checked the output, in SDL's milliseconds. looked_at: u64 = 0, const Source = union(enum) { @@ -108,8 +108,8 @@ pub const Output = struct { output.gpa.destroy(output); } - /// Once a frame: now and then, whether the output has changed between headphones and anything - /// else, for OpenAL's HRTF. + /// Called once a frame. Every `look_interval`, checks whether the output has switched between + /// headphones and anything else, for OpenAL's HRTF. pub fn update(output: *Output) void { const renderer = output.openAl() orelse return; if (renderer.settings.hrtf != .auto or renderer.channels != 2) return; @@ -124,7 +124,7 @@ pub const Output = struct { renderer.followOutput(on); } - /// Sets when OpenAL Soft renders for headphones, where it plays. + /// Sets when OpenAL Soft renders for headphones. Does nothing with the software mixer. pub fn setHrtf(output: *Output, hrtf: openal.Hrtf) void { const renderer = output.openAl() orelse return; const lock = lockOf(output.stream); @@ -133,7 +133,7 @@ pub const Output = struct { renderer.setHrtf(hrtf, headphones()); } - /// Turns OpenAL Soft's reverbs on or off, where it plays. + /// Turns OpenAL Soft's reverbs on or off. Does nothing with the software mixer. pub fn setReverb(output: *Output, on: bool) void { const renderer = output.openAl() orelse return; const lock = lockOf(output.stream); @@ -150,7 +150,7 @@ pub const Output = struct { output.master = if (settings) |chosen| .init(output.rate, output.channels, chosen) else null; } - /// OpenAL Soft's renderer, where it plays. + /// OpenAL Soft's renderer, or null with the software mixer. fn openAl(output: *Output) ?*openal.Renderer { return switch (output.source) { .openal => |renderer| renderer, diff --git a/src/platform/gpu/geometry.zig b/src/platform/gpu/geometry.zig index cb2f82d6..5fc71eac 100644 --- a/src/platform/gpu/geometry.zig +++ b/src/platform/gpu/geometry.zig @@ -36,9 +36,12 @@ pub const Geometry = struct { c.SDL_UploadToGPUBuffer(copy, &.{ .transfer_buffer = geometry.transfer, .offset = geometry.size }, &.{ .buffer = geometry.indices, .size = index_bytes }, true); } - /// Buffers of at least `needed` bytes each, in powers of two from 64 KiB. + /// The smallest buffers `make` creates. + const least_size: u32 = std.fmt.parseIntSizeSuffix("64KiB", 10) catch unreachable; + + /// Buffers of at least `needed` bytes each, in powers of two from `least_size`. fn make(handle: *c.SDL_GPUDevice, needed: u32) Error!Geometry { - const size = std.math.ceilPowerOfTwo(u32, @max(needed, 64 * 1024)) catch return error.OutOfMemory; + const size = std.math.ceilPowerOfTwo(u32, @max(needed, least_size)) catch return error.OutOfMemory; const transfer_size = std.math.mul(u32, size, 2) catch return error.OutOfMemory; const vertices = c.SDL_CreateGPUBuffer(handle, &.{ .usage = c.SDL_GPU_BUFFERUSAGE_VERTEX, .size = size }) orelse return sdl.fail("SDL_CreateGPUBuffer"); errdefer c.SDL_ReleaseGPUBuffer(handle, vertices); diff --git a/src/platform/link.zig b/src/platform/link.zig index 553d9da2..f45895cf 100644 --- a/src/platform/link.zig +++ b/src/platform/link.zig @@ -25,7 +25,7 @@ pub const default_port = 22539; const most_waiting = 4 * (link.header_size + link.max_payload); /// How many bytes a read takes at most. -const read_size = 16 * 1024; +const read_size = std.fmt.parseIntSizeSuffix("16KiB", 10) catch unreachable; /// How many bytes of the game's a write gathers before they go out. const write_size = 256; diff --git a/src/platform/openal.zig b/src/platform/openal.zig index 80443bab..c55ea488 100644 --- a/src/platform/openal.zig +++ b/src/platform/openal.zig @@ -1,24 +1,24 @@ -//! The Miles Sound System's calls ([`engine/mss.zig`](../engine/mss.zig)'s `Driver`), played by -//! OpenAL Soft in place of the 3D providers the game chose from: Miles's own, DirectSound3D, EAX -//! and A3D. OpenAL renders into memory through its loopback device, which the platform's audio -//! stream pulls from, so none of OpenAL's own device backends are used. +//! Plays the Miles Sound System's calls ([`engine/mss.zig`](../engine/mss.zig)'s `Driver`) with +//! OpenAL Soft, replacing the 3D providers the game chose from: Miles's own, DirectSound3D, EAX +//! and A3D. OpenAL renders into memory through its loopback device, and the platform's audio +//! stream pulls from there, so none of OpenAL's device backends are used. //! -//! What the game asks of its provider carries over as it is: each 3D sample's distances, with the -//! inverse distance clamped model that DirectSound3D used; its cone; its velocity's Doppler shift; -//! its pitch and volume. Samples play from ahead, turned by their pan, and streams play in stereo -//! straight to the speakers. +//! Everything the game sets on its provider is passed on unchanged: each 3D sample's distances, +//! with DirectSound3D's inverse distance clamped model, its cone, its velocity for the Doppler +//! shift, its pitch and its volume. 2D samples play from in front of the listener, turned by their +//! pan, and streams play in stereo straight to the speakers. //! //! **Improvements:** //! //! - OpenAL's band-limited sinc resampler, and its smoothing of every change. -//! - UHJ stereo, or HRTF for headphones, where Miles panned left and right, and as many speakers -//! as the device has. +//! - UHJ stereo, or HRTF for headphones, instead of Miles's left and right panning, and output to +//! as many speakers as the device has. //! - A listener that moves, a stronger Doppler shift, sounds with a size, high frequencies that //! fade with distance, and a subwoofer. -//! - Reverbs. The 3D sounds ring in the generic room the game asks EAX for, though it sets the -//! effect's volume to nothing; in a hangar while a launch shows one from within; and in a small -//! room of the ship in a carrier's rooms and briefing room. The cockpit's own voice rings in a -//! cabin. +//! - Reverb. The 3D sounds get the generic room reverb the game asks EAX for, though the game sets +//! the effect's volume to zero. During a launch seen from inside a hangar they get a hangar's +//! reverb, and in a carrier's rooms and the briefing room, a small room's. The cockpit's voice +//! gets a cabin's reverb. const std = @import("std"); const Allocator = std.mem.Allocator; @@ -51,7 +51,7 @@ pub const Hrtf = enum { on, off, - /// Whether HRTF is used, where the output is `headphones` or not. + /// Whether HRTF is used, given whether the output is `headphones`. fn used(hrtf: Hrtf, headphones: bool) bool { return switch (hrtf) { .on => true, @@ -81,7 +81,8 @@ const speed_of_sound: f32 = mss.positional.speed_of_sound * velocity_scale; /// speed of sound over this, so the shift keeps within the bounds it had. const doppler_factor: f32 = 10; -/// The least reference distance and the lowest pitch OpenAL is given, which take no 0. +/// The smallest reference distance and the lowest pitch passed to OpenAL, which doesn't accept 0 +/// for either. const least_reference_distance: f32 = 0.001; const lowest_pitch: f32 = 0.001; @@ -883,9 +884,9 @@ pub const Renderer = struct { return buffer.frames; } - /// A 3D sample is placed from the listener, falls off with distance, and sends to the - /// reverb, which falls off as it does. A stereo one is spatialized all the same: without a - /// radius, OpenAL Soft places both its channels at the source. + /// A 3D sample is placed relative to the listener, falls off with distance, and sends to the + /// reverb, which falls off the same way. A stereo sample is spatialized too: without a radius, + /// OpenAL Soft places both its channels at the source. fn setUp3D(renderer: *Renderer, voice: *Voice) void { const source = voice.source; c.alSourcei(source, c.AL_SOURCE_RELATIVE, c.AL_TRUE); diff --git a/src/platform/shader_cache.zig b/src/platform/shader_cache.zig index d02b460e..d14ac3c0 100644 --- a/src/platform/shader_cache.zig +++ b/src/platform/shader_cache.zig @@ -30,7 +30,7 @@ pub const folder = "cache/shaders"; const Hash = [Sha256.digest_length]u8; /// The cache's files. Change the version when the payload's layout changes. -const Files = cache_file.Files(.{ .folder = folder, .magic = "ORSH".*, .version = 3, .Key = Hash, .max_bytes = 16 * 1024 * 1024 }); +const Files = cache_file.Files(.{ .folder = folder, .magic = "ORSH".*, .version = 3, .Key = Hash, .max_bytes = std.fmt.parseIntSizeSuffix("16MiB", 10) catch unreachable }); comptime { // The header's size, which `docs/port/renderer.md` gives the offsets after. diff --git a/src/platform/shader_compiler.zig b/src/platform/shader_compiler.zig index 419c2a3a..1078732c 100644 --- a/src/platform/shader_compiler.zig +++ b/src/platform/shader_compiler.zig @@ -21,7 +21,7 @@ extern fn openreliant_shader_diagnostic(result: *const Native) [*:0]const u8; extern fn openreliant_shader_free(result: *Native) void; /// The largest part of a shader's source the compiler takes. -pub const max_source_bytes = 1024 * 1024; +pub const max_source_bytes = std.fmt.parseIntSizeSuffix("1MiB", 10) catch unreachable; /// The message for a source or a name that holds a NUL byte, which the libraries can't take. const nul_message = "shader source and filename cannot contain NUL"; @@ -103,7 +103,7 @@ pub fn compile(gpa: Allocator, name: []const u8, source: []const u8) Allocator.E pub fn compileParts(gpa: Allocator, kind: Kind, stage: Stage, parts: []const Part, preamble: []const u8) Allocator.Error!Result { std.debug.assert(parts.len > 0 and parts.len <= max_parts); for (parts) |part| { - if (part.source.len > max_source_bytes) return .{ .diagnostic = try gpa.print("{s}: shader source exceeds 1 MiB", .{part.name}) }; + if (part.source.len > max_source_bytes) return .{ .diagnostic = try gpa.print("{s}: shader source exceeds {Bi}", .{ part.name, max_source_bytes }) }; if (std.mem.findScalar(u8, part.source, 0) != null or std.mem.findScalar(u8, part.name, 0) != null) return .{ .diagnostic = try gpa.dupe(u8, nul_message) }; } diff --git a/src/platform/texture_cache.zig b/src/platform/texture_cache.zig index 9ead6f48..7becdcae 100644 --- a/src/platform/texture_cache.zig +++ b/src/platform/texture_cache.zig @@ -30,7 +30,7 @@ const log = std.log.scoped(.textures); pub const folder = "cache/textures"; /// The cache's files. Change the version when the payload's layout changes. -const Files = cache_file.Files(.{ .folder = folder, .magic = "ORTX".*, .version = 2, .Key = Compressor.Key, .max_bytes = 1 << 30, .any_case = true }); +const Files = cache_file.Files(.{ .folder = folder, .magic = "ORTX".*, .version = 2, .Key = Compressor.Key, .max_bytes = std.fmt.parseIntSizeSuffix("1GiB", 10) catch unreachable, .any_case = true }); comptime { // The layout the files of version 2 were written with, before the caches shared their files. diff --git a/src/platform/video.zig b/src/platform/video.zig index f93cc51c..4da12871 100644 --- a/src/platform/video.zig +++ b/src/platform/video.zig @@ -1,12 +1,13 @@ -//! The movies' packets, decoded by FFmpeg's Bink decoders (`deps/ffmpeg`) for the engine's -//! stand-in for Bink ([`engine/bink.zig`](../engine/bink.zig)'s `Codec`), and audio files by its -//! MP3, FLAC, Vorbis and Opus decoders. Each Bink decoder is set up as FFmpeg's reader of the -//! container, its Bink demuxer, sets it up: the video's with the file's signature as its tag and -//! the header's flags as its extra data, an audio track's with its rate, its channels and the -//! file's signature. The MP3 and FLAC decoders read all they need from what they're sent, and the -//! Vorbis and Opus decoders take an Ogg stream's headers as their extra data. +//! Decodes the movies' packets with FFmpeg's Bink decoders (`deps/ffmpeg`), for the engine's +//! replacement of Bink ([`engine/bink.zig`](../engine/bink.zig)'s `Codec`), and audio files with +//! FFmpeg's MP3, FLAC, Vorbis and Opus decoders. Each Bink decoder is set up the way FFmpeg's Bink +//! demuxer sets it up. The video decoder gets the file's signature as its codec tag and the +//! header's flags as extra data. An audio track's decoder gets the track's rate and channels, and +//! the file's signature as extra data. The MP3 and FLAC decoders read everything they need from +//! the data they are sent, and the Vorbis and Opus decoders get an Ogg stream's headers as extra +//! data. //! -//! FFmpeg is built without threads, so it is used from the thread the game runs on alone. +//! FFmpeg is built without threads, so it is only used from the game's thread. const std = @import("std"); const Allocator = std.mem.Allocator; @@ -17,8 +18,8 @@ const bink = openreliant.engine.bink; const math = openreliant.engine.surrender.math; const wave = openreliant.wave; -/// The decoders, which hold nothing between the streams they open. FFmpeg's own warnings and -/// errors go to the log (`logs.route`). +/// The decoders. They keep no state between the streams they open. FFmpeg's warnings and errors go +/// to the log (`logs.route`). pub const Decoders = struct { pub fn init() Decoders { return .{}; @@ -67,8 +68,9 @@ const Setup = union(enum) { file: bink.Encoding, }; -/// A decoder set up for `setup`. A Bink decoder's 4 bytes of extra data hold the video's flags, or -/// an audio track's movie's signature, and a Vorbis or Opus decoder's hold the stream's headers. +/// Opens a decoder set up for `setup`. A Bink video decoder gets the video's flags as 4 bytes of +/// extra data, and a Bink audio decoder gets the movie's signature. A Vorbis or Opus decoder gets +/// the stream's headers. fn open(setup: Setup) bink.Error!bink.Stream { const id: c.enum_AVCodecID = switch (setup) { .video => c.AV_CODEC_ID_BINKVIDEO, @@ -103,8 +105,8 @@ fn open(setup: Setup) bink.Error!bink.Stream { .vorbis => |headers| @memcpy(try extraData(set_up, headers.len), headers), .opus => |headers| { @memcpy(try extraData(set_up, headers.len), headers); - // The packets' timestamps count samples at Opus's rate. The decoder moves them past - // the pre-skip it drops, and logs a warning where it has no timebase for them. + // The packets' timestamps count samples at Opus's rate. The decoder shifts them + // past the pre-skip it drops, and logs a warning if it has no timebase for them. set_up.pkt_timebase = .{ .num = 1, .den = openreliant.ogg.opus_rate }; }, }, @@ -119,8 +121,8 @@ fn open(setup: Setup) bink.Error!bink.Stream { return stream; } -/// `size` bytes of extra data for a decoder, padded as FFmpeg reads past them, which the context -/// frees as it closes. +/// Allocates `size` bytes of extra data for a decoder, with the padding FFmpeg needs because it +/// reads past the end. The context frees them when it closes. fn extraData(context: *c.AVCodecContext, size: usize) bink.Error![]u8 { const length = std.math.cast(c_int, size) orelse return error.Decoding; const extradata: [*]u8 = @ptrCast(c.av_mallocz(size + c.AV_INPUT_BUFFER_PADDING_SIZE) orelse return error.OutOfMemory); @@ -134,9 +136,12 @@ fn signature(revision: u8) c_uint { return std.mem.readInt(u32, &[4]u8{ 'B', 'I', 'K', revision }, .little); } -/// Hands `packet` to the stream's decoder, in a buffer padded as FFmpeg reads past a packet's end. +/// Sends `packet` to the stream's decoder, copied into a buffer with the padding FFmpeg needs +/// because it reads past a packet's end. A FLAC file is sent whole, so a packet larger than FFmpeg +/// can take fails. fn send(stream: *Stream, packet: []const u8) bink.Error!void { - if (c.av_new_packet(stream.packet, @intCast(packet.len)) < 0) return error.OutOfMemory; + const size = std.math.cast(c_int, packet.len) orelse return error.Decoding; + if (c.av_new_packet(stream.packet, size) < 0) return error.OutOfMemory; defer c.av_packet_unref(stream.packet); @memcpy(stream.packet.data[0..packet.len], packet); if (c.avcodec_send_packet(stream.context, stream.packet) < 0) return error.Decoding; @@ -188,7 +193,7 @@ fn receive(_: *anyopaque, handle: bink.Stream, gpa: Allocator, pcm: *std.ArrayLi const interleaved: [*]const f32 = @ptrCast(@alignCast(frame.data[0])); for (out, interleaved[0..out.len]) |*to, from| to.* = sample16(from); }, - // FLAC's formats: 16-bit samples as they are, and deeper ones in the top bits of 32. + // FLAC's formats: 16-bit samples as they are, and deeper samples in the top bits of 32. c.AV_SAMPLE_FMT_S16 => { const interleaved: [*]const i16 = @ptrCast(@alignCast(frame.data[0])); @memcpy(out, interleaved[0..out.len]); @@ -202,13 +207,13 @@ fn receive(_: *anyopaque, handle: bink.Stream, gpa: Allocator, pcm: *std.ArrayLi return true; } -/// A 32-bit sample as a 16-bit one: its top 16 bits, as FFmpeg's conversion takes them. +/// Converts a 32-bit sample to 16 bits by keeping its top 16 bits, as FFmpeg's conversion does. fn top16(level: i32) i16 { return @intCast(level >> 16); } -/// A float sample from -1 to 1 as a 16-bit one, rounded to the nearest and a half to the even one, -/// as FFmpeg's conversion rounds (`lrintf`). +/// Converts a float sample from -1 to 1 to 16 bits, rounding halves to even as FFmpeg's conversion +/// does (`lrintf`). fn sample16(level: f32) i16 { const rounded = math.roundEven(level * wave.full_scale); return @intFromFloat(std.math.clamp(rounded, std.math.minInt(i16), std.math.maxInt(i16))); @@ -228,8 +233,7 @@ fn close(_: *anyopaque, handle: bink.Stream) void { test sample16 { try std.testing.expectEqual(0, sample16(0)); try std.testing.expectEqual(16384, sample16(0.5)); - // A half goes to the even one. - // A step of the 16-bit scale as a float. + // One step of the 16-bit scale as a float. Halves round to the even neighbour. const step: f32 = 1.0 / @as(f32, wave.full_scale); try std.testing.expectEqual(2, sample16(2.5 * step)); try std.testing.expectEqual(-2, sample16(-2.5 * step)); @@ -256,8 +260,8 @@ test "MP3 frames decode to their samples" { const codec = decoders.codec(); const stream = try codec.openFile(.mp3); defer codec.close(stream); - // Two frames of MPEG-2 Layer III at 22,050 Hz in joint stereo, nothing but their headers set: - // silence, 576 samples of each channel each. + // Two frames of MPEG-2 Layer III at 22,050 Hz in joint stereo, with only their headers set: + // silence, 576 samples per channel in each frame. const frame = openreliant.mp3.testing.silent_frame; var pcm: std.ArrayList(i16) = .empty; defer pcm.deinit(std.testing.allocator); @@ -270,7 +274,7 @@ test "a FLAC file decodes to a WAVE file of its samples" { const gpa = std.testing.allocator; var decoders: Decoders = .init(); const codec = decoders.codec(); - // One frame of 192 samples of each channel at 44,100 Hz, each channel at a constant level. + // One frame of 192 samples per channel at 44,100 Hz, each channel at a constant level. const file = try openreliant.flac.testing.constant(gpa, 44100, &.{ 1000, -2000 }, 192); defer gpa.free(file); var whole: bink.AudioDecoding = try .beginFlac(codec, file); @@ -285,8 +289,8 @@ test "a FLAC file decodes to a WAVE file of its samples" { try std.testing.expectEqual(1000, std.mem.readInt(i16, parsed.data[at * 4 ..][0..2], .little)); try std.testing.expectEqual(-2000, std.mem.readInt(i16, parsed.data[at * 4 + 2 ..][0..2], .little)); } - // A frame at a time, as a mod's music decodes over several of the game's frames: the file's - // one frame, then the end. + // A frame at a time, as a mod's music is decoded over several of the game's frames: the + // file's only frame, then the end. var decoding: bink.AudioDecoding = try .beginFlac(codec, file); try std.testing.expect(!try decoding.step(gpa, 1)); try std.testing.expect(try decoding.step(gpa, 1)); @@ -299,7 +303,7 @@ test "an Ogg file decodes to a WAVE file of its samples, up to where its last pa const gpa = std.testing.allocator; var decoders: Decoders = .init(); const codec = decoders.codec(); - // Opus: three packets of 20 ms of silence at 48,000 Hz in stereo, 960 samples of each channel + // Opus: three packets of 20 ms of silence at 48,000 Hz in stereo, 960 samples per channel in // each. The decoder drops the pre-skip of 312 samples at the start, and the last page ends the // stream 400 samples before the end of the last packet. const frame = 960; @@ -314,8 +318,8 @@ test "an Ogg file decodes to a WAVE file of its samples, up to where its last pa try std.testing.expectEqual((3 * frame - 400 - 312) * 2 * @sizeOf(i16), opus_wave.data.len); try std.testing.expect(std.mem.allEqual(u8, opus_wave.data, 0)); // Vorbis: five packets of short blocks of silence at 22,050 Hz in one channel. The first - // decodes to nothing and each of the others to 128 samples, and the last page ends the stream - // 400 samples in, 112 before the end of the last packet. + // decodes to nothing and each of the others to 128 samples. The last page ends the stream + // after 400 samples, 112 before the end of the last packet. const vorbis = try openreliant.ogg.testing.vorbis(gpa, 22050, 1, 5, 400); defer gpa.free(vorbis); var vorbis_decoding: bink.AudioDecoding = try .beginOgg(codec, gpa, vorbis); diff --git a/src/scripting/drawing.zig b/src/scripting/drawing.zig index 81b5aed2..3ff67be9 100644 --- a/src/scripting/drawing.zig +++ b/src/scripting/drawing.zig @@ -66,8 +66,8 @@ pub const Assets = struct { /// The frame it was last drawn in. drawn: u64, }; - /// A sprite set of the game's, which every script draws from alike, and the file its shapes - /// are read from. + /// One of the game's sprite sets, shared by every script, and the file its shapes are read + /// from. const SpriteSet = struct { name: []u8, file: []u8, @@ -157,8 +157,8 @@ pub const Assets = struct { return image; } - /// Takes pictures out of the cache until one more file of `needed` bytes fits, the one drawn - /// longest ago first. Fails where the fonts, the sprite sets and the pictures drawn in the last + /// Removes pictures from the cache, the one drawn longest ago first, until a file of `needed` + /// bytes fits. Fails if the fonts, the sprite sets and the pictures drawn in the last /// `kept_frames` leave no room. fn makeRoom(assets: *Assets, gpa: Allocator, needed: usize) !void { while (true) { @@ -184,11 +184,11 @@ pub const Assets = struct { return found; } - /// The game's sprite set `name`, such as `frntend5.spr`, read the first time a script draws - /// from it: a mod's copy, else the member of `resource.hog`, with the mods' pictures standing - /// in for its shapes, as the game's screens read their sets. A set is cached by the archive's - /// name for it, which leaves out its folder (`bigfile.memberName`), and keeps that name for the - /// pictures, so it's the cache's copy, not the script's string. + /// Loads the game's sprite set `name`, such as `frntend5.spr`, the first time a script draws + /// from it. A mod's copy takes priority over the member of `resource.hog`, and mods' pictures + /// replace its shapes, as for the game's own screens. Sets are cached under their member name, + /// without the folder (`bigfile.memberName`), and the pictures keep the cache's copy of that + /// name, not the script's string. fn loadSpriteSet(assets: *Assets, call: Call, name: []const u8) !*hud.Art { var buffer: [bigfile.member_name_room]u8 = undefined; const member = bigfile.memberName(&buffer, name); @@ -358,8 +358,7 @@ pub const ShapeStyle = struct { color: Colour = default_colour, alpha: f32 = 1, scale: f32 = 1, - /// A sprite set of the game's to draw from, such as `frntend5.spr`, in place of the layer's - /// own. + /// A game sprite set to draw from instead of the layer's own, such as `frntend5.spr`. sprites: ?[]const u8 = null, }; @@ -407,7 +406,7 @@ const Command = union(enum) { line: struct { from: Where, to: Where, colour: [4]f32, width: f32 }, rectangle: struct { from: [2]f32, to: [2]f32, colour: [4]f32 }, picture: struct { image: *srtexture.Image, at: [2]f32, size: [2]f32, colour: [4]f32 }, - /// A shape of the layer's sprite set, or of `sprites`, a set the scripts named. + /// A shape from the layer's sprite set, or from `sprites`, a set that a script named. shape: struct { sprites: ?*hud.Art, index: usize, at: [2]f32, colour: [4]f32, scale: f32 }, power_ball: struct { setting: [2]f32, at: [2]f32, size: [2]f32, colour: [4]f32 }, }; @@ -499,8 +498,8 @@ fn screenPoint(at: @Vector(3, f32)) [2]f32 { return .{ at[0], at[1] }; } -/// Where the front end and the rooms place the game's 640 by 480 screen in the window `view` is -/// drawn on. +/// Where the front end and the rooms place the game's 640 by 480 screen in the window that `view` +/// is drawn on. fn gameScreen(view: View) canvas.Placement { return .of(view.screen, canvas.fitted); } @@ -576,14 +575,14 @@ pub fn Package(comptime which: Which) type { pub const quit = if (which == .ui) front_end.functions.quit else {}; pub const pointer = if (which == .ui) front_end.functions.pointer else {}; - pub const from_game_screen = if (which == .ui) api.Function("Where the point `at` of the game's 640 by 480 screen lies in the window, in pixels, as the front end and the rooms fit that screen to the window, in its middle: for drawing over a place on one of their pictures.", &.{"at"}, struct { + pub const from_game_screen = if (which == .ui) api.Function("Converts the point `at` on the game's 640 by 480 screen to window pixels, as the front end and the rooms centre that screen in the window. Use it to draw over a spot on one of their pictures.", &.{"at"}, struct { fn place(call: Call, at: @Vector(3, f32)) @Vector(3, f32) { const placed = gameScreen(viewOf(call)).inWindow(.{ at[0], at[1] }); return .{ placed[0], placed[1], at[2] }; } }.place) else {}; - pub const to_game_screen = if (which == .ui) api.Function("Where the window's pixel `at`, such as the pointer's, lies on the game's 640 by 480 screen, as `from_game_screen` places that screen.", &.{"at"}, struct { + pub const to_game_screen = if (which == .ui) api.Function("Converts the window pixel `at`, such as the pointer's position, to a point on the game's 640 by 480 screen. The inverse of `from_game_screen`.", &.{"at"}, struct { fn place(call: Call, at: @Vector(3, f32)) @Vector(3, f32) { const placed = gameScreen(viewOf(call)).onScreen(.{ at[0], at[1] }); return .{ placed[0], placed[1], at[2] }; @@ -616,7 +615,7 @@ pub fn Package(comptime which: Which) type { } }.draw) else {}; - pub const shape = api.Function("Draws shape `index` of the game's sprite set for this layer (the flight display's, or the front end screen's), or of the set the style's `sprites` names, with its anchor at `at`, in window pixels. The style's `scale` multiplies its size in the game's pixels.", &.{ "at", "index", "style" }, struct { + pub const shape = api.Function("Draws shape `index` with its anchor at `at`, in window pixels. The shape comes from the game's sprite set for this layer (the flight display's, or the front end screen's), or from the set that the style's `sprites` names. The style's `scale` multiplies its size in the game's pixels.", &.{ "at", "index", "style" }, struct { fn draw(call: Call, at: @Vector(3, f32), index: u32, given: ?ShapeStyle) void { const style = given orelse ShapeStyle{}; const scripts = presentation.Presentation.of(call); diff --git a/src/scripting/front_end.zig b/src/scripting/front_end.zig index 8ed8aee7..d24ea8ed 100644 --- a/src/scripting/front_end.zig +++ b/src/scripting/front_end.zig @@ -66,9 +66,7 @@ pub const functions = struct { pub const pointer = api.Field(?Pointer, "Where the pointer is, in pixels from the window's top left corner, and whether its left button is down; nil before it has been over the window.", struct { pub fn get(call: Call) ?Pointer { const host = Presentation.of(call).host orelse return null; - // The window gives the pointer's place as fractions of its size. - const share: @Vector(2, f32) = host.devices.mouse.at orelse return null; - const at = share * @as(@Vector(2, f32), @floatFromInt(@as(@Vector(2, u32), host.window))); + const at = host.devices.mouse.pixelsIn(host.window) orelse return null; return .{ .at = .{ at[0], at[1], 0 }, .down = host.devices.mouse.buttons.left }; } }); diff --git a/src/scripting/game.zig b/src/scripting/game.zig index 7f3dbf99..887354ab 100644 --- a/src/scripting/game.zig +++ b/src/scripting/game.zig @@ -63,7 +63,7 @@ const world = @import("world.zig"); /// Limits for game scripts: 100 milliseconds per call, since they run as the game plays, and /// 64 MiB per mod. -pub const limits: runtime.Limits = .{ .time = .fromMilliseconds(100), .memory = 64 << 20 }; +pub const limits: runtime.Limits = .{ .time = .fromMilliseconds(100), .memory = runtime.memory_per_mod }; /// The game side of mods' scripts while a game runs. pub const Game = struct { diff --git a/src/scripting/hooks.zig b/src/scripting/hooks.zig index 5e4d2708..56972a3f 100644 --- a/src/scripting/hooks.zig +++ b/src/scripting/hooks.zig @@ -777,7 +777,7 @@ fn testIds(hooks: *const Hooks, hook: Hook, buffer: []u32) []const u32 { test "a hook's handlers run the later mods' first, each mod's in the order it added them" { const gpa = std.testing.allocator; - const runtime = try Runtime.create(gpa, std.testing.io, &.{}, .{ .side = .game, .limits = .{ .time = .fromSeconds(1), .memory = 1 << 20 }, .seed = 1, .version = "0.7.0" }); + const runtime = try Runtime.create(gpa, std.testing.io, &.{}, runtime_module.testing.options); defer runtime.destroy(); var scripts: engine_hooks.Scripts = .{ .context = undefined, .vtable = undefined }; var hooks: Hooks = .init(gpa, runtime, &scripts); diff --git a/src/scripting/load.zig b/src/scripting/load.zig index 223aa3f4..fa91f836 100644 --- a/src/scripting/load.zig +++ b/src/scripting/load.zig @@ -24,7 +24,7 @@ const packages = @import("packages.zig"); /// Limits for load scripts: 1 second per call and 64 MiB per mod. Changing records needs far less, /// so only a broken script hits them. -pub const limits: runtime.Limits = .{ .time = .fromSeconds(1), .memory = 64 << 20 }; +pub const limits: runtime.Limits = .{ .time = .fromSeconds(1), .memory = runtime.memory_per_mod }; /// The seed for `math.random` in load scripts. It's fixed so that the records come out the same on /// every machine. @@ -225,7 +225,7 @@ test "a failed script's changes are undone" { var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); var held = try testing.records3(arena.allocator()); - try runWithin(gpa, io, mods_held.list, &held, "0.7.0", .{ .time = .fromMilliseconds(50), .memory = 1 << 20 }, .{}); + try runWithin(gpa, io, mods_held.list, &held, "0.7.0", .{ .time = .fromMilliseconds(50), .memory = runtime.testing.limits.memory }, .{}); // Every script failed, including the one that timed out and the one that ran out of memory, and // the last one still ran. diff --git a/src/scripting/missiles.zig b/src/scripting/missiles.zig index 3fdffcaa..3af5b0ec 100644 --- a/src/scripting/missiles.zig +++ b/src/scripting/missiles.zig @@ -294,7 +294,7 @@ test "handles name missiles until their flight ends" { slot.object.velocity = .of(.{ 0, 3, 4 }); const record = all.missiles.add(.{ .slot = slot, .launcher = launcher, .launcher_reuses = all.reuses[launcher], .type = .of(.raptor), .target = .at(launcher, null) }).?; - const scripts = try Runtime.create(gpa, std.testing.io, &.{}, .{ .side = .game, .limits = .{ .time = .fromSeconds(1), .memory = 1 << 20 }, .seed = 1, .version = "0.7.0" }); + const scripts = try Runtime.create(gpa, std.testing.io, &.{}, runtime_module.testing.options); defer scripts.destroy(); @import("objects.zig").register(scripts); register(scripts); diff --git a/src/scripting/objects.zig b/src/scripting/objects.zig index f35671c6..22766748 100644 --- a/src/scripting/objects.zig +++ b/src/scripting/objects.zig @@ -650,7 +650,7 @@ test "handles name objects until they are removed" { _ = try mission.add(.of(.predator), .{ 0, 0, 0 }); const sabre = try mission.add(.of(.sabre), .{ 0, 100, 0 }); - const scripts = try Runtime.create(gpa, std.testing.io, &.{}, .{ .side = .game, .limits = .{ .time = .fromSeconds(1), .memory = 1 << 20 }, .seed = 1, .version = "0.7.0" }); + const scripts = try Runtime.create(gpa, std.testing.io, &.{}, runtime_module.testing.options); defer scripts.destroy(); register(scripts); scripts.objects = mission.objects; @@ -716,7 +716,7 @@ test "handles name objects until they are removed" { try std.testing.expectEqual(gameobj.Quadrants{ .left = 0, .right = 1, .fore = 2, .aft = 3 }, changed.object.shields); try bind.testing.expectSourceError(thread, "sabre.shields = { left = -1, right = 0, fore = 0, aft = 0 }", "object.shields: left: expected a number from 0"); var whole: [96]u8 = undefined; - try bind.testing.expectSourceError(thread, try std.fmt.bufPrint(&whole, "sabre.armor = {{ left = {d}, right = 0, fore = 0, aft = 0 }}", .{combat.startingArmor() + 1}), "object.armor: left: expected a number from 0"); + try bind.testing.expectSourceError(thread, try std.mem.print(&whole, "sabre.armor = {{ left = {d}, right = 0, fore = 0, aft = 0 }}", .{combat.startingArmor() + 1}), "object.armor: left: expected a number from 0"); try bind.testing.expectSourceError(thread, "sabre.orientation = { right = vector.zero, down = vector.zero, forward = vector.zero }", "aren't zero or parallel"); // Once its slot is reset, the handle is no longer valid. diff --git a/src/scripting/orders.zig b/src/scripting/orders.zig index bc383f47..824e122c 100644 --- a/src/scripting/orders.zig +++ b/src/scripting/orders.zig @@ -271,7 +271,7 @@ test "scripts read the order table and an object's orders, and end them" { defer mission.deinit(); _ = try mission.add(.of(.predator), .{ 0, 0, 0 }); const sabre = try mission.add(.of(.sabre), .{ 0, 100, 0 }); - const scripts = try runtime.Runtime.create(gpa, std.testing.io, &.{}, .{ .side = .game, .limits = .{ .time = .fromSeconds(1), .memory = 1 << 20 }, .seed = 1, .version = "0.7.0" }); + const scripts = try runtime.Runtime.create(gpa, std.testing.io, &.{}, runtime.testing.options); defer scripts.destroy(); objects.register(scripts); packages.push(scripts); diff --git a/src/scripting/presentation.zig b/src/scripting/presentation.zig index 2d399216..d2c5b749 100644 --- a/src/scripting/presentation.zig +++ b/src/scripting/presentation.zig @@ -59,7 +59,7 @@ const game_module = @import("game.zig"); /// Limits for player and menu scripts: 100 milliseconds per call, and 64 MiB per mod, as for the /// game's. -pub const limits: runtime.Limits = .{ .time = .fromMilliseconds(100), .memory = 64 << 20 }; +pub const limits: runtime.Limits = .{ .time = .fromMilliseconds(100), .memory = runtime.memory_per_mod }; /// The scripts of each kind that runs on the presentation side. const Place = enum { menu, player }; @@ -93,8 +93,8 @@ pub const Host = struct { pub const Rooms = struct { carrier: rooms.Carrier, - /// The view the player stands in, or walks into, by its place in the views - /// (`rooms.installed`). + /// The view the player stands in, or walks into, by its index in the rooms' views, + /// counting from 0 (`records.room_views` counts from 1). view: u8, /// The place whose screen shows, or the news report while it plays; null while the player /// walks the rooms. @@ -776,7 +776,7 @@ test "a display reads the cockpit's instruments in any view" { // In the chase view, where the game draws none of them, the readings are as the instruments // would show them; the caption has typed the date's six letters, the last from the game's code // page, which takes two bytes in UTF-8 and goes back to one as the display draws it. - const text = try std.fmt.bufPrint(&expected, "{d} {d} 0.25 0.50 0.75 0.50 false|5 5 1 5 4 0|7 12|match_speed,smart_targeting,ecm,reverse_thrust|3:07|Chase|Mai 3\xE9|5 7", .{ made, asked }); + const text = try std.mem.print(&expected, "{d} {d} 0.25 0.50 0.75 0.50 false|5 5 1 5 4 0|7 12|match_speed,smart_targeting,ecm,reverse_thrust|3:07|Chase|Mai 3\xE9|5 7", .{ made, asked }); try std.testing.expectEqualStrings(text, fixture.shown.layers.get(.hud).text.items); fixture.shown.endGame(); } @@ -843,7 +843,7 @@ test "a display reads the radar's contacts and the target display" { const rings = hud.ShipStatus.rings(slot).?; const form = hud.target_display.Form.of(hud.targetWindow(slot)).?; var expected: [256]u8 = undefined; - const text = try std.fmt.bufPrint(&expected, "1 {d} target {d} {d} {d}|{s} Sabre Ace {d} {d} {d} {d} engine Engine 0.26 nil fore", .{ + const text = try std.mem.print(&expected, "1 {d} target {d} {d} {d}|{s} Sabre Ace {d} {d} {d} {d} engine Engine 0.26 nil fore", .{ sabre, contact.at[0], contact.at[1], contact.height, @tagName(form), facts.range, facts.speed, std.math.clamp(rings.shields[0], 0, hud.ShipStatus.arc_levels), std.math.clamp(rings.armor[3], 0, hud.ShipStatus.arc_levels), @@ -942,7 +942,7 @@ test "a display reads the windows and the display's text" { var buffer: [engine.game.mission.wing_size]hud.wing_status.Entry = undefined; const wing = hud.wing_status.entries(all, &buffer); var expected: [256]u8 = undefined; - const text = try std.fmt.bufPrint(&expected, "0.25 0.50 1.00|{d} {d} {d}|1 0 1 {d:.2}|2 2 Patrol true 2 nil|Target|Bogey|Sub|comms_window|jump", .{ + const text = try std.mem.print(&expected, "0.25 0.50 1.00|{d} {d} {d}|1 0 1 {d:.2}|2 2 Patrol true 2 nil|Target|Bogey|Sub|comms_window|jump", .{ shares.get(.shields), shares.get(.guns), shares.get(.engines), wing[0].armorShare(), }); try std.testing.expectEqualStrings(text, fixture.shown.layers.get(.hud).text.items); @@ -998,7 +998,7 @@ test "a display reads what flashes, the charges, and how far the windows and the host.views.set(.hud, .{ .font = &fixture.font, .gpa = gpa, .screen = .{ 640, 480 }, .scale = 1 }); fixture.shown.frame(host); var expected: [256]u8 = undefined; - const text = try std.fmt.bufPrint(&expected, "enemy_lock,ecm|true|0.50 nil 1.00|left,aft|opening 0.50 open 1.00 nil|0.4|1 0|{} 3000", .{hud.gunnery.pairShown(slot)}); + const text = try std.mem.print(&expected, "enemy_lock,ecm|true|0.50 nil 1.00|left,aft|opening 0.50 open 1.00 nil|0.4|1 0|{} 3000", .{hud.gunnery.pairShown(slot)}); try std.testing.expectEqualStrings(text, fixture.shown.layers.get(.hud).text.items); fixture.shown.endGame(); } @@ -1293,7 +1293,7 @@ test "ui.pointer is in the window's pixels" { try std.testing.expectEqualStrings("400 150 true", layer.text.items); } -test "menu scripts follow the rooms, and ask the CD player for a list" { +test "menu scripts follow the rooms and turn the CD player's pages" { var fixture: Fixture = undefined; try fixture.init(&.{.{ "a", @@ -1309,7 +1309,7 @@ test "menu scripts follow the rooms, and ask the CD player for a list" { \\ require("openreliant.ui").text(vector.zero, table.concat(told, ",")) \\ if rooms.place == "cd_player" then \\ assert(rooms.carrier == "reliant" and rooms.view == 5 and rooms.cd_player_page == 1) - \\ -- The page asked for is the page until the CD player turns to it. + \\ -- A requested page reads back as the page until the CD player turns to it. \\ assert(rooms.set_cd_player_page(2) and rooms.cd_player_page == 2) \\ assert(rooms.cd_player_pages == 2 and not pcall(rooms.set_cd_player_page, 3)) \\ else @@ -1322,10 +1322,10 @@ test "menu scripts follow the rooms, and ask the CD player for a list" { }}); defer fixture.deinit(); const shown = fixture.shown; - // Outside the rooms, nothing is told. + // Outside the rooms, no event fires. fixture.frame(0.016, .{ 640, 480 }); try std.testing.expectEqualStrings("", shown.layers.get(.ui).text.items); - // The player walks into view 5, and opens the CD player there: each is told once. + // The player walks into view 5 and opens the CD player there: each fires one event. fixture.rooms = .{ .carrier = .reliant, .view = 4 }; fixture.frame(0.016, .{ 640, 480 }); fixture.frame(0.016, .{ 640, 480 }); @@ -1334,7 +1334,7 @@ test "menu scripts follow the rooms, and ask the CD player for a list" { fixture.frame(0.016, .{ 640, 480 }); try std.testing.expectEqual(1, shown.takeCdPlayerPage().?); try std.testing.expectEqual(null, shown.takeCdPlayerPage()); - // Leaving the rooms is told with nil. + // Leaving the rooms fires an event with nil. fixture.rooms = null; fixture.frame(0.016, .{ 640, 480 }); try std.testing.expectEqualStrings("5 nil,5 cd_player,nil nil", shown.layers.get(.ui).text.items); @@ -1664,7 +1664,7 @@ test "a script draws more pictures over time than the cache holds" { var files: std.ArrayList(struct { []const u8, []const u8 }) = .empty; try files.append(arena.allocator(), .{ "mod.ini", "[Scripts]\nMenu=a.luau\n" }); try files.append(arena.allocator(), .{ "a.luau", "local n = 0\nreturn {engine_handlers = {on_frame = function() n += 1; require('openreliant.ui').picture(vector.zero, 'p' .. n .. '.png') end}}" }); - for (1..pictures + 1) |n| try files.append(arena.allocator(), .{ try std.fmt.allocPrint(arena.allocator(), "p{d}.png", .{n}), png.written() }); + for (1..pictures + 1) |n| try files.append(arena.allocator(), .{ try arena.allocator().print("p{d}.png", .{n}), png.written() }); var fixture: Fixture = undefined; try fixture.init(&.{.{ "a", files.items }}); defer fixture.deinit(); diff --git a/src/scripting/records/maneuvers.zig b/src/scripting/records/maneuvers.zig index c6792544..9a00f586 100644 --- a/src/scripting/records/maneuvers.zig +++ b/src/scripting/records/maneuvers.zig @@ -39,7 +39,7 @@ pub fn count(held: *const Records) usize { /// Adds a maneuver after the last, named after its number, with an empty script. pub fn append(state: *State, held: *Records) void { - const name = std.fmt.allocPrint(held.arena, "maneuver {d}", .{held.maneuvers.len}) catch state.raise("records." ++ list_name ++ ": out of memory", .{}); + const name = held.arena.print("maneuver {d}", .{held.maneuvers.len}) catch state.raise("records." ++ list_name ++ ": out of memory", .{}); held.maneuvers = table.appended(aidefend.Compiled, state, held, held.maneuvers, .{ .definition = .{ .name = name } }, list_name); } diff --git a/src/scripting/records/room_views.zig b/src/scripting/records/room_views.zig index 3b490a02..d506e8ba 100644 --- a/src/scripting/records/room_views.zig +++ b/src/scripting/records/room_views.zig @@ -1,8 +1,9 @@ //! The rooms' views as scripts see them in `openreliant.records.room_views` -//! ([#1169](https://github.com/OpenReliant/openreliant/issues/1169)): the Reliant's and the Yamato's, -//! numbered 1 to 125 in the order they lie in the executable (`interface.rooms.views`). Each is a -//! proxy for its `interface.rooms.View`, whose fields load scripts change in place, as they do a -//! record's, so that a mod can change what the rooms play and where they lead. +//! ([#1169](https://github.com/OpenReliant/openreliant/issues/1169)): the Reliant's and the +//! Yamato's, numbered 1 to 125 in the order they are stored in the executable +//! (`interface.rooms.views`). Each is a proxy for its `interface.rooms.View`. Load scripts change +//! its fields in place, as with a record, so a mod can change what the rooms play and where they +//! lead. const std = @import("std"); @@ -25,7 +26,7 @@ pub const script_name = "RoomView"; /// How the reference and the definitions introduce the views. pub const each = "Each of the rooms' views"; -pub const entries_about = "The rooms' views, the Reliant's and the Yamato's, 1 to 125 in the order they lie in the executable. Load scripts change their fields."; +pub const entries_about = "The rooms' views, the Reliant's and the Yamato's, numbered 1 to 125 in the order they are stored in the executable. Load scripts can change their fields."; /// What the proxies need of the views (`table.Table`). pub const list_name = "room_views"; @@ -34,7 +35,7 @@ pub const item_noun = "a view"; pub const list_tag = @backingInt(runtime.Tag.room_views); pub const item_tag = @backingInt(runtime.Tag.room_view); -/// How many views the records hold: the executable's. +/// How many views the records hold: as many as the executable has. pub fn count(held: *const Records) usize { return held.room_views.len; } @@ -44,9 +45,19 @@ const proxies = table.Table(@This()); pub const register = proxies.register; pub const push = proxies.push; -/// A view's fields, as scripts name them: those of `interface.rooms.View` but its address. +/// A view's fields, as scripts name them: all fields of `interface.rooms.View` except its address. pub const Field = enum { movie, loop, label, exits, action, hotspot, sound }; +comptime { + // Scripts see every field of a view except its address, under the same name. + for (@typeInfo(rooms.View).@"struct".field_names) |name| { + if (!std.mem.eql(u8, name, "address") and !@hasField(Field, name)) @compileError("records.room_views doesn't show a view's " ++ name); + } + for (std.enums.values(Field)) |field| { + if (!@hasField(rooms.View, @tagName(field))) @compileError("rooms.View has no field " ++ @tagName(field)); + } +} + /// A view's exits as scripts read them: the numbers of the views they lead to. const Exits = values.List(u32, rooms.View.max_exits); @@ -61,13 +72,13 @@ pub fn TypeOf(comptime field: Field) type { /// What the reference says of `field`. pub fn about(field: Field) []const u8 { return switch (field) { - .movie => std.fmt.comptimePrint("The movie on the way into the view, a Bink file of the disc's archive or a mod's, such as `rel_l2cd.bik`, at most {d} characters; nil for none.", .{movie.max_name}), - .loop => "The movie the view loops once the player is there, as `movie` is given; nil for none, which leaves the way in's last frame on the screen.", - .label => "The words that name the way into the view, which show at the foot of the screen while the pointer rests on its hotspot.", - .exits => std.fmt.comptimePrint("The views it leads on to, at most {d}, by their numbers.", .{rooms.View.max_exits}), + .movie => std.fmt.comptimePrint("The movie that plays on the way into the view: a Bink file from the disc's archive or from a mod, such as `rel_l2cd.bik`, at most {d} characters; nil for none.", .{movie.max_name}), + .loop => "The movie that loops while the player is in the view, given like `movie`; nil for none, which leaves the last frame of the way in on the screen.", + .label => "The text that names the way into the view. It shows at the bottom of the screen while the pointer is over the view's hotspot.", + .exits => std.fmt.comptimePrint("The numbers of the views this view leads to, at most {d}.", .{rooms.View.max_exits}), .action => "What happens once the player is in the view, such as `cd_player`, which opens the CD player.", - .hotspot => "Where the pointer finds the way into the view, on each view it's an exit of, on the game's 640 by 480 screen.", - .sound => "The sound of the rooms' `vrsnd.fat` the way into the view plays, by its number; nil for none.", + .hotspot => "The area of the game's 640 by 480 screen where the pointer selects the way into this view, in each view that has it as an exit.", + .sound => "The number of the sound from the rooms' `vrsnd.fat` that plays on the way into the view; nil for none.", }; } @@ -82,7 +93,7 @@ pub fn getField(state: *State, item: table.Item, field: Field) void { switch (field) { .movie => values.push(state, ?[]const u8, view.movie), .loop => values.push(state, ?[]const u8, view.loop), - // The labels are the game's text, where `table.pushValue` reads the ITAC's. + // The labels are in the game's text, while `table.pushValue` reads the ITAC's. .label => table.pushWords(state, item.records, view.label, .text), .exits => { var exits: Exits = .{}; @@ -146,7 +157,7 @@ test "the rooms' views" { try bind.testing.runSource(thread, \\local views = records.room_views \\assert(#views == 125 and tostring(views[1]) == "RoomView") - \\-- Where the Reliant's rooms start: by the ladder to the bunk, leading on to views 3, 4 and 2. + \\-- The Reliant's rooms start by the ladder to the bunk, with exits to views 3, 4 and 2. \\local start = views[1] \\assert(start.movie == "rel_ladd_bunk.bik" and start.loop == "rel_doorloop.bik") \\assert(start.label == "Walk to Bunk" and start.action == "none" and start.sound == nil) diff --git a/src/scripting/runtime.zig b/src/scripting/runtime.zig index 35974788..532dd85b 100644 --- a/src/scripting/runtime.zig +++ b/src/scripting/runtime.zig @@ -126,6 +126,10 @@ pub const Limits = struct { memory: usize, }; +/// How much memory one mod's scripts may use, on every side (`game.limits`, +/// `presentation.limits`, `load.limits`). +pub const memory_per_mod = std.fmt.parseIntSizeSuffix("64MiB", 10) catch unreachable; + pub const Options = struct { side: Side, limits: Limits, @@ -136,6 +140,14 @@ pub const Options = struct { shared: Shared = .{}, }; +/// Runtimes for tests. +pub const testing = struct { + /// One second per call and 1 MiB per mod, plenty for the tests' scripts. + pub const limits: Limits = .{ .time = .fromSeconds(1), .memory = std.fmt.parseIntSizeSuffix("1MiB", 10) catch unreachable }; + /// A game-side runtime with `limits`. + pub const options: Options = .{ .side = .game, .limits = limits, .seed = 1, .version = "0.7.0" }; +}; + /// What every state shares. pub const Shared = struct { /// The mods' storage (`storage.zig`); null if it isn't kept. @@ -1321,7 +1333,7 @@ test "Runtime.recompileFolders" { try load.testing.makeMods(io, tmp.dir, &.{.{ "a", &.{ .{ "mod.ini", "" }, .{ "value.luau", "return 1" } } }}); var opened: mods.Mods = try .open(gpa, io, tmp.dir, null); defer opened.close(gpa); - const scripts = try Runtime.create(gpa, io, opened.list, .{ .side = .game, .limits = .{ .time = .fromSeconds(1), .memory = 1 << 20 }, .seed = 1, .version = "0.7.0" }); + const scripts = try Runtime.create(gpa, io, opened.list, testing.options); defer scripts.destroy(); const before = try scripts.open(0, .global, null); const first = scripts.run(before, "value").?; @@ -1349,7 +1361,7 @@ test "a context closed during its own call keeps its thread until no call runs" try load.testing.makeMods(io, tmp.dir, &.{.{ "a", &.{.{ "mod.ini", "" }} }}); var opened: mods.Mods = try .open(gpa, io, tmp.dir, null); defer opened.close(gpa); - const scripts = try Runtime.create(gpa, io, opened.list, .{ .side = .game, .limits = .{ .time = .fromSeconds(1), .memory = 1 << 20 }, .seed = 1, .version = "0.7.0" }); + const scripts = try Runtime.create(gpa, io, opened.list, testing.options); defer scripts.destroy(); const context = try scripts.open(0, .global, null); // A function that closes its own context, as a missile's script that sets off its missile diff --git a/src/scripting/snapshot.zig b/src/scripting/snapshot.zig index 7bcbe29b..15c25157 100644 --- a/src/scripting/snapshot.zig +++ b/src/scripting/snapshot.zig @@ -33,7 +33,7 @@ pub const extension = ".scripts"; /// The most a file of scripts' state may take, as loading reads it (`GameScripts.loaded`): far more /// than mods keep, with room for several at their limits for storage and `on_save` at once, while a /// damaged file can't make loading take any amount of memory. `take` keeps within it. -pub const max_size = 256 << 20; +pub const max_size = std.fmt.parseIntSizeSuffix("256MiB", 10) catch unreachable; /// What the file starts with, and the version of its form. const magic = "ORSV"; diff --git a/src/scripting/storage.zig b/src/scripting/storage.zig index 39bbb354..93c930d8 100644 --- a/src/scripting/storage.zig +++ b/src/scripting/storage.zig @@ -49,7 +49,7 @@ const magic = "ORST"; const version: u16 = 1; /// The longest file a mod's global sections may take. -const max_file = 16 << 20; +const max_file = std.fmt.parseIntSizeSuffix("16MiB", 10) catch unreachable; /// What a global sections' file holds before its sections: its magic, its version and its count of /// sections. @@ -57,7 +57,7 @@ const file_header = magic.len + @sizeOf(u16) + @sizeOf(u32); /// Room the engine keeps in each mod's file for what it stores for the mod itself, such as its /// options (`options.zig`) and its game modes' progress, past what the mod's scripts may store. -const engine_room: usize = 1 << 20; +const engine_room = std.fmt.parseIntSizeSuffix("1MiB", 10) catch unreachable; /// The most a mod's scripts may keep in each place, in sections and their fields, as the memory /// they take. A section's binary form takes no more than its memory, so a file of global sections diff --git a/src/scripting/stored.zig b/src/scripting/stored.zig index 47d95642..7d751879 100644 --- a/src/scripting/stored.zig +++ b/src/scripting/stored.zig @@ -106,7 +106,7 @@ const max_depth = data.max_depth; /// as it checks the value. A table that holds the same table twice is copied twice, so a few /// tables can stand for a great many, and the count also bounds the time the check takes, which the /// scripts' time limit doesn't reach. -pub const max_kept: usize = 16 << 20; +pub const max_kept = std.fmt.parseIntSizeSuffix("16MiB", 10) catch unreachable; /// How much stack copying one level takes. const level_stack = 5; @@ -130,7 +130,7 @@ fn check(state: *State, at: i32, label: []const u8, depth: u32, left: *usize) vo if (!state.checkStack(level_stack)) state.raise("{s}: out of memory", .{label}); const kind = state.typeOf(at); const takes = @sizeOf(Value) + if (kind == .string) state.toString(at).?.len else 0; - if (takes > left.*) state.raise("{s}: too much data to keep, more than {d} bytes", .{ label, max_kept }); + if (takes > left.*) state.raise("{s}: too much data to keep, more than {Bi}", .{ label, max_kept }); left.* -= takes; switch (kind) { .nil, .none, .boolean, .number, .vector, .string => {}, diff --git a/src/scripting/util.zig b/src/scripting/util.zig index 844b2395..f13c9f3f 100644 --- a/src/scripting/util.zig +++ b/src/scripting/util.zig @@ -112,7 +112,7 @@ test "scripts read objects' orientations, and give util whole ones" { try mission.init(gpa); defer mission.deinit(); const player = try mission.add(.of(.predator), .{ 0, 0, 0 }); - const scripts = try runtime.Runtime.create(gpa, std.testing.io, &.{}, .{ .side = .game, .limits = .{ .time = .fromSeconds(1), .memory = 1 << 20 }, .seed = 1, .version = "0.7.0" }); + const scripts = try runtime.Runtime.create(gpa, std.testing.io, &.{}, runtime.testing.options); defer scripts.destroy(); objects.register(scripts); packages.push(scripts); diff --git a/src/scripting/vfs.zig b/src/scripting/vfs.zig index cad07f44..a36bac73 100644 --- a/src/scripting/vfs.zig +++ b/src/scripting/vfs.zig @@ -185,9 +185,10 @@ test "a file bigger than the mod's scripts have left of their memory is refused, const gpa = std.testing.allocator; const io = std.testing.io; const load = @import("load.zig"); + const limits = @import("runtime.zig").testing.limits; var tmp = std.testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); - const big = try gpa.alloc(u8, 2 << 20); + const big = try gpa.alloc(u8, 2 * limits.memory); defer gpa.free(big); @memset(big, 'x'); try load.testing.makeMods(io, tmp.dir, &.{.{ @@ -213,7 +214,7 @@ test "a file bigger than the mod's scripts have left of their memory is refused, var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); var held = try load.testing.records3(arena.allocator()); - try load.testing.runLimited(gpa, io, mods.list, &held, "0.7.0", .{ .time = .fromSeconds(1), .memory = 1 << 20 }, .{}); + try load.testing.runLimited(gpa, io, mods.list, &held, "0.7.0", limits, .{}); // The script ran to its end. try std.testing.expectEqual(7, held.guns[0].range); } diff --git a/src/tools/sltool/dte.zig b/src/tools/sltool/dte.zig index 23fef866..176fbe5d 100644 --- a/src/tools/sltool/dte.zig +++ b/src/tools/sltool/dte.zig @@ -491,11 +491,11 @@ fn check(ctx: Context, mission: dte.Mission) !void { var failure: dte.source.compile.Failure = undefined; const built = dte.source.compile.compile(gpa, tree, &failure) catch |err| switch (err) { - error.Invalid => return fail(out, try std.fmt.allocPrint(gpa, "source, at {f}", .{failure})), + error.Invalid => return fail(out, try gpa.print("source, at {f}", .{failure})), else => return err, }; if (try dte.same.difference(gpa, mission, try .parse(built))) |difference| { - return fail(out, try std.fmt.allocPrint(gpa, "built again from its source: {s}", .{difference})); + return fail(out, try gpa.print("built again from its source: {s}", .{difference})); } // And the engine binds it, as starting the mission does. var bound: openreliant.engine.game.mission.Mission = try .bind(gpa, built); diff --git a/src/tools/sltool/fat.zig b/src/tools/sltool/fat.zig index 02bf95a6..d7c719f3 100644 --- a/src/tools/sltool/fat.zig +++ b/src/tools/sltool/fat.zig @@ -1,6 +1,6 @@ -//! `sltool fat ...`: read `.fat` sound banks and save their sounds as WAV files, named as a mod's -//! sounds in their place are (`fat.soundName`). It reads the banks of the format's first version -//! too, which Privateer 2 ships, and saves their sounds as stored. +//! `sltool fat ...`: reads `.fat` sound banks and saves their sounds as WAV files, with the names +//! a mod's replacement sounds use (`fat.soundName`). It also reads banks in the format's first +//! version, which Privateer 2 ships, and saves their sounds as they are stored. const std = @import("std"); diff --git a/src/tools/sltool/fm8.zig b/src/tools/sltool/fm8.zig index 46919661..11c27c39 100644 --- a/src/tools/sltool/fm8.zig +++ b/src/tools/sltool/fm8.zig @@ -67,6 +67,9 @@ fn info(ctx: Context, bytes: []u8) !void { try ctx.stdout.writeByte('\n'); } +/// The largest frame `encode` reads. +const max_frame_file = std.fmt.parseIntSizeSuffix("64MiB", 10) catch unreachable; + /// Writes `film_path`, a film of the PNG files in `frames_path`, in the order of their names. fn encode(ctx: Context, frames_path: []const u8, film_path: []const u8) !void { const io = ctx.io; @@ -90,7 +93,7 @@ fn encode(ctx: Context, frames_path: []const u8, film_path: []const u8) !void { const frames = try ctx.arena.alloc([]const u8, names.items.len); var size: ?[2]u32 = null; for (names.items, frames) |name, *frame| { - const bytes = try dir.readFileAlloc(io, name, ctx.arena, .limited(64 * 1024 * 1024)); + const bytes = try dir.readFileAlloc(io, name, ctx.arena, .limited(max_frame_file)); const picture = try png.read(ctx.arena, bytes); const own: [2]u32 = .{ picture.width, picture.height }; if (size) |first| if (!std.mem.eql(u32, &first, &own)) { diff --git a/src/tools/sltool/main.zig b/src/tools/sltool/main.zig index 7594ae02..a404436c 100644 --- a/src/tools/sltool/main.zig +++ b/src/tools/sltool/main.zig @@ -286,7 +286,7 @@ test UniqueNames { test count { var buffer: [32]u8 = undefined; - try std.testing.expectEqualStrings("1 part, 2 faces, 3 vertices", try std.fmt.bufPrint(&buffer, "{f}, {f}, {f}", .{ count(1, "part"), count(2, "face"), countAs(3, "vertex", "vertices") })); + try std.testing.expectEqualStrings("1 part, 2 faces, 3 vertices", try std.mem.print(&buffer, "{f}, {f}, {f}", .{ count(1, "part"), count(2, "face"), countAs(3, "vertex", "vertices") })); } test positional { diff --git a/src/tools/sltool/shp.zig b/src/tools/sltool/shp.zig index 8e55d33b..641c4f51 100644 --- a/src/tools/sltool/shp.zig +++ b/src/tools/sltool/shp.zig @@ -579,7 +579,7 @@ const Beside = struct { } /// The largest buffer or picture read beside a glTF file. - const max_resource = 1 << 30; + const max_resource = std.fmt.parseIntSizeSuffix("1GiB", 10) catch unreachable; }; /// Writes `model` as the glTF file `out_path`, its parts at level `lod`, with its buffer beside it, diff --git a/src/tools/sltool/tcache.zig b/src/tools/sltool/tcache.zig index 6b53f85e..65c519ac 100644 --- a/src/tools/sltool/tcache.zig +++ b/src/tools/sltool/tcache.zig @@ -190,6 +190,6 @@ test "extracts a texture" { } }; try command.run(ctx); - const written = try tmp.dir.readFileAlloc(std.testing.io, "out/Kiev_1.png", arena, .limited(1 << 20)); + const written = try tmp.dir.readFileAlloc(std.testing.io, "out/Kiev_1.png", arena, .unlimited); try std.testing.expectEqualSlices(u8, png.signature, written[0..8]); } diff --git a/src/tools/tablegen/main.zig b/src/tools/tablegen/main.zig index 72dc5edf..bcf728ee 100644 --- a/src/tools/tablegen/main.zig +++ b/src/tools/tablegen/main.zig @@ -196,7 +196,7 @@ const Mode = union(enum) { }; /// The most of Ghidra's listing this reads: it is not one of the game's files, and runs longer. -const max_listing_size = 256 << 20; +const max_listing_size = std.fmt.parseIntSizeSuffix("256MiB", 10) catch unreachable; /// The payload executable at `path`, read whole, as a reader of its memory. fn loadBinary(init: std.process.Init, arena: std.mem.Allocator, path: []const u8) !image.Reader {