s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (4 total matches found)
brax.terrainconvert / Editor/HeightmapConverter.cs
Editor library
using System;
using System.Linq;
using Sandbox;

namespace Editor.TerrainConvert;

/// <summary>
/// Which source channel to read the height value from.
/// </summary>
public enum HeightChannel
{
	/// <summary> Red channel only. Standard for grayscale heightmaps. </summary>
	Red,
	Green,
	Blue,
	Alpha,
	/// <summary> Rec.709 weighted luminance of RGB. </summary>
	Luminance,
	/// <summary> Average of the RGB channels. </summary>
	Average,
	/// <summary> The largest of the RGB channels. </summary>
	Max,
}

/// <summary>
/// Bit depth of the output .raw file. s&box terrain stores heights as 16-bit,
/// so 16-bit is recommended for the best precision.
/// </summary>
public enum HeightBitDepth
{
	/// <summary> 16-bit per height sample (recommended). 2 bytes per pixel. </summary>
	Bit16,
	/// <summary> 8-bit per height sample. 1 byte per pixel. </summary>
	Bit8,
}

/// <summary>
/// How the source values are remapped into the 0..1 height range before quantizing.
/// </summary>
public enum HeightNormalize
{
	/// <summary> Use values as-is, clamped to 0..1. </summary>
	Clamp,
	/// <summary> Stretch the min..max range of the image to fill 0..1 (good for HDR/EXR). </summary>
	MinMaxStretch,
}

/// <summary>
/// Byte order of multi-byte (16-bit) samples in the output file.
/// </summary>
public enum HeightByteOrder
{
	/// <summary> Little-endian. What the s&box terrain importer reads by default ("Windows"). </summary>
	LittleEndian,
	/// <summary> Big-endian ("Mac"). </summary>
	BigEndian,
}

/// <summary>
/// Settings for converting an image into a terrain heightmap .raw file.
/// </summary>
public class HeightmapConvertSettings
{
	/// <summary>
	/// Which channel of the source image holds the height. Most heightmaps are
	/// grayscale, where every channel is identical - <see cref="HeightChannel.Red"/> works for those.
	/// </summary>
	[Property]
	public HeightChannel Channel { get; set; } = HeightChannel.Red;

	/// <summary>
	/// 16-bit gives the smoothest terrain and matches what s&box stores internally.
	/// </summary>
	[Property]
	public HeightBitDepth BitDepth { get; set; } = HeightBitDepth.Bit16;

	/// <summary>
	/// Output heightmaps are always square. If 0, the smaller of the source
	/// dimensions is used. The image is resampled to this resolution.
	/// </summary>
	[Property, Range( 0, 8192 )]
	public int Resolution { get; set; } = 0;

	/// <summary>
	/// Round the output resolution down to the nearest power of two (e.g 1024, 2048).
	/// The terrain system expects power-of-two heightmaps, so leave this on.
	/// </summary>
	[Property]
	public bool PowerOfTwo { get; set; } = true;

	/// <summary>
	/// How source values are mapped to the height range. Use <see cref="HeightNormalize.MinMaxStretch"/>
	/// for HDR images that don't already fill 0..1.
	/// </summary>
	[Property]
	public HeightNormalize Normalize { get; set; } = HeightNormalize.Clamp;

	/// <summary>
	/// Flip the image vertically. Image and terrain coordinate origins often differ;
	/// toggle this if your terrain comes out mirrored north/south.
	/// </summary>
	[Property]
	public bool FlipVertical { get; set; } = false;

	/// <summary>
	/// Invert the heights (high becomes low). Useful for depth/inverted maps.
	/// </summary>
	[Property]
	public bool Invert { get; set; } = false;

	/// <summary>
	/// Byte order of 16-bit samples. The s&box importer reads little-endian by default.
	/// </summary>
	[Property, ShowIf( nameof( BitDepth ), HeightBitDepth.Bit16 )]
	public HeightByteOrder ByteOrder { get; set; } = HeightByteOrder.LittleEndian;
}

/// <summary>
/// Converts loaded images into raw single-channel heightmaps compatible with the
/// s&box terrain importer (square, 8 or 16 bit, raw samples with no header).
/// </summary>
public static class HeightmapConverter
{
	/// <summary>
	/// Convert a bitmap into a raw heightmap byte buffer.
	/// </summary>
	/// <param name="bitmap">Source image.</param>
	/// <param name="settings">Conversion settings.</param>
	/// <param name="resolution">The square resolution of the produced heightmap.</param>
	/// <returns>Raw heightmap bytes ready to write to a .raw file.</returns>
	public static byte[] Convert( Bitmap bitmap, HeightmapConvertSettings settings, out int resolution )
	{
		ArgumentNullException.ThrowIfNull( bitmap );
		ArgumentNullException.ThrowIfNull( settings );

		var pixels = bitmap.GetPixels();
		int count = bitmap.Width * bitmap.Height;

		// Extract the chosen channel as a float per pixel, then run the shared pipeline.
		var values = new float[count];
		for ( int i = 0; i < count; i++ )
			values[i] = SampleChannel( pixels[i], settings.Channel );

		return BuildRaw( values, bitmap.Width, bitmap.Height, settings, out resolution );
	}

	/// <summary>
	/// Convert a decoded EXR into a raw heightmap byte buffer, reading height from the
	/// chosen channel (falling back to Y/R/first channel if the exact one is absent).
	/// </summary>
	public static byte[] Convert( ExrImage exr, HeightmapConvertSettings settings, out int resolution )
	{
		ArgumentNullException.ThrowIfNull( exr );
		ArgumentNullException.ThrowIfNull( settings );

		var values = SampleChannel( exr, settings.Channel );
		return BuildRaw( values, exr.Width, exr.Height, settings, out resolution );
	}

	/// <summary>
	/// Shared pipeline: resample a single-channel float plane to a square power-of-two,
	/// normalize into 0..1, optionally invert, and quantize to raw bytes.
	/// </summary>
	static byte[] BuildRaw( float[] values, int srcWidth, int srcHeight, HeightmapConvertSettings settings, out int resolution )
	{
		resolution = ResolveResolution( srcWidth, srcHeight, settings );

		// Bilinear resample to a square of the target resolution, working entirely in float
		// so we don't lose precision on high bit-depth / HDR sources. Resampling produces a
		// fresh array; otherwise clone so the in-place normalize/invert never mutates a
		// caller-owned buffer (e.g. an EXR's cached channel plane).
		values = srcWidth != resolution || srcHeight != resolution
			? ResampleBilinear( values, srcWidth, srcHeight, resolution, resolution )
			: (float[])values.Clone();

		ApplyNormalize( values, settings.Normalize );

		if ( settings.Invert )
		{
			for ( int i = 0; i < values.Length; i++ )
				values[i] = 1f - values[i];
		}

		return Quantize( values, resolution, settings );
	}

	static float[] ResampleBilinear( float[] src, int srcW, int srcH, int dstW, int dstH )
	{
		var dst = new float[dstW * dstH];

		for ( int y = 0; y < dstH; y++ )
		{
			float fy = dstH > 1 ? (float)y / (dstH - 1) * (srcH - 1) : 0f;
			int y0 = (int)fy;
			int y1 = Math.Min( y0 + 1, srcH - 1 );
			float ty = fy - y0;

			for ( int x = 0; x < dstW; x++ )
			{
				float fx = dstW > 1 ? (float)x / (dstW - 1) * (srcW - 1) : 0f;
				int x0 = (int)fx;
				int x1 = Math.Min( x0 + 1, srcW - 1 );
				float tx = fx - x0;

				float top = MathX.Lerp( src[y0 * srcW + x0], src[y0 * srcW + x1], tx );
				float bottom = MathX.Lerp( src[y1 * srcW + x0], src[y1 * srcW + x1], tx );
				dst[y * dstW + x] = MathX.Lerp( top, bottom, ty );
			}
		}

		return dst;
	}

	static float[] SampleChannel( ExrImage exr, HeightChannel channel )
	{
		// For multi-channel EXRs, blend RGB the same way the bitmap path does. For the
		// common single-channel (Y) heightmap, every option resolves to that one plane.
		switch ( channel )
		{
			case HeightChannel.Red: return exr.GetHeightChannel( "R" );
			case HeightChannel.Green: return exr.GetHeightChannel( "G" );
			case HeightChannel.Blue: return exr.GetHeightChannel( "B" );
			case HeightChannel.Alpha: return exr.GetHeightChannel( "A" );
		}

		var r = exr.GetChannel( "R" );
		var g = exr.GetChannel( "G" );
		var b = exr.GetChannel( "B" );

		// No RGB set - it's a luminance/single-channel image, use it directly.
		if ( r is null || g is null || b is null )
			return exr.GetHeightChannel();

		var outv = new float[r.Length];
		for ( int i = 0; i < outv.Length; i++ )
		{
			outv[i] = channel switch
			{
				HeightChannel.Luminance => 0.2126f * r[i] + 0.7152f * g[i] + 0.0722f * b[i],
				HeightChannel.Average => (r[i] + g[i] + b[i]) / 3f,
				HeightChannel.Max => Math.Max( r[i], Math.Max( g[i], b[i] ) ),
				_ => r[i],
			};
		}
		return outv;
	}

	static int ResolveResolution( int width, int height, HeightmapConvertSettings settings )
	{
		int res = settings.Resolution > 0 ? settings.Resolution : Math.Min( width, height );

		if ( settings.PowerOfTwo )
			res = RoundDownToPowerOfTwo( res );

		return Math.Clamp( res, 4, 16384 );
	}

	static float SampleChannel( Color c, HeightChannel channel ) => channel switch
	{
		HeightChannel.Red => c.r,
		HeightChannel.Green => c.g,
		HeightChannel.Blue => c.b,
		HeightChannel.Alpha => c.a,
		HeightChannel.Luminance => 0.2126f * c.r + 0.7152f * c.g + 0.0722f * c.b,
		HeightChannel.Average => (c.r + c.g + c.b) / 3f,
		HeightChannel.Max => Math.Max( c.r, Math.Max( c.g, c.b ) ),
		_ => c.r,
	};

	static void ApplyNormalize( float[] values, HeightNormalize mode )
	{
		if ( mode == HeightNormalize.MinMaxStretch )
		{
			float min = values.Min();
			float max = values.Max();
			float range = max - min;

			if ( range > 1e-6f )
			{
				for ( int i = 0; i < values.Length; i++ )
					values[i] = (values[i] - min) / range;
				return;
			}
			// Flat image - fall through to clamp.
		}

		for ( int i = 0; i < values.Length; i++ )
			values[i] = Math.Clamp( values[i], 0f, 1f );
	}

	static byte[] Quantize( float[] values, int resolution, HeightmapConvertSettings settings )
	{
		bool flip = settings.FlipVertical;

		if ( settings.BitDepth == HeightBitDepth.Bit8 )
		{
			var bytes = new byte[resolution * resolution];
			for ( int y = 0; y < resolution; y++ )
			{
				int srcY = flip ? resolution - 1 - y : y;
				for ( int x = 0; x < resolution; x++ )
				{
					float v = Math.Clamp( values[srcY * resolution + x], 0f, 1f );
					bytes[y * resolution + x] = (byte)MathF.Round( v * byte.MaxValue );
				}
			}
			return bytes;
		}
		else
		{
			bool little = settings.ByteOrder == HeightByteOrder.LittleEndian;
			var bytes = new byte[resolution * resolution * 2];
			for ( int y = 0; y < resolution; y++ )
			{
				int srcY = flip ? resolution - 1 - y : y;
				for ( int x = 0; x < resolution; x++ )
				{
					float v = Math.Clamp( values[srcY * resolution + x], 0f, 1f );
					ushort h = (ushort)MathF.Round( v * ushort.MaxValue );

					int o = (y * resolution + x) * 2;
					if ( little )
					{
						bytes[o] = (byte)(h & 0xFF);
						bytes[o + 1] = (byte)(h >> 8);
					}
					else
					{
						bytes[o] = (byte)(h >> 8);
						bytes[o + 1] = (byte)(h & 0xFF);
					}
				}
			}
			return bytes;
		}
	}

	/// <summary>
	/// Rounds a value down to the nearest power of two.
	/// </summary>
	public static int RoundDownToPowerOfTwo( int value )
	{
		if ( value < 1 ) return 1;
		value |= value >> 1;
		value |= value >> 2;
		value |= value >> 4;
		value |= value >> 8;
		value |= value >> 16;
		return value - (value >> 1);
	}
}
brax.terrainconvert / Editor/HeightmapConvertWindow.cs
Editor library
using System;
using System.IO;
using Sandbox;

namespace Editor.TerrainConvert;

/// <summary>
/// Editor tool that converts an image (.png, .jpg, .tga, .tif, .psd, .exr ...) into a
/// raw single-channel heightmap (.raw) that the s&box terrain importer can load.
/// </summary>
[EditorApp( "Heightmap Converter", "terrain", "Convert an image into a terrain heightmap (.raw)" )]
public class HeightmapConvertWindow : Widget
{
	HeightmapConvertSettings Settings { get; set; } = new();

	string inputFile;
	Bitmap loadedBitmap;
	ExrImage loadedExr;
	int srcWidth, srcHeight;

	bool HasImage => loadedBitmap is not null || loadedExr is not null;

	Label inputLabel;
	Label outputInfoLabel;
	Button convertButton;

	public HeightmapConvertWindow() : this( null ) { }

	public HeightmapConvertWindow( Widget parent ) : base( parent )
	{
		WindowFlags = WindowFlags.Dialog | WindowFlags.Customized | WindowFlags.WindowTitle | WindowFlags.CloseButton | WindowFlags.WindowSystemMenuHint;
		DeleteOnClose = true;
		WindowTitle = "Convert Image to Heightmap";
		SetWindowIcon( "terrain" );

		Layout = Layout.Column();
		Layout.Spacing = 8;
		Layout.Margin = 16;

		var warning = new WarningBox(
			"Converts an image into a raw heightmap (.raw) for the terrain system.\n" +
			"Output is square, single-channel, 8 or 16 bit. Import it via the Terrain component's heightmap import.", this );
		Layout.Add( warning );

		// Input file row
		{
			var row = Layout.Row();
			row.Spacing = 8;

			inputLabel = new Label( "No image selected", this );
			inputLabel.WordWrap = false;
			row.Add( inputLabel, 1 );

			var browse = new Button( "Browse Image...", "image", this );
			browse.Clicked = PickInputFile;
			row.Add( browse );

			Layout.Add( row );
		}

		// Settings sheet
		{
			var so = EditorUtility.GetSerializedObject( Settings );
			so.OnPropertyChanged += _ => UpdateInfo();

			var sheet = new ControlSheet();
			sheet.AddObject( so );
			Layout.Add( sheet );
		}

		outputInfoLabel = new Label( "", this );
		outputInfoLabel.Color = Theme.TextControl.WithAlpha( 0.6f );
		Layout.Add( outputInfoLabel );

		Layout.AddStretchCell();

		// Bottom bar
		{
			var row = Layout.Row();
			row.Margin = new Sandbox.UI.Margin( 0, 8, 0, 0 );
			row.AddStretchCell();

			convertButton = new Button.Primary( "Convert & Save...", "file_download", this );
			convertButton.Clicked = ConvertAndSave;
			convertButton.Enabled = false;
			row.Add( convertButton );

			Layout.Add( row );
		}

		Width = 440;
		MinimumWidth = 380;
		Height = 520;

		UpdateInfo();

		Show();
		Focus();
	}

	void PickInputFile()
	{
		var fd = new FileDialog( null )
		{
			Title = "Select Source Image",
		};
		fd.SetFindFile();
		fd.SetModeOpen();
		fd.SetNameFilter( "Images (*.png *.jpg *.jpeg *.tga *.tif *.tiff *.psd *.exr *.bmp *.raw)" );

		if ( !fd.Execute() )
			return;

		LoadInput( fd.SelectedFile );
	}

	void LoadInput( string path )
	{
		loadedBitmap?.Dispose();
		loadedBitmap = null;
		loadedExr = null;
		inputFile = null;

		try
		{
			var bytes = File.ReadAllBytes( path );

			// EXR isn't supported by the Skia-backed Bitmap loader, so decode it ourselves.
			bool isExr = path.EndsWith( ".exr", StringComparison.OrdinalIgnoreCase ) || ExrImage.IsExr( bytes );
			if ( isExr )
			{
				loadedExr = ExrImage.Load( bytes );
				srcWidth = loadedExr.Width;
				srcHeight = loadedExr.Height;
				inputFile = path;
			}
			else
			{
				var bitmap = Bitmap.CreateFromBytes( bytes );
				if ( bitmap is null || !bitmap.IsValid )
				{
					EditorUtility.DisplayDialog( "Couldn't load image",
						$"Failed to decode '{Path.GetFileName( path )}'.\nThis image format may not be supported." );
					return;
				}

				loadedBitmap = bitmap;
				srcWidth = bitmap.Width;
				srcHeight = bitmap.Height;
				inputFile = path;
			}
		}
		catch ( Exception e )
		{
			EditorUtility.DisplayDialog( "Couldn't load image", e.Message );
			return;
		}

		UpdateInfo();
	}

	void UpdateInfo()
	{
		if ( !HasImage )
		{
			inputLabel.Text = "No image selected";
			outputInfoLabel.Text = "Pick an image to begin.";
			convertButton.Enabled = false;
			return;
		}

		string kind = loadedExr is not null ? "EXR" : "image";
		inputLabel.Text = $"{Path.GetFileName( inputFile )}  ({srcWidth}x{srcHeight}, {kind})";
		convertButton.Enabled = true;

		int res = PreviewResolution();
		string depth = Settings.BitDepth == HeightBitDepth.Bit16 ? "16-bit" : "8-bit";
		int bytes = res * res * (Settings.BitDepth == HeightBitDepth.Bit16 ? 2 : 1);
		outputInfoLabel.Text = $"Output: {res}x{res}, {depth} → {bytes / 1024:n0} KB raw";
	}

	int PreviewResolution()
	{
		int res = Settings.Resolution > 0 ? Settings.Resolution : Math.Min( srcWidth, srcHeight );
		if ( Settings.PowerOfTwo )
			res = HeightmapConverter.RoundDownToPowerOfTwo( res );
		return Math.Clamp( res, 4, 16384 );
	}

	void ConvertAndSave()
	{
		if ( !HasImage )
			return;

		var fd = new FileDialog( null )
		{
			Title = "Save Heightmap",
			DefaultSuffix = ".raw",
		};
		fd.Directory = Path.GetDirectoryName( inputFile );
		fd.SelectFile( $"{Path.GetFileNameWithoutExtension( inputFile )}.raw" );
		fd.SetFindFile();
		fd.SetModeSave();
		fd.SetNameFilter( "Raw Heightmap (*.raw *.r16 *.r8)" );

		if ( !fd.Execute() )
			return;

		try
		{
			int resolution;
			var data = loadedExr is not null
				? HeightmapConverter.Convert( loadedExr, Settings, out resolution )
				: HeightmapConverter.Convert( loadedBitmap, Settings, out resolution );
			File.WriteAllBytes( fd.SelectedFile, data );

			EditorUtility.DisplayDialog( "Heightmap saved",
				$"Wrote {resolution}x{resolution} heightmap to:\n{fd.SelectedFile}\n\n" +
				"Import it from a Terrain component: open its heightmap import and choose this .raw file." );
		}
		catch ( Exception e )
		{
			EditorUtility.DisplayDialog( "Conversion failed", e.Message );
		}
	}

	public override void OnDestroyed()
	{
		base.OnDestroyed();
		loadedBitmap?.Dispose();
		loadedBitmap = null;
		loadedExr = null;
	}
}
brax.terrainconvert / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "terrainconvert" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "terrainconvert" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "brax" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "brax.terrainconvert" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-26T23:15:58.0786236Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.205.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.205.0")]
brax.terrainconvert / Editor/ExrImage.cs
Editor library
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;

namespace Editor.TerrainConvert;

/// <summary>
/// Minimal OpenEXR reader for scanline images. Source 2's own EXR loader is internal to
/// the engine and can't be called from library code, so this decodes the float data directly.
///
/// Supports: scanline (non-tiled) images, HALF/FLOAT/UINT channels, and NONE/ZIP/ZIPS/RLE
/// compression - which covers the vast majority of heightmap exports. Tiled, deep, and the
/// lossy/wavelet codecs (PIZ, PXR24, B44, DWAA/DWAB) are not supported and throw a clear error.
/// </summary>
public sealed class ExrImage
{
	public int Width { get; private set; }
	public int Height { get; private set; }

	/// <summary> Channel name -> linear float plane (row-major, Width*Height). </summary>
	readonly Dictionary<string, float[]> channels = new( StringComparer.Ordinal );

	public IReadOnlyCollection<string> ChannelNames => channels.Keys;
	public bool HasChannel( string name ) => channels.ContainsKey( name );

	enum PixelType { Uint = 0, Half = 1, Float = 2 }

	enum Compression { None = 0, Rle = 1, Zips = 2, Zip = 3, Piz = 4, Pxr24 = 5, B44 = 6, B44A = 7, Dwaa = 8, Dwab = 9 }

	readonly record struct ChannelInfo( string Name, PixelType Type )
	{
		public int SampleSize => Type == PixelType.Half ? 2 : 4;
	}

	/// <summary>
	/// Returns true if the bytes start with the OpenEXR magic number.
	/// </summary>
	public static bool IsExr( byte[] data )
		=> data is { Length: >= 4 } && data[0] == 0x76 && data[1] == 0x2f && data[2] == 0x31 && data[3] == 0x01;

	/// <summary>
	/// Decode an EXR from a byte buffer.
	/// </summary>
	public static ExrImage Load( byte[] data )
	{
		if ( !IsExr( data ) )
			throw new InvalidDataException( "Not an OpenEXR file (bad magic number)." );

		var img = new ExrImage();
		img.Parse( data );
		return img;
	}

	/// <summary>
	/// Get a channel as a float plane, or null if absent. Common heightmap names are "Y"
	/// (luminance), "R", or a plain unnamed channel.
	/// </summary>
	public float[] GetChannel( string name ) => channels.TryGetValue( name, out var v ) ? v : null;

	/// <summary>
	/// Pick the most sensible single channel to treat as height: explicit preference first,
	/// then Y / R / the first available channel.
	/// </summary>
	public float[] GetHeightChannel( string preferred = null )
	{
		if ( preferred is not null && channels.TryGetValue( preferred, out var p ) )
			return p;

		foreach ( var name in new[] { "Y", "R", "G", "B" } )
			if ( channels.TryGetValue( name, out var v ) )
				return v;

		foreach ( var v in channels.Values )
			return v;

		return null;
	}

	void Parse( byte[] data )
	{
		int pos = 4; // skip magic

		int version = ReadInt32( data, ref pos );
		bool tiled = (version & 0x200) != 0;
		bool deep = (version & 0x800) != 0;
		bool multipart = (version & 0x1000) != 0;

		if ( tiled ) throw new NotSupportedException( "Tiled EXR images are not supported - re-export as a scanline image." );
		if ( deep ) throw new NotSupportedException( "Deep EXR images are not supported." );
		if ( multipart ) throw new NotSupportedException( "Multi-part EXR images are not supported." );

		var chans = new List<ChannelInfo>();
		var compression = Compression.None;
		int xMin = 0, yMin = 0, xMax = 0, yMax = 0;

		// Header attributes, terminated by an empty name.
		while ( true )
		{
			string name = ReadNullString( data, ref pos );
			if ( name.Length == 0 ) break;

			string type = ReadNullString( data, ref pos );
			int size = ReadInt32( data, ref pos );
			int valueStart = pos;

			switch ( name )
			{
				case "channels":
					ParseChannels( data, valueStart, chans );
					break;
				case "compression":
					compression = (Compression)data[valueStart];
					break;
				case "dataWindow":
					int p = valueStart;
					xMin = ReadInt32( data, ref p );
					yMin = ReadInt32( data, ref p );
					xMax = ReadInt32( data, ref p );
					yMax = ReadInt32( data, ref p );
					break;
			}

			pos = valueStart + size; // skip to next attribute
		}

		if ( chans.Count == 0 )
			throw new InvalidDataException( "EXR has no channels." );

		Width = xMax - xMin + 1;
		Height = yMax - yMin + 1;

		if ( Width <= 0 || Height <= 0 )
			throw new InvalidDataException( $"EXR has invalid data window ({Width}x{Height})." );

		int linesPerBlock = compression switch
		{
			Compression.None or Compression.Rle or Compression.Zips => 1,
			Compression.Zip or Compression.Pxr24 => 16,
			Compression.Piz or Compression.B44 or Compression.B44A or Compression.Dwaa => 32,
			Compression.Dwab => 256,
			_ => 1,
		};

		if ( compression is Compression.Piz or Compression.Pxr24 or Compression.B44 or Compression.B44A or Compression.Dwaa or Compression.Dwab )
			throw new NotSupportedException( $"EXR compression '{compression}' is not supported. Re-export the heightmap as Uncompressed, ZIP, or RLE." );

		// Allocate channel planes.
		foreach ( var c in chans )
			channels[c.Name] = new float[Width * Height];

		// Scanline offset table: one ulong per block.
		int blockCount = (Height + linesPerBlock - 1) / linesPerBlock;
		var offsets = new long[blockCount];
		for ( int i = 0; i < blockCount; i++ )
			offsets[i] = (long)ReadUInt64( data, ref pos );

		int rowBytes = 0;
		foreach ( var c in chans )
			rowBytes += Width * c.SampleSize;

		// Each block: int32 yStart, int32 dataSize, then (compressed) pixel data.
		foreach ( var off in offsets )
		{
			int bp = (int)off;
			int yStart = ReadInt32( data, ref bp );
			int dataSize = ReadInt32( data, ref bp );

			int lines = Math.Min( linesPerBlock, yMax - yStart + 1 );
			int uncompressedSize = rowBytes * lines;

			byte[] block;
			if ( compression == Compression.None || dataSize >= uncompressedSize )
			{
				// Stored uncompressed (NONE, or a block that didn't compress smaller).
				block = new byte[uncompressedSize];
				Array.Copy( data, bp, block, 0, uncompressedSize );
			}
			else
			{
				block = Decompress( compression, data, bp, dataSize, uncompressedSize );
			}

			ScatterBlock( block, chans, rowBytes, Width, yStart - yMin, lines );
		}
	}

	void ScatterBlock( byte[] block, List<ChannelInfo> chans, int rowBytes, int width, int rowOffset, int lines )
	{
		for ( int i = 0; i < lines; i++ )
		{
			int rowBase = i * rowBytes;
			int channelOffset = 0;
			int destRow = rowOffset + i;

			foreach ( var c in chans )
			{
				int src = rowBase + channelOffset;
				var plane = channels[c.Name];
				int destBase = destRow * width;

				for ( int x = 0; x < width; x++ )
				{
					plane[destBase + x] = ReadSample( block, src, c.Type );
					src += c.SampleSize;
				}

				channelOffset += width * c.SampleSize;
			}
		}
	}

	static float ReadSample( byte[] b, int offset, PixelType type ) => type switch
	{
		PixelType.Half => (float)BitConverter.UInt16BitsToHalf( (ushort)(b[offset] | (b[offset + 1] << 8)) ),
		PixelType.Float => BitConverter.Int32BitsToSingle( b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] << 24) ),
		PixelType.Uint => (uint)(b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] << 24)),
		_ => 0f,
	};

	static void ParseChannels( byte[] data, int pos, List<ChannelInfo> chans )
	{
		while ( true )
		{
			string name = ReadNullString( data, ref pos );
			if ( name.Length == 0 ) break;

			int ptype = ReadInt32( data, ref pos );
			pos += 1;      // pLinear
			pos += 3;      // reserved
			ReadInt32( data, ref pos ); // xSampling
			ReadInt32( data, ref pos ); // ySampling

			chans.Add( new ChannelInfo( name, (PixelType)ptype ) );
		}
	}

	static byte[] Decompress( Compression compression, byte[] data, int offset, int size, int uncompressedSize )
	{
		// Step 1: codec-specific decompression into a temp buffer.
		byte[] tmp = compression switch
		{
			Compression.Zip or Compression.Zips => Inflate( data, offset, size, uncompressedSize ),
			Compression.Rle => RleDecode( data, offset, size, uncompressedSize ),
			_ => throw new NotSupportedException( $"EXR compression '{compression}' is not supported." ),
		};

		// Step 2: undo EXR's byte predictor + interleave (shared by ZIP and RLE).
		Predictor( tmp );
		return Interleave( tmp );
	}

	static byte[] Inflate( byte[] data, int offset, int size, int expected )
	{
		using var ms = new MemoryStream( data, offset, size );
		using var z = new ZLibStream( ms, CompressionMode.Decompress );
		var outBuf = new byte[expected];
		int read = 0;
		while ( read < expected )
		{
			int n = z.Read( outBuf, read, expected - read );
			if ( n == 0 ) break;
			read += n;
		}
		return outBuf;
	}

	static byte[] RleDecode( byte[] data, int offset, int size, int expected )
	{
		var outBuf = new byte[expected];
		int o = 0;
		int i = offset;
		int end = offset + size;

		while ( i < end && o < expected )
		{
			sbyte count = (sbyte)data[i++];
			if ( count < 0 )
			{
				int n = -count;
				while ( n-- > 0 && i < end && o < expected )
					outBuf[o++] = data[i++];
			}
			else
			{
				int n = count + 1;
				byte v = data[i++];
				while ( n-- > 0 && o < expected )
					outBuf[o++] = v;
			}
		}
		return outBuf;
	}

	// EXR delta predictor: each byte is reconstructed from the running difference.
	static void Predictor( byte[] b )
	{
		for ( int i = 1; i < b.Length; i++ )
		{
			int d = b[i - 1] + b[i] - 128;
			b[i] = (byte)d;
		}
	}

	// EXR de-interleave: data is split into two halves that must be zippered back together.
	static byte[] Interleave( byte[] src )
	{
		int len = src.Length;
		var outB = new byte[len];
		int t1 = 0;
		int t2 = (len + 1) / 2;
		int s = 0;

		while ( true )
		{
			if ( s < len ) outB[s++] = src[t1++]; else break;
			if ( s < len ) outB[s++] = src[t2++]; else break;
		}
		return outB;
	}

	static int ReadInt32( byte[] b, ref int pos )
	{
		int v = b[pos] | (b[pos + 1] << 8) | (b[pos + 2] << 16) | (b[pos + 3] << 24);
		pos += 4;
		return v;
	}

	static ulong ReadUInt64( byte[] b, ref int pos )
	{
		ulong v = 0;
		for ( int i = 0; i < 8; i++ )
			v |= (ulong)b[pos + i] << (i * 8);
		pos += 8;
		return v;
	}

	static string ReadNullString( byte[] b, ref int pos )
	{
		int start = pos;
		while ( pos < b.Length && b[pos] != 0 ) pos++;
		string s = Encoding.ASCII.GetString( b, start, pos - start );
		pos++; // skip null
		return s;
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 4,
    "Files": [
        {
            "Ident": "brax.terrainconvert",
            "Path": "Editor/HeightmapConverter.cs",
            "FileName": "HeightmapConverter.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 301868,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Linq;\nusing Sandbox;\n\nnamespace Editor.TerrainConvert;\n\n/// <summary>\n/// Which source channel to read the height value from.\n/// </summary>\npublic enum HeightChannel\n{\n\t/// <summary> Red channel only. Standard for grayscale heightmaps. </summary>\n\tRed,\n\tGreen,\n\tBlue,\n\tAlpha,\n\t/// <summary> Rec.709 weighted luminance of RGB. </summary>\n\tLuminance,\n\t/// <summary> Average of the RGB channels. </summary>\n\tAverage,\n\t/// <summary> The largest of the RGB channels. </summary>\n\tMax,\n}\n\n/// <summary>\n/// Bit depth of the output .raw file. s&box terrain stores heights as 16-bit,\n/// so 16-bit is recommended for the best precision.\n/// </summary>\npublic enum HeightBitDepth\n{\n\t/// <summary> 16-bit per height sample (recommended). 2 bytes per pixel. </summary>\n\tBit16,\n\t/// <summary> 8-bit per height sample. 1 byte per pixel. </summary>\n\tBit8,\n}\n\n/// <summary>\n/// How the source values are remapped into the 0..1 height range before quantizing.\n/// </summary>\npublic enum HeightNormalize\n{\n\t/// <summary> Use values as-is, clamped to 0..1. </summary>\n\tClamp,\n\t/// <summary> Stretch the min..max range of the image to fill 0..1 (good for HDR/EXR). </summary>\n\tMinMaxStretch,\n}\n\n/// <summary>\n/// Byte order of multi-byte (16-bit) samples in the output file.\n/// </summary>\npublic enum HeightByteOrder\n{\n\t/// <summary> Little-endian. What the s&box terrain importer reads by default (\"Windows\"). </summary>\n\tLittleEndian,\n\t/// <summary> Big-endian (\"Mac\"). </summary>\n\tBigEndian,\n}\n\n/// <summary>\n/// Settings for converting an image into a terrain heightmap .raw file.\n/// </summary>\npublic class HeightmapConvertSettings\n{\n\t/// <summary>\n\t/// Which channel of the source image holds the height. Most heightmaps are\n\t/// grayscale, where every channel is identical - <see cref=\"HeightChannel.Red\"/> works for those.\n\t/// </summary>\n\t[Property]\n\tpublic HeightChannel Channel { get; set; } = HeightChannel.Red;\n\n\t/// <summary>\n\t/// 16-bit gives the smoothest terrain and matches what s&box stores internally.\n\t/// </summary>\n\t[Property]\n\tpublic HeightBitDepth BitDepth { get; set; } = HeightBitDepth.Bit16;\n\n\t/// <summary>\n\t/// Output heightmaps are always square. If 0, the smaller of the source\n\t/// dimensions is used. The image is resampled to this resolution.\n\t/// </summary>\n\t[Property, Range( 0, 8192 )]\n\tpublic int Resolution { get; set; } = 0;\n\n\t/// <summary>\n\t/// Round the output resolution down to the nearest power of two (e.g 1024, 2048).\n\t/// The terrain system expects power-of-two heightmaps, so leave this on.\n\t/// </summary>\n\t[Property]\n\tpublic bool PowerOfTwo { get; set; } = true;\n\n\t/// <summary>\n\t/// How source values are mapped to the height range. Use <see cref=\"HeightNormalize.MinMaxStretch\"/>\n\t/// for HDR images that don't already fill 0..1.\n\t/// </summary>\n\t[Property]\n\tpublic HeightNormalize Normalize { get; set; } = HeightNormalize.Clamp;\n\n\t/// <summary>\n\t/// Flip the image vertically. Image and terrain coordinate origins often differ;\n\t/// toggle this if your terrain comes out mirrored north/south.\n\t/// </summary>\n\t[Property]\n\tpublic bool FlipVertical { get; set; } = false;\n\n\t/// <summary>\n\t/// Invert the heights (high becomes low). Useful for depth/inverted maps.\n\t/// </summary>\n\t[Property]\n\tpublic bool Invert { get; set; } = false;\n\n\t/// <summary>\n\t/// Byte order of 16-bit samples. The s&box importer reads little-endian by default.\n\t/// </summary>\n\t[Property, ShowIf( nameof( BitDepth ), HeightBitDepth.Bit16 )]\n\tpublic HeightByteOrder ByteOrder { get; set; } = HeightByteOrder.LittleEndian;\n}\n\n/// <summary>\n/// Converts loaded images into raw single-channel heightmaps compatible with the\n/// s&box terrain importer (square, 8 or 16 bit, raw samples with no header).\n/// </summary>\npublic static class HeightmapConverter\n{\n\t/// <summary>\n\t/// Convert a bitmap into a raw heightmap byte buffer.\n\t/// </summary>\n\t/// <param name=\"bitmap\">Source image.</param>\n\t/// <param name=\"settings\">Conversion settings.</param>\n\t/// <param name=\"resolution\">The square resolution of the produced heightmap.</param>\n\t/// <returns>Raw heightmap bytes ready to write to a .raw file.</returns>\n\tpublic static byte[] Convert( Bitmap bitmap, HeightmapConvertSettings settings, out int resolution )\n\t{\n\t\tArgumentNullException.ThrowIfNull( bitmap );\n\t\tArgumentNullException.ThrowIfNull( settings );\n\n\t\tvar pixels = bitmap.GetPixels();\n\t\tint count = bitmap.Width * bitmap.Height;\n\n\t\t// Extract the chosen channel as a float per pixel, then run the shared pipeline.\n\t\tvar values = new float[count];\n\t\tfor ( int i = 0; i < count; i++ )\n\t\t\tvalues[i] = SampleChannel( pixels[i], settings.Channel );\n\n\t\treturn BuildRaw( values, bitmap.Width, bitmap.Height, settings, out resolution );\n\t}\n\n\t/// <summary>\n\t/// Convert a decoded EXR into a raw heightmap byte buffer, reading height from the\n\t/// chosen channel (falling back to Y/R/first channel if the exact one is absent).\n\t/// </summary>\n\tpublic static byte[] Convert( ExrImage exr, HeightmapConvertSettings settings, out int resolution )\n\t{\n\t\tArgumentNullException.ThrowIfNull( exr );\n\t\tArgumentNullException.ThrowIfNull( settings );\n\n\t\tvar values = SampleChannel( exr, settings.Channel );\n\t\treturn BuildRaw( values, exr.Width, exr.Height, settings, out resolution );\n\t}\n\n\t/// <summary>\n\t/// Shared pipeline: resample a single-channel float plane to a square power-of-two,\n\t/// normalize into 0..1, optionally invert, and quantize to raw bytes.\n\t/// </summary>\n\tstatic byte[] BuildRaw( float[] values, int srcWidth, int srcHeight, HeightmapConvertSettings settings, out int resolution )\n\t{\n\t\tresolution = ResolveResolution( srcWidth, srcHeight, settings );\n\n\t\t// Bilinear resample to a square of the target resolution, working entirely in float\n\t\t// so we don't lose precision on high bit-depth / HDR sources. Resampling produces a\n\t\t// fresh array; otherwise clone so the in-place normalize/invert never mutates a\n\t\t// caller-owned buffer (e.g. an EXR's cached channel plane).\n\t\tvalues = srcWidth != resolution || srcHeight != resolution\n\t\t\t? ResampleBilinear( values, srcWidth, srcHeight, resolution, resolution )\n\t\t\t: (float[])values.Clone();\n\n\t\tApplyNormalize( values, settings.Normalize );\n\n\t\tif ( settings.Invert )\n\t\t{\n\t\t\tfor ( int i = 0; i < values.Length; i++ )\n\t\t\t\tvalues[i] = 1f - values[i];\n\t\t}\n\n\t\treturn Quantize( values, resolution, settings );\n\t}\n\n\tstatic float[] ResampleBilinear( float[] src, int srcW, int srcH, int dstW, int dstH )\n\t{\n\t\tvar dst = new float[dstW * dstH];\n\n\t\tfor ( int y = 0; y < dstH; y++ )\n\t\t{\n\t\t\tfloat fy = dstH > 1 ? (float)y / (dstH - 1) * (srcH - 1) : 0f;\n\t\t\tint y0 = (int)fy;\n\t\t\tint y1 = Math.Min( y0 + 1, srcH - 1 );\n\t\t\tfloat ty = fy - y0;\n\n\t\t\tfor ( int x = 0; x < dstW; x++ )\n\t\t\t{\n\t\t\t\tfloat fx = dstW > 1 ? (float)x / (dstW - 1) * (srcW - 1) : 0f;\n\t\t\t\tint x0 = (int)fx;\n\t\t\t\tint x1 = Math.Min( x0 + 1, srcW - 1 );\n\t\t\t\tfloat tx = fx - x0;\n\n\t\t\t\tfloat top = MathX.Lerp( src[y0 * srcW + x0], src[y0 * srcW + x1], tx );\n\t\t\t\tfloat bottom = MathX.Lerp( src[y1 * srcW + x0], src[y1 * srcW + x1], tx );\n\t\t\t\tdst[y * dstW + x] = MathX.Lerp( top, bottom, ty );\n\t\t\t}\n\t\t}\n\n\t\treturn dst;\n\t}\n\n\tstatic float[] SampleChannel( ExrImage exr, HeightChannel channel )\n\t{\n\t\t// For multi-channel EXRs, blend RGB the same way the bitmap path does. For the\n\t\t// common single-channel (Y) heightmap, every option resolves to that one plane.\n\t\tswitch ( channel )\n\t\t{\n\t\t\tcase HeightChannel.Red: return exr.GetHeightChannel( \"R\" );\n\t\t\tcase HeightChannel.Green: return exr.GetHeightChannel( \"G\" );\n\t\t\tcase HeightChannel.Blue: return exr.GetHeightChannel( \"B\" );\n\t\t\tcase HeightChannel.Alpha: return exr.GetHeightChannel( \"A\" );\n\t\t}\n\n\t\tvar r = exr.GetChannel( \"R\" );\n\t\tvar g = exr.GetChannel( \"G\" );\n\t\tvar b = exr.GetChannel( \"B\" );\n\n\t\t// No RGB set - it's a luminance/single-channel image, use it directly.\n\t\tif ( r is null || g is null || b is null )\n\t\t\treturn exr.GetHeightChannel();\n\n\t\tvar outv = new float[r.Length];\n\t\tfor ( int i = 0; i < outv.Length; i++ )\n\t\t{\n\t\t\toutv[i] = channel switch\n\t\t\t{\n\t\t\t\tHeightChannel.Luminance => 0.2126f * r[i] + 0.7152f * g[i] + 0.0722f * b[i],\n\t\t\t\tHeightChannel.Average => (r[i] + g[i] + b[i]) / 3f,\n\t\t\t\tHeightChannel.Max => Math.Max( r[i], Math.Max( g[i], b[i] ) ),\n\t\t\t\t_ => r[i],\n\t\t\t};\n\t\t}\n\t\treturn outv;\n\t}\n\n\tstatic int ResolveResolution( int width, int height, HeightmapConvertSettings settings )\n\t{\n\t\tint res = settings.Resolution > 0 ? settings.Resolution : Math.Min( width, height );\n\n\t\tif ( settings.PowerOfTwo )\n\t\t\tres = RoundDownToPowerOfTwo( res );\n\n\t\treturn Math.Clamp( res, 4, 16384 );\n\t}\n\n\tstatic float SampleChannel( Color c, HeightChannel channel ) => channel switch\n\t{\n\t\tHeightChannel.Red => c.r,\n\t\tHeightChannel.Green => c.g,\n\t\tHeightChannel.Blue => c.b,\n\t\tHeightChannel.Alpha => c.a,\n\t\tHeightChannel.Luminance => 0.2126f * c.r + 0.7152f * c.g + 0.0722f * c.b,\n\t\tHeightChannel.Average => (c.r + c.g + c.b) / 3f,\n\t\tHeightChannel.Max => Math.Max( c.r, Math.Max( c.g, c.b ) ),\n\t\t_ => c.r,\n\t};\n\n\tstatic void ApplyNormalize( float[] values, HeightNormalize mode )\n\t{\n\t\tif ( mode == HeightNormalize.MinMaxStretch )\n\t\t{\n\t\t\tfloat min = values.Min();\n\t\t\tfloat max = values.Max();\n\t\t\tfloat range = max - min;\n\n\t\t\tif ( range > 1e-6f )\n\t\t\t{\n\t\t\t\tfor ( int i = 0; i < values.Length; i++ )\n\t\t\t\t\tvalues[i] = (values[i] - min) / range;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Flat image - fall through to clamp.\n\t\t}\n\n\t\tfor ( int i = 0; i < values.Length; i++ )\n\t\t\tvalues[i] = Math.Clamp( values[i], 0f, 1f );\n\t}\n\n\tstatic byte[] Quantize( float[] values, int resolution, HeightmapConvertSettings settings )\n\t{\n\t\tbool flip = settings.FlipVertical;\n\n\t\tif ( settings.BitDepth == HeightBitDepth.Bit8 )\n\t\t{\n\t\t\tvar bytes = new byte[resolution * resolution];\n\t\t\tfor ( int y = 0; y < resolution; y++ )\n\t\t\t{\n\t\t\t\tint srcY = flip ? resolution - 1 - y : y;\n\t\t\t\tfor ( int x = 0; x < resolution; x++ )\n\t\t\t\t{\n\t\t\t\t\tfloat v = Math.Clamp( values[srcY * resolution + x], 0f, 1f );\n\t\t\t\t\tbytes[y * resolution + x] = (byte)MathF.Round( v * byte.MaxValue );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn bytes;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tbool little = settings.ByteOrder == HeightByteOrder.LittleEndian;\n\t\t\tvar bytes = new byte[resolution * resolution * 2];\n\t\t\tfor ( int y = 0; y < resolution; y++ )\n\t\t\t{\n\t\t\t\tint srcY = flip ? resolution - 1 - y : y;\n\t\t\t\tfor ( int x = 0; x < resolution; x++ )\n\t\t\t\t{\n\t\t\t\t\tfloat v = Math.Clamp( values[srcY * resolution + x], 0f, 1f );\n\t\t\t\t\tushort h = (ushort)MathF.Round( v * ushort.MaxValue );\n\n\t\t\t\t\tint o = (y * resolution + x) * 2;\n\t\t\t\t\tif ( little )\n\t\t\t\t\t{\n\t\t\t\t\t\tbytes[o] = (byte)(h & 0xFF);\n\t\t\t\t\t\tbytes[o + 1] = (byte)(h >> 8);\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tbytes[o] = (byte)(h >> 8);\n\t\t\t\t\t\tbytes[o + 1] = (byte)(h & 0xFF);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn bytes;\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Rounds a value down to the nearest power of two.\n\t/// </summary>\n\tpublic static int RoundDownToPowerOfTwo( int value )\n\t{\n\t\tif ( value < 1 ) return 1;\n\t\tvalue |= value >> 1;\n\t\tvalue |= value >> 2;\n\t\tvalue |= value >> 4;\n\t\tvalue |= value >> 8;\n\t\tvalue |= value >> 16;\n\t\treturn value - (value >> 1);\n\t}\n}\n"
        },
        {
            "Ident": "brax.terrainconvert",
            "Path": "Editor/HeightmapConvertWindow.cs",
            "FileName": "HeightmapConvertWindow.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 301868,
            "IsPrivate": false,
            "Code": "using System;\nusing System.IO;\nusing Sandbox;\n\nnamespace Editor.TerrainConvert;\n\n/// <summary>\n/// Editor tool that converts an image (.png, .jpg, .tga, .tif, .psd, .exr ...) into a\n/// raw single-channel heightmap (.raw) that the s&box terrain importer can load.\n/// </summary>\n[EditorApp( \"Heightmap Converter\", \"terrain\", \"Convert an image into a terrain heightmap (.raw)\" )]\npublic class HeightmapConvertWindow : Widget\n{\n\tHeightmapConvertSettings Settings { get; set; } = new();\n\n\tstring inputFile;\n\tBitmap loadedBitmap;\n\tExrImage loadedExr;\n\tint srcWidth, srcHeight;\n\n\tbool HasImage => loadedBitmap is not null || loadedExr is not null;\n\n\tLabel inputLabel;\n\tLabel outputInfoLabel;\n\tButton convertButton;\n\n\tpublic HeightmapConvertWindow() : this( null ) { }\n\n\tpublic HeightmapConvertWindow( Widget parent ) : base( parent )\n\t{\n\t\tWindowFlags = WindowFlags.Dialog | WindowFlags.Customized | WindowFlags.WindowTitle | WindowFlags.CloseButton | WindowFlags.WindowSystemMenuHint;\n\t\tDeleteOnClose = true;\n\t\tWindowTitle = \"Convert Image to Heightmap\";\n\t\tSetWindowIcon( \"terrain\" );\n\n\t\tLayout = Layout.Column();\n\t\tLayout.Spacing = 8;\n\t\tLayout.Margin = 16;\n\n\t\tvar warning = new WarningBox(\n\t\t\t\"Converts an image into a raw heightmap (.raw) for the terrain system.\\n\" +\n\t\t\t\"Output is square, single-channel, 8 or 16 bit. Import it via the Terrain component's heightmap import.\", this );\n\t\tLayout.Add( warning );\n\n\t\t// Input file row\n\t\t{\n\t\t\tvar row = Layout.Row();\n\t\t\trow.Spacing = 8;\n\n\t\t\tinputLabel = new Label( \"No image selected\", this );\n\t\t\tinputLabel.WordWrap = false;\n\t\t\trow.Add( inputLabel, 1 );\n\n\t\t\tvar browse = new Button( \"Browse Image...\", \"image\", this );\n\t\t\tbrowse.Clicked = PickInputFile;\n\t\t\trow.Add( browse );\n\n\t\t\tLayout.Add( row );\n\t\t}\n\n\t\t// Settings sheet\n\t\t{\n\t\t\tvar so = EditorUtility.GetSerializedObject( Settings );\n\t\t\tso.OnPropertyChanged += _ => UpdateInfo();\n\n\t\t\tvar sheet = new ControlSheet();\n\t\t\tsheet.AddObject( so );\n\t\t\tLayout.Add( sheet );\n\t\t}\n\n\t\toutputInfoLabel = new Label( \"\", this );\n\t\toutputInfoLabel.Color = Theme.TextControl.WithAlpha( 0.6f );\n\t\tLayout.Add( outputInfoLabel );\n\n\t\tLayout.AddStretchCell();\n\n\t\t// Bottom bar\n\t\t{\n\t\t\tvar row = Layout.Row();\n\t\t\trow.Margin = new Sandbox.UI.Margin( 0, 8, 0, 0 );\n\t\t\trow.AddStretchCell();\n\n\t\t\tconvertButton = new Button.Primary( \"Convert & Save...\", \"file_download\", this );\n\t\t\tconvertButton.Clicked = ConvertAndSave;\n\t\t\tconvertButton.Enabled = false;\n\t\t\trow.Add( convertButton );\n\n\t\t\tLayout.Add( row );\n\t\t}\n\n\t\tWidth = 440;\n\t\tMinimumWidth = 380;\n\t\tHeight = 520;\n\n\t\tUpdateInfo();\n\n\t\tShow();\n\t\tFocus();\n\t}\n\n\tvoid PickInputFile()\n\t{\n\t\tvar fd = new FileDialog( null )\n\t\t{\n\t\t\tTitle = \"Select Source Image\",\n\t\t};\n\t\tfd.SetFindFile();\n\t\tfd.SetModeOpen();\n\t\tfd.SetNameFilter( \"Images (*.png *.jpg *.jpeg *.tga *.tif *.tiff *.psd *.exr *.bmp *.raw)\" );\n\n\t\tif ( !fd.Execute() )\n\t\t\treturn;\n\n\t\tLoadInput( fd.SelectedFile );\n\t}\n\n\tvoid LoadInput( string path )\n\t{\n\t\tloadedBitmap?.Dispose();\n\t\tloadedBitmap = null;\n\t\tloadedExr = null;\n\t\tinputFile = null;\n\n\t\ttry\n\t\t{\n\t\t\tvar bytes = File.ReadAllBytes( path );\n\n\t\t\t// EXR isn't supported by the Skia-backed Bitmap loader, so decode it ourselves.\n\t\t\tbool isExr = path.EndsWith( \".exr\", StringComparison.OrdinalIgnoreCase ) || ExrImage.IsExr( bytes );\n\t\t\tif ( isExr )\n\t\t\t{\n\t\t\t\tloadedExr = ExrImage.Load( bytes );\n\t\t\t\tsrcWidth = loadedExr.Width;\n\t\t\t\tsrcHeight = loadedExr.Height;\n\t\t\t\tinputFile = path;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar bitmap = Bitmap.CreateFromBytes( bytes );\n\t\t\t\tif ( bitmap is null || !bitmap.IsValid )\n\t\t\t\t{\n\t\t\t\t\tEditorUtility.DisplayDialog( \"Couldn't load image\",\n\t\t\t\t\t\t$\"Failed to decode '{Path.GetFileName( path )}'.\\nThis image format may not be supported.\" );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tloadedBitmap = bitmap;\n\t\t\t\tsrcWidth = bitmap.Width;\n\t\t\t\tsrcHeight = bitmap.Height;\n\t\t\t\tinputFile = path;\n\t\t\t}\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tEditorUtility.DisplayDialog( \"Couldn't load image\", e.Message );\n\t\t\treturn;\n\t\t}\n\n\t\tUpdateInfo();\n\t}\n\n\tvoid UpdateInfo()\n\t{\n\t\tif ( !HasImage )\n\t\t{\n\t\t\tinputLabel.Text = \"No image selected\";\n\t\t\toutputInfoLabel.Text = \"Pick an image to begin.\";\n\t\t\tconvertButton.Enabled = false;\n\t\t\treturn;\n\t\t}\n\n\t\tstring kind = loadedExr is not null ? \"EXR\" : \"image\";\n\t\tinputLabel.Text = $\"{Path.GetFileName( inputFile )}  ({srcWidth}x{srcHeight}, {kind})\";\n\t\tconvertButton.Enabled = true;\n\n\t\tint res = PreviewResolution();\n\t\tstring depth = Settings.BitDepth == HeightBitDepth.Bit16 ? \"16-bit\" : \"8-bit\";\n\t\tint bytes = res * res * (Settings.BitDepth == HeightBitDepth.Bit16 ? 2 : 1);\n\t\toutputInfoLabel.Text = $\"Output: {res}x{res}, {depth} \u2192 {bytes / 1024:n0} KB raw\";\n\t}\n\n\tint PreviewResolution()\n\t{\n\t\tint res = Settings.Resolution > 0 ? Settings.Resolution : Math.Min( srcWidth, srcHeight );\n\t\tif ( Settings.PowerOfTwo )\n\t\t\tres = HeightmapConverter.RoundDownToPowerOfTwo( res );\n\t\treturn Math.Clamp( res, 4, 16384 );\n\t}\n\n\tvoid ConvertAndSave()\n\t{\n\t\tif ( !HasImage )\n\t\t\treturn;\n\n\t\tvar fd = new FileDialog( null )\n\t\t{\n\t\t\tTitle = \"Save Heightmap\",\n\t\t\tDefaultSuffix = \".raw\",\n\t\t};\n\t\tfd.Directory = Path.GetDirectoryName( inputFile );\n\t\tfd.SelectFile( $\"{Path.GetFileNameWithoutExtension( inputFile )}.raw\" );\n\t\tfd.SetFindFile();\n\t\tfd.SetModeSave();\n\t\tfd.SetNameFilter( \"Raw Heightmap (*.raw *.r16 *.r8)\" );\n\n\t\tif ( !fd.Execute() )\n\t\t\treturn;\n\n\t\ttry\n\t\t{\n\t\t\tint resolution;\n\t\t\tvar data = loadedExr is not null\n\t\t\t\t? HeightmapConverter.Convert( loadedExr, Settings, out resolution )\n\t\t\t\t: HeightmapConverter.Convert( loadedBitmap, Settings, out resolution );\n\t\t\tFile.WriteAllBytes( fd.SelectedFile, data );\n\n\t\t\tEditorUtility.DisplayDialog( \"Heightmap saved\",\n\t\t\t\t$\"Wrote {resolution}x{resolution} heightmap to:\\n{fd.SelectedFile}\\n\\n\" +\n\t\t\t\t\"Import it from a Terrain component: open its heightmap import and choose this .raw file.\" );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tEditorUtility.DisplayDialog( \"Conversion failed\", e.Message );\n\t\t}\n\t}\n\n\tpublic override void OnDestroyed()\n\t{\n\t\tbase.OnDestroyed();\n\t\tloadedBitmap?.Dispose();\n\t\tloadedBitmap = null;\n\t\tloadedExr = null;\n\t}\n}\n"
        },
        {
            "Ident": "brax.terrainconvert",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301868,
            "IsPrivate": false,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"terrainconvert\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"terrainconvert\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"brax\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"brax.terrainconvert\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"26\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-06-26T23:15:58.0786236Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.205.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.205.0\")]"
        },
        {
            "Ident": "brax.terrainconvert",
            "Path": "Editor/ExrImage.cs",
            "FileName": "ExrImage.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 301868,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Text;\n\nnamespace Editor.TerrainConvert;\n\n/// <summary>\n/// Minimal OpenEXR reader for scanline images. Source 2's own EXR loader is internal to\n/// the engine and can't be called from library code, so this decodes the float data directly.\n///\n/// Supports: scanline (non-tiled) images, HALF/FLOAT/UINT channels, and NONE/ZIP/ZIPS/RLE\n/// compression - which covers the vast majority of heightmap exports. Tiled, deep, and the\n/// lossy/wavelet codecs (PIZ, PXR24, B44, DWAA/DWAB) are not supported and throw a clear error.\n/// </summary>\npublic sealed class ExrImage\n{\n\tpublic int Width { get; private set; }\n\tpublic int Height { get; private set; }\n\n\t/// <summary> Channel name -> linear float plane (row-major, Width*Height). </summary>\n\treadonly Dictionary<string, float[]> channels = new( StringComparer.Ordinal );\n\n\tpublic IReadOnlyCollection<string> ChannelNames => channels.Keys;\n\tpublic bool HasChannel( string name ) => channels.ContainsKey( name );\n\n\tenum PixelType { Uint = 0, Half = 1, Float = 2 }\n\n\tenum Compression { None = 0, Rle = 1, Zips = 2, Zip = 3, Piz = 4, Pxr24 = 5, B44 = 6, B44A = 7, Dwaa = 8, Dwab = 9 }\n\n\treadonly record struct ChannelInfo( string Name, PixelType Type )\n\t{\n\t\tpublic int SampleSize => Type == PixelType.Half ? 2 : 4;\n\t}\n\n\t/// <summary>\n\t/// Returns true if the bytes start with the OpenEXR magic number.\n\t/// </summary>\n\tpublic static bool IsExr( byte[] data )\n\t\t=> data is { Length: >= 4 } && data[0] == 0x76 && data[1] == 0x2f && data[2] == 0x31 && data[3] == 0x01;\n\n\t/// <summary>\n\t/// Decode an EXR from a byte buffer.\n\t/// </summary>\n\tpublic static ExrImage Load( byte[] data )\n\t{\n\t\tif ( !IsExr( data ) )\n\t\t\tthrow new InvalidDataException( \"Not an OpenEXR file (bad magic number).\" );\n\n\t\tvar img = new ExrImage();\n\t\timg.Parse( data );\n\t\treturn img;\n\t}\n\n\t/// <summary>\n\t/// Get a channel as a float plane, or null if absent. Common heightmap names are \"Y\"\n\t/// (luminance), \"R\", or a plain unnamed channel.\n\t/// </summary>\n\tpublic float[] GetChannel( string name ) => channels.TryGetValue( name, out var v ) ? v : null;\n\n\t/// <summary>\n\t/// Pick the most sensible single channel to treat as height: explicit preference first,\n\t/// then Y / R / the first available channel.\n\t/// </summary>\n\tpublic float[] GetHeightChannel( string preferred = null )\n\t{\n\t\tif ( preferred is not null && channels.TryGetValue( preferred, out var p ) )\n\t\t\treturn p;\n\n\t\tforeach ( var name in new[] { \"Y\", \"R\", \"G\", \"B\" } )\n\t\t\tif ( channels.TryGetValue( name, out var v ) )\n\t\t\t\treturn v;\n\n\t\tforeach ( var v in channels.Values )\n\t\t\treturn v;\n\n\t\treturn null;\n\t}\n\n\tvoid Parse( byte[] data )\n\t{\n\t\tint pos = 4; // skip magic\n\n\t\tint version = ReadInt32( data, ref pos );\n\t\tbool tiled = (version & 0x200) != 0;\n\t\tbool deep = (version & 0x800) != 0;\n\t\tbool multipart = (version & 0x1000) != 0;\n\n\t\tif ( tiled ) throw new NotSupportedException( \"Tiled EXR images are not supported - re-export as a scanline image.\" );\n\t\tif ( deep ) throw new NotSupportedException( \"Deep EXR images are not supported.\" );\n\t\tif ( multipart ) throw new NotSupportedException( \"Multi-part EXR images are not supported.\" );\n\n\t\tvar chans = new List<ChannelInfo>();\n\t\tvar compression = Compression.None;\n\t\tint xMin = 0, yMin = 0, xMax = 0, yMax = 0;\n\n\t\t// Header attributes, terminated by an empty name.\n\t\twhile ( true )\n\t\t{\n\t\t\tstring name = ReadNullString( data, ref pos );\n\t\t\tif ( name.Length == 0 ) break;\n\n\t\t\tstring type = ReadNullString( data, ref pos );\n\t\t\tint size = ReadInt32( data, ref pos );\n\t\t\tint valueStart = pos;\n\n\t\t\tswitch ( name )\n\t\t\t{\n\t\t\t\tcase \"channels\":\n\t\t\t\t\tParseChannels( data, valueStart, chans );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"compression\":\n\t\t\t\t\tcompression = (Compression)data[valueStart];\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"dataWindow\":\n\t\t\t\t\tint p = valueStart;\n\t\t\t\t\txMin = ReadInt32( data, ref p );\n\t\t\t\t\tyMin = ReadInt32( data, ref p );\n\t\t\t\t\txMax = ReadInt32( data, ref p );\n\t\t\t\t\tyMax = ReadInt32( data, ref p );\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tpos = valueStart + size; // skip to next attribute\n\t\t}\n\n\t\tif ( chans.Count == 0 )\n\t\t\tthrow new InvalidDataException( \"EXR has no channels.\" );\n\n\t\tWidth = xMax - xMin + 1;\n\t\tHeight = yMax - yMin + 1;\n\n\t\tif ( Width <= 0 || Height <= 0 )\n\t\t\tthrow new InvalidDataException( $\"EXR has invalid data window ({Width}x{Height}).\" );\n\n\t\tint linesPerBlock = compression switch\n\t\t{\n\t\t\tCompression.None or Compression.Rle or Compression.Zips => 1,\n\t\t\tCompression.Zip or Compression.Pxr24 => 16,\n\t\t\tCompression.Piz or Compression.B44 or Compression.B44A or Compression.Dwaa => 32,\n\t\t\tCompression.Dwab => 256,\n\t\t\t_ => 1,\n\t\t};\n\n\t\tif ( compression is Compression.Piz or Compression.Pxr24 or Compression.B44 or Compression.B44A or Compression.Dwaa or Compression.Dwab )\n\t\t\tthrow new NotSupportedException( $\"EXR compression '{compression}' is not supported. Re-export the heightmap as Uncompressed, ZIP, or RLE.\" );\n\n\t\t// Allocate channel planes.\n\t\tforeach ( var c in chans )\n\t\t\tchannels[c.Name] = new float[Width * Height];\n\n\t\t// Scanline offset table: one ulong per block.\n\t\tint blockCount = (Height + linesPerBlock - 1) / linesPerBlock;\n\t\tvar offsets = new long[blockCount];\n\t\tfor ( int i = 0; i < blockCount; i++ )\n\t\t\toffsets[i] = (long)ReadUInt64( data, ref pos );\n\n\t\tint rowBytes = 0;\n\t\tforeach ( var c in chans )\n\t\t\trowBytes += Width * c.SampleSize;\n\n\t\t// Each block: int32 yStart, int32 dataSize, then (compressed) pixel data.\n\t\tforeach ( var off in offsets )\n\t\t{\n\t\t\tint bp = (int)off;\n\t\t\tint yStart = ReadInt32( data, ref bp );\n\t\t\tint dataSize = ReadInt32( data, ref bp );\n\n\t\t\tint lines = Math.Min( linesPerBlock, yMax - yStart + 1 );\n\t\t\tint uncompressedSize = rowBytes * lines;\n\n\t\t\tbyte[] block;\n\t\t\tif ( compression == Compression.None || dataSize >= uncompressedSize )\n\t\t\t{\n\t\t\t\t// Stored uncompressed (NONE, or a block that didn't compress smaller).\n\t\t\t\tblock = new byte[uncompressedSize];\n\t\t\t\tArray.Copy( data, bp, block, 0, uncompressedSize );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tblock = Decompress( compression, data, bp, dataSize, uncompressedSize );\n\t\t\t}\n\n\t\t\tScatterBlock( block, chans, rowBytes, Width, yStart - yMin, lines );\n\t\t}\n\t}\n\n\tvoid ScatterBlock( byte[] block, List<ChannelInfo> chans, int rowBytes, int width, int rowOffset, int lines )\n\t{\n\t\tfor ( int i = 0; i < lines; i++ )\n\t\t{\n\t\t\tint rowBase = i * rowBytes;\n\t\t\tint channelOffset = 0;\n\t\t\tint destRow = rowOffset + i;\n\n\t\t\tforeach ( var c in chans )\n\t\t\t{\n\t\t\t\tint src = rowBase + channelOffset;\n\t\t\t\tvar plane = channels[c.Name];\n\t\t\t\tint destBase = destRow * width;\n\n\t\t\t\tfor ( int x = 0; x < width; x++ )\n\t\t\t\t{\n\t\t\t\t\tplane[destBase + x] = ReadSample( block, src, c.Type );\n\t\t\t\t\tsrc += c.SampleSize;\n\t\t\t\t}\n\n\t\t\t\tchannelOffset += width * c.SampleSize;\n\t\t\t}\n\t\t}\n\t}\n\n\tstatic float ReadSample( byte[] b, int offset, PixelType type ) => type switch\n\t{\n\t\tPixelType.Half => (float)BitConverter.UInt16BitsToHalf( (ushort)(b[offset] | (b[offset + 1] << 8)) ),\n\t\tPixelType.Float => BitConverter.Int32BitsToSingle( b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] << 24) ),\n\t\tPixelType.Uint => (uint)(b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] << 24)),\n\t\t_ => 0f,\n\t};\n\n\tstatic void ParseChannels( byte[] data, int pos, List<ChannelInfo> chans )\n\t{\n\t\twhile ( true )\n\t\t{\n\t\t\tstring name = ReadNullString( data, ref pos );\n\t\t\tif ( name.Length == 0 ) break;\n\n\t\t\tint ptype = ReadInt32( data, ref pos );\n\t\t\tpos += 1;      // pLinear\n\t\t\tpos += 3;      // reserved\n\t\t\tReadInt32( data, ref pos ); // xSampling\n\t\t\tReadInt32( data, ref pos ); // ySampling\n\n\t\t\tchans.Add( new ChannelInfo( name, (PixelType)ptype ) );\n\t\t}\n\t}\n\n\tstatic byte[] Decompress( Compression compression, byte[] data, int offset, int size, int uncompressedSize )\n\t{\n\t\t// Step 1: codec-specific decompression into a temp buffer.\n\t\tbyte[] tmp = compression switch\n\t\t{\n\t\t\tCompression.Zip or Compression.Zips => Inflate( data, offset, size, uncompressedSize ),\n\t\t\tCompression.Rle => RleDecode( data, offset, size, uncompressedSize ),\n\t\t\t_ => throw new NotSupportedException( $\"EXR compression '{compression}' is not supported.\" ),\n\t\t};\n\n\t\t// Step 2: undo EXR's byte predictor + interleave (shared by ZIP and RLE).\n\t\tPredictor( tmp );\n\t\treturn Interleave( tmp );\n\t}\n\n\tstatic byte[] Inflate( byte[] data, int offset, int size, int expected )\n\t{\n\t\tusing var ms = new MemoryStream( data, offset, size );\n\t\tusing var z = new ZLibStream( ms, CompressionMode.Decompress );\n\t\tvar outBuf = new byte[expected];\n\t\tint read = 0;\n\t\twhile ( read < expected )\n\t\t{\n\t\t\tint n = z.Read( outBuf, read, expected - read );\n\t\t\tif ( n == 0 ) break;\n\t\t\tread += n;\n\t\t}\n\t\treturn outBuf;\n\t}\n\n\tstatic byte[] RleDecode( byte[] data, int offset, int size, int expected )\n\t{\n\t\tvar outBuf = new byte[expected];\n\t\tint o = 0;\n\t\tint i = offset;\n\t\tint end = offset + size;\n\n\t\twhile ( i < end && o < expected )\n\t\t{\n\t\t\tsbyte count = (sbyte)data[i++];\n\t\t\tif ( count < 0 )\n\t\t\t{\n\t\t\t\tint n = -count;\n\t\t\t\twhile ( n-- > 0 && i < end && o < expected )\n\t\t\t\t\toutBuf[o++] = data[i++];\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tint n = count + 1;\n\t\t\t\tbyte v = data[i++];\n\t\t\t\twhile ( n-- > 0 && o < expected )\n\t\t\t\t\toutBuf[o++] = v;\n\t\t\t}\n\t\t}\n\t\treturn outBuf;\n\t}\n\n\t// EXR delta predictor: each byte is reconstructed from the running difference.\n\tstatic void Predictor( byte[] b )\n\t{\n\t\tfor ( int i = 1; i < b.Length; i++ )\n\t\t{\n\t\t\tint d = b[i - 1] + b[i] - 128;\n\t\t\tb[i] = (byte)d;\n\t\t}\n\t}\n\n\t// EXR de-interleave: data is split into two halves that must be zippered back together.\n\tstatic byte[] Interleave( byte[] src )\n\t{\n\t\tint len = src.Length;\n\t\tvar outB = new byte[len];\n\t\tint t1 = 0;\n\t\tint t2 = (len + 1) / 2;\n\t\tint s = 0;\n\n\t\twhile ( true )\n\t\t{\n\t\t\tif ( s < len ) outB[s++] = src[t1++]; else break;\n\t\t\tif ( s < len ) outB[s++] = src[t2++]; else break;\n\t\t}\n\t\treturn outB;\n\t}\n\n\tstatic int ReadInt32( byte[] b, ref int pos )\n\t{\n\t\tint v = b[pos] | (b[pos + 1] << 8) | (b[pos + 2] << 16) | (b[pos + 3] << 24);\n\t\tpos += 4;\n\t\treturn v;\n\t}\n\n\tstatic ulong ReadUInt64( byte[] b, ref int pos )\n\t{\n\t\tulong v = 0;\n\t\tfor ( int i = 0; i < 8; i++ )\n\t\t\tv |= (ulong)b[pos + i] << (i * 8);\n\t\tpos += 8;\n\t\treturn v;\n\t}\n\n\tstatic string ReadNullString( byte[] b, ref int pos )\n\t{\n\t\tint start = pos;\n\t\twhile ( pos < b.Length && b[pos] != 0 ) pos++;\n\t\tstring s = Encoding.ASCII.GetString( b, start, pos - start );\n\t\tpos++; // skip null\n\t\treturn s;\n\t}\n}\n"
        }
    ]
}