Editor-side service for taking, encoding, saving and sharing screenshots (SuperShot). It captures bitmaps, applies edits, builds filenames, saves/overwrites files, copies paths to clipboard, opens folders/URLs, posts images to Discord via a webhook helper, and handles package thumbnail presets.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Sandbox;
namespace Editor.SuperShot;
public static class SuperShotService
{
static int _sessionCounter = 1;
public static Bitmap CaptureRaw( CaptureSettings capture )
{
return SuperShotCapture.Capture( capture );
}
public static Bitmap CaptureFinished( CaptureSettings capture, EditSettings edit )
{
var raw = CaptureRaw( capture );
if ( raw is null )
return null;
if ( edit is null || edit.IsDefault() )
return raw;
var finished = SuperShotEdit.Apply( raw, edit );
raw.Dispose();
return finished;
}
public static string Extension( ShotFormat format ) => format switch
{
ShotFormat.Jpg => "jpg",
ShotFormat.WebP => "webp",
_ => "png"
};
public static string MimeType( ShotFormat format ) => format switch
{
ShotFormat.Jpg => "image/jpeg",
ShotFormat.WebP => "image/webp",
_ => "image/png"
};
public static byte[] Encode( Bitmap bitmap, ShotFormat format, int quality )
{
return format switch
{
ShotFormat.Jpg => bitmap.ToJpg( quality ),
ShotFormat.WebP => bitmap.ToWebP( quality ),
_ => bitmap.ToPng()
};
}
public static bool IsPackageThumbnail( ShotResolution resolution )
{
return resolution is ShotResolution.PackageSquare
or ShotResolution.PackageWide
or ShotResolution.PackageTall;
}
public static string PackageThumbnailPrefix( ShotResolution resolution ) => resolution switch
{
ShotResolution.PackageSquare => "sbox_square_",
ShotResolution.PackageWide => "sbox_wide_",
ShotResolution.PackageTall => "sbox_tall_",
_ => null
};
public static string BuildFilename( OutputSettings output, CaptureSettings capture = null )
{
var scene = SuperShotContext.ActiveScene?.Name;
if ( string.IsNullOrWhiteSpace( scene ) )
scene = "scene";
scene = Sanitize( scene );
var project = CurrentProjectName();
int w = 0, h = 0;
if ( capture is not null )
(w, h) = SuperShotContext.ResolveSize( capture );
var now = DateTime.Now;
var name = (output.FilenameTemplate ?? "{scene}_{date}_{time}")
.Replace( "{scene}", scene )
.Replace( "{project}", project )
.Replace( "{resolution}", w > 0 ? $"{w}x{h}" : "" )
.Replace( "{w}", w > 0 ? w.ToString() : "" )
.Replace( "{h}", h > 0 ? h.ToString() : "" )
.Replace( "{date}", now.ToString( "yyyy-MM-dd" ) )
.Replace( "{time}", now.ToString( "HH-mm-ss" ) )
.Replace( "{counter}", (_sessionCounter++).ToString( "D3" ) );
name = Sanitize( name ).Trim( '_', ' ', '-' );
if ( string.IsNullOrWhiteSpace( name ) )
name = scene;
if ( capture is not null && PackageThumbnailPrefix( capture.Resolution ) is { } prefix )
name = prefix + name;
return $"{name}.{Extension( output.Format )}";
}
static string CurrentProjectName()
{
try
{
var title = Project.Current?.Config?.Title;
if ( !string.IsNullOrWhiteSpace( title ) )
return Sanitize( title ).Trim();
}
catch { }
return "project";
}
public static string Save( Bitmap bitmap, OutputSettings output, CaptureSettings capture = null )
{
if ( bitmap is null )
return null;
try
{
var folder = output.ResolveFolder();
Directory.CreateDirectory( folder );
var path = Path.Combine( folder, BuildFilename( output, capture ) );
File.WriteAllBytes( path, Encode( bitmap, output.Format, output.Quality ) );
Log.Info( $"[Supershot] Saved {path} ({bitmap.Width}x{bitmap.Height})" );
if ( output.OpenFolderAfterSave )
RevealInExplorer( path );
return path;
}
catch ( Exception e )
{
Log.Error( $"[Supershot] Save failed: {e.Message}" );
return null;
}
}
// Overwrite an existing file in place, keeping its extension's format and the bitmap's current resolution.
public static string SaveOver( Bitmap bitmap, string path, int quality )
{
if ( bitmap is null || string.IsNullOrEmpty( path ) )
return null;
try
{
var format = FormatFromExtension( path );
File.WriteAllBytes( path, Encode( bitmap, format, quality ) );
Log.Info( $"[Supershot] Overwrote {path} ({bitmap.Width}x{bitmap.Height})" );
return path;
}
catch ( Exception e )
{
Log.Error( $"[Supershot] Overwrite failed: {e.Message}" );
return null;
}
}
public static ShotFormat FormatFromExtension( string path )
{
return Path.GetExtension( path ).ToLowerInvariant() switch
{
".jpg" or ".jpeg" => ShotFormat.Jpg,
".webp" => ShotFormat.WebP,
_ => ShotFormat.Png
};
}
// Capture+save Square, Wide and Tall package thumbnails via the clean QuickCapture path (no photo-edit pass).
public static int CaptureAllPackageThumbnails()
{
ShotResolution[] set = { ShotResolution.PackageSquare, ShotResolution.PackageWide, ShotResolution.PackageTall };
int saved = 0;
foreach ( var res in set )
{
var path = QuickCapture( res );
if ( path is not null )
saved++;
}
Log.Info( $"[Supershot] Captured {saved}/{set.Length} package thumbnails." );
return saved;
}
public static bool CopyPathToClipboard( string path )
{
if ( string.IsNullOrEmpty( path ) )
{
Log.Warning( "[Supershot] Nothing saved yet to copy a path for." );
return false;
}
try
{
EditorUtility.Clipboard.Copy( path );
Log.Info( $"[Supershot] Copied path to clipboard: {path}" );
return true;
}
catch ( Exception e )
{
Log.Warning( $"[Supershot] Could not copy to clipboard: {e.Message}" );
return false;
}
}
static CaptureSettings CloneCaptureWithResolution( CaptureSettings source, ShotResolution resolution )
{
return new CaptureSettings
{
Source = source.Source,
Resolution = resolution,
SuperSampling = source.SuperSampling,
OverrideFov = source.OverrideFov,
Fov = source.Fov,
TransparentBackground = source.TransparentBackground,
HideUI = source.HideUI,
HideTags = source.HideTags,
Delay = source.Delay,
PostFx = source.PostFx
};
}
public static string CaptureAndSave( CaptureSettings capture, EditSettings edit, OutputSettings output )
{
using var bitmap = CaptureFinished( capture, edit );
if ( bitmap is null )
return null;
return Save( bitmap, output, capture );
}
// Fast local save at a resolution preset. Never posts to Discord (use the explicit Capture + Post actions).
public static string QuickCapture( ShotResolution resolution )
{
var settings = SuperShotSettings.Current;
var capture = CloneCaptureWithResolution( settings.Capture, resolution );
return CaptureAndSave( capture, null, settings.Output );
}
public static async Task CaptureAndPostAll( string messageOverride = null )
{
var settings = SuperShotSettings.Current;
var path = CaptureAndSave( settings.Capture, settings.Edit, settings.Output );
if ( path is null )
return;
var targets = EnabledWebhooks( settings.Share );
if ( targets.Count == 0 )
{
Log.Warning( "[Supershot] No enabled Discord channels configured (open the Studio Discord tab)." );
return;
}
var message = WithMessageOverride( settings.Share.Message, messageOverride );
var bytes = File.ReadAllBytes( path );
int sent = await DiscordWebhook.PostImageToMany( targets, bytes, Path.GetFileName( path ), message, MimeType( settings.Output.Format ) );
Log.Info( $"[Supershot] Posted to {sent}/{targets.Count} Discord channel(s)." );
}
static DiscordMessage WithMessageOverride( DiscordMessage shared, string overrideContent )
{
if ( string.IsNullOrWhiteSpace( overrideContent ) )
return shared;
var message = shared?.Clone() ?? new DiscordMessage();
message.Content = overrideContent.Trim();
return message;
}
public static async Task CaptureAndPostTo( DiscordWebhookConfig webhook )
{
if ( webhook is null || !webhook.IsConfigured )
{
Log.Warning( "[Supershot] That channel has no webhook URL set." );
return;
}
var settings = SuperShotSettings.Current;
var path = CaptureAndSave( settings.Capture, settings.Edit, settings.Output );
if ( path is null )
return;
var bytes = File.ReadAllBytes( path );
if ( await DiscordWebhook.PostImage( webhook.Url, bytes, Path.GetFileName( path ), webhook.ResolveMessage( settings.Share.Message ), MimeType( settings.Output.Format ) ) )
Log.Info( $"[Supershot] Posted to '{webhook.Name}'." );
}
public static List<DiscordWebhookConfig> EnabledWebhooks( ShareSettings share )
{
var result = new List<DiscordWebhookConfig>();
if ( share?.Webhooks is null )
return result;
foreach ( var wh in share.Webhooks )
{
if ( wh is not null && wh.Enabled && wh.IsConfigured )
result.Add( wh );
}
return result;
}
public static void RevealInExplorer( string path )
{
try
{
if ( string.IsNullOrEmpty( path ) )
return;
if ( Directory.Exists( path ) )
EditorUtility.OpenFolder( path );
else
EditorUtility.OpenFileFolder( path );
}
catch ( Exception e )
{
Log.Warning( $"[Supershot] Could not open folder: {e.Message}" );
}
}
// Steam Community artwork upload page for s&box (appid 590830).
public const string SboxArtPageUrl = "https://steamcommunity.com/sharedfiles/edititem/590830/3/";
public static void OpenSboxArtPage()
{
try
{
EditorUtility.OpenFile( SboxArtPageUrl );
}
catch ( Exception e )
{
Log.Warning( $"[Supershot] Could not open the s&box art page: {e.Message}" );
}
}
public static void CaptureAndUploadToArtPage()
{
var settings = SuperShotSettings.Current;
var path = CaptureAndSave( settings.Capture, settings.Edit, settings.Output );
UploadToArtPage( path );
}
public static void UploadToArtPage( string savedPath )
{
if ( !string.IsNullOrEmpty( savedPath ) )
{
CopyPathToClipboard( savedPath );
Log.Info( "[Supershot] Saved shot path copied to clipboard - paste it into Steam's upload file picker." );
}
else
{
Log.Warning( "[Supershot] Nothing saved to upload - capture a shot first." );
}
OpenSboxArtPage();
}
static string Sanitize( string input )
{
foreach ( var c in Path.GetInvalidFileNameChars() )
input = input.Replace( c, '_' );
return input;
}
}