Thursday 3 May 2012

Multi-Line Names

We still have a problem with long names - they come out too small.
If we show less of it, it defeats the point of having a long name to start with.

The stone axe could be addressed by putting axe on a second line and is quite easy to do as it has a space in the middle.
function ReplaceSubString( thisString$ , thisOld$ , thisNew$ )
   thisLen = len( thisOld$ ) 
   thisStart = FindSubString( thisString$ , thisOld$ , 1 )
   while thisStart > 0
      thisString$ = left( thisString$ , thisStart - 1 ) + thisNew$ + right( thisString$ , len( thisString$ ) - (thisStart + thisLen - 1) )
      thisStart = FindSubString( thisString$ , thisOld$ , 1 )
   endwhile
endfunction thisString$
This utility function replaces all occurences of one string with another. The logic is fairly straight forward.
  • It uses FindSubString() to check if the old string exists.
  • If so, the main string is split into three, with the old string being the middle piece.
  • The string is re-built with the left piece, the new string and the right piece.
  • It repeats until the old string is no longer found.
By using it on the name string, we can look for a space and replace it with a line break.

In the SetPanelSprite() function, replace the following code.
setTextString( thisNameText , ObjectName( thisObject ) )
setTextString( thisShadText , ObjectName( thisObject ) )
With this new version.
thisString$ = ReplaceSubString( ObjectName( thisObject ) , " " , chr(10) )
setTextString( thisNameText , thisString$ )
setTextString( thisShadText , thisString$ )
Which passes the object name through the new ReplaceSubString() function, swapping any space for character code 10 - which is a line-feed.
Now any name containing a space will be split into multiple lines.

The stone axe text looks a good size now, but we don't know what size that is.  To find out, we can add a bit of code to hijack the routine, putting the text size above the name temporarily.

Before the first setTextY() line, add the following code.
// Add Text Size to Name temporarily
thisString$ = str(getTextSize( thisNameText ),2) + chr(10) + thisString$
setTextString( thisNameText , thisString$ )
setTextString( thisShadText , thisString$ )
// End of Additional Code
Which now gives us values for each piece of text indicating it's size, any less than 24.00 have been scaled down.
Since this code comes after the resize code, the values do go outside the icon area.

Looking at the results, I think a size of 18 would probably produce a better start size.
thisSize# = IconSize() * 0.28125
What do you think?
We can improve visibility on this in a number of ways.
  • Adjust the panel background
  • Adjust the name and shadow colours
  • design the icons with a standard background behind the name.
But these can be fine tuned when we have more icons to test the impact.

Panel Icon Text Size

There are a number of things not quite right with the text so far.
  • stone axe simply doesn't fit
  • resource doesn't fit either
  • stone is too low 
These are three issues all caused by the size of the text.

What might sound strange is that to address this, the first thing I'm going to do is make the text larger.  By changing the text size from 15 to 25 in the SetPanelSprite() function.
At this size, "tree" (4 characters) just about fills the icon width and "stone" (5 characters) is too wide, this has effectively set a maximum for the text size.

What I'm actually looking at here, though is how readable the text is.  If it's no good at it's maximum size, there is no point looking at other sizes.

Though not great, it is readable.  Darkening the panel will improve this. So initially, the text size will 24 - because its an even number and will make "stone" slightly smaller.

But if we change the number of columns later, the icon size will change, so we have to do it in a way which will also change the text.
thisIconMax# = IconSize() - 4.0
thisSize# = IconSize() * 0.375
setTextSize( thisNameText , thisSize# )
Local variable thisIconMax# is the maximum width of the text - it allows for a 4 pixel gap between names.

The current icon size is 64 pixels and we want a text size of 24, the value we multiply the icon size by ( 0.375) is worked out from 24 / 64.

Once the text size has been set, we can measure the resulting width of the text object using getTextTotalWidth().
thisScale# = thisIconMax# / getTextTotalWidth( thisNameText )
By dividing thisIconMax# by the width of the text object, we get a scale value.
  • higher than 1.0 indicates thisIconMax# is bigger than the text.
  • exactly 1.0 indicates they are the same width
  • lower than 1.0 indicates thisIconMax# is smaller than the text.
In the third case, the text needs to be shrunk.  The new size is the original value ( 0.375 x the icon size ), multiplied by the scale value.
if thisScale# < 1.0 then setTextSize( thisNameText , thisSize# * thisScale# )
Then we use the size of one object to set the size of the other.
setTextSize( thisShadText , getTextSize( thisNameText ) )
Lets look at an example.

If a text size of 24 produced text which was 80 pixels wide, then the scale value would be (64.0 - 4.0) / 80.0 or 60.0 / 80.0 = 0.75

As this is less than 1.0, the the new text size would be 24 * 0.75 = 18

This would produce a width of 80 * 0.75 or 60

Since the height of the text has been changed, the Y position of the text needs to be reset to align it with the bottom of the icon.
setTextY( thisShadText , getSpriteY( thisSprite ) + IconSize() - getTextTotalHeight( thisNameText ) )
setTextY( thisNameText , getTextY( thisShadText ) - 1 )
Now the text will shrink if necessary to fit. This uses getTextTotalHeight() to allow for multi-line names, which is the next step.

Panel Icon Text

The depth and position of the sprite is set in the PositionPanelIcons() function, so the depth and position of the text should also be set there.
setSpritePosition( panelIconSprite( icon ) , x , y )
setSpriteDepth( panelIconSprite( icon ) , iconDepth )
thisX# = x + IconSize() / 2.0
thisY# = y + IconSize() - getTextTotalHeight( panelIconNameText( icon ) )
setTextPosition( panelIconShadText( icon ) , thisX# , thisY# )
setTextPosition( panelIconNameText( icon ) , thisX# - 1 , thisY# - 1 )
setTextDepth( panelIconShadText( icon ) , iconDepth - 1 )
setTextDepth( panelIconNameText( icon ) , iconDepth - 2 )
The text is centrally aligned so the X position is middle of the icon. The Y position is the bottom of the icon less the height of the text object.  The shadow is set to the resulting position and the name is positioned 1 pixel up and left.

The depth of the text items needs to be above the icon image with the shadow first, then the text on top.

The "drawing" of the icon images is done in the SetPanelSprite() function, this is where we will set the text for the icons too. Since this is called by both the group and object routines, this will display text for both.

This (with comments added) is what we have so far.
function SetPanelSprite( thisPos , thisFrame )
   thisSprite = panelIconSprite( thisPos )
   if thisFrame = 0
      // No Icon - Clear Image and Reset Animation
      setSpriteImage( thisSprite , 0 )
      clearSpriteAnimationFrames( thisSprite )
      // Clear Text

   else
      // Icon - Set Image and Animation Frame
      setSpriteImage( thisSprite , IconImage() )
      setSpriteAnimation( thisSprite , 64 , 64 , 252 )
      setSpriteFrame( thisSprite , thisFrame )
      // Set Text

   endif
endfunction
The // Clear Text and // Set Text comments show where the text changes go.

At the start of the function, where we get the sprite reference, we also need a few more bits of information.
thisNameText = panelIconNameText( thisPos )
thisShadText = panelIconShadText( thisPos )
This gets the references for both text items.

The code to clear the text is simply.
// Clear Text
setTextString( thisNameText , "" )
setTextString( thisShadText , "" )
Since a null string won't show, we don't need to do anything extra here.

If there is a frame, then we need to find the object it belongs to.
// Set Text
thisObject = FindPanelObjectByFrame( thisFrame )
This looks like a new function, and it is in a way, but we only need a slight change from what we have in the FindObjectByPanelSprite() function, which starts by getting the frame of the sprite. If we could jump in here - as we have the frame - we could use the existing code.
function FindObjectByPanelSprite( thisSprite )
   thisObject = 0
   thisFrame = getSpriteCurrentFrame( thisSprite )
   thisString$ = GameGroups()
   if PanelGroup() > 0 then thisString$ = thisString$ + ObjectDetail( PanelGroup() )
...
To do this we split the function at the point we want to enter from - after the frame has been obtained - and put our new function name in there.

Above it we will add some code to link the old call to the new one.

In practical - less editing - terms, we insert a block of code immediately after the old function first line.
   thisFrame = getSpriteCurrentFrame( thisSprite )
   thisObject = FindPanelObjectByFrame( thisFrame )
endfunction thisObject 

function FindPanelObjectByFrame( thisFrame )
This creates a smaller version of the FindObjectByPanelSprite() function and starts a new function FindPanelObjectByFrame() to pick up the old code.

Delete the now duplicate line from the old code.
thisFrame = getSpriteCurrentFrame( thisSprite )
and the result should look like this.
function FindObjectByPanelSprite( thisSprite )
   thisFrame = getSpriteCurrentFrame( thisSprite )
   thisObject = FindPanelObjectByFrame( thisFrame )
endfunction thisObject

function FindPanelObjectByFrame( thisFrame )
   thisObject = 0
   thisString$ = GameGroups()
   if PanelGroup() > 0 then thisString$ = thisString$ + ObjectDetail( PanelGroup() )
...
This may all be changed back when the finished routine is optimised.

Back at the SetPanelSprite() function, we now use the name of the retrieved object to set the text of the icon.
// Set Text
thisObject = FindPanelObjectByFrame( thisFrame )
setTextString( thisNameText , ObjectName( thisObject ) )
setTextString( thisShadText , ObjectName( thisObject ) )
This just dumps it in as is for now.

Finally we set the size of the text to an arbitrary value of 15 to give an idea of how it looks.
setTextSize( thisNameText , 15 )
setTextSize( thisShadText , 15 )
Now a test run gives us our first look at the icon text.
A few issues to deal with, but nothing unexpected.

Panel Icon UDT

All the object loaded have names, but these are not currently used anywhere.

To make the objects in the panel more easily identified, we can add the name to the bottom of each icon.

Since we don't know in advance the colour of the icon, we need a way to make the text visible over any colour.

This is done by putting something behind the text that is a contrasting colour, either a box or - in this case - a shadow.

If we make the text a bright colour such as yellow, and put a black shadow behind it, it should be easy against any icon.  If this does not work, we will try something else.

To create the text, we need two text objects - one for the text, the other for it's shadow.  These will be used with the sprite currently stored in the panelIconSprite[] array.

Rather than have more arrays, the best option is to move from a dedicated sprite array to a UDT array for the icon.
type panelIconType
   sprite
   nameText
   shadText
endtype
Now we can create a new array called panelIcon[] in the Initialise() function to replace panelIconSprite[].

The initialise code is changed from.
dim panelIconSprite[ panelIconMax() ]
   for icon = 1 to panelIconMax()
      panelIconSprite[ icon ] = createSprite( 0 )
      setSpriteSize( panelIconSprite[ icon ] , IconSize() - 4, IconSize() - 4)
      setSpriteColor( panelIconSprite[ icon ] , 255 , 255 , 255 , 127 )
   next icon
To this.
dim PanelIcon[ panelIconMax() ] as panelIconType
   for icon = 1 to panelIconMax()
      panelIcon[ icon ].sprite = createSprite( 0 )
      setSpriteSize( panelIconSprite( icon ) , IconSize() - 4, IconSize() - 4)
      setSpriteColor( panelIconSprite( icon ) , 255 , 255 , 255 , 127 )
      panelIcon[ icon ].nameText = createText( "" )
      panelIcon[ icon ].shadText = createText( "" )
      setTextAlignment( PanelIconNameText( icon ) , 1 )
      setTextAlignment( PanelIconShadText( icon ) , 1 )
      setTextColor( PanelIconNameText( icon ) , 255 , 255 , 128 , 255 )
      setTextColor( PanelIconShadText( icon ) , 0 , 0 , 0 , 255 )
   next icon
Which now uses individual fields for all the values needed for this icon. The text alignment and colour is also set here.

A series of return functions are created.
// Return functions - PanelIcon Array

function PanelIconIsValid( thisIcon )
   thisBool = ( thisIcon > 0 and thisIcon <= PanelIconMax() )
endfunction thisBool

function PanelIconSprite( thisIcon )
   if PanelIconIsValid( thisIcon )
      thisSprite = panelIcon[ thisIcon ].sprite
   else
      thisSprite = -1
   endif
endfunction thisSprite

function PanelIconNameText( thisIcon )
   if PanelIconIsValid( thisIcon )
      thisText = panelIcon[ thisIcon ].nameText
   else
      thisText = -1
   endif
endfunction thisText

function PanelIconShadText( thisIcon )
   if PanelIconIsValid( thisIcon )
      thisText = panelIcon[ thisIcon ].shadText
   else
      thisText = -1
   endif
endfunction thisText
Now to keep the old functions working, every use of the panelIconSprite[] array needs to be changed.  There should be.
  • Two in PositionPanelIcons().
  • Three in SetPanelIcons().
  • One in SetPanelSprite().
Where the value is put into the array, this needs changing to panelIcon[].sprite.

Where a value is read from and array (which will be most of them), it is changed to the function call PanelIconSprite().

In both cases, the variables used as the parameter (the thing in square brackets) is kept the same.  See the  initialise code for examples.

Perhaps now it's apparent why so often we copy the sprite references from the arrays into local variables like thisSprite.  It is to reduce changes like this.

Panel Group Selection

Now we have the panel displaying some icons, it's time to get back to the functions that deal with pointer selection, so we can change the displayed group.

Game input is handled by the checkGamePointer() function, let's review what it does as a reminder.

An outer structure checks the state of the pointer, Just Pressed, Just Released or Held.

Within the first part - getPointerPressed() - the Y position of the pointer is checked against two sets of conditions.
  • If it's over the panel text object, the panel state is changed between opened and closed.
  • If Y is less than this, the work area is processed.
We now want to add the third possibility - that is it over the panel - using the else command.  With a few comments added, this makes the start of that function look like this.
function checkGamePointer()
   if getPointerPressed() > 0
      if PointDY() >= getTextY( panelText() ) and PointDY() < getSpriteY( panelSprite() )
         // Pointer is over Panel Control Text
         ToggleTrayOpen()
      elseif PointDY() < getTextY( panelText() )
         // Pointer is over Work Area
         touch.spritePicked = getSpriteHit( PointWX() , pointWY() )
         if SpritePicked() > 0
            touch.objectPicked = FindWorkObjectBySprite( SpritePicked() )
            if ObjectPicked() > 0
               touch.pickOffsetX# = PointWX() - getSpriteXByOffset( SpritePicked() )
               touch.pickOffsetY# = PointWY() - getSpriteYByOffset( SpritePicked() )
            endif
         endif
      else
         // Pointer is over Panel

      endif
   elseif getPointerReleased() > 0
We will now add the code to the new block indicated by // Pointer is over Panel.

The code almost the same as the work area code, but will be dealing with display coordinates rather than world coordinates.
// Pointer is over Panel
touch.spritePicked = getSpriteHit( PointDX() , pointDY() )
if SpritePicked() > 0
   touch.objectPicked = FindObjectByPanelSprite( SpritePicked() )
   if ObjectPicked() > 0
      if ObjectisGroup( ObjectPicked() ) = 0
         // Object is not a group object (so can be dragged)
         touch.pickOffsetX# = PointDX() - getSpriteXByOffset( SpritePicked() )
         touch.pickOffsetY# = PointDY() - getSpriteYByOffset( SpritePicked() )
         // Create work object for dragging

      else
         // Object is a group object
         if ObjectPicked() <> PanelGroup()
            panel.group = ObjectPicked()
            SetPanelIcons()
         endif
         touch.objectPicked = 0
      endif
   endif
endif
As before, the .objectPicked field of the touch global is used to store the object and .pickOffsetX# and .pickOffsetY#, the offset of the pointer from the middle of that object.  However in this case, we check if the object is a group object before storing these values.

This also calls a new function FindObjectByPanelSprite(), where the work area used FindWorkObjectBySprite(), because panel icons are not work objects and so a different routine is needed.
function FindObjectByPanelSprite( thisSprite )
   thisObject = 0
   thisFrame = getSpriteCurrentFrame( thisSprite )
   thisString$ = GameGroups()
   if PanelGroup() > 0 then thisString$ = thisString$ + ObjectDetail( PanelGroup() )
   for i = 1 to len( thisString$ ) / 2
      thisCode$ = mid( thisString$ , i * 2 - 1 , 2 )
      thisObject = Base96Decode( thisCode$ )
      if thisObject > 0
         if ObjectIcon( thisObject ) = thisFrame then exit
         thisObject = 0
      endif
   next i
endfunction thisObject
This uses the getSpriteCurrentFrame() command to get the current frame of the sprite provided.  This corresponds to the .icon field of the object array so can be used to locate the correct object.

We don't need to check the full list of objects, just the ones currently shown.

Next, it creates a string of Base96 codes by combining the string of groups with members - GameGroups(), with the codes for the member objects - ObjectDetail(), of the currently selected group - PanelGroup().

The resulting string contains Base96 codes for all the icons currently shown in the panel.

This list is processed in the normal manner, extracting two characters, decoding them and then checking to see if the icon for that object matches the frame we are looking for.

Back with the pointer code, once the object is identified as a group object, then it is checked to make sure it is different from the currently selected group.

If so, the current group is changed and the panel redrawn.

Finally, the selected object is reset to zero so the user cant drag the group icon by keeping the pointer pressed.

Now the two groups can be selected on the panel and the correct icons will be shown.

Debug Log File

When there is too much going on to show everything on screen, details of what is going on can be sent to a log file that can be checked later.

This can be done with a very simple function.
function DebugOut( thisString$ )
   thisFile = opentowrite( "debug.txt" , 1 )
   writeline( thisFile , thisString$ )
   closefile( thisFile )
endfunction
This opens a file called debug.txt in append mode (the 1 parameter after the name), writes a string of text to the file, then closes the file.

At the start of the program - just before the call to Initialise(), we now check for and remove any old debug log.
// *** DIAGNOSTIC LOGGING ***
if getFileExists( "debug.txt" ) then deleteFile( "debug.txt" )
DebugOut( "Calling Initialise()" )
// *** DIAGNOSTIC LOGGING *** 
This ensures that each run of the program clears the file and starts again.  It then uses the new function to log the fact that it is calling the Initialise() function.

The comment lines before and after are to draw attention to the presence of this code to make it easier to find and remove later.

The program will run as before, but now a file will be created called debug.txt which will look like this.
Calling Initialise()
AGK uses a designated write area, which varies from system to system.  On Windows this is in an AGK folder within your My Documents folder, with a sub folder for the project and it's media.

On my system, the file is in.

S:\Users\Marl\Documents\AGK\Build-It\media

With the file being created ok, we can now set about locating the problem by putting diagnostic logging calls at key places in the LoadObjectData() function.

At the very start, we add lines which indicate the start of the Load function and the parameter passed in quotes.
function LoadObjectData( thisName$ )
   // Pass the name without extension

// *** DIAGNOSTIC LOGGING ***
DebugOut( "LoadObjectData() called with " + chr(34) + thisName$ + chr(34) )
// *** DIAGNOSTIC LOGGING ***
After the closefile() command, we indicate how many lines were read.
// *** DIAGNOSTIC LOGGING ***
DebugOut( "Lines read : " + str( thisArraySize ) )
// *** DIAGNOSTIC LOGGING ***
At the end of the for i=1 to topObject() loop, just before the last endif, we output the object details stored in the array.
// *** DIAGNOSTIC LOGGING ***
DebugOut( "Object : "+str(i)+" = "+chr(34)+ObjectName(i)+chr(34)+", Icon: "+str(ObjectIcon(i))+", Detail: "+chr(34)+ObjectDetail(i)+chr(34)) 
// *** DIAGNOSTIC LOGGING ***

         endif
      next i
      // Recipes Go Here
This shows the initial values set for the name, icon and detail strings of each object.

At the start of the group membership section, we log the fact.
      // Group Memberships

// *** DIAGNOSTIC LOGGING ***
DebugOut( "Group Memberships") 
// *** DIAGNOSTIC LOGGING ***

And within the group loop, we log if a group is found with members.
      for i=1 to topObject()
         if ObjectIsGroup( i ) > 0 and ObjectDetail( i ) <> ""

// *** DIAGNOSTIC LOGGING ***
DebugOut("Group "+str(i)+" has members "+chr(34)+ObjectDetail( i )+chr(34))
// *** DIAGNOSTIC LOGGING ***

            game.groupObject$ = game.groupObject$ + Base96Encode( i )
Following the FindObjectByName() call, we log the used name and resulting object reference.
               thisObject$ = mid( thisString$ , thisStart , thisComma - thisStart )
               // convert to base96
               thisObject = FindObjectByName( thisObject$ )

// *** DIAGNOSTIC LOGGING ***
DebugOut("..Name "+chr(34)+thisObject$+chr(34)+" is object "+str( thisObject ))
// *** DIAGNOSTIC LOGGING ***

               if thisObject > 0 then thisCoded$ = thisCoded$ + Base96Encode( thisObject )
This covers the main stages of the conversion of the group memberships and should give a better idea of the problem.

When we run the program, and then check debug.txt, we see this.
Calling Initialise()
LoadObjectData() called with "standard"
Lines read : 14
Object : 1 = "tool", Icon: 1, Detail: "stone axe"
Object : 2 = "resource", Icon: 2, Detail: "tree,stone"
Object : 3 = "device", Icon: 3, Detail: ""
Object : 4 = "job", Icon: 4, Detail: ""
Object : 5 = "process", Icon: 5, Detail: ""
Object : 6 = "storage", Icon: 6, Detail: ""
Object : 7 = "tree", Icon: 7, Detail: "Home to Birds and Monkeys"
Object : 8 = "stone", Icon: 8, Detail: "Small hard piece of planet"
Object : 9 = "stone axe", Icon: 9, Detail: "Primative chopping device"
Object : 10 = "log", Icon: 10, Detail: "Rough tree part, might burn well"
Object : 11 = "kindling", Icon: 11, Detail: "Basis of a good fire"
Object : 12 = "spark", Icon: 12, Detail: "Warning - fire Hazard"
Object : 13 = "flame", Icon: 13, Detail: "Fire's younger sibling"
Object : 14 = "fire", Icon: 14, Detail: "Good source of heat and light"
Group Memberships
Group 1 has members "stone axe"
..Name "stone axe" is object 0
Group 2 has members "tree,stone"
..Name "tree" is object 0
..Name "stone" is object 0
Which shows the problem is that all object names are returning object value zero. The problem is in the FindObjectByName() function.

Within that function, we log the fact that the function has been called, the parameter passed and the result.
function FindObjectByName( thisName$ )
   thisName$ = upper( thisName$ )
   for thisObject = 1 to TopObject()
      if upper( ObjectName( thisObject )) = thisName$ then exit
   next thisObject

// *** DIAGNOSTIC LOGGING ***
DebugOut("FindObjectByName() Called with "+chr( 34 )+thisName$+chr(34)+", Result: "+str(thisObject))
// *** DIAGNOSTIC LOGGING ***

   if thisObject > TopObject() then thisObject = 0
endfunction thisObject
Which now changes the bottom part of the log file to
Group Memberships
Group 1 has members "stone axe"
FindObjectByName() Called with "stone axe", Result: 15
..Name "stone axe" is object 0
Group 2 has members "tree,stone"
FindObjectByName() Called with "tree", Result: 15
..Name "tree" is object 0
FindObjectByName() Called with "stone", Result: 15
..Name "stone" is object 0
What is interesting here is that the logged output shows the parameter passed in each case as lower case, yet the first call of the function changes this to upper case using the Upper() command.

Testing the Upper() command with some test code such as
testString$ = "Hello World"
print( chr(34) + upper( testString$ ) + chr(34) )
which returns "", shows that it is this command which is the problem.

This was logged as issue 282 on the AGK reporting board ( code.google.com ).

The workaround for this - to get the project running - is to change the strings to lower case, rather than upper case.  The effect is the same, we simply want the case to be the same in the comparison.
This involves changing all references to upper() to lower().
function FindObjectByName( thisName$ )
   thisName$ = lower( thisName$ )
   for thisObject = 1 to TopObject()
      if lower( ObjectName( thisObject )) = thisName$ then exit
   next thisObject
With this change, the app is back to its working state.
Now the detail fields for objects 1 and 2 are populated correctly and the stone axe is once again displayed.

The debug.txt file also shows everything as it should be.
Calling Initialise()
LoadObjectData() called with "standard"
Lines read : 14
Object : 1 = "tool", Icon: 1, Detail: "stone axe"
Object : 2 = "resource", Icon: 2, Detail: "tree,stone"
Object : 3 = "device", Icon: 3, Detail: ""
Object : 4 = "job", Icon: 4, Detail: ""
Object : 5 = "process", Icon: 5, Detail: ""
Object : 6 = "storage", Icon: 6, Detail: ""
Object : 7 = "tree", Icon: 7, Detail: "Home to Birds and Monkeys"
Object : 8 = "stone", Icon: 8, Detail: "Small hard piece of planet"
Object : 9 = "stone axe", Icon: 9, Detail: "Primative chopping device"
Object : 10 = "log", Icon: 10, Detail: "Rough tree part, might burn well"
Object : 11 = "kindling", Icon: 11, Detail: "Basis of a good fire"
Object : 12 = "spark", Icon: 12, Detail: "Warning - fire Hazard"
Object : 13 = "flame", Icon: 13, Detail: "Fire's younger sibling"
Object : 14 = "fire", Icon: 14, Detail: "Good source of heat and light"
Group Memberships
Group 1 has members "stone axe"
FindObjectByName() Called with "stone axe", Result: 9
..Name "stone axe" is object 9
Group 2 has members "tree,stone"
FindObjectByName() Called with "tree", Result: 7
..Name "tree" is object 7
FindObjectByName() Called with "stone", Result: 8
..Name "stone" is object 8
With that confirmed as working, we can now go through and remove all the diagnostic logging code and the block added to print the details in the main loop.

Though I'm sure we'll be seeing it again soon so the DebugOut() function can stay.

Tracking down this issue was made so much easier by the fact the program is modular, and this also meant that the change to rectify it was limited to just one place.

AGK V1070 Bug Hunt

A new version of AGK was released on Tuesday ( 1st May 2012 ), Version 107.0.

This has been promised for some time, it adds a number of new commands and fixes a number of issues with the last release (which was version 106.5).

After removing the old version and installing the new one, the Build-It project was recompiled and tested to make sure it still works.

While most of the app appears to work normally, now the panel no longer displays the object icon as it did before.
It appears that the new version has introduced a "bug".

There are two approaches to this.
  • Revert back to the old version and wait for a fix
  • Find out why it no longer works and report the bug.
The problem with the first option is that the developers of AGK may not yet be aware of the bug, they will have tested the new commands and fixes for previous bugs, but this may not be part of that code.

If everyone simply reverts back to the old version, without looking into the cause, it may be some time before the bug is found - let alone fixed.

As this project worked fine before the upgrade, we know for certain it's the change to AGK which has caused it, we don't know specifically what is not working, but we have an idea where it will be found.

It is something to do with the objects being displayed.

The fact that some icons are visible, that these are in the right place and show the right icon and the fact that only the groups with members are shown indicates that the code involved in this is probably still OK.

This eliminates the following from possible causes.
  • Object and Image Load - as some objects are correct and all are loaded the same way.
  • Base96 Encoding / decoding - as this is needed to show group icons.
  • Panel update routines - as the same code is used for objects and groups.
The problem appears to be with showing group members, which are stored in the .detail field of the object array.

Adding the following code to the main loop will show the current contents of certain key variables.
print( "PanelGroup() : " + chr(34) + str( PanelGroup() ) + chr(34) )
for i=1 to topObject()
   print(str(i)+" : " + chr(34) + ObjectDetail(i) + chr(34) )
next i
This prints the currently selected group icon from PanelGroup(), to verify the correct group is being used.  The quotes were not actually needed here, but were simply left over from the code copied to make this line.

Then it loops through objects loaded displaying the .detail field for each array entry.
This should contain the encoded string for group members and descriptions for objects. It appears the problem is with the former.

Since only groups which have members are currently shown in the panel - and these ARE being shown, the group membership codes are getting lost after this fact is checked.  Somewhere in the LoadObjectData() function.  Probably as the member objects are converted to codes.

While dumping information to the screen has worked well for us so far, we have reached the limit of what we can find out with this method, to track this issue down, we are going to need more advanced diagnostics.

Tuesday 1 May 2012

Panel Icon Optimising

There is a duplicated block of code now which will fit into it's own function.
function SetPanelSprite( thisPos , thisFrame )
   thisSprite = panelIconSprite[ thisPos ]
   if thisFrame = 0
      setSpriteImage( thisSprite , 0 )
      clearSpriteAnimationFrames( thisSprite )
   else
      setSpriteImage( thisSprite , IconImage() )
      setSpriteAnimation( thisSprite , 64 , 64 , 252 )
      setSpriteFrame( thisSprite , thisFrame )
   endif
endfunction
This was lifted straight out of the group loop, so can simply be replaced with the call
SetPanelSprite( thisPos , thisFrame )
thisSprite = panelIconSprite[ thisPos ]
In the members loop for the objects,
thisSprite = panelIconSprite[ nextIcon ]
setSpriteImage( thisSprite , IconImage() )
setSpriteAnimation( thisSprite , 64 , 64 , 252 )
setSpriteFrame( thisSprite , thisFrame )
Can be replaced with,
SetPanelSprite( nextIcon , thisFrame )
thisSprite = panelIconSprite[ nextIcon ]
Finally, in the remainder loop.
thisSprite = panelIconSprite[ i ]
setSpriteImage( thisSprite , 0 )
clearSpriteAnimationFrames( thisSprite )
Is replaced with,
SetPanelSprite( i , 0 )
thisSprite = panelIconSprite[ i ]
So the function now looks like this.
function SetPanelIcons()
   // Process Groups
   thisGroups = len(GameGroups()) / 2.0
   firstGroupIcon = panelIconMax() - thisGroups + 1
   for i=0 to thisGroups-1
      thisCode$ = mid( GameGroups() , i * 2 + 1 , 2 )
      thisObject = Base96Decode( thisCode$ )
      if thisObject = 0
         thisFrame = 0
      else
         thisFrame = ObjectIcon( thisObject )
      endif
      thisPos = firstGroupIcon + i
      SetPanelSprite( thisPos , thisFrame )
      thisSprite = panelIconSprite[ thisPos ]
      if thisObject = PanelGroup()
         setSpriteColorAlpha( thisSprite , PGROUP_ALPHAHI )
      else
         setSpriteColorAlpha( thisSprite , PGROUP_ALPHA )
      endif
   next i
   // Process Objects
   if PanelGroup() > 0
      thisString$ = ObjectDetail( PanelGroup() )
      thisMembers = len(thisString$) / 2.0
      nextIcon = 1
      for i=0 to thisMembers - 1
         thisCode$ = mid( thisString$ , i * 2 + 1 , 2 )
         thisObject = Base96Decode( thisCode$ )
//         if ObjectIsKnown( thisObject ) > 0
            thisFrame = ObjectIcon( thisObject )
            SetPanelSprite( nextIcon , thisFrame )
            thisSprite = panelIconSprite[ nextIcon ]
            setSpriteColorAlpha( thisSprite , PGROUP_ALPHAHI )
            inc nextIcon , 1
            if nextIcon >= firstGroupIcon then exit
//         endif
      next i

      // Reset Remainder
      for i = nextIcon to firstGroupIcon - 1
         SetPanelSprite( i , 0 )
         thisSprite = panelIconSprite[ nextIcon ]
         setSpriteColorAlpha( thisSprite , PGROUP_ALPHA )
      next i
   endif
endfunction

More Panel Icons

With all the group icons visible at once, there has to be a way to tell which group the objects belong to.

Highlighting the current group works in a similar way as highlighting menu items, but as there is no way to know in advance which colours will be used for icons, it's safer to modify it's transparency

As with the menu, these use a set of constants to define the alpha levels both normal and highlighted icons.
#constant PGROUP_ALPHA   127
#constant PGROUP_ALPHAHI 255
The initial colours should use a yellow tint for highlighted icons and a darker grey colour for others.

Following on from the last code in the group portion of the SetPanelIcons() function, goes the code to set the colour.
// Change Icon Colour
if thisObject = PanelGroup()
   setSpriteColorAlpha( thisSprite , PGROUP_ALPHAHI )
else
   setSpriteColorAlpha( thisSprite , PGROUP_ALPHA )
endif
This checks to see if the code for the object currently being processed matches the code for the currently selected group and uses the appropriate alpha value.
This is all that is needed for the group icons.

The object icons are done in the same way as the group icons, so a lot of the code can be reused.
This.
   // Process Objects
   if PanelGroup() > 0
      thisString$ = ObjectDetail( PanelGroup() )
      thisMembers = len(thisString$) / 2.0
      nextIcon = 1
      for i=0 to thisMembers - 1
         thisCode$ = mid( thisString$ , i * 2 + 1 , 2 )
         thisObject = Base96Decode( thisCode$ )
//         if ObjectIsKnown( thisObject ) > 0
            thisFrame = ObjectIcon( thisObject )
            thisSprite = panelIconSprite[ nextIcon ]
            setSpriteImage( thisSprite , IconImage() )
            setSpriteAnimation( thisSprite , 64 , 64 , 252 )
            setSpriteFrame( thisSprite , thisFrame )
            setSpriteColorAlpha( thisSprite , PGROUP_ALPHAHI )
            inc nextIcon , 1
            if nextIcon >= firstGroupIcon then exit
//         endif
      next i
      // Reset Remainder
This routine works from a member list, so if there is no active group, then there must be no member list and this routine cannot run. This is the purpose of the first check.

The list is copied to a local variable and its length used to find the number of members.

Now not every object is known about, so as we go through the list, any unknowns are ignored.  Only known objects will be displayed.

Because of this, we can't use the loop counter to get the panel position - as was done with the groups - and so a local variable nextIcon is defined to indicate this.

Now it loops through the list members as before, getting the coded ID and decoding it.

Here, there is a commented out line which checks if an object is known.  As this is an if command, the corresponding endif is also commented out later.

This will be how the unknown objects will be ignored later.  Since we have not yet set which objects are known, for now, this is commented out so all objects will be shown.

There are changes to increment and check the nextIcon variable, and the remainder of the code sets the sprite as was done for the groups.

At the end of this routine, nextIcon points to the first panel position not used by objects and firstGroupIcon points to the position after the last panel position available for objects.

All that remains is to reset the sprites in the gap.
      // Reset Remainder
      for i = nextIcon to firstGroupIcon - 1
         thisSprite = panelIconSprite[ i ]
         setSpriteImage( thisSprite , 0 )
         clearSpriteAnimationFrames( thisSprite )
         setSpriteColorAlpha( thisSprite , PGROUP_ALPHA )
      next i
   endif
endfunction
Now the panel shows an object icon for the axe which corresponding to the highlighted tool group.

Panel Icons

To put the icons into the panel will require a new function,
function SetPanelIcons()

endfunction
which will be called each time the icons are changed - particularly when a different group is selected.

First, we get the number of groups with members and store the value.
thisGroups = len(GameGroups()) / 2.0
Since each group is encoded in Base96 which uses 2 chars per object, we divide the length of the encoded list by 2 to get the number of groups.

We determine which panel icon will be the first used by a group.
firstGroupIcon = panelIconMax() - thisGroups + 1
If there are no groups, the following loop will not run.
for i=0 to thisGroups-1
otherwise, this loop processes each one.
thisCode$ = mid( GameGroups() , i * 2 + 1 , 2 )
thisObject = Base96Decode( thisCode$ )
if thisObject > 0
   thisFrame = ObjectIcon( thisObject )
else
   thisFrame = 0
endif
This extracts the coded object index from the group list and decodes it to obtain the object number.

If the object number is greater than zero then the icon field for that object is used for the frame, otherwise a value of zero is used.

The frame number is checked by the next piece of code.
thisPos = firstGroupIcon + i
thisSprite = panelIconSprite[ thisPos ]
if thisFrame = 0
   setSpriteImage( thisSprite , 0 )
   clearSpriteAnimationFrames( thisSprite )
else
   setSpriteImage( thisSprite , IconImage() )
   setSpriteAnimation( thisSprite , 64 , 64 , 252 )
   setSpriteFrame( thisSprite , thisFrame )
endif
If it is zero, indicating for some reason that we don't have an image for this object, then the panel sprite is set back to it's default.

If there is a frame number, then the icon sheet image is assigned to the sprite, animation is enabled for 64 x 64 frames and the correct frame selected.

The loop is closed and the function so far, should look like this.
function SetPanelIcons()
   // Process Groups
   thisGroups = len(GameGroups()) / 2.0
   firstGroupIcon = panelIconMax() - thisGroups + 1
   for i=0 to thisGroups-1
      thisCode$ = mid( GameGroups() , i * 2 + 1 , 2 )
      thisObject = Base96Decode( thisCode$ )
      if thisObject = 0
         thisFrame = 0
      else
         thisFrame = ObjectIcon( thisObject )
      endif
      thisPos = firstGroupIcon + i
      thisSprite = panelIconSprite[ thisPos ]
      if thisFrame = 0
         setSpriteImage( thisSprite , 0 )
         clearSpriteAnimationFrames( thisSprite )
      else
         setSpriteImage( thisSprite , IconImage() )
         setSpriteAnimation( thisSprite , 64 , 64 , 252 )
         setSpriteFrame( thisSprite , thisFrame )
      endif
      // Change Icon Colour

   next i
   // Process Objects

endfunction
Add a call to change the icons at the start of the case STATE_RUNNING block of code
case STATE_RUNNING
   // Running Code
   if getTextVisible( PanelText() ) = 0
      // Game starting - Activate Transition
      setTextVisible( panelText() , 1 )
      setTextColor( panelText() , 127 , 255 , 127 , 0 )
      PositionPanelIcons()
      setPanelIcons()
      StartTransition( TRANS_FADEIN , MENU_FADE )
   elseif (Transition() = TRANS_FADEIN) or (Transition() = TRANS_FADEOUT)
And the first icons can be tested.

When I try this, the whole thing crashes.  So begins an hour of isolating code and printing variables until the problem can be tracked down.

In this case, it is caused by a bug in AGK, where data is getting corrupted on access.

Since there is a new release imminent with a number of bug fixes, I am hoping this will be picked up.

In the meantime, a workaround (for me at least) is to change the following function.
function GameGroups()
endfunction game.groupObject$
Into this.
function GameGroups()
thisString$ = game.groupObject$
endfunction thisString$
Which does exactly the same thing but with an extra step.

That step seems like enough to prevent the problem.
And now if we increase the columns to 5.
#constant PANEL_COLS   5
It becomes.

Panel Layout

At the moment, this size of the slide out icon panel is set by the resolution of the screen - specifically the width - and the number of icons specified in the constants PANEL_ROWS and PANEL_COLS.

To recap,
  • The panel width is the full width of the display
  • The icon width (in pixels) is the width of the display in pixels divided by the number of columns
  • The icon height is the same as the width
  • The panel height is the icon height multiplied by the number of rows
It was done this way so that the icons are square and evenly distributed on the panel, irrespective of the resolution of the device being used.

So on my phone, for example, in portrait (vertical orientation) the display is 480 pixels wide by 800 high.  Based on 4 columns and 3 rows, this gives;
  • The panel width is 480 pixels
  • The icon width is 120 pixels ( 480 / 4 )
  • The icon height is 120 pixels
  • The panel height is 360 pixels (120 x 3 )
So on my phone, the panel occupies 360 / 800 or 45% of the height of the screen.

On a baseline android phone, with a resolution of 320 x 480 (portrait), the icons would be 80 x 80 pixels and the panel would be 320 x 240 or 50% of the height of the screen.

In both cases, the icons are being scaled up from their 64 x 64 pixel source and the panel is taking up about half of the screen space when open.

Increasing the number of icons across - the number of columns - the icon size will be smaller and so the panel height will be smaller.  This will also allow more icons in the panel.
Making the icons closer to their original size (as stored in the image) will improve the quality of the icons by reducing the amount of up-scaling needed.

In the example images above - for a 320x480 display - the middle option ( 5 Columns) would show the icons as they were drawn with no scaling at all.

The icons within the panel represent two type of things; Objects and Groups.
Ideally, objects should be closer to the work area, so start in the top left corner.

Putting the group icons at the end, separates the two types and the empty icons between, provide a buffer between them.

The objects shown in the panel, will be members of the currently selected group.  This requires another value to be stored, in a field called group, which will be part of the panelType UDT.
type panelType
   height#
   speed#
   isOpen
   image
   sprite
   text
   sound
   iconMax
   group
endtype
As always, it has a return function.
function PanelGroup()
endfunction panel.group
And is set in the Initialise() function.
panel.group = 0
and at the end of the function, by changing the line.
game.groupObject$ = Base96Sort( GameGroups() )
into
if len( GameGroups() ) > 0
   game.groupObject$ = Base96Sort( GameGroups() )
   panel.group = Base96Decode( left( GameGroups() , 2) )
else
   panel.group = 0
endif
Which now decodes the first group ID ( if any exist ) and stores it.

Saturday 28 April 2012

Game Start

After a short break, it's time to get back to the game portion of project.


Firstly, the test code in the case STATE_INTRO block needs to be removed.  This is replaced with the last two commented out lines from the previous block.
// Workaround to ensure frame rate ok before menu is called
if timer() > 3.0 then setGameState( STATE_MENU )
The comment slashes (//) have been removed from the second line and the value increased from, 2.0 to 3.0 to allow for loading time.

This advances the game state from STATE_INTRO to STATE_MENU after the game has been running three seconds.

Now the beginning the select statement should look like this.
   select GameState()
      case STATE_SETUP
         // Setup Code
         testNum = LoadObjectData( "standard" )
         setGameState( STATE_INTRO )
      endcase
      case STATE_INTRO
         // Intro Code
         // Workaround to ensure frame rate ok before menu is called
         if timer() > 3.0 then setGameState( STATE_MENU )
      endcase
      case STATE_DEMO
         // Demo Code
      endcase
      case STATE_MENU
         // Menu Code
We transition out of the menu, what is needed next is a transition into the game.

At the moment, the panel text Objects - used to open and close the panel - is visible as soon as the project starts, it should really only become visible once a game starts.

This is done by making it invisible, just after it's depth is set in the Initialise() function.
setTextVisible( panelText() , 0 )
This uses the setTextVisible() command with a 0 parameter.  Now the text will not be visible during the setup, initialise and menu states.

Now as we did with the menu, we can check if this is visible to determine when the game block is being called for the first time.

Inside the case STATE_RUNNING, block, the text is checked to see if it is visible.
if getTextVisible( PanelText() ) = 0
   // Game starting 
   setTextVisible( panelText() , 1 )
   setTextColor( panelText() , 127 , 255 , 127 , 0 )
   PositionPanelIcons()
   //  Activate Transition
   StartTransition( TRANS_FADEIN , MENU_FADE )
If the text is not visible, it is made visible and it's colour set, including an alpha value of zero to make it fully transparent.

The PositionPanelIcons() function is used to make sure the panel is in the right position, then - as with the menu - a transition is started.  This uses the parameters from the menu transition at the moment.

The next block of code is very similar to that used in the menu.
elseif (Transition() = TRANS_FADEIN) or (Transition() = TRANS_FADEOUT) 
   thisAlpha = 255.0 * TransitionCount()
   if Transition() = TRANS_FADEOUT then thisAlpha = 255 - thisAlpha
   setTextColorAlpha( panelText() , thisAlpha )
   if TransitionCount() = 1.0
      if Transition() = TRANS_FADEOUT
         // Game Exiting    
      
      else
         // Game Starting
      
      endif
      StopTransition()     
   endif
endif
Here only the alpha of the panel text is changed, though the method is identical.

An empty if/else/endif structure has been used in the middle as a place to put code for the end of the fade transition, when added.

The existing call to checkGamePointer() now follows this code.

With this, the word Objects at the top of the panel should fade in when the game starts.

Monday 23 April 2012

Loading Icons

At the moment, the icon images are loaded in the Initialise() function with the line.
display.iconImage = SafeloadImage( "objects.png" )
This now needs to be changed to.
display.iconImage = 0
and the loading takes place within the outer if/elseif loop of the LoadObjectData() function.
if getFileExists( thisTextName$ ) and thisImageName$ <> ""
   // Both data files exist
   display.iconImage = SafeloadImage( thisImageName$ )
   // Prepare temp array
   dim tempArray[ 1000 ] as string
This uses the variable thisImageName$ rather than an absolute name.

We are almost ready to be able to see the loaded object in the game, there is just one more thing to add.

Since we are storing the groups in the same array as the standard objects, we need a quick way to identify them.  This saves having to search the array each time we want an object group.

These can be stored in the same way group members are stored, as a string of Base96 Codes, and as with the group members, this needs to be done after the object array is set up so that the indexes are correct.

The group list is stored in a string field of the gameType UDT.
type GameType
   state
   level
   topObject
   groupObject$
endtype
It has the usual return function.
function GameGroups()
endfunction game.groupObject$
And is set up in the Initialise() function.
game.groupObject$ = ""
AND in the LoadObjectData() function.
// Group Memberships
game.groupObject$ = ""
for i=1 to topObject()
All pretty familiar stuff by now.

Just inside the if ObjectIsGroup( i ) > 0 block, we put the line which adds the group to the list.
if ObjectIsGroup( i ) > 0 and ObjectDetail( i ) <> ""
   game.groupObject$ = game.groupObject$ + Base96Encode( i )   
This means that only groups which contain objects will be included in the group list.

After the for/next loop has completed, near the end of the function, the final list is sorted.
      next i
      game.groupObject$ = Base96Sort( GameGroups() )
   endif
endfunction thisArraySize
Now we not only have a list of groups, but we know that they are only the groups with member objects.


End of Week 2 (ish)

I took a break after week one, so allowing for that, this is the end of the second week of posts.

The Blog is closer to three weeks old, but either way, it's come a long way.

We now have object loaded from a data file, but to see them in the game, needs an updated icon sheet.

Here is the source, without transparency - to go in the artwork folder with the name standard source.png.
Here, I filled the magenta bits in orange to help me line up the new graphics.  Filling the orange grid with magenta when all the objects have been added will put this back to normal.

And this is the transparent one for the media folder with the name standard.png.
They have both been optimised to keep the sizes down.

And here's a complete code check  - Just over 900 lines - to keep everything in sync.

// 23 April 2012 - Tabs converted to 3 spaces
// Constants must go at start of program

#constant PANEL_ROWS   3
#constant PANEL_COLS   4

#constant DEPTH_MENU   10
#constant DEPTH_SCORE   20
#constant DEPTH_PANEL   30
#constant DEPTH_WORK   40

#constant MAX_WORKOBJECTS   100

#constant FRAME_HIGHLIGHT   251
#constant FRAME_TRASHCAN      252

#constant STATE_SETUP   0
#constant STATE_INTRO   1
#constant STATE_DEMO   2
#constant STATE_MENU   3
#constant STATE_RUNNING   4
#constant STATE_EXIT   99

#constant MENU_FADE      1.0
#constant MENU_ITEMS      2

#constant MENU_DIVTOP   0.25
#constant MENU_DIVSIZE   0.10

#constant MENU_RED      255
#constant MENU_GREEN   165
#constant MENU_BLUE      0
#constant MENU_REDHI   255
#constant MENU_GREENHI   255
#constant MENU_BLUEHI   0

#constant TRANS_NONE   0
#constant TRANS_FADEIN   1
#constant TRANS_FADEOUT   2

// User Defined Types - custom data structures

type displayType
   physWidth#
   physHeight#
   virtWidth#
   virtHeight#
   iconSize#
   textHeight#
   lastFPS#
   iconImage
   highSprite
   transType
   transCount#
   transStep#
endtype

type touchType
   pointDX#
   pointDY#
   pointWX#
   pointWY#
   spritePicked
   objectPicked
   menuPicked
   pickOffsetX#
   pickOffsetY#
endtype

type panelType
   height#
   speed#
   isOpen
   image
   sprite
   text
   sound
   iconMax
endtype

type WorkObjectType
   isFree
   objectID
   sprite
endtype

type GameType
   state
   level
   topObject
endtype

type MainObjectType
   name$
   detail$
   icon
   isKnown
   isGroup
endtype

// I must remember to put this somewhere

global numWorkObjects = 0

Initialise()

// Temporary Objects

imageRef = SafeloadImage( "finger.png" )
spriteRef = createSprite( imageRef )

// Main Loop
do
   LoopStart()
   select GameState()
      case STATE_SETUP
         // Setup Code
         testNum = LoadObjectData( "standard" )
         setGameState( STATE_INTRO )
         // Workaround to ensure frame rate ok before menu is called
         // if timer() > 2.0 then setGameState( STATE_MENU )
      endcase
      case STATE_INTRO
         // Intro Code
         print(" Objects : " + str( TopObject() ) )
         for i=1 to TopObject()
            printc( str(i) + " : " + ObjectName( i ) + " : " + str( ObjectIcon( i) ) )
            if ObjectIsGroup( i ) > 0
               printc( ", Group: ")
               thisString$ = ObjectDetail( i )
               thisNum = len( thisString$ ) / 2
               for j = 1 to thisNum
                  thisCode$ = mid( thisString$ , j*2 - 1 , 2)
                  thisObject = Base96Decode( thisCode$ )
                  printc( ObjectName( thisObject ) )
                  if j < thisNum then printc( ", " )
               next j
               print( "." )
            else
               print( ", " + chr(34) + ObjectDetail( i ) + chr( 34 ))
            endif
         next i
         print( " ----- ")
      endcase
      case STATE_DEMO
         // Demo Code
      endcase
      case STATE_MENU
         // Menu Code
         if getTextVisible( menuText[ 1 ]) = 0
            // Menu starting - Activate Transition
            StartTransition( TRANS_FADEIN , MENU_FADE )
            thisX# = VirtWidth() / 2.0
            thisY# = VirtHeight() * MENU_DIVTOP
            thisSize# = VirtHeight() * MENU_DIVSIZE
            for i=1 to MENU_ITEMS
               setTextSize( menuText[ i ] , thisSize# )
               setTextAlignment( menuText[ i ] , 1 )
               setTextDepth( menuText[ i ] , DEPTH_MENU )
               setTextPosition( menuText[ i ] , thisX# , thisY# )
               setTextVisible( menuText[ i ] , 1 )
               setTextColor( menuText[ i ] , MENU_RED , MENU_GREEN, MENU_BLUE , 0 )
               thisY# = thisY# + getTextTotalHeight( menuText[ i ] ) * 2.0
            next i
         elseif (Transition() = TRANS_FADEIN) or (Transition() = TRANS_FADEOUT)
            thisAlpha = 255.0 * TransitionCount()
            if Transition() = TRANS_FADEOUT then thisAlpha = 255 - thisAlpha
            for i=1 to MENU_ITEMS
               setTextColorAlpha( menuText[ i ] , thisAlpha )
            next i
            if TransitionCount() = 1.0
               if Transition() = TRANS_FADEOUT
                  // Menu Exiting
                  for i=1 to MENU_ITEMS
                     setTextVisible( menuText[ i ] , 0 )
                  next i
                  select MenuPicked()
                     case 1
                        // Menu Option 1
                        setGameState( STATE_RUNNING )
                     endcase
                     case 2
                        // Menu Option 2
                        setGameState( STATE_EXIT )
                     endcase
                  endselect
               endif
               StopTransition()
            endif
         endif
         checkMenuPointer()
      endcase
      case STATE_RUNNING
         // Running Code
         checkGamePointer()
      endcase
      case default
         // Picks up unknown states - including STATE_EXIT
         exit
      endcase
   endselect

   // Position temporary finger sprite
   setSpritePosition( spriteRef , PointDX() , PointDY() )

   PrintDiagnostics()
   sync()
loop

end

// Loop Functions

function LoopStart()
   display.lastFPS# = screenFPS()
   panel.speed# = panelHeight() / (lastFPS() * 1.25)
   touch.pointDX# = getPointerX()
   touch.pointDY# = getPointerY()
   touch.pointWX# = ScreentoWorldX(PointDX())
   touch.pointWY# = ScreentoWorldY(PointDY())
   if Transition() > TRANS_NONE then UpdateTransition()
endfunction

// Game Functions

function checkGamePointer()
   if getPointerPressed() > 0
      if PointDY() >= getTextY( panelText() ) and PointDY() < getSpriteY( panelSprite() )
         ToggleTrayOpen()
      elseif PointDY() < getTextY( panelText() )
         touch.spritePicked = getSpriteHit( PointWX() , pointWY() )
         if SpritePicked() > 0
            touch.objectPicked = FindWorkObjectBySprite( SpritePicked() )
            if ObjectPicked() > 0
             touch.pickOffsetX# = PointWX() - getSpriteXByOffset( SpritePicked() )
             touch.pickOffsetY# = PointWY() - getSpriteYByOffset( SpritePicked() )
            endif
         endif
      endif
   elseif getPointerReleased() > 0
      if thisObjectPicked > 0
         thisX# = PointWX() - PickOffsetX()
         thisY# = PointWY() - PickOffsetY()
         HideHighLight()
         if WorldToScreenY( thisY#) >= getTextY( panelText() )
            DelWorkObject( ObjectPicked() )
         else
            setSpritePositionByOffset( SpritePicked() , thisX# , thisY# )
            setSpriteDepth( SpritePicked() , DEPTH_WORK - 1 )
            thisObjectUnder = WorkObjectIsOver( ObjectPicked() )
            if thisObjectUnder > 0
               // This is where the merge objects code goes
            endif
         endif
      endif
      touch.spritePicked = 0
      touch.objectPicked = 0
      touch.pickOffsetX# = 0.0
      touch.pickOffsetY# = 0.0
   elseif getPointerState() > 0
      if ObjectPicked() > 0
         thisX# = PointWX() - PickOffsetX()
         thisY# = PointWY() - PickOffsetY()
         setSpritePositionByOffset( SpritePicked() , thisX# , thisY# )
         setSpriteDepth( SpritePicked() , DEPTH_PANEL - 5 )
         if WorldToScreenY( thisY#) >= getTextY( panelText() )
            HighlightSprite( SpritePicked() , FRAME_TRASHCAN )
         else
            thisObjectUnder = WorkObjectIsOver( ObjectPicked() )
            if thisObjectUnder > 0
               HighlightSprite( WorkObjectSprite( thisObjectUnder ) , FRAME_HIGHLIGHT )
            else
               HideHighLight()
            endif
         endif
      endif
   endif

   if panelIsOpen() = 1
      if getSpriteY( panelSprite() ) > ( PhysHeight() - panelHeight() )
         MovePanel( -panelSpeed() )
      endif
   else
      if getSpriteY( panelSprite() ) < PhysHeight()
         MovePanel( panelSpeed() )
      endif
   endif
endfunction

// Menu Functions

function checkMenuPointer()
   thisMenuSelected = 0
   for i=1 to MENU_ITEMS
      if getTextHitTest( menuText[ i ] , PointDX() , PointDY() ) > 0
         thisMenuSelected = i
         exit
      endif
   next i
   if getPointerPressed() > 0
      // Pointer Pressed
      touch.menuPicked = thisMenuSelected
   elseif getPointerReleased() > 0
      // Pointer Released
      if thisMenuSelected > 0 and thisMenuSelected = MenuPicked() then StartTransition( TRANS_FADEOUT , MENU_FADE )
   elseif getPointerState() > 0
      // PointerHeld
      if MenuPicked() > 0
         if thisMenuSelected = MenuPicked()
            setTextColorRed( menuText[ MenuPicked() ] , MENU_REDHI )
            setTextColorGreen( menuText[ MenuPicked() ] , MENU_GREENHI )
            setTextColorBlue( menuText[ MenuPicked() ] , MENU_BLUENHI )
         else
            setTextColorRed( menuText[ MenuPicked() ] , MENU_RED )
            setTextColorGreen( menuText[ MenuPicked() ] , MENU_GREEN )
            setTextColorBlue( menuText[ MenuPicked() ] , MENU_BLUE )
         endif
      endif
   endif
endfunction

// Transition Functions

function StartTransition( thisType , thisTime# )
   display.transType = thisType
   display.transCount# = 0.0
   display.transStep# = 1.0 / ( lastFPS() * thisTime# )
endfunction

function StopTransition()
   display.transType = TRANS_NONE
endfunction

function UpdateTransition()
   if TransitionCount() < 1.0 then display.transCount# = TransitionCount() + TransitionStep()
   if TransitionCount() > 1.0 then display.transCount# = 1.0
endfunction

// Diagnostic Functions

function PrintDiagnostics()
   printc( "Phy: " + str( PhysWidth() , 0) + " x " + str( PhysHeight() , 0 ))
   printc( ", Vir: " + str( VirtWidth() , 1) + " x " + str( VirtHeight() , 1 ))
   print( ", @ " + str ( lastFPS() , 2 ) + " fps" + chr(10) )
   printc("Transition Type : " + str( Transition() ) )
   printc(", Count: " + str( TransitionCount() , 3 ) )
   print(", Step: " + str( TransitionStep() , 3 ) + chr(10))
   print("Game State : " + str( GameState() ) + chr(10))
   print("Objects : " + str( TopObject() ) + chr(10) )

   printc( "Pointer (Display): " )
   print( str( PointDX() ) + " x " + str( PointDY() ) )
   printc( "Pointer (World): " )
   print( str( PointWX() ) + " x " + str( PointWY() ) )
   printc( "Sprite: ")
   print( str( getSpriteHit( PointDX() , PointDY() )))
   printc( "Sprite Picked: ")
   printc( str( SpritePicked() ))
   printc( ", Object Picked: ")
   printc( str( ObjectPicked() ))
   printc( ", Over : " )
   print( str( WorkObjectIsOver( ObjectPicked() ) ))
endfunction

// Panel Functions

function ToggleTrayOpen()
   panel.isOpen = 1 - panelIsOpen()
   if panelSound() > 0 then playSound( panelSound() )
endfunction

function MovePanel( distance# )
   setSpriteY( panelSprite() , getSpriteY( panelSprite() ) + distance# )
   setTextY( panelText() , getTextY( panelText() ) + distance# )
   PositionPanelIcons()
endfunction

function PositionPanelIcons()
   iconDepth = getSpriteDepth( PanelSprite() ) - 1
   for icon = 1 to panelIconMax()
      offset = icon - 1
      col = offset mod PANEL_COLS
      row = offset / PANEL_COLS
      x = col * IconSize() + 2
      y = row * IconSize() + getSpriteY( PanelSprite() ) + 2
      setSpritePosition( panelIconSprite[ icon ] , x , y )
      setSpriteDepth( panelIconSprite[ icon ] , iconDepth )
   next icon
endfunction

// Load Functions

function SafeLoadImage( thisFileName$ )
   if getFileExists( thisFileName$ )
      thisImageRef = loadImage( thisFileName$ )
   else
      thisImageRef = 0
   endif
endfunction thisImageRef

function SafeLoadSound( thisFileName$ )
   if getFileExists( thisFileName$ )
      thisSoundRef = loadSound( thisFileName$ )
   else
      thisSoundRef = 0
   endif
endfunction thisSoundRef

// Base 96 Functions

function Base96Encode( thisValue )
   lowValue = thisValue mod 96
   highValue = ( thisValue / 96 ) mod 96
   thisString$ = chr( highValue + 32 ) + chr( lowValue + 32 )
endfunction thisString$

function Base96Decode( thisString$ )
   thisLen = len(thisString$)
   if thisLen = 0
      thisValue = 0
   else
      if thisLen = 1 then thisString$ = " " + thisString$
      highValue = asc( left( thisString$ , 1 ) ) - 32
      lowValue = asc( mid( thisString$ , 2, 1 ) ) - 32
      thisValue = highValue * 96 + lowValue
   endif
endfunction thisValue

function Base96Sort( thisString$ )
   thisLen = len(thisString$)
   if thisLen > 2 and (thisLen && 1) = 0
      thisArraySize = thisLen / 2
      dim tempArray[ thisArraySize ] as string
      for i=1 to thisArraySize
         tempArray[ i ] = mid( thisString$ , i*2 - 1 , 2)
      next i
      SortTempArray( thisArraySize )
      thisString$ = ""
      for i=1 to thisArraySize
         thisString$ = thisString$ + tempArray[ i ]
      next i
   endif
endfunction thisString$

function SortTempArray( thisSize )
   for i=1 to thisSize - 1
      thisLow = i
      for j = i + 1 to thisSize
         if tempArray[j] < tempArray[thisLow] then thisLow = j
      next j
      if thisLowest <> i
         temp$ = tempArray[i]
         tempArray[i] = tempArray[thisLow]
         tempArray[thisLow] = temp$
      endif
   next i
endfunction

// Main Object Functions

function ObjectValid( thisObject )
   thisBool = ( thisObject > 0 and thisObject <= TopObject() )
endfunction thisBool

function FindObjectByName( thisName$ )
   thisName$ = upper( thisName$ )
   for thisObject = 1 to TopObject()
      if upper( ObjectName( thisObject )) = thisName$ then exit
   next thisObject
   if thisObject > TopObject() then thisObject = 0
endfunction thisObject

// Work Object Functions

function TopWorkObject()
endfunction numWorkObjects

function WorkObjectValid( thisObject )
   thisBool = ( thisObject > 0 and thisObject <= TopWorkObject() )
endfunction thisBool

function FirstFreeWorkObject()
   for thisObject = 1 to TopWorkObject()
      if WorkObjectIsFree( thisObject ) > 0 then exit
   next thisObject
   if thisObject > MAX_WORKOBJECTS then thisObject = 0
endfunction thisObject

function AddWorkObject( thisObjectID, thisSprite )
   thisObject = FirstFreeWorkObject()
   if thisObject > TopWorkObject() then numWorkObjects = thisObject
   if WorkObjectValid( thisObject )
      workObject[ thisObject ].isFree = 0
      workObject[ thisObject ].objectID = thisObjectID
      workObject[ thisObject ].sprite = thisSprite
   endif
endfunction thisObject

function DelWorkObject( thisObject )
   if WorkObjectValid( thisObject )
      workObject[ thisObject ].isFree = 1
      if WorkObjectSprite( thisObject ) > 0
         thisSprite = WorkObjectSprite( thisObject )
         workObject[ thisObject ].sprite = 0
         if getSpriteExists( thisSprite ) then deleteSprite( thisSprite )
      endif
      while WorkObjectIsFree( TopWorkObject() ) = 1 and TopWorkObject() >= 0
         dec numWorkObjects , 1
      endwhile
   endif
endfunction

function CreateWorkObjectSprite( thisObject , thisX , thisY )
   if WorkObjectIsFree( thisObject ) = 0
      if WorkObjectSprite( thisObject ) = 0 then workObject[ thisObject ].sprite = createSprite( IconImage() )
      thisSprite = WorkObjectSprite( thisObject )
      fixSpriteToScreen( thisSprite , 0 )
      setSpriteSize( thisSprite , IconSize() , - 1)
      setSpriteOffset( thisSprite , iconSize() / 2.0 , iconSize() / 2.0 )
      setSpritePositionByOffset( thisSprite , thisX , thisY )
      setSpriteDepth( thisSprite , DEPTH_WORK - 1 )
      setSpriteAnimation( thisSprite , 64 , 64 , 252 )
      setSpriteFrame( thisSprite , WorkObjectID( thisObject ) )
   endif
endfunction

function WorkObjectIsOver( thisWorkObject )
   thisSprite = WorkObjectSprite( thisWorkObject )
   if thisSprite > 0
      thisX# = getSpriteXByOffset( thisSprite )
      thisY# = getSpriteYByOffset( thisSprite )
      for thisObject = 1 to TopWorkObject()
         if thisObject <> thisWorkObject
            thisSprite = WorkObjectSprite( thisObject )
            if thisSprite > 0
               diffX# = abs( getSpriteXByOffset( thisSprite ) - thisX# )
               diffY# = abs( getSpriteYByOffset( thisSprite ) - thisY# )
               if diffX# < iconSize() and diffY# < iconSize() then exit
            endif
         endif
      next thisObject
      if thisObject > TopWorkObject() then thisObject = 0
   else
      thisObject = -1
   endif
endfunction thisObject

function FindWorkObjectBySprite( thisSprite )
   for thisObject = 1 to TopWorkObject()
      if WorkObjectSprite( thisObject ) = thisSprite then exit
   next thisObject
   if thisObject > TopWorkObject() then thisObject = 0
endfunction thisObject

function HideHighLight()
   if HighSprite() > 0
      if getSpriteExists( HighSprite() ) then deleteSprite( HighSprite() )
      display.highSprite = 0
   endif
endfunction

function HighlightSprite( thisSprite , thisFrame )
   if thisSprite > 0 and thisFrame > 0 and thisFrame < 253
      if getSpriteExists( thisSprite )
         HideHighLight()
         display.highSprite = cloneSprite( thisSprite )
         setSpriteFrame( HighSprite() , thisFrame )
         setSpriteDepth( HighSprite() , getSpriteDepth( HighSprite() ) - 1)
      endif
   endif
endfunction

// Game State Functions

function SetGameState( thisState )
   game.state = thisState
endfunction

// Data Load Functions

function LoadObjectData( thisName$ )
   // Pass the name without extension
   thisTextName$ = thisName$ + ".txt"
   thisImageName$ = FindImageFile( thisName$ )
   if getFileExists( thisTextName$ ) and thisImageName$ <> ""
      // Both data files exist
      dim tempArray[ 1000 ] as string
      thisArraySize = 0
      thisFile = openToRead( thisTextName$ )
      thisString$ = readLine( thisFile )
      repeat
         if thisString$ <> ""
            inc thisArraySize , 1
            tempArray[ thisArraySize ]  = thisString$
         endif
         thisString$ = readLine( thisFile )
      until fileEof( thisFile ) > 0
      closefile( thisFile )
      // Convert Data to Array
      game.topObject = thisArraySize
      dim MainObject[ topObject() ] as MainObjectType
      for i=1 to topObject()
         thisString$ = tempArray[ i ]
         thisLen = len( thisString$ )
         thisComma = FindSubString( thisString$ , "," , 1 )
         thisColon = FindSubString( thisString$ , ":" , thisComma + 1 )
         if thisComma > 0
            MainObject[ i ].name$ = left( thisString$ , thisComma - 1)
            if thisColon > 0
               MainObject[ i ].detail$ = right( thisString$ , thisLen - thisColon )
               thisLen = thisColon - 1
               thisString$ = left( thisString$ , thisLen)
            else
               MainObject[ i ].detail$ = ""
            endif
            thisString$ = right( thisString$ , thisLen - thisComma )
            thisValue = val( thisString$ )
            MainObject[ i ].icon = abs( thisValue )
            MainObject[ i ].isKnown = 0
            if thisValue < 0
               MainObject[ i ].isGroup = 1
            else
               MainObject[ i ].isGroup = 0
            endif
         endif
      next i
      // Recipes Go Here
      for i=topObject() + 1 to thisArraySize

      next i
      // Clear Temp Array
      dim tempArray[ 0 ]
      // Group Memberships
      for i=1 to topObject()
         if ObjectIsGroup( i ) > 0 and ObjectDetail( i ) <> ""
            thisString$ = ObjectDetail( i )
            thisLen = len( thisString$ )
            thisStart = 1
            thisCoded$ = ""
            while thisStart < thisLen
               thisComma = FindSubString( thisString$ , "," , thisStart )
               if thisComma = 0 then thisComma = thisLen + 1
               thisObject$ = mid( thisString$ , thisStart , thisComma - thisStart )
               // convert to base96
               thisObject = FindObjectByName( thisObject$ )
               if thisObject > 0 then thisCoded$ = thisCoded$ + Base96Encode( thisObject )
               thisStart = thisComma + 1
            endwhile
            MainObject[ i ].detail$ = Base96Sort( thisCoded$ )
         endif
      next i
   endif
endfunction thisArraySize

function FindImageFile( thisName$ )
   thisString$ = ""
   if getFileExists( thisName$ + ".png" )
      thisString$ = thisName$ + ".png"
   elseif getFileExists( thisName$ + ".jpg" )
      thisString$ = thisName$ + ".jpg"
   elseif getFileExists( thisName$ + ".bmp" )
      thisString$ = thisName$ + ".bmp"
   endif
endfunction thisString$

// String Functions

function FindSubString( thisString$ , thisSearch$ , thisStart )
   thisLen = len( thisSearch$ )
   thisSearch$ = upper( thisSearch$ )
   thisString$ = upper( thisString$ )
   thisEnd = len( thisString$ ) - thisLen + 1
   for thisPos = thisStart to thisEnd
      if mid( thisString$ , thisPos , thisLen ) = thisSearch$ then exit
   next i
   if thisPos > thisEnd then thisPos = 0
endfunction thisPos


// Initialisation Functions

function Initialise()
   // Prepare Globals
   global game as gameType
      game.level = 0
      game.state = STATE_SETUP
      game.topObject = 0

   global touch as touchType
      touch.pointDX# = 0.0
      touch.pointDY# = 0.0
      touch.pointWX# = 0.0
      touch.pointWY# = 0.0
      touch.spritePicked = 0
      touch.objectPicked = 0
      touch.menuPicked = 0
      touch.pickOffsetX# = 0.0
      touch.pickOffsetY# = 0.0

   global display as displayType
      display.physWidth# = getDeviceWidth()
      display.physHeight# = getDeviceHeight()
      display.virtWidth# = PhysWidth()
      display.virtHeight# = PhysHeight()
      display.iconSize# = PhysWidth() / PANEL_COLS
      display.textHeight# = PhysHeight() / 20.0
      display.highSprite = 0
      display.iconImage = SafeloadImage( "objects.png" )
      display.transType = TRANS_NONE
      display.transCount# = 0.0
      display.transStep# = 0.0

   global panel as panelType
      panel.iconMax = PANEL_ROWS * PANEL_COLS
      panel.height# = IconSize() * PANEL_ROWS
      panel.speed# = 0.0
      panel.isOpen = 0
      panel.Image = SafeloadImage( "panel.jpg" )
      panel.sprite = createSprite( PanelImage() )
      panel.text = createText( "Objects" )
      panel.sound = SafeLoadSound("panel.wav")

   setImageWrapU( PanelImage() , 1 )
   setImageWrapV( PanelImage() , 1 )

   uScale# = getImageWidth( PanelImage() ) / PhysWidth()
   vScale# = getImageHeight( PanelImage() ) / PanelHeight()
   setSpriteUVScale( PanelSprite() , uScale# , vScale# )
   setSpriteSize( PanelSprite() , PhysWidth() , PanelHeight() )
   setSpritePosition( PanelSprite() , 0 , PhysHeight() )
   setSpriteDepth( PanelSprite() , DEPTH_PANEL)

   setTextSize( panelText() , textHeight() )
   setTextAlignMent( panelText() , 1)
   setTextPosition( panelText() , PhysWidth() / 2.0 , PhysHeight() - TextHeight() )
   setTextDepth( panelText() , DEPTH_PANEL - 1 )

   // Prepare Arrays

   dim panelIconSprite[ panelIconMax() ]
      for icon = 1 to panelIconMax()
         panelIconSprite[ icon ] = createSprite( 0 )
         setSpriteSize( panelIconSprite[ icon ] , IconSize() - 4, IconSize() - 4)
         setSpriteColor( panelIconSprite[ icon ] , 255 , 255 , 255 , 127 )
      next icon

   dim workObject[ MAX_WORKOBJECTS ] as WorkObjectType
      for thisObject = 1 to MAX_WORKOBJECTS
         workObject[ thisObject ].isFree = 1
         workObject[ thisObject ].objectID = 0
         workObject[ thisObject ].sprite = 0
      next thisObject
   setVirtualResolution( VirtWidth(), VirtHeight() )
   PositionPanelIcons()

   dim menuString[ 5 ] as string = [ "" , "Start Game" , "Exit", "Option 4" , "Option 5" , "Option 6" ]
   dim menuText[ MENU_ITEMS ]
      for i=1 to MENU_ITEMS
         menuText[ i ] = createText( menuString[i] )
         setTextVisible( menuText[ i ] , 0 )
      next i

   dim MainObject[0] as MainObjectType

endfunction

// Return Functions - Game

function GameLevel()
endfunction game.level
function GameState()
endfunction game.state
function TopObject()
endfunction game.topObject

// Return Functions - Display

function PhysWidth()
endfunction display.physWidth#
function PhysHeight()
endfunction display.physHeight#
function VirtWidth()
endfunction display.virtWidth#
function VirtHeight()
endfunction display.virtHeight#
function IconSize()
endfunction display.iconSize#
function TextHeight()
endfunction display.textHeight#
function LastFPS()
endfunction display.lastFPS#
function IconImage()
endfunction display.iconImage
function HighSprite()
endfunction display.highSprite
function Transition()
endfunction display.transType
function TransitionCount()
endfunction display.transCount#
function TransitionStep()
endfunction display.transStep#

// Return Functions - Touch

function PointDX()
endfunction touch.pointDX#
function PointDY()
endfunction touch.pointDY#
function PointWX()
endfunction touch.pointWX#
function PointWY()
endfunction touch.pointWY#
function SpritePicked()
endfunction touch.spritePicked
function ObjectPicked()
endfunction touch.objectPicked
function MenuPicked()
endfunction touch.menuPicked
function PickOffsetX()
endfunction touch.pickOffsetX#
function PickOffsetY()
endfunction touch.pickOffsetY#

// Return Functions - Panel

function PanelHeight()
endfunction panel.height#
function PanelSpeed()
endfunction panel.speed#
function PanelIsOpen()
endfunction panel.isOpen
function PanelImage()
endfunction panel.image
function PanelSprite()
endfunction panel.sprite
function PanelText()
endfunction panel.text
function PanelSound()
endfunction panel.sound
function PanelIconMax()
endfunction panel.iconMax

// Return Functions - WorkObject Array

function WorkObjectIsFree( thisObject )
   if WorkObjectValid( thisObject )
      thisIsFree = workObject[ thisObject ].isFree
   else
      thisIsFree = -1
   endif
endfunction thisIsFree

function WorkObjectID( thisObject )
   if WorkObjectValid( thisObject )
      thisObjectID = workObject[ thisObject ].objectID
   else
      thisObjectID = -1
   endif
endfunction thisObjectID

function WorkObjectSprite( thisObject )
   if WorkObjectValid( thisObject )
      thisSprite = workObject[ thisObject ].sprite
   else
      thisSprite = -1
   endif
endfunction thisSprite

// Return Functions - Main Object

function ObjectName( thisObject )
   if ObjectValid( thisObject )
      thisString$ = MainObject [ thisObject ].name$
   else
      thisString$ = ""
   endif
endfunction thisString$

function ObjectDetail( thisObject )
   if ObjectValid( thisObject )
      thisString$ = MainObject [ thisObject ].detail$
   else
      thisString$ = ""
   endif
endfunction thisString$


function ObjectIcon( thisObject )
   if ObjectValid( thisObject )
      thisIcon = MainObject [ thisObject ].icon
   else
      thisIcon = -1
   endif
endfunction thisIcon

function ObjectIsKnown( thisObject )
   if ObjectValid( thisObject )
      thisBool = MainObject [ thisObject ].isKnown
   else
      thisBool = -1
   endif
endfunction thisBool

function ObjectIsGroup( thisObject )
   if ObjectValid( thisObject )
      thisBool = MainObject [ thisObject ].isGroup
   else
      thisBool = -1
   endif
endfunction thisBool
//

Sunday 22 April 2012

Group Encoding

All the remains of the group membership read routine is the code that converts the string names to Base96.

We will also need a routine to deal with the recipes - which comes later. For now we will just add a comment and the outline of a loop.

Here is the location - between the end of the conversion loop and the start of the group loop.
 ...(conversion loop)...
      if thisValue < 0
         MainObject[ i ].isGroup = 1
      else
         MainObject[ i ].isGroup = 0
      endif
   endif
next i
// Recipes Go Here
for i=topObject() + 1 to thisArraySize

next i
// Clear Temp Array
dim tempArray[ 0 ]
// Group Memberships
for i=1 to topObject()
   if ObjectIsGroup( i ) > 0 and ObjectDetail( i ) <> ""
      ...(group membership loop)...
Also in this section is where the temp array is cleared.

Within the if/endif block for the object groups - immediately after thisStart is set to one, a new local variables is created to hold the encoded string.
thisCoded$ = ""
Now where we had the comment line.
// convert to base96
The following lines are used to replace it.
thisObject = FindObjectByName( thisObject$ )
if thisObject > 0 then thisCoded$ = thisCoded$ + Base96Encode( thisObject )
This looks for the object name in the object array, and if it is found, encodes it's index using Base96 and adds it to the thisCoded$.

When all the objects have been processed ( after the endwhile command), the string is sorted and put back into the detail$ field.
MainObject[ i ].detail$ = Base96Sort( thisCoded$ )
Which completes the group membership encoding.

To test this, we change the group print command to a printc command in the test code.
printc( ", Group: ")
And follow it with the following lines.
thisString$ = ObjectDetail( i )
thisNum = len( thisString$ ) / 2
for j = 1 to thisNum
   thisCode$ = mid( thisString$ , j*2 - 1 , 2)
   thisObject = Base96Decode( thisCode$ )
   printc( ObjectName( thisObject ) )
   if j < thisNum then printc( ", " )
next j
print( "." )
Firstly this transfers the coded string into local variable thisString$, and puts half it's length (the number of encoded objects) into thisNum.

It then performs a loop decoding each one in turn and printing the results. If the one being printed is not the last one, it also prints a comma.

At the end, it prints a full stop and ends the line.
For anyone wondering why tree comes before stone in the member list for resource - which was supposedly sorted when it was encoded.

The answer is, that it was sorted based on the object index, not it's name.  In index order, they are sorted.

If we want them sorted by name, then we have two choices.
  • Sort the object array by object name when first read
  • Add another function to sort the group members based on object name.
For now we wont be doing either.  But the option is there for the future.