Game/PhotoMode.cs
namespace Monolith;
/// <summary>
/// Capture tools for the store page. P hides the HUD, O takes a screenshot, and F6 video
/// recording hides the HUD by itself.
///
/// **Why this exists.** The store screenshot page asks you to avoid text and logos and let the
/// gameplay speak for itself, and this game draws a HUD every frame: a progress bar, a wallet, a
/// charge readout, a crosshair. Every screenshot taken normally has all of it in shot. There was
/// no way to get a clean frame without deleting components, so the useful screenshots were
/// effectively impossible to take.
///
/// **The HUD is HIDDEN, not disabled.** Turning the component off would stop its `OnUpdate`,
/// which is where the pause is decided, where the cursor is managed and where the charge marker
/// is projected. Half the game would quietly change behaviour in exactly the frames being
/// photographed. Setting a class that makes it invisible leaves every one of those systems
/// running, so what you capture is the real game with the overlay taken off the glass.
///
/// **Deliberately not automated.** An earlier idea was a camera tour that flew to a few angles
/// and captured on a timer. It would have produced pictures of an empty arena: what makes a
/// screenshot worth looking at here is a Spotter mid-lock, a floor cycle caught on the amber
/// frame, shrapnel rings on the ground with the chunks still in the air. A machine does not know
/// when that is happening and a player does. This puts the shutter under your thumb instead.
/// </summary>
public sealed class PhotoMode : Component
{
/// <summary>
/// Native resolution capture. Passing the real window size means the shot matches what you
/// framed rather than being re-rendered at some other aspect and cropped by the store.
/// </summary>
private static readonly int Width = 1920;
private static readonly int Height = 1080;
// REAL clock. Photo mode deliberately works while the world is frozen, and a GameTimeSince
// stops advancing there, so the shutter cooldown would never expire: you would get exactly
// one screenshot per pause and no indication why the second did nothing. Same trap as the
// state diagnostic in GameBootstrap.
private TimeSince timeSinceShot = 99f;
protected override void OnStart()
{
Log.Info( "[photo] ready. P hides the HUD, O captures 1920x1080, "
+ "F6 records video and hides the HUD while it runs." );
}
protected override void OnUpdate()
{
// NOT gated on GameTime.Paused. Capturing with the world frozen is often the ONLY way to
// get a clean frame of something fast, and a pause menu open behind a hidden HUD is
// exactly the moment you want the shutter to work.
WatchRecording();
if ( Input.Pressed( "PhotoHud" ) )
{
Hud.PhotoHidden = !Hud.PhotoHidden;
Log.Info( Hud.PhotoHidden
? "[photo] HUD hidden. O to capture, P to bring it back."
: "[photo] HUD restored." );
}
if ( !Input.Pressed( "PhotoShot" ) )
return;
// A held key would otherwise fill the folder in a second.
if ( timeSinceShot < 0.4f )
return;
timeSinceShot = 0f;
Capture();
}
private bool wasRecording;
private bool hidForRecording;
/// <summary>
/// Hides the HUD for the duration of an F6 video recording, and puts it back afterwards.
/// </summary>
/// <remarks>
/// **Recording cannot be started from code, so this hooks the state instead.** The engine
/// exposes `Game.IsRecordingVideo` as a read-only property and documents F6 and a `video`
/// console command as the ways in, but there is no published API to trigger either.
/// `Sandbox.ConsoleSystem` exists as a type with no documented members, and guessing at a
/// method signature there would be a compile error rather than something catchable.
///
/// Watching the flag gets the useful half anyway. The store wants gameplay without text or
/// logos, and remembering to hide the HUD before every take is exactly the sort of thing you
/// forget until you have recorded the good one with a progress bar across it.
///
/// Only restores the HUD if RECORDING hid it. Someone who pressed P first and then started
/// recording wants it to stay hidden when they stop.
/// </remarks>
private void WatchRecording()
{
bool recording;
try { recording = Game.IsRecordingVideo; }
catch ( Exception ) { return; }
if ( recording == wasRecording )
return;
wasRecording = recording;
if ( recording )
{
hidForRecording = !Hud.PhotoHidden;
if ( hidForRecording )
Hud.PhotoHidden = true;
Log.Info( "[photo] recording started, HUD hidden." );
return;
}
if ( hidForRecording )
{
Hud.PhotoHidden = false;
hidForRecording = false;
}
Log.Info( "[photo] recording stopped." );
}
private static void Capture()
{
try
{
Game.TakeHighResScreenshot( Width, Height );
Log.Info( $"[photo] captured {Width}x{Height} to "
+ "D:/SteamLibrary/steamapps/common/sbox/screenshots" );
}
catch ( Exception e )
{
// Falls back to the plain capture, which goes to the Steam screenshot library. Worth
// having: a failed screenshot at the moment you finally caught a good frame is a
// genuinely annoying way to lose one.
Log.Warning( $"[photo] high-res capture failed ({e.Message}), trying plain." );
try { Game.TakeScreenshot(); }
catch ( Exception inner ) { Log.Warning( $"[photo] capture failed: {inner.Message}" ); }
}
}
}