diff --git a/2D/Screens/ChatScreen.cs b/2D/Screens/ChatScreen.cs index a2903147c..47aa1d284 100644 --- a/2D/Screens/ChatScreen.cs +++ b/2D/Screens/ChatScreen.cs @@ -96,8 +96,8 @@ namespace ClassicalSharp { } public override void Dispose() { - if( !String.IsNullOrEmpty( textInput.chatInputText ) ) { - Window.chatInInputBuffer = textInput.chatInputText; + if( !textInput.chatInputText.Empty ) { + Window.chatInInputBuffer = textInput.chatInputText.ToString(); } chatFont.Dispose(); chatBoldFont.Dispose(); @@ -143,7 +143,7 @@ namespace ClassicalSharp { announcementDisplayTime = DateTime.UtcNow; if( !String.IsNullOrEmpty( text ) ) { List parts = Utils2D.SplitText( GraphicsApi, text, true ); - Size size = Utils2D.MeasureSize( Utils.StripColours( text ), announcementFont, true ); + Size size = Utils2D.MeasureSize( parts, announcementFont, true ); int x = Window.Width / 2 - size.Width / 2; int y = Window.Height / 4 - size.Height / 2; announcementTexture = Utils2D.MakeTextTexture( parts, announcementFont, size, x, y ); @@ -180,7 +180,8 @@ namespace ClassicalSharp { void OpenTextInputBar( string initialText ) { suppressNextPress = true; HandlesAllInput = true; - textInput.chatInputText = initialText; + textInput.chatInputText.Clear(); + textInput.chatInputText.Append( 0, initialText ); textInput.Init(); } diff --git a/2D/Screens/ErrorScreen.cs b/2D/Screens/ErrorScreen.cs index 81ca0c9dc..db04caf29 100644 --- a/2D/Screens/ErrorScreen.cs +++ b/2D/Screens/ErrorScreen.cs @@ -37,7 +37,7 @@ namespace ClassicalSharp { Texture ModifyText( int yOffset, string text, Font font ) { List parts = Utils2D.SplitText( GraphicsApi, text, true ); - Size size = Utils2D.MeasureSize( Utils.StripColours( text ), font, true ); + Size size = Utils2D.MeasureSize( parts, font, true ); int x = Window.Width / 2 - size.Width / 2; int y = Window.Height / 2 - yOffset; return Utils2D.MakeTextTexture( parts, font, size, x, y ); diff --git a/2D/Screens/FpsScreen.cs b/2D/Screens/FpsScreen.cs index 6019ac895..e8d19af2e 100644 --- a/2D/Screens/FpsScreen.cs +++ b/2D/Screens/FpsScreen.cs @@ -6,8 +6,10 @@ namespace ClassicalSharp { public class FpsScreen : Screen { readonly Font font; + UnsafeString text; public FpsScreen( Game window ) : base( window ) { font = new Font( "Arial", 13 ); + text = new UnsafeString( 96 ); } TextWidget fpsTextWidget; @@ -17,20 +19,24 @@ namespace ClassicalSharp { fpsTextWidget.Render( delta ); } - const string formatString = "FPS: {0} (min {1}), chunks/s: {2}, vertices: {3}"; double accumulator, maxDelta; - int fpsCount; - - void UpdateFPS( double delta ) { + int fpsCount; + unsafe void UpdateFPS( double delta ) { fpsCount++; maxDelta = Math.Max( maxDelta, delta ); accumulator += delta; long vertices = Window.Vertices; if( accumulator >= 1 ) { - string text = String.Format( formatString, (int)( fpsCount / accumulator ), - (int)( 1f / maxDelta ), Window.ChunkUpdates, vertices ); - fpsTextWidget.SetText( text ); + fixed( char* ptr = text.value ) { + char* ptr2 = ptr; + text.Clear( ptr2 ) + .Append( ref ptr2, "FPS: " ).AppendNum( ref ptr2, (int)( fpsCount / accumulator ) ) + .Append( ref ptr2, " (min " ).AppendNum( ref ptr2, (int)( 1f / maxDelta ) ) + .Append( ref ptr2, "), chunks/s: " ).AppendNum( ref ptr2, Window.ChunkUpdates ) + .Append( ref ptr2, ", vertices: " ).AppendNum( ref ptr2, Window.Vertices ); + } + fpsTextWidget.SetText( text.value ); maxDelta = 0; accumulator = 0; fpsCount = 0; diff --git a/2D/Widgets/ExtPlayerListWidget.cs b/2D/Widgets/ExtPlayerListWidget.cs index 50fbdd69f..2f9fed776 100644 --- a/2D/Widgets/ExtPlayerListWidget.cs +++ b/2D/Widgets/ExtPlayerListWidget.cs @@ -38,7 +38,7 @@ namespace ClassicalSharp { GroupName = p.GroupName; GroupRank = p.GroupRank; List parts = Utils2D.SplitText( graphics, Name, true ); - Size size = Utils2D.MeasureSize( Utils.StripColours( Name ), font, true ); + Size size = Utils2D.MeasureSize( parts, font, true ); Texture = Utils2D.MakeTextTexture( parts, font, size, 0, 0 ); } diff --git a/2D/Widgets/PlayerListWidget.cs b/2D/Widgets/PlayerListWidget.cs index 49cd6aa9d..888fe95ba 100644 --- a/2D/Widgets/PlayerListWidget.cs +++ b/2D/Widgets/PlayerListWidget.cs @@ -32,7 +32,7 @@ namespace ClassicalSharp { Name = p.DisplayName; PlayerId = p.ID; List parts = Utils2D.SplitText( graphics, Name, true ); - Size size = Utils2D.MeasureSize( Utils.StripColours( Name ), font, true ); + Size size = Utils2D.MeasureSize( parts, font, true ); Texture = Utils2D.MakeTextTexture( parts, font, size, 0, 0 ); } } diff --git a/2D/Widgets/TextGroupWidget.cs b/2D/Widgets/TextGroupWidget.cs index 7dd4f6180..ae6139526 100644 --- a/2D/Widgets/TextGroupWidget.cs +++ b/2D/Widgets/TextGroupWidget.cs @@ -33,7 +33,7 @@ namespace ClassicalSharp { Size size = new Size( 0, defaultHeight ); if( !String.IsNullOrEmpty( text ) ) { parts = Utils2D.SplitText( GraphicsApi, text, true ); - size = Utils2D.MeasureSize( Utils.StripColours( text ), font, true ); + size = Utils2D.MeasureSize( parts, font, true ); } int x = HorizontalDocking == Docking.LeftOrTop ? XOffset : Window.Width - size.Width - XOffset; diff --git a/2D/Widgets/TextInputWidget.cs b/2D/Widgets/TextInputWidget.cs index 4c0fc4825..97e62639d 100644 --- a/2D/Widgets/TextInputWidget.cs +++ b/2D/Widgets/TextInputWidget.cs @@ -6,7 +6,7 @@ using System.Windows.Forms; namespace ClassicalSharp { public sealed class TextInputWidget : Widget { - + public TextInputWidget( Game window, Font font, Font boldFont ) : base( window ) { HorizontalDocking = Docking.LeftOrTop; VerticalDocking = Docking.BottomOrRight; @@ -19,6 +19,7 @@ namespace ClassicalSharp { typingLogPos = Window.ChatInputLog.Count; // Index of newest entry + 1. this.font = font; this.boldFont = boldFont; + chatInputText = new UnsafeString( 64 ); } Texture chatInputTexture, chatCaretTexture; @@ -26,7 +27,7 @@ namespace ClassicalSharp { int caretPos = -1; int typingLogPos = 0; public int ChatInputYOffset; - public string chatInputText = ""; + internal UnsafeString chatInputText; readonly Font font, boldFont; public override void Render( double delta ) { @@ -39,19 +40,20 @@ namespace ClassicalSharp { X = 10; DrawTextArgs caretArgs = new DrawTextArgs( GraphicsApi, "_", Color.White, false ); chatCaretTexture = Utils2D.MakeTextTexture( boldFont, 0, 0, ref caretArgs ); + string value = chatInputText.value; - if( chatInputText.Length == 0 ) { + if( chatInputText.Empty ) { caretPos = -1; } - Size size = Utils2D.MeasureSize( chatInputText, font, false ); + Size size = Utils2D.MeasureSize( value, font, false ); if( caretPos == -1 ) { chatCaretTexture.X1 = 10 + size.Width; size.Width += chatCaretTexture.Width; } else { - Size trimmedSize = Utils2D.MeasureSize( chatInputText.Substring( 0, caretPos ), font, false ); + Size trimmedSize = Utils2D.MeasureSize( value.Substring( 0, caretPos ), font, false ); chatCaretTexture.X1 = 10 + trimmedSize.Width; - Size charSize = Utils2D.MeasureSize( chatInputText.Substring( caretPos, 1 ), font, false ); + Size charSize = Utils2D.MeasureSize( value.Substring( caretPos, 1 ), font, false ); chatCaretTexture.Width = charSize.Width; } size.Height = Math.Max( size.Height, chatCaretTexture.Height ); @@ -60,7 +62,7 @@ namespace ClassicalSharp { using( Bitmap bmp = new Bitmap( size.Width, size.Height ) ) { using( Graphics g = Graphics.FromImage( bmp ) ) { Utils2D.DrawRect( g, backColour, 0, 0, bmp.Width, bmp.Height ); - DrawTextArgs args = new DrawTextArgs( GraphicsApi, chatInputText, Color.White, false ); + DrawTextArgs args = new DrawTextArgs( GraphicsApi, value, Color.White, false ); Utils2D.DrawText( g, font, ref args, 0, 0 ); } chatInputTexture = Utils2D.Make2DTexture( GraphicsApi, bmp, 10, y ); @@ -91,9 +93,9 @@ namespace ClassicalSharp { } public void SendTextInBufferAndReset() { - Window.SendChat( chatInputText ); + Window.SendChat( chatInputText.ToString() ); typingLogPos = Window.ChatInputLog.Count; // Index of newest entry + 1. - chatInputText = ""; + chatInputText.Clear(); Dispose(); } @@ -149,9 +151,9 @@ namespace ClassicalSharp { public override bool HandlesKeyPress( char key ) { if( chatInputText.Length < 64 && !IsInvalidChar( key ) ) { if( caretPos == -1 ) { - chatInputText += key; + chatInputText.Append( chatInputText.Length, key ); } else { - chatInputText = chatInputText.Insert( caretPos, new String( key, 1 ) ); + chatInputText.InsertAt( caretPos, key ); caretPos++; } Dispose(); @@ -162,14 +164,14 @@ namespace ClassicalSharp { } void BackspaceKey() { - if( chatInputText.Length > 0 ) { + if( !chatInputText.Empty ) { if( caretPos == -1 ) { - chatInputText = chatInputText.Remove( chatInputText.Length - 1, 1 ); + chatInputText.DeleteAt( chatInputText.Length - 1 ); Dispose(); Init(); } else if( caretPos > 0 ) { caretPos--; - chatInputText = chatInputText.Remove( caretPos, 1 ); + chatInputText.DeleteAt( caretPos ); Dispose(); Init(); } @@ -177,8 +179,8 @@ namespace ClassicalSharp { } void DeleteKey() { - if( chatInputText.Length > 0 && caretPos != -1 ) { - chatInputText = chatInputText.Remove( caretPos, 1 ); + if( !chatInputText.Empty && caretPos != -1 ) { + chatInputText.DeleteAt( caretPos ); if( caretPos >= chatInputText.Length ) caretPos = -1; Dispose(); Init(); @@ -186,7 +188,7 @@ namespace ClassicalSharp { } void RightKey() { - if( chatInputText.Length > 0 && caretPos != -1 ) { + if( !chatInputText.Empty && caretPos != -1 ) { caretPos++; if( caretPos >= chatInputText.Length ) caretPos = -1; Dispose(); @@ -195,7 +197,7 @@ namespace ClassicalSharp { } void LeftKey() { - if( chatInputText.Length > 0 ) { + if( !chatInputText.Empty ) { if( caretPos == -1 ) caretPos = chatInputText.Length; caretPos--; if( caretPos < 0 ) caretPos = 0; @@ -208,7 +210,8 @@ namespace ClassicalSharp { if( Window.ChatInputLog.Count > 0 ) { typingLogPos--; if( typingLogPos < 0 ) typingLogPos = 0; - chatInputText = Window.ChatInputLog[typingLogPos]; + chatInputText.Clear(); + chatInputText.Append( 0, Window.ChatInputLog[typingLogPos] ); caretPos = -1; Dispose(); Init(); @@ -218,11 +221,11 @@ namespace ClassicalSharp { void DownKey() { if( Window.ChatInputLog.Count > 0 ) { typingLogPos++; + chatInputText.Clear(); if( typingLogPos >= Window.ChatInputLog.Count ) { typingLogPos = Window.ChatInputLog.Count; - chatInputText = ""; } else { - chatInputText = Window.ChatInputLog[typingLogPos]; + chatInputText.Append( 0, Window.ChatInputLog[typingLogPos] ); } caretPos = -1; Dispose(); @@ -251,17 +254,17 @@ namespace ClassicalSharp { } if( caretPos == -1 ) { - chatInputText += text; + chatInputText.Append( chatInputText.Length, text ); } else { - chatInputText = chatInputText.Insert( caretPos, text ); + chatInputText.Append( caretPos, text ); caretPos += text.Length; } Dispose(); Init(); return true; } else if( key == Key.C && controlDown ) { - if( !String.IsNullOrEmpty( chatInputText ) ) { - Clipboard.SetText( chatInputText ); + if( !chatInputText.Empty ) { + Clipboard.SetText( chatInputText.ToString() ); } return true; } diff --git a/2D/Widgets/TextWidget.cs b/2D/Widgets/TextWidget.cs index b29daa39e..d9f2bc2e7 100644 --- a/2D/Widgets/TextWidget.cs +++ b/2D/Widgets/TextWidget.cs @@ -34,7 +34,7 @@ namespace ClassicalSharp { List parts = null; Size size = new Size( 0, defaultHeight ); parts = Utils2D.SplitText( GraphicsApi, text, true ); - size = Utils2D.MeasureSize( Utils.StripColours( text ), font, true ); + size = Utils2D.MeasureSize( parts, font, true ); X = CalcAdjOffset( XOffset, Window.Width, size.Width, HorizontalDocking ); Y = CalcAdjOffset( YOffset, Window.Height, size.Height, VerticalDocking ); diff --git a/ClassicalSharp.csproj b/ClassicalSharp.csproj index c6e6cad56..31158cc17 100644 --- a/ClassicalSharp.csproj +++ b/ClassicalSharp.csproj @@ -156,6 +156,7 @@ + diff --git a/Game/Game.Chat.cs b/Game/Game.Chat.cs index b4d0a663b..242f0f76a 100644 --- a/Game/Game.Chat.cs +++ b/Game/Game.Chat.cs @@ -39,6 +39,7 @@ namespace ClassicalSharp { RaiseEvent( ChatReceived, new ChatEventArgs( text, 0 ) ); } + ChatEventArgs chatArgs = new ChatEventArgs( "", 0 ); public void AddChat( string text, byte type ) { CpeMessageType cpeType = (CpeMessageType)type; if( cpeType == 0 ) { @@ -59,7 +60,9 @@ namespace ClassicalSharp { } else if( cpeType == CpeMessageType.Announcement ) { Announcement = text; } - RaiseEvent( ChatReceived, new ChatEventArgs( text, type ) ); + chatArgs.Text = text; + chatArgs.Type = type; + RaiseEvent( ChatReceived, chatArgs ); } const string fileNameFormat = "yyyy-MM-dd"; diff --git a/Game/Game.Events.cs b/Game/Game.Events.cs index ca375601c..9688a9fe5 100644 --- a/Game/Game.Events.cs +++ b/Game/Game.Events.cs @@ -122,7 +122,7 @@ namespace ClassicalSharp { public sealed class ChatEventArgs : EventArgs { - public readonly byte Type; + public byte Type; public string Text; diff --git a/Rendering/PlayerRenderer.cs b/Rendering/PlayerRenderer.cs index d6029e5f8..4b91e1f59 100644 --- a/Rendering/PlayerRenderer.cs +++ b/Rendering/PlayerRenderer.cs @@ -24,7 +24,7 @@ namespace ClassicalSharp.Renderers { using( Font font = new Font( "Arial", 14 ) ) { List parts = Utils2D.SplitText( Graphics, player.DisplayName, true ); - Size size = Utils2D.MeasureSize( Utils.StripColours( player.DisplayName ), font, true ); + Size size = Utils2D.MeasureSize( parts, font, true ); nameTexture = Utils2D.MakeTextTexture( parts, font, size, 0, 0 ); nameWidth = size.Width; nameHeight = size.Height; diff --git a/Utils/UnsafeString.cs b/Utils/UnsafeString.cs new file mode 100644 index 000000000..9d8262443 --- /dev/null +++ b/Utils/UnsafeString.cs @@ -0,0 +1,117 @@ +using System; + +namespace ClassicalSharp { + + // Class used to minimise memory allocations of strings. + // Really, only useful for FpsScreen and TextInputWidget as they allocate lots of very small strings. + // Seriously, you should *not* use this anywhere else, as Empty and Length are O(N). + internal unsafe class UnsafeString { + + internal string value; + internal int capacity; + + public UnsafeString( int capacity ) { + this.capacity = capacity; + value = new String( '\0', capacity ); + } + + public UnsafeString Append( int index, char c ) { + fixed( char* ptr = value ) { + ptr[index] = c; + return this; + } + } + + public UnsafeString Append( int index, string s ) { + fixed( char* ptr = value ) { + char* offsetPtr = ptr + index; + return Append( ref offsetPtr, s ); + } + } + + public UnsafeString Append( ref char* ptr, string s ) { + for( int i = 0; i < s.Length; i++ ) { + *ptr++ = s[i]; + } + return this; + } + + static char[] numBuffer = new char[20]; + public UnsafeString AppendNum( ref char* ptr, long num ) { + int index = 0; + numBuffer[index++] = (char)( '0' + ( num % 10 ) ); + num /= 10; + + while( num > 0 ) { + numBuffer[index++] = (char)( '0' + ( num % 10 ) ); + num /= 10; + } + for( int i = index - 1; i >= 0; i-- ) { + *ptr++ = numBuffer[i]; + } + return this; + } + + public UnsafeString Clear() { + fixed( char* ptr = value ) { + return Clear( ptr ); + } + } + + public UnsafeString Clear( char* ptr ) { + for( int i = 0; i < capacity; i++ ) { + *ptr++ = '\0'; + } + return this; + } + + public void DeleteAt( int index ) { + fixed( char* ptr = value ) { + char* offsetPtr = ptr + index; + for( int i = index; i < capacity - 1; i++ ) { + *offsetPtr = *( offsetPtr + 1 ); + offsetPtr++; + } + } + } + + public void InsertAt( int index, char c ) { + fixed( char* ptr = value ) { + char* offsetPtr = ptr + capacity - 1; + for( int i = capacity - 1; i > index; i-- ) { + *offsetPtr = *( offsetPtr - 1 ); + offsetPtr--; + } + offsetPtr = ptr + index; + *offsetPtr = c; + } + } + + + public bool Empty { + get { + for( int i = 0; i < capacity; i++ ) { + if( value[i] != '\0' ) + return false; + } + return true; + } + } + + public int Length { + get { + int len = capacity; + for( int i = capacity - 1; i >= 0; i-- ) { + if( value[i] != '\0' ) + break; + len--; + } + return len; + } + } + + public override string ToString() { + return value.TrimEnd( '\0' ); + } + } +}