Jump to content


  • Content Count

  • Joined

  • Last visited

  • Days Won


Everything posted by DavidRM

  1. DavidRM

    License Serial Number invalid

    Just to mention it... I had this problem too. I never received an email with my new information. I was able to get it from clientarea.*, though. -David
  2. DavidRM

    Bugs and Features

    I just saw it happen when I selected a block of lines from the bottom up (instead of top down). -David
  3. DavidRM

    PixiJS v5

    How hard would it be to to get the PixiJS unit included with SMS updated to the latest release (v5)? Thanks! -David
  4. I've had some players report that their iPhone browser has a problem loading my Paintball Net UI web page (which was built with SMS). That is, they have to do some kind of "delete data" for the URL, then the page will load again. I'll provide more details from one of the users when I get it. Any ideas? Thanks. -David
  5. DavidRM

    Page Loading Issues with iPhone Browser

    That seems to have fixed it. Thanks! -David
  6. DavidRM

    Page Loading Issues with iPhone Browser

    I will try that. Thanks! -David
  7. DavidRM

    Page Loading Issues with iPhone Browser

    Those details I mentioned: I have to go into settings, general, iPhone storage, scroll down to safari, select website data. Then swipe left on [the domain name] and press delete Thoughts? -David
  8. DavidRM

    Promises revisited

    I've had to use promises extensively, especially for accessing MySql. I've used Warley Alex's posted promise unit instead of the RTL unit.
  9. DavidRM

    Node.JS Game Loop

    Here is my Smart Mobile Studio adaptation of the Node.JS "node-gameloop" module. My primary modification was some code to make the *next* interval line up with the desired number of ms per tick. I also tightened the window for choosing SetTimeout vs SetImmediate. I needed this to provide the beating heart of Paintball Net's simulation engine. Which, sure, only beats 10 times per second, but I wanted it to be as accurate as possible. 🙂 -David unit UPBNGameGameLoop; interface uses System.Types, System.Types.Convert, System.Time, System.Streams, System.Reader, System.Writer, System.Device.Storage, SmartNJ.System, SmartNJ.Streams, SmartNJ.Device.Storage, SmartNJ.Application, NodeJS.Core; var GameLoopActive: boolean; type TGameLoopCallback = procedure(delta: float); procedure StartGameLoop(update: TGameLoopCallback; msInterval: integer); procedure StopGameLoop; implementation procedure SetImmediate(const Entrypoint: TProcedureRef); external 'setImmediate'; const Seconds2Nano = 1e9; Nano2Seconds = 1 / Seconds2Nano; MS2Nano = 1e6; Nano2MS = 1 / MS2Nano; var _loopProc: TGameLoopCallback; _loopLengthHR: float; _loopPrevHR, _loopTargetHR: float; function GetHRTime: float; var hrTime: Variant; begin // I need a more up-to-date node to use the bigint() function. // asm // @Result = process.hrtime.bigint(); // end; asm @hrTime = process.hrtime(); end; Result := (hrTime[0] * Seconds2Nano) + hrtime[1]; end; procedure DoGameLoop; var now, delta: float; remaining: integer; begin if GameLoopActive then begin now := GetHRTime; if now >= _loopTargetHR then begin delta := now - _loopPrevHR; _loopPrevHR := now; _loopTargetHR := now + _loopLengthHR; if delta > _loopLengthHR then begin _loopTargetHR -= (delta - _loopLengthHR); end; _loopProc(delta * Nano2Seconds); end; if GameLoopActive then begin remaining := Trunc((_loopTargetHR - GetHRTime) * Nano2MS); if remaining >= 4 {16} then SetTimeout(DoGameLoop, remaining) else SetImmediate(DoGameLoop); end; end; end; procedure StartGameLoop(update: TGameLoopCallback; msInterval: integer); begin if not GameLoopActive then begin GameLoopActive := True; _loopLengthHR := msInterval * MS2Nano; _loopProc := update; _loopPrevHR := GetHRTime; _loopTargetHR := _loopPrevHR; SetImmediate(DoGameLoop); end; end; procedure StopGameLoop; begin GameLoopActive := False; _loopProc := nil; end; end.
  10. Updated 5/2019 to add: Now online here: Paintball Net --- Back in the mid-90's, my brother created what he called an "action MUD". He called it "Paintball Net". PBN used ANSI text terminal commands to create a combined roll-and-scroll and animated text experience. ! (exclamation points) were trees, _ (underscores) were grass, ^ (up carets) were mountains, and more. Your "avatar" was a "Y" (uppercase y) and enemies were * (asterisks). You used MUD-like commands to move around the world and target your paintball gun and buy and sell and more. I wish I had a screenshot of what the first version looked like, but you'll just have to imagine. 😃 He asked me to create the GUI for the game. That was 1996, and I had just purchased Delphi 2 and wrapped up my first "learn Delphi" project. This sounded like a good next step, and "PBTerm" was born. I *do* have screenshots of that. Oh, yeah. Love those 16-color Windows graphics. 😃 During the years it was online, the game evolved and upgraded to look more like this: We were never going to win any awards, but we had a very devoted player community. 😃 Paintball Net was online from the summer of 1996 through the summer of 2000, when we took it offline to focus on other projects. The game was never huge, but we had thousands of players come through over that period. And since then, every year at least a few of the players have reached out and asked/begged/pleaded/demanded if we were going to put it back online. There really wasn't much chance of the original version going back online. The original server, written in ANSI C for Linux had proven rather fragile, and was a big part of why we took the game offline. It was taking hours every day just to keep it up and running, in addition to time spent managing the community of players. On top of that, a hard drive incident in 2008 had cost me the source code of a number of core third-party/modified components for PBTerm. This past summer, though, I realized I might be able to make the game live again using Smart Mobile Studio. I would do a straight port of the ANSI C server to Smart Pascal using Node.JS and WebSocket, and I would rebuild the PBTerm client as a browser-based client. I'm not going to say it was *easy*, but it has been a lot of fun. ANSI C converts to Pascal without a lot of painful gyrations. And Node.JS seems a LOT more flexible, stable, and powerful than the TCP sockets approach we were using before. Also, game development is a lot easier when you already own all the graphic and audio resources. 😃 This is the server "in action"... Recreating the client has been more complicated. 20+ years of Delphi habits had to be adapted to the new reality of SMS and a browser-based UI. But that's coming together too. It might be obvious, but I'm not targeting this game at mobile. Paintball Net needed a mouse and keyboard in 1996, it's gonna need a mouse and keyboard in 2018. Also, I've made as few modifications to the gameplay as possible. I really wanted to bring back the original as much as I could. Today I got the handful of sound effects integrated, which was easier than I expected. I'm planning to start testing soon. I just need to line up a server to use and find a few volunteers. I'm excited. This would never have happened without Smart Mobile Studio. So I figured I would share. Merry Xmas! -David
  11. What I'm trying to do: Keep the web page for the app separate from the backend server for the app. Right now, I have the web page serving the client, which opens a Websocket (ws) to the backend server. I want to add HTTPS to the web page and I want to support secure Websockets to the backend. The first part seems pretty straightforward. I'll just get an SSL certificate for the web page. But... How does that affect the second part? Can I still open a non-secure WS connection from the client? Or will the browser balk? Also, what do I need to do to the backend server to get it to handle secure WS connections? Thanks! -David
  12. DavidRM

    How do I use Secure Websockets (WSS)?

    Alrighty then. I have a plan of attack now. THanks! -David
  13. DavidRM

    How do I use Secure Websockets (WSS)?

    Some of my users have connected with mobile devices. I've done it with a Google Pixel 2, though not for long stretches of time. I haven't heard of any dropping connections like that. IPhones and Android both.
  14. DavidRM

    How do I use Secure Websockets (WSS)?

    @lynkfs It sounds like I should get the Websocket upgraded to SSL before messing with the web page. But since the backend doesn't have a domain name, I'm not sure how the certificate would work. This part is all quite new to me. (Hell, before last fall NodeJS/Websocket was totally new to me). I'm kinda figuring out as I go.
  15. DavidRM

    Server Side file creation and writing to files

    Fine. Answer your own question. I actually *knew* this one... 😉
  16. DavidRM

    Request for feedback: Changing class names in the RTL

    This. ^^^^^ That said, I vote for the change. -David
  17. DavidRM

    UI layout

    Now add some TW3Labels to your layout demo and experience Real Pain. 😉
  18. DavidRM

    Node.JS WebSocket Client Socket

    This is me sharing again. The NodeJS WebSocket server socket is implemented in SMS. But I didn't find an implementation of the WebSocket client socket. Sometimes an SMS NodeJS server needs to connect to another server. So I kinda hacked this together using the server socket as a guide. unit UPBNCommonNJWebSocket; interface uses System.Types, System.Types.Convert, System.Time, System.Streams, System.Reader, System.Writer, System.Device.Storage, System.Objects, SmartNJ.System, SmartNJ.Streams, SmartNJ.Device.Storage, SmartNJ.Application, NodeJS.Core, NodeJS.WebSocket, SmartNJ.Server.WebSocket; type // Forward declarations TNJWebSocket = class; TNJWebSocketOpenEvent = procedure (Sender: TNJWebSocket); TNJWebSocketCloseEvent = procedure (Sender: TNJWebSocket; Code: integer; const Reason: string); TNJWebSocketErrorEvent = procedure (Sender: TNJWebSocket; Error: TJSErrorObject); TNJWebSocketMessageEvent = procedure (Sender: TNJWebSocket; Message: TNJWebsocketMessage); TNJWebSocket = class(TW3ErrorObject) private FSocket: JWsSocket; public property WSSocket: JWsSocket read FSocket; function SocketState: JWsReadyState; function Connected: boolean; function URL: string; function Protocol: string; procedure Connect(URL: string; Protocols: array of string); overload; procedure Connect(URL: string); overload; procedure Disconnect; overload; procedure Send(const Data: variant); overload; procedure Send(const Text: string); overload; procedure Send(const Data: TStream); overload; property TagData: variant; constructor Create; override; destructor Destroy; override; procedure Ping; published property OnOpen: TNJWebSocketOpenEvent; property OnClosed: TNJWebSocketCloseEvent; property OnMessage: TNJWebSocketMessageEvent; property OnError: TNJWebSocketErrorEvent; end; implementation constructor TNJWebSocket.Create; begin inherited Create; // We dont want to throw exceptions whenever SetLastError() is called ErrorOptions.ThrowExceptions := false; end; destructor TNJWebSocket.Destroy; begin FSocket := nil; inherited; end; procedure TNJWebSocket.Ping; begin if FSocket <> nil then asm (@FSocket).ping(function() {}); end; end; function TNJWebSocket.Protocol: string; begin if FSocket <> nil then Result := FSocket.protocol; end; function TNJWebSocket.URL:String; begin if FSocket <> nil then Result:=FSocket.url; end; function TNJWebSocket.SocketState: JWsReadyState; begin if FSocket <> nil then Result := JWsReadyState( integer( FSocket.readyState) ) else Result := rsClosed; end; function TNJWebSocket.Connected: boolean; begin Result := SocketState = rsOpen; end; procedure TNJWebSocket.Connect(URL: string); begin Connect(Url, []); end; procedure TNJWebSocket.Connect(URL: string; Protocols: Array of string); begin ClearLastError(); (* disconnect socket if already connected *) if connected then disconnect(); (* Allocate new socket *) var WebSocket = WebSocketAPI; asm (@self.FSocket) = new (@WebSocket)(@URL, @Protocols); end; // initialize standard socket events FSocket.on("open", procedure begin if assigned(OnOpen) then OnOpen(self); end); FSocket.on("error", procedure (error: variant) begin SetLastError("internal websocket error"); if assigned(OnError) then OnError(self, TJSErrorObject(error)); end); FSocket.on("message", procedure (message: variant) var ResData: TNJWebsocketMessage; begin if message.IsUint8Array then begin ResData.wiType := mtBinary; ResData.wiBuffer := JBuffer(message); end else begin ResData.wiType := mtText; ResData.wiText := message; end; if assigned(OnMessage) then OnMessage(self, ResData); end); Variant(FSocket).on("close", procedure (code: integer; reason: string) begin if assigned(OnClosed) then OnClosed(self, code, reason); end); end; procedure TNJWebSocket.Disconnect; begin ClearLastError(); if Connected then begin try FSocket.close(); finally FSocket := nil; end; end; end; procedure TNJWebSocket.Send(const Data: variant); begin FSocket.send(data); end; procedure TNJWebSocket.Send(const Text: string); begin FSocket.send(Text); end; procedure TNJWebSocket.Send(const Data: TStream); begin if Data <> nil then begin if Data.position < Data.Size then begin // Get bytes from stream var Bytes := Data.Read(Data.Size - Data.Position); // Convert to typed-array var TypedArray := TDataType.BytesToTypedArray(bytes); // Send as binary FSocket.Send(TypedArray); end; end; end; end. It might be a bit simplified, and it doesn't really follow the SMS component name convention, but it does what I need. And I figured I would share. -David
  19. DavidRM

    RTL Search

    I would find a built-in search of the RTL quite handy. Sorta like the "Find in Files...", where it brings up a list of hits, but searching the current SMS RTL folders and files instead. Could even just be a checkbox option in "Find in Files..." This would be a step in the direction of "better documentation". 😃 Or, at least, easier to review what's already available. Currently, I do this using Notepad++, but that doesn't let me open the results in the SMS IDE. Being in the IDE would provide access to "Find Declaration" and similar features. -David
  20. DavidRM

    Font size and style in buttons

    Suggestion 2. If I want bold, I'll add it.
  21. DavidRM

    My SMS Project: Paintball Net Revival

    paintballnet.net is the domain name. But it's not propagating for some reason. The alternate link is davidrm.com/paintballnet/play. It hasn't officially launched yet, but it's there. 😃 -David
  22. How can I embed an SMS project (web page) in a WordPress page? I am able to use <iframe></iframe> to almost get there, but the SMS project "takes over" the whole browser tab. What I want is for the SMS project to "live" within the content of the page, so the WP menus and sidebars are still there. Thanks. -David
  23. DavidRM

    Embed SMS Form Project in WordPress Page?

    Nope. That wasn't enough. Maybe there's something I need to do in the project itself?
  24. DavidRM

    Embed SMS Form Project in WordPress Page?

    <iframe class="iframe-mockup" id="iphone5-iframe" src="/livedemo/Games/Missile Command/www" frameborder="0" allowfullscreen=""></iframe> The "allowfullscreen" looks promising. I'll try that. Thanks! -David
  25. DavidRM

    Chat Scroller - Need Some Tips

    Here's the overhauled version, that only uses as many actual on-screen elements as required to show the visible items. It's been working pretty well. unit UPBTInfoScroll; interface uses System.Types, System.Lists, System.Time, System.Colors, SmartCL.System, SmartCL.Theme, SmartCL.Components, SmartCL.Controls.Label, SmartCL.Scroll, SmartCL.Controls.ScrollBar, SmartCL.Css.Classes; type TPBTScrollItemData = class(TObject) private FItemText: string; FItemTop: integer; FItemHeight: integer; public function ItemStyleName: string; virtual; function InnerHtml: string; virtual; property ItemText: string read FItemText write FItemText; property ItemTop: integer read FItemTop write FItemTop; property ItemHeight: integer read FItemHeight write FItemHeight; end; TPBTScrollInfoItemData = class(TPBTScrollItemData) public function ItemStyleName: string; override; end; TPBTScrollErrorItemData = class(TPBTScrollItemData) public function ItemStyleName: string; override; end; TPBTChatType = (ctAnnounce, ctChat, ctSay, ctShout, ctWhisper, ctTell, ctPlan, ctTeamPlan, ctWizChat); TPBTScrollChatItemData = class(TPBTScrollItemData) private FChatType: TPBTChatType; FChatHeader: string; FChatColor: TColor; public function ItemStyleName: string; override; function InnerHtml: string; override; property ChatType: TPBTChatType read FChatType write FChatType; property ChatHeader: string read FChatHeader write FChatHeader; property ChatColor: TColor read FChatColor write FChatColor; property ChatText: string read ItemText write ItemText; end; TPBTScrollItem = class(TW3CustomControl) protected FItemData: TPBTScrollItemData; procedure SetItemData(aValue: TPBTScrollItemData); public function CreationFlags: TW3CreationFlags; override; procedure InitializeObject; override; procedure UpdateDisplay; virtual; property ItemData: TPBTScrollItemData read FItemData write SetItemData; end; TPBTScrollItemClass = class of TPBTScrollItem; TPBTScrollInfoItem = class(TPBTScrollItem) end; TPBTScrollErrorItem = class(TPBTScrollInfoItem) end; TPBTScrollChatItem = class(TPBTScrollItem) end; TPBTScrollItems = class(TW3ScrollContent) private function GetItem(Index: Integer): TPBTScrollItem; function GetCount: integer; public function Add: TPBTScrollItem; overload; function Add(aScrollItemClass: TPBTScrollItemClass): TPBTScrollItem; overload; procedure Clear; virtual; procedure FinalizeObject; override; procedure Delete(aIndex: integer); published property Items[index: Integer]: TPBTScrollItem read GetItem; default; property Count: integer read GetCount; end; TPBTInfoScrollAddItemDataCallback = function: TPBTScrollItemData; TPBTInfoScrollAddItemCallback = function: TPBTScrollItem; TPBTInfoScroll = class(TW3ScrollControl) private FDataItems: array of TPBTScrollItemData; FNewItems: array of TPBTScrollItemData; function GetItems: TPBTScrollItems; function GetScrollIsAtBottom: boolean; procedure ScrollControllerScrolling(Sender: TObject); protected procedure InitializeObject; override; procedure Resize; override; function GetScrollContentClass: TW3ScrollContentClass; override; public function CreationFlags: TW3CreationFlags; override; procedure Clear; procedure Refresh; procedure ScrollToBottom; procedure RealignItems; procedure UpdateVisibleItems; procedure AddItem(aCallback: TPBTInfoScrollAddItemDataCallback); procedure AddText(const aText: string); procedure AddChat(aChatType: TPBTChatType; const aChatHeader, aChatText: string; aChatColor: TColor); procedure AddInfo(const aInfoText: string); procedure AddError(const aErrorText: string); function MaxInfoWidth: integer; function MinInfoWidth: integer; published property Items: TPBTScrollItems read GetItems; property ScrollIsAtBottom: boolean read GetScrollIsAtBottom; end; implementation function TPBTScrollItemData.ItemStyleName: string; begin Result := 'TPBTScrollItem'; end; function TPBTScrollItemData.InnerHtml: string; begin Result := Format('<span style="user-select:text;-moz-user-select:text;-webkit-user-select:text;-ms-user-select:text;-khtml-user-select:text">%s</span>', [TString.EncodeTags(FItemText)]); end; function TPBTScrollInfoItemData.ItemStyleName: string; begin Result := 'TPBTScrollInfoItem'; end; function TPBTScrollErrorItemData.ItemStyleName: string; begin Result := 'TPBTScrollErrorItem'; end; function TPBTScrollChatItemData.ItemStyleName: string; begin Result := 'TPBTScrollChatItem'; end; function TPBTScrollChatItemData.InnerHtml: string; begin Result := Format('<span style="color:%s;user-select:text;-moz-user-select:text;-webkit-user-select:text;-ms-user-select:text;-khtml-user-select:text"><b>%s</b>: %s</span>', [ColorToWebStr(FChatColor), TString.EncodeTags(FChatHeader), TString.EncodeTags(ChatText)]); end; // TPBTScrollItem procedure TPBTScrollItem.SetItemData(aValue: TPBTScrollItemData); begin if FItemData <> nil then TagStyle.RemoveClassFromControl(Handle, FItemData.ItemStyleName); FItemData := aValue; if FItemData <> nil then begin Top := FItemData.ItemTop; TagStyle.AddClassToControl(Handle, FItemData.ItemStyleName); end; UpdateDisplay; end; function TPBTScrollItem.CreationFlags: TW3CreationFlags; begin inherited; Include(Result, cfAllowSelection); end; procedure TPBTScrollItem.InitializeObject; begin inherited; SetContentSelectionMode(tsmText); w3_setStyle(Handle, 'min-width', '100%'); end; procedure TPBTScrollItem.UpdateDisplay; begin if FItemData <> nil then InnerHTML := FItemData.InnerHtml else InnerHTML := ''; end; // TPBTScrollInfoItem // TPBTScrollErrorItem // TPBTScrollChatItem // TPBTScrollItems function TPBTScrollItems.GetItem(Index: Integer): TPBTScrollItem; begin Result := TPBTScrollItem(GetChildren[Index]); end; function TPBTScrollItems.GetCount: integer; begin Result := GetChildCount; end; function TPBTScrollItems.Add: TPBTScrollItem; begin Result := Add(TPBTScrollItem); end; function TPBTScrollItems.Add(aScrollItemClass: TPBTScrollItemClass): TPBTScrollItem; begin Result := aScrollItemClass.Create(Self); end; procedure TPBTScrollItems.Clear; begin // can't get rid of from [0]. Not sure why. while Count > 0 do GetChildren[Count - 1].Free; Height := 0; end; procedure TPBTScrollItems.FinalizeObject; begin Clear; inherited; end; procedure TPBTScrollItems.Delete(aIndex: integer); begin if aIndex < GetChildCount then GetChildren[aIndex].Free; end; // TPBTInfoScroll function TPBTInfoScroll.GetItems: TPBTScrollItems; begin Result := TPBTScrollItems(Content); end; function TPBTInfoScroll.GetScrollIsAtBottom: boolean; begin Result := (Content.Height <= ClientHeight) or (ScrollController.ContentTop <= -(Content.Height - (ClientHeight + 10))) end; procedure TPBTInfoScroll.ScrollControllerScrolling(Sender: TObject); begin UpdateVisibleItems; end; procedure TPBTInfoScroll.InitializeObject; begin inherited; SetBarSize(CNT_SCROLLBAR_SIZE); ScrollBars := sbScrollBar; ScrollController.OnScrolling := ScrollControllerScrolling; end; procedure TPBTInfoScroll.Resize; begin inherited; Content.Width := ClientWidth - CNT_SCROLLBAR_SIZE; Refresh; end; function TPBTInfoScroll.GetScrollContentClass: TW3ScrollContentClass; begin Result := TPBTScrollItems; end; function TPBTInfoScroll.CreationFlags: TW3CreationFlags; begin inherited; // Allow key-capture and selection include(result, cfKeyCapture); include(result, cfAllowSelection); end; procedure TPBTInfoScroll.Clear; begin FDataItems.Clear; Refresh; end; procedure TPBTInfoScroll.Refresh; begin if Width > 0 then begin var wasAtBottom := GetScrollIsAtBottom; RealignItems; ScrollController.Refresh; if wasAtBottom then ScrollToBottom; UpdateVisibleItems; end; end; procedure TPBTInfoScroll.ScrollToBottom; begin ScrollController.ScrollTo(0, -(Content.Height - ClientHeight)); end; procedure TPBTInfoScroll.RealignItems; begin // recalculate the size of each itemData if (FDataItems.Length > 0) or (FNewItems.Length > 0) then Items.BeginUpdate; try var aItem := Items.Add; try var aTop: integer := 0; for var aItemData in FDataItems do begin aItem.ItemData := aItemData; aItemData.ItemHeight := aItem.Height; aItemData.ItemTop := aTop; Inc(aTop, aItemData.ItemHeight); end; for var aItemData in FNewItems do begin FDataItems.Add(aItemData); aItem.ItemData := aItemData; aItemData.ItemHeight := aItem.Height; aItemData.ItemTop := aTop; Inc(aTop, aItemData.ItemHeight); end; FNewItems.Clear; Content.Height := aTop; if Content.Height < ClientHeight then Content.Height := ClientHeight; finally aItem.Free; end; finally Items.EndUpdate; end; end; procedure TPBTInfoScroll.UpdateVisibleItems; var aStartIdx, aEndIdx, aDataIdx, viewTop: integer; begin viewTop := ScrollController.ContentTop; if viewTop < 0 then viewTop := -viewTop; // find first visible itemData // start from the bottom of the list aStartIdx := FDataItems.Length; while (aStartIdx > 0) do begin Dec(aStartIdx); if FDataItems[aStartIdx].ItemTop <= viewTop then break; end; // find last visible itemData aEndIdx := aStartIdx; while (aEndIdx < FDataItems.Length) do begin if (aEndIdx = FDataItems.Length - 1) or (FDataItems[aEndIdx].ItemTop > viewTop + ClientHeight) then break; Inc(aEndIdx); end; Items.BeginUpdate; try if (aStartIdx >= FDataItems.Length) then Items.Clear else begin // adjust item count while Items.Count < (aEndIdx - aStartIdx) + 1 do Items.Add; while Items.Count > (aEndIdx - aStartIdx) + 1 do Items.Delete(Items.Count - 1); aDataIdx := aStartIdx; for var ii := 0 to Items.Count -1 do begin var aItem := Items[ii]; aItem.ItemData := FDataItems[aDataIdx]; Inc(aDataIdx); end; end; finally Items.EndUpdate; end; end; procedure TPBTInfoScroll.AddItem(aCallback: TPBTInfoScrollAddItemDataCallback); const margin = 5; begin var aItemData: TPBTScrollItemData := aCallback; if Width = 0 then begin FNewItems.Add(aItemData); exit; end; var wasAtBottom := (ScrollController.ContentTop <= -(Content.Height - (ClientHeight + 2 * margin))); Items.BeginUpdate; try if FDataItems.Length > 0 then aItemData.ItemTop := FDataItems[FDataItems.Length - 1].ItemTop + FDataItems[FDataItems.Length - 1].ItemHeight; FDataItems.Add(aItemData); var aItem := Items.Add; aItem.ItemData := aItemData; aItemData.ItemHeight := aItem.Height; Content.BeginUpdate; try Content.Height := aItem.Top + aItem.Height; if Content.Height <= ClientHeight then begin Content.Height := ClientHeight; wasAtBottom := True; end; finally Content.EndUpdate; end; finally Items.EndUpdate; end; ScrollController.Refresh; if wasAtBottom then ScrollToBottom; UpdateVisibleItems; end; procedure TPBTInfoScroll.AddText(const aText: string); begin AddItem( lambda Result := new TPBTScrollItemData; Result.ItemText := aText; end ); end; procedure TPBTInfoScroll.AddChat(aChatType: TPBTChatType; const aChatHeader, aChatText: string; aChatColor: TColor); begin AddItem( lambda Result := new TPBTScrollChatItemData; TPBTScrollChatItemData(Result).ChatType := aChatType; TPBTScrollChatItemData(Result).ChatHeader := aChatHeader; TPBTScrollChatItemData(Result).ChatText := aChatText; TPBTScrollChatItemData(Result).ChatColor := aChatColor; end ); end; procedure TPBTInfoScroll.AddInfo(const aInfoText: string); begin AddItem( lambda Result := new TPBTScrollInfoItemData; Result.ItemText := aInfoText; end ); end; procedure TPBTInfoScroll.AddError(const aErrorText: string); begin AddItem( lambda Result := new TPBTScrollErrorItemData; Result.ItemText := aErrorText; end ); end; function TPBTInfoScroll.MaxInfoWidth: integer; var aMax: string; begin while aMax.Length < 70 do aMax += 'XXXXXXXXXX'; Result := MeasureText(aMax).tmWidth + GetBarSize; end; function TPBTInfoScroll.MinInfoWidth: integer; begin Result := MaxInfoWidth div 2; end; end. There are some bits that specific to my Paintball Net project, like the style names, which I have keyed to CSS. -David