mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-06 13:28:32 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
654 lines
12 KiB
C#
654 lines
12 KiB
C#
using System.Globalization;
|
|
|
|
namespace Sandbox
|
|
{
|
|
/// <summary>
|
|
/// A lightweight string parser with cursor-based navigation.
|
|
/// Designed for parsing text files, CSS, and other structured text formats.
|
|
/// Uses ref struct to stay stack-allocated for performance.
|
|
/// </summary>
|
|
internal ref struct Parse
|
|
{
|
|
/// <summary>Source file name for error reporting</summary>
|
|
public string FileName;
|
|
|
|
/// <summary>The text being parsed</summary>
|
|
public string Text;
|
|
|
|
/// <summary>Current position in the text</summary>
|
|
public int Pointer;
|
|
int lineOffset;
|
|
|
|
public Parse( string value, string filename = "nofile", int lineOffset = 0 ) : this()
|
|
{
|
|
FileName = filename;
|
|
Text = value;
|
|
|
|
this.lineOffset = lineOffset;
|
|
}
|
|
|
|
|
|
|
|
public int Length => Text.Length;
|
|
public bool IsEnd => Pointer >= Length;
|
|
public char Current => IsEnd ? '\0' : Text[Pointer];
|
|
public char Next => Pointer + 1 >= Length ? '\0' : Text[Pointer + 1];
|
|
public bool IsWhitespace => char.IsWhiteSpace( Current );
|
|
public bool IsNewline => Current == '\n' || Current == '\r';
|
|
public bool IsDigit => char.IsDigit( Current );
|
|
public bool IsLetter => char.IsLetter( Current );
|
|
|
|
public Parse JumpToEndOfLine( bool afterNewline )
|
|
{
|
|
var p = this;
|
|
|
|
while ( !p.IsEnd && !p.IsNewline )
|
|
{
|
|
p.Pointer++;
|
|
}
|
|
|
|
if ( afterNewline )
|
|
{
|
|
while ( !p.IsEnd && p.IsNewline )
|
|
{
|
|
p.Pointer++;
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
public bool IsOneOf( string chars )
|
|
{
|
|
if ( chars == null )
|
|
return false;
|
|
|
|
return chars.IndexOf( Current ) >= 0;
|
|
}
|
|
|
|
public string Read( int chars )
|
|
{
|
|
if ( chars <= 0 ) throw new System.Exception( $"Tried to read {chars} chars" );
|
|
|
|
var result = Text.Substring( Pointer, chars );
|
|
Pointer += chars;
|
|
return result;
|
|
}
|
|
|
|
public string ReadRemaining( bool acceptNone = false )
|
|
{
|
|
if ( IsEnd && acceptNone ) return string.Empty;
|
|
if ( IsEnd ) throw new System.Exception( $"Tried to ReadRemaining but we're at the end" );
|
|
|
|
var result = Text.Substring( Pointer );
|
|
Pointer = Length;
|
|
return result;
|
|
}
|
|
|
|
public Parse SkipWhitespaceAndNewlines( string andCharacters = null )
|
|
{
|
|
while ( !IsEnd )
|
|
{
|
|
if ( !IsWhitespace && !IsNewline && !IsOneOf( andCharacters ) )
|
|
return this;
|
|
|
|
Pointer++;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
public string ReadUntilWhitespaceOrNewlineOrEnd( string andCharacters = null )
|
|
{
|
|
var p = this;
|
|
|
|
while ( true )
|
|
{
|
|
if ( p.IsEnd || p.IsNewline || p.IsWhitespace || IsOneOf( andCharacters ) )
|
|
return this.Read( p.Pointer - Pointer );
|
|
|
|
p.Pointer++;
|
|
}
|
|
}
|
|
|
|
public string ReadUntilWhitespaceOrNewlineOrEndAndObeyBrackets()
|
|
{
|
|
var p = this;
|
|
int inside = 0;
|
|
|
|
while ( true )
|
|
{
|
|
var lineEnder = p.IsNewline || p.IsWhitespace;
|
|
if ( inside > 0 ) lineEnder = false;
|
|
|
|
if ( p.IsEnd || lineEnder )
|
|
return this.Read( p.Pointer - Pointer );
|
|
|
|
if ( p.Is( '(' ) ) inside++;
|
|
if ( p.Is( ')' ) ) inside--;
|
|
|
|
p.Pointer++;
|
|
}
|
|
}
|
|
|
|
public string ReadInnerBrackets( char inner = '(', char outer = ')' )
|
|
{
|
|
int inside = 0;
|
|
int iStart = Pointer;
|
|
int iEnd;
|
|
|
|
while ( true )
|
|
{
|
|
if ( IsEnd )
|
|
return null;
|
|
|
|
if ( Is( inner ) )
|
|
{
|
|
if ( inside == 0 ) iStart = Pointer + 1;
|
|
inside++;
|
|
}
|
|
|
|
if ( Is( outer ) )
|
|
{
|
|
inside--;
|
|
if ( inside == 0 )
|
|
{
|
|
iEnd = Pointer;
|
|
Pointer++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Pointer++;
|
|
}
|
|
|
|
return Text.Substring( iStart, iEnd - iStart );
|
|
}
|
|
|
|
public string ReadWord( string endOnCharacter = null, bool readUntilEnd = false )
|
|
{
|
|
var p = this;
|
|
|
|
while ( true )
|
|
{
|
|
if ( p.IsEnd && !readUntilEnd )
|
|
return null;
|
|
|
|
if ( p.IsEnd || p.IsWhitespace || p.IsNewline || p.IsOneOf( endOnCharacter ) )
|
|
return this.Read( p.Pointer - Pointer );
|
|
|
|
p.Pointer++;
|
|
}
|
|
}
|
|
|
|
public string ReadChars( string chars = null, bool readUntilEnd = false )
|
|
{
|
|
var p = this;
|
|
|
|
while ( true )
|
|
{
|
|
if ( p.IsEnd && !readUntilEnd )
|
|
return null;
|
|
|
|
if ( p.IsEnd || !p.IsOneOf( chars ) )
|
|
{
|
|
if ( p.Pointer == Pointer ) return null;
|
|
|
|
return this.Read( p.Pointer - Pointer );
|
|
}
|
|
|
|
p.Pointer++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a sentence until the next statement divided by ,
|
|
/// Returns the sentence
|
|
/// </summary>
|
|
public string ReadSentence()
|
|
{
|
|
var p = this;
|
|
while ( !p.Is( "," ) && !p.IsEnd )
|
|
{
|
|
if ( p.Is( "(" ) )
|
|
{
|
|
while ( !p.Is( ")" ) && !p.IsEnd )
|
|
{
|
|
p.Pointer++;
|
|
}
|
|
}
|
|
else
|
|
p.Pointer++;
|
|
}
|
|
|
|
return this.Read( p.Pointer - Pointer );
|
|
}
|
|
|
|
public string ReadUntil( string c1 )
|
|
{
|
|
var p = this;
|
|
|
|
while ( !p.IsEnd )
|
|
{
|
|
if ( p.IsOneOf( c1 ) )
|
|
return this.Read( p.Pointer - Pointer );
|
|
|
|
p.Pointer++;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public string ReadUntilOrEnd( string c1, bool acceptNone = false )
|
|
{
|
|
var p = this;
|
|
|
|
while ( !p.IsEnd )
|
|
{
|
|
if ( p.IsOneOf( c1 ) )
|
|
{
|
|
if ( p.Pointer == Pointer )
|
|
return string.Empty;
|
|
|
|
return this.Read( p.Pointer - Pointer );
|
|
}
|
|
|
|
p.Pointer++;
|
|
}
|
|
|
|
return this.ReadRemaining( acceptNone );
|
|
}
|
|
|
|
public (string, string) ReadKeyValue()
|
|
{
|
|
var key = ReadUntilOrEnd( ":" );
|
|
if ( string.IsNullOrWhiteSpace( key ) ) throw new System.Exception( $"Expected key {FileAndLine}" );
|
|
Pointer++;
|
|
|
|
if ( IsEnd ) throw new System.Exception( $"Expected value {FileAndLine}" );
|
|
|
|
var value = ReadUntilOrEnd( ";" );
|
|
if ( string.IsNullOrWhiteSpace( value ) ) throw new System.Exception( $"Expected value {FileAndLine}" );
|
|
Pointer++;
|
|
|
|
return (key.Trim(), value.Trim());
|
|
}
|
|
|
|
public bool TryReadTime( out float val )
|
|
{
|
|
val = 0;
|
|
var p = this;
|
|
|
|
p = p.SkipWhitespaceAndNewlines();
|
|
|
|
var numStart = p.Pointer;
|
|
|
|
while ( !p.IsEnd )
|
|
{
|
|
if ( p.IsDigit || p.Current == '.' )
|
|
{
|
|
p.Pointer++;
|
|
continue;
|
|
}
|
|
|
|
if ( p.Current == 's' || p.Current == 'S' )
|
|
{
|
|
var len = p.Pointer - numStart;
|
|
var str = p.Text.Substring( numStart, len );
|
|
|
|
if ( !float.TryParse( str, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed ) )
|
|
return false;
|
|
|
|
Pointer = p.Pointer + 1;
|
|
|
|
val = parsed * 1000.0f;
|
|
return true;
|
|
}
|
|
|
|
if ( p.Current == 'm' || p.Current == 'M' )
|
|
{
|
|
if ( p.Next != 's' && p.Next != 'S' )
|
|
return false;
|
|
|
|
var len = p.Pointer - numStart;
|
|
var str = p.Text.Substring( numStart, len );
|
|
|
|
|
|
if ( !float.TryParse( str, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed ) )
|
|
return false;
|
|
|
|
Pointer = p.Pointer + 2;
|
|
val = parsed;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal bool TryReadLength( out Sandbox.UI.Length outval )
|
|
{
|
|
outval = 0;
|
|
var p = this;
|
|
|
|
if ( p.IsEnd ) return false;
|
|
p = p.SkipWhitespaceAndNewlines();
|
|
if ( p.IsEnd ) return false;
|
|
|
|
var numStart = p.Pointer;
|
|
|
|
var w = p.ReadWord( ")", true );
|
|
if ( w == null ) return false;
|
|
|
|
var v = Sandbox.UI.Length.Parse( w );
|
|
if ( !v.HasValue ) return false;
|
|
|
|
outval = v.Value;
|
|
Pointer = p.Pointer;
|
|
return true;
|
|
}
|
|
|
|
internal bool TryReadRepeat( out string outval )
|
|
{
|
|
outval = "";
|
|
var p = this;
|
|
|
|
p = p.SkipWhitespaceAndNewlines();
|
|
|
|
if ( !p.IsLetter )
|
|
return false;
|
|
|
|
var w = p.ReadWord( null, true );
|
|
switch ( w )
|
|
{
|
|
case "no-repeat":
|
|
case "repeat-x":
|
|
case "repeat-y":
|
|
case "repeat":
|
|
case "clamp":
|
|
outval = w;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
Pointer += w.Length;
|
|
return true;
|
|
}
|
|
|
|
internal bool TryReadMaskMode( out string outval )
|
|
{
|
|
outval = "";
|
|
var p = this;
|
|
|
|
p = p.SkipWhitespaceAndNewlines();
|
|
|
|
if ( !p.IsLetter )
|
|
return false;
|
|
|
|
var w = p.ReadWord( null, true );
|
|
switch ( w )
|
|
{
|
|
case "match-source":
|
|
case "alpha":
|
|
case "luminance":
|
|
outval = w;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
Pointer += w.Length;
|
|
return true;
|
|
}
|
|
|
|
internal bool TryReadLineStyle( out string outval )
|
|
{
|
|
outval = "";
|
|
var p = this;
|
|
|
|
p = p.SkipWhitespaceAndNewlines();
|
|
|
|
if ( !p.IsLetter )
|
|
return false;
|
|
|
|
var w = p.ReadWord( null, true );
|
|
switch ( w )
|
|
{
|
|
case "none":
|
|
case "solid":
|
|
// case "double":
|
|
// case "dotted":
|
|
// case "dashed":
|
|
// case "inset":
|
|
// case "outset":
|
|
// case "ridge":
|
|
// case "groove":
|
|
// case "hidden":
|
|
outval = w;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
Pointer += w.Length;
|
|
return true;
|
|
}
|
|
|
|
public bool TryReadFloat( out float outval )
|
|
{
|
|
outval = 0;
|
|
var p = this;
|
|
|
|
if ( p.IsEnd ) return false;
|
|
p = p.SkipWhitespaceAndNewlines();
|
|
if ( p.IsEnd ) return false;
|
|
|
|
var w = p.ReadChars( "-0123456789.Ee", true );
|
|
if ( w == null )
|
|
return false;
|
|
|
|
if ( !float.TryParse( w, NumberStyles.Float, CultureInfo.InvariantCulture, out outval ) )
|
|
return false;
|
|
|
|
Pointer += w.Length;
|
|
|
|
// if it ends in f, skip it
|
|
if ( Current == 'f' )
|
|
Pointer++;
|
|
|
|
return true;
|
|
}
|
|
|
|
internal bool TryReadColor( out Color outval )
|
|
{
|
|
outval = default;
|
|
var p = this;
|
|
|
|
if ( p.IsEnd ) return false;
|
|
p = p.SkipWhitespaceAndNewlines();
|
|
if ( p.IsEnd ) return false;
|
|
|
|
int inside = 0;
|
|
var numStart = p.Pointer;
|
|
|
|
while ( !p.IsEnd )
|
|
{
|
|
if ( p.Current == '(' )
|
|
inside++;
|
|
|
|
if ( p.Current == ')' )
|
|
inside--;
|
|
|
|
if ( inside < 0 )
|
|
return false;
|
|
|
|
if ( inside == 0 && p.IsOneOf( " ;\t\n\r," ) )
|
|
break;
|
|
|
|
p.Pointer++;
|
|
}
|
|
|
|
if ( numStart == p.Pointer )
|
|
return false;
|
|
|
|
var c = p.Text.Substring( numStart, p.Pointer - numStart );
|
|
|
|
var color = Color.Parse( c );
|
|
if ( !color.HasValue ) return false;
|
|
|
|
Pointer = p.Pointer;
|
|
outval = color.Value;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <para>
|
|
/// Typically used to parse shorthand position & size combinations, like those seen inside
|
|
/// mask and background shorthands.
|
|
/// <code><position> [ / <size> ]</code>
|
|
/// </para>
|
|
/// </summary>
|
|
internal bool TryReadPositionAndSize( out Sandbox.UI.Length positionX, out Sandbox.UI.Length positionY, out Sandbox.UI.Length sizeX, out Sandbox.UI.Length sizeY )
|
|
{
|
|
// Initial values
|
|
positionX = 0;
|
|
positionY = 0;
|
|
sizeX = UI.Length.Auto;
|
|
sizeY = UI.Length.Auto;
|
|
|
|
//
|
|
// <position>
|
|
//
|
|
if ( TryReadLength( out positionX ) )
|
|
{
|
|
if ( !TryReadLength( out positionY ) )
|
|
positionY = positionX;
|
|
|
|
SkipWhitespaceAndNewlines();
|
|
|
|
//
|
|
// [ / <size> ]?
|
|
//
|
|
if ( TrySkip( "/" ) )
|
|
{
|
|
// We have a size
|
|
if ( !TryReadLength( out sizeX ) )
|
|
return false; // Invalid - expected a length
|
|
|
|
if ( !TryReadLength( out sizeY ) )
|
|
return true; // We don't require a length here
|
|
}
|
|
|
|
SkipWhitespaceAndNewlines();
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal bool TryReadShadowInset( out bool isInset )
|
|
{
|
|
isInset = false;
|
|
var p = this;
|
|
|
|
p = p.SkipWhitespaceAndNewlines();
|
|
|
|
if ( !p.IsLetter )
|
|
return false;
|
|
|
|
var w = p.ReadWord( null, true );
|
|
switch ( w )
|
|
{
|
|
case "inset":
|
|
isInset = true;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
Pointer += w.Length;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if the string at the pointer is this
|
|
/// </summary>
|
|
public bool Is( string v, int offset = 0, bool ignorecase = false )
|
|
{
|
|
var len = v.Length;
|
|
|
|
for ( int i = 0; i < len; i++ )
|
|
{
|
|
if ( !Is( v[i], i, ignorecase ) )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Skip this string if it exists
|
|
/// </summary>
|
|
public bool TrySkip( string v, int offset = 0, bool ignorecase = false )
|
|
{
|
|
var len = v.Length;
|
|
|
|
for ( int i = 0; i < len; i++ )
|
|
{
|
|
if ( !Is( v[i], i, ignorecase ) )
|
|
return false;
|
|
}
|
|
|
|
Pointer += len;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Skip comma and then possible whitespace
|
|
/// </summary>
|
|
public bool TrySkipCommaSeparation()
|
|
{
|
|
SkipWhitespaceAndNewlines();
|
|
|
|
if ( Current != ',' ) return false;
|
|
|
|
Read( 1 );
|
|
SkipWhitespaceAndNewlines();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if the char at the pointer is this
|
|
/// </summary>
|
|
public bool Is( char v, int offset = 0, bool ignorecase = false )
|
|
{
|
|
var ptr = Pointer + offset;
|
|
if ( ptr >= Length ) return false;
|
|
if ( ptr < 0 ) return false;
|
|
|
|
if ( ignorecase )
|
|
return char.ToLowerInvariant( Text[ptr] ) == char.ToLowerInvariant( v );
|
|
|
|
return Text[ptr] == v;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the line we're currently on
|
|
/// </summary>
|
|
public int CurrentLine
|
|
{
|
|
get
|
|
{
|
|
var substr = Text.Substring( 0, Math.Min( Pointer, Text.Length ) );
|
|
var lines = substr.Count( x => x == '\n' );
|
|
return lines + lineOffset;
|
|
}
|
|
}
|
|
|
|
public string FileAndLine => $"[{FileName}:{CurrentLine}]";
|
|
}
|
|
}
|