HeroEngine Forums
Welcome, Guest. Please login or Register for HeroCloud Account.

Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Topics - FI-ScottZ

Pages: 1 2 [3] 4 5 6
GUI Creation / autosetarea
« on: Jul 17, 14, 05:40:57 PM »
Just a note that I have found that the 'autosetarea' property of the GUIScrollablePanel class apparently does not take into account the effective scale of its items.  I noticed this with a panel whose effective scale is less than 1 and so it allowed the scrolling to go past the size of its items.

I turned off autosetarea, and now manually set the area to be the total size of the items multiplied by the effective scale of the panel (since each item is at absolute scale of 1*) and that works.  However, the size of the thumb is still being autoset and also does not take into account the effective scale, so it ends up larger than it should be.

Hopefully, in the future the effective scale will be used in the source code for auto setting the area and thumb size.

*If the items were not all scale of 1, we would need to add up the effective size of each individual item to get the proper area.

We have a number of places where we are creating billboards from GUI windows (CreateBillboardFromGUIControl).  Since the Quartz update happened, they no longer appear.  I am not sure if this happened after the first Quartz, or Quartz.A but I think it was Quartz.A.

I have confirmed that the billboard is created and not hidden, and calling RemoveBillboardFromGUIControl() on the gui confirms that the gui is visible.  These billboards had been working previously.

I wonder if this might be related to Quartz.A: "gluedtonode" offset, where perhaps the gui is being shown somewhere not inside of the billboard?  However the position and size properties of the gui seem to be fine.

Another thing I notice is that the billboard created is put into the *everywhere* room, though I don't know if that was always the case since I never thought to look at that.

To Recreate:

Create these functions in a client script so they can be called from the Console:
Code: [Select]
function CreatePanel()
  gui as NodeRef of Class GUIControl = CreateNodeFromPrototype("_panel")
  gui.build = true
  gui.size.x = 200
  gui.size.y = 200
  println("New _panel: "+gui)
function PutGUIInBB(guiControlID as String)
  var bb = CreateBillboardFromGUIControl(guiControlID)
  println("PutGUIInBB, guiControlID: "+guiControlID+" bb: "+bb)
function RemoveGUIFromBB(input as String)
  tokens as List of String
  Tokenize(input, tokens)
  guiControlID as ID = tokens[1]
  destroyBB as Boolean
  if tokens.length > 1
    destroyBB = tokens[2]
  RemoveBillboardFromGUIControl(guiControlID, destroyBB)

Enter this line into the Console:
call scriptName CreatePanel
Observe that the panel shows fine.

Copy the panel ID displayed in the console and enter this line:
call scriptName PutGUIInBB;panelID
There is no visible gui-on-billboard.  By entering Select Mode in HB, you can select the billboard to see it is not hidden, and use /sn to look at the gui panel properties.

call scriptName RemoveGUIFromBB;panelID true
To remove the billboard and see the panel again.

If you are like me then you find it annoying needing to click "Yes" to the dialog Do you want to allow the following program to make changes on this computer? everytime I start up HeroBlade or the Repo Browser or the Player Client.  But, of course, I don't want to disable User Access Control altogether, and Windows does not allow exceptions to that Control.

So I researched a way to bypass that dialog on the internet and am sharing it here for anyone interested. It's a bit of a hack and sad that MS makes us jump through these hoops, but at least this works.

The gist of it is that one can set up a scheduled task to run HeroBlade using the highest permissions, bypassing the UAC, and create a shortcut to run that task.  But of course HeroBlade does not run properly when run directly from the executable, so running it directly from the task is out.  So here are the steps (this assumes you have Administrator privileges on your pc):

  • Create a batch file that will open the appropriate heal file.  To do that, just create a text file and change its extension to "bat".  You can still edit that file in any text editor such as Notepad.  Since I was going to be creating a few of these, I just put them in a folder in my C drive called "Admin Batches", starting with HeroBlade.bat.
  • In HeroBlade.bat I put one line:
    START C:\"Program Files (x86)"\HeroEngine\HEROBLADEHC022\HeroBladeHC022.heal
    You will need to substitute the location and name of your heroblade heal file.  Also note, I tried it with quotes around the whole path and it did not work; only when they were just around Program Files (x86).
  • Open the Windows Task Scheduler.  Easiest way is to click Run from the Start Menu (if it is enabled) and enter "taskschd.msc".  Otherwise you may right-click "My Computer", select Manage, then navigate to the "Task Scheduler Library".
  • Right-click where the tasks list is and select Create New Task.  In the dialog that opens, name the task HeroBlade and be sure the "Run with highest privileges" box is checked on the General tab.  Then on the Actions tab click New...  Make the action Start a Program and for program/script browse to the HeroBlade.bat file.  Click OK to complete the action, then OK to complete the task.  Now running that task will launch HeroBlade with no confirmation needed.  You can run it directly from the Task Scheduler to test it.
  • To make it more convenient, right-click an empty space on your desktop and select New-->Shortcut.  Enter
    schtasks.exe /run /tn "HeroBlade"
    for the location of the item, then name the shortcut HeroBlade.
  • Right-click the shortcut and open its Properties.  On the Shortcut tab change the icon and browse to the HeroBlade executable to get its icon.  Click OK on the properties window.
  • Drag that shortcut to the taskbar to pin it there.

Voila!  You now have a pinned shortcut to open HeroBlade without needing confirmation.  You can repeat the process for the repo browser and generic player client heal files.  This, combined with the auto-login that was added to HeroBlade in the last engine update, makes getting in a breeze.

General Discussion / Automated testing, anyone?
« on: Apr 04, 14, 07:37:44 AM »
I was passed this article recently:


It got me thinking of how we could potentially simulate a player to act as a an automated tester.  I am curious if anyone has been trying this in HeroEngine.

Scripting & Programming / New Timer Field
« on: Mar 29, 14, 05:56:58 PM »
With the new Timer field added by the Quartz update, timerTimeSource, I am a little bit confused.  In particular, there are now numerous places in the code where SYSTEM.TIME.NOW was changed to SYSTEM.TIME.LOCAL, such as in GUIA_Interpolate.  Can you explain why and is that something we should generally be doing?

GUI Creation / Enabling SelectedHover
« on: Mar 29, 14, 01:00:15 AM »
In redoing our changes to clean engine scripts after the engine update, I was reminded of an old fix we had made which I have never seen someone mention.

When using the GUI Editor and editing a texture (by clicking the [...] button) the selectedHover tab was always disabled.  So we just changed that gui to have that tab enabled and then can use it in the editor.

I have no idea if there was a reason it was disabled, but it seems fine with it enabled.

Developer Created Tutorials / Making Spec Editor Cells for Lists
« on: Sep 05, 13, 04:02:13 PM »
I guess this is not so much a tutorial, but some tips for making a gui that were requested of me and I figure I may as well share them with everyone.

The standard spec editor cells available do not natively support list fields (at least not very well and with no interaction), therefore you may well want to create a custom editor cell for list fields.

For one, you need to be pretty familiar with gui editing and how to work with standard controls such as text boxes, check boxes, buttons, and drop-down menus, specifically how to do both input and output with them.

What you would do is make a gui control with a panel as the parent control and have its class be a custom gui control class which is at least derived from _GUINodePropertyEditorCell, or possibly from a child class of that if you are basing it on a particular existing cell that uses a different class.

Use the gui editor to add appropriate controls to your cell based on your needs, and write the class script with at least the methods _setCollectionCellValue(), _setCollectionCellValueFromNodeRefByField(), _updateNodeFieldWithCellValue(), and _getCollectionCellValue().  Those first two are for getting values from the spec into the cell for  display while the others do the opposite, getting the value from the display into the spec.

So, for a list field, what we did was create a cell that has a drop-down menu for displaying the values in the list, and 3 buttons:  Add, Change, and Delete.  How you get the values into the drop-down depends on how the user will interact with the cell.  If they will only type stuff in, you would have a text box as well.  If the user clicks Add, the contents of the text box is added to the drop-down, while the Change and Delete buttons both deal with the value currently selected.  In some of ours, we have the Add and Change buttons open a dialog for selection by the user.  Specifically, one uses the $GUI.LaunchFileDialogForControl() method for choosing a file from the repository, and another uses the public function _createSpecSelector() of the _GUISpecSelector class to allow selection of a spec from another oracle.  That last one is very handy for having specs reference specs in other oracles.

When you read or write the cell, you deal with the contents of that drop-down since that represents the list contents.

There are many other ways it could be done.  What it boils down to is that you need a cell that can represent the contents of a list and some controls that allow the user to manipulate that list, as well as the back-end code to read and write the cell.

Finally, you need to specify that your cell be used for a particular field.  That happens in the shared function _createNodePropertyEditorCellForField() of the client-side spec class.  For instance, for an integer field if you want to use a numeric up-down instead of the standard text box for a field called "activationCost"

Code: [Select]
shared function _createNodePropertyEditorCellForField( fieldName as String ) as NodeRef of Class _GUINodePropertyEditorCell
  editCell as NodeRef of Class _GUINodePropertyEditorCell
  when tolower( fieldname )
    is "activationcost"
      editCell = CreateNodeFromPrototype( "_NodePropertyEditorCellNumericField" )
      editCell.build = true
  return editCell

In the code after building it, you might perform any number of customizations to the editCell as needed.

And for numeric fields, you may also want to use the shared function _nodePropertyEditorCellNumericFieldRange() in a spec to specify the range for a given field.

Once you are very familiar with creating interactive guis and how to make custom cells, you can let your imagination run wild on the types of interfaces you can add to spec editors to meet your specific needs.

Scripting & Programming / resizeable field watching script
« on: Jul 19, 13, 07:09:39 AM »
I recently noticed that for us the client-side field resizeable does not have _GUIBaseWindowClassMethods as its "Watching Script".  I am not sure if we did that some time in the past, or if it is that way for everyone else, as well.

If so, that field needs to have _GUIBaseWindowClassMethods as its "Watching Script" in order for it to work with the function fieldChangeNotification() in that script.

Scripting & Programming / Chat System Issues [Resolved]
« on: Jun 21, 13, 10:57:09 AM »
NOTE: As of the Quartz engine release, MOST of this fix is now part of the Clean Engine code.  No need to do this manually anymore, except the checking of the (possible) chat channel password when subscribing.

#1 Incorrect Subscriber Counts

For server class _ChatChannel, both methods _SubscribeToChatChannelThroughChatSystemCommand and _UnsubscribeFromChatChannelThroughChatSystemCommand change the _chatChannelSubscriberCount field regardless of whether the client was actually added or removed.

This is particularly noticable in the World chat channel.  _ChatHandler method _ChatSystemOnPlayerEnteredArea subscribes a player to the world channel every time that player enters a new area instance.   _AddClientDestination is smart enough not to add the player as a destination multiple times, but the channel's _chatChannelSubscriberCount is changed every time.

Likewise, in the less likely scenario that an attempt was made to unsubscribe a player from a channel they were not in, the subscriber count is nonetheless decremented.

Thus, both methods should probably check first if the player will actually be added or removed, and only if so would it change the subscriber count and generate the subscription event.  Better still might be to not use a separate integer field but instead use the size of the client destination list.

Developer Created Tutorials / Printout of Lobby System Status
« on: Jun 14, 13, 10:05:56 AM »
I have been working on the Lobby System for some days now, and to make the changes I make to the system more transparent, I wrote this method for the server $_LOBBY system node.
  • Create a server class.
  • Use the System Nodes Configuration GUI to glom it onto the server $_LOBBY system node.
  • Add the method below to that class methods script.
  • (Optional) Create a server command to call that method.

Code: [Select]
remote method EchoLobbySystem()
  where me is kindof _LobbySystem
    //First call the Control Instance:
    if not (me._IsLobbySystemControlInstance() or me._IsLobbySystemWorkerInstance())
      call area me._GetLobbySystemAreaID() instance 0 me.EchoLobbySystem()
    msg as String
    LLD as NodeRef of Class _LogicalLobbyDirectory
    LD as NodeRef of Class _LobbyDirectory
    lobby as NodeRef of Class _Lobby
    presenceLobby as NodeRef of Class _LogicalLobbyDirectoryPresenceLobby
    playerName as String
    LWL as NodeRef of Class _LightweightLobby

    //From the control instance, call each worker instance:
    if me._IsLobbySystemControlInstance()
      msg = "$RControl Instance$R LW Directories: "+me._lightweightLobbyDirectoryMap.length+"$R"
      foreach HWLDid in me._lightweightLobbyDirectoryMap
        msg += "  HW Lobby Directory ID: "+HWLDid+"$R"
        foreach LWLD in me._lightweightLobbyDirectoryMap[HWLDid]
          if LWLD != None
            msg += "   LW Dir ID: "+LWLD._lobbySystemEntityUniqueIdentifier+" _replicationGroupRef: "+LWLD._replicationGroupRef+"$R   Server Destinations:$R"
            //Cannot call LWLD._GetReplicationGroup() because LWLD is a proxy node (replicated here)
            //and _GetReplicationGroup is not labeled to work for proxied nodes.
      msg += "$R LW Lobbies: "+me._lightweightLobbyMap.length+"$R"
      foreach LWLid in me._lightweightLobbyMap
        LWL = me._lightweightLobbyMap[LWLid]
        if LWL != None
          msg += "  LW Lobby ID: "+LWL._lobbySystemEntityUniqueIdentifier+" _replicationGroupRef: "+LWL._replicationGroupRef+"$R   Server Destinations:$R"
          //Cannot call LWL._GetReplicationGroup() because LWL is a proxy node (replicated here)
          //and _GetReplicationGroup is not labeled to work for proxied nodes.
      //Call on each other instance:
      loop i from 1 to me._GetLobbySystemMaxWorkerInstances()
        call area me._GetLobbySystemAreaID() instance IntToId(i) me.EchoLobbySystem()
    msg = "$RLobby System Instance: "+GetInstanceNumber()+"$R--------------------------------$RLogical Lobbies:$R"
    //Now we are on a worker instance.
    foreach LLDid in me._lobbySystemLogicalDirectoryMap
      LLD = me._lobbySystemLogicalDirectoryMap[LLDid]
      msg += "--"+LLD._lobbyDirectoryName+"["+LLD._lobbySystemEntityUniqueIdentifier+"]$R"
      //Presence Lobbies:
      msg += "   Presence Lobbies:$R"
      foreach lobbyID in LLD._logicalLobbyDirectoryPresenceLobbyIDMap
        presenceLobby = LLD._logicalLobbyDirectoryPresenceLobbyIDMap[lobbyID]
        msg += "    "+presenceLobby._lobbyName+"["+presenceLobby._lobbySystemEntityUniqueIdentifier+"]$R     occupants: "+presenceLobby._lobbyOccupants.length+"/"+presenceLobby._lobbyPlayerCapacity+"$R"
        foreach pnid in presenceLobby._logicalLobbyDirectoryPresenceLobbyPlayerNames
          playerName = presenceLobby._logicalLobbyDirectoryPresenceLobbyPlayerNames[pnid]
          msg += "      "+playerName+"["+pnid+"]$R"
        msg += "    -----$R"
      msg += "   Directories ("+LLD._lobbyDirectoryMap.length+"):$R"
      foreach LDid in LLD._lobbyDirectoryMap
        LD = LLD._lobbyDirectoryMap[LDid]
        msg += "    "+LD._lobbyDirectoryName+"["+LD._lobbySystemEntityUniqueIdentifier+"]$R     lobbies ("+LD._lobbyDirectoryLobbyMap.length+"):$R"
        foreach Lid in LD._lobbyDirectoryLobbyMap
          lobby = LD._lobbyDirectoryLobbyMap[Lid]
          msg += "      "+lobby._lobbyName+"["+Lid+"]$R       occupants: "+lobby._lobbyOccupants.length+"/"+lobby._lobbyPlayerCapacity+"$R"

      //Lobby public (displayable) data:
      //TODO (?)
      msg += "***********************************$R"

This is just something I wrote off the top of my head, and could probably be improved.  I have not tested it fully in all cases, particularly when players are in lobbies.

Feel free to use it.

General Discussion / Lobby System Issues
« on: Jun 12, 13, 09:38:07 AM »
EDIT: The Quartz engine update now includes both fixes #1 and #2. The "edit" part at the end of #2 is probably not needed since I believe that function does get called by $_LOBBY, so "me" should still work there.
Upon checking, that instance of EventRaised() is being called on a _PlayerDataSystemAreaEventListener node which is what "me" will refer to.  So it does actually need to be "$_LOBBY" instead of "me".

#1 Missing functionality
The server _LobbySystemClassMethods script has code to support chat commands, however it is done using HE_ProcessCommandInput(), which means it must be registered as a chat command via
Code: [Select]
/register add /helobby script="_LobbySystemClassMethods"The chat commands for the Chat System are made available by including /hechat in _CommandHandler methods _ProcessCommand and _isHECommand, so /helobby was perhaps meant to go in there as well?

Also, the lobby HE_ProcessCommandInput method does not make use of HE_CommandUsage, even though that function is defined.

#2 Code Errors
Within the lobby chat commands, it uses "me" as the listener to method calls such as _RequestCreateLobby. Below in that script is a definition for EventRaised, so I presume that the lobby system node was intended to be the listener.  However since HE_ProcessCommandInput is a function, "me" refers to _CommandHandler and so $_LOBBY should be entered as the listener rather than "me".

That then requires the server _LobbySystem class definition to have ObsListener as a parent.

Likewise, in server _LobbySystemClassMethods the function for handling player events:
Code: [Select]
//Handles player event messages in order to properly remove players from lobbies when they log out.
shared function EventRaisedNotify(obsSubject as NodeRef of Class ObsSubject, obsListener as NodeRef of Class ObsListener, data as NodeRef)
  handled as Boolean
  if HasMethod($_LOBBY, "HE_LobbySystemEventRaisedNotify")
    handled = me.HE_LobbySystemEventRaisedNotify(obsSubject, obsListener, data)
needs to use $_LOBBY in place of "me".

General Discussion / Social System Issues
« on: May 22, 13, 05:26:17 AM »
I made this topic separate from the other Social System topic so this can focus more on issues of the system itself while the other can focus more on how to use it.

When updating our override class for the $INPUT system node, I was reminded of this workaround that I had to put in before.

The desire was to use our own prototype in the method _GetDragSelectControl() for the dragSelectControl instead of "_dragSelectRectangle".

Code: [Select]
// Gets the default DragSelect GUIControl which will be rendered during DragSelect operations.
// The returned control must inherit from _AbstractInputDragSelectionListener.
method _GetDragSelectControl() as NodeRef of Class GUIControl
  dragSelectControl as NodeRef of Class GUIControl
  handled as Boolean
  if HasMethod(me, "HE_GetDragSelectControl")
    handled = me.HE_GetDragSelectControl(dragSelectControl)
      if handled
        assert(dragSelectControl is kindof _AbstractInputDragSelectionListener, "Error! Drag selection control(" + dragSelectControl + ") does not inherit from _AbstractInputDragSelectionListener!")
  if not handled
    dragSelectControl = FindGUIControlByName($GUI._getBaseGameLayer(), "_dragSelectRectangle")
    if dragSelectControl = None
      dragSelectControl = CreateNodeFromPrototype("_dragSelectRectangle")

      dragSelectControl.build = true
      dragSelectControl.layer = $GUI._getBaseGameLayer().getMyLayerName()
  return dragSelectControl

So we added a definition for HE_GetDragSelectControl() to our class, but the problem is that in _MethodCallbacksClassMethods, it is defined with this form:
Code: [Select]
method HE_GetDragSelectControl(dragSelectControl as NodeRef of Class GUIControl) as Boolean
  return false

Since dragSelectControl  is passed in using "as", we cannot assign to it in order to use our own prototype like so:
Code: [Select]
dragSelectControl = CreateNodeFromPrototype("Voz_DragSelectRectangle")
To workaround this, we changed the definition of HE_GetDragSelectControl() in _MethodCallbacksClassMethods to use "references" for the dragSelectControl.  I didn't like having to change  _MethodCallbacksClassMethods, but I could not see a way around this.  Hopefully that can be added to the Clean Engine in the future.  An alternative would be an override method that allows us to specify the name of the prototype to use.


I did just recently think of another way to work around this.  By ensuring that our control is added to the $GUI._getBaseGameLayer() and calling it "_dragSelectRectangle" and then returning FALSE, the remaining default code in _GetDragSelectControl() will find that control using FindGUIControlByName() and so not change it and also will correctly return dragSelectControl with its value filled in.

But naturally, that is a less than ideal situation and still would not work if HE_GetDragSelectControl() returned true.

General Discussion / [Fixed in sapphire m] SubstituteFont
« on: Apr 11, 13, 01:35:10 PM »
We have been using SubstituteFont(GAMETEXT, "GUI\VOZ\Fonts\biondi.ttf") with the path to our ttf font in our repository since the Sapphire release, and it was working fine.

Just recently I am finding that the font is no longer loading properly.  There are error messages in the Console upon first getting to the Char Selection area (we are calling SubstituteFont in the method HE_FirstChance).  These are the errors:
12:56:18: !ERROR!Fonts:Font file does not exist C:\Windows\Fonts\GUI\VOZ\Fonts\biondi.ttf
12:56:18: !ERROR!System:Font face "GUI\VOZ\Fonts\biondi.ttf" is not loaded, or is a non-TrueType font.
12:56:18: !ERROR!Fonts:Error - Missing font file format C:\Windows\Fonts\GUI\VOZ\Fonts\biondi.ttf
12:56:18: !ERROR!Fonts:Font file does not exist C:\Windows\Fonts\GUI\VOZ\Fonts\biondi.ttf

Note that even though we specify the path as relative to the repo, the error message says it looked locally on the C drive.

As I said it was working fine, and we did not change the font file nor the code loading it.  biondi is actually a different font file I uploaded to test whether the issue was only with the font we had been using, but the results are the same.  We know the fonts are both TrueType.  Also, I had another team member try it out on their computer with the same results, so I know it is not just happening for me.

Is it possible anything changed recently with regards to how the engine loads fonts?

This is maybe not so much a tutorial but some code snippets.

The issue:
Prototypes are useful for storing named constants used in code due the ease with which they can be accessed via the $PROTONAME convention.  Often times, you may wish to use the same constant on both the server and client side, but manually updating both can be a chore.

Find a more automatic way to synchronize values in matching prototypes between server and client side so that one need only update the server side.

I explored this in the past.  You can use a combination of two external functions on the client side: UnmarshalPrototype() and SendPersistedClientCLI(), along with the server function MarshalPrototype() and script XMLParser.

Basically, you have two parts to update: the local copy of the prototype (the thing that would be changed if you used the CLI command /mp) and the  persisted version (modified via |mp).  You could only do the persisted one if you like, but then you would not see those changes until your local version was recreated.

(NOTE: In the last line where I called the client, VOZ is just the name of the script I had the function in.)
Code: (hsl) [Select]
//Mostly useful:
//This affects both the local client copy for as long as they are logged in,
//and the changes do persist to other sessions or to other clients,
//i.e. this is like setting values via both:
//  /mp
//  |mp
//Both server and client side must have the same prototype name defined
//and the underlying classes must have matching fields.
//not all fields work in SendPersistedClientCLI, such as lists,
//lookuplists, and some other complex types.
function SyncClientProtoToServer(protoName as String, playerID as ID)
  var proto = GetPrototype(protoName)
  if proto != None
    //Get the marshalled prototype for changing the local copy:
    var marshalProto = MarshalPrototype(proto)
    //Get the master list of field names and values for changing the persisted copy:
    var fields = XMLParser:Parse(marshalProto)
    var masterFieldList = XMLParser:DeepParse(fields["value"])

    call client playerID VOZ:DuplicateProtoFields(protoName, marshalProto, masterFieldList)

Code: (hsl) [Select]
remote function DuplicateProtoFields(protoName as String, marshProto as String, masterFieldList as List of LookupList indexed by String of String)
  //for the local temporary copy; UnmarshalPrototype is like using /mp
  proto as NodeRef = GetPrototype(protoName)
  UnmarshalPrototype(marshProto, proto, true)
  //for the permanent copy; SendPersistedClientCLI is like using |mp (but "|" is NOT to be used in this string)
  foreach field in masterFieldList
    var str = "mp "+protoName+";"+field["name"]+"="+field["value"]
    println(str) //echo the command to the Console for observation

To make it easy to use, I created a server chat command "voz" so I can enter into chat lines such as:
/voz syncproto myProtoName
where that command calls the function and syncs the prototype on my client.

NOTICE: The documentation comment before the server version states that this only works well for stanard one-dimensional fields, such as string, integer and boolean.  I had trouble making it work for more complex fields like list or class.  Even when I tried to build commands for SendPersistedClientCLI() using "mlp" I had problems that I didn't bother taking the time to try to resolve.

But if these are for basic named constants, such as numeric limits, this may suffice.

More specialized functions can be written to deal with lists and that will follow.

Pages: 1 2 [3] 4 5 6