Updated Functions example for SetGraphics

' force strict coding standard
Strict

' import the diddy module
Import diddy

' the starting point for a Monkey app
Function Main:Int()
	' create the "game"
	New MyGame
	Return 0
End

' MyGame
Class MyGame Extends DiddyApp
	' save the starting seed
	Field startingSeed:Int
	Field mouseOn:Bool = true

	Method OnCreate:Int()
		Super.OnCreate()
		' set the seed to use the system time
		startingSeed = RealMillisecs()
		Seed = startingSeed
		HideMouse()
		Return 0
	End

	Method OnUpdate:Int()
		If KeyHit(KEY_ENTER)
			If mouseOn
				HideMouse()
				mouseOn = False
			Else
				ShowMouse()
				mouseOn = True
			End
		End
		If KeyHit(KEY_F1) or (TouchHit(0) And TouchY() < SCREEN_HEIGHT2)
			LaunchBrowser("http://www.google.com")
		End
		If KeyHit(KEY_F2) or (TouchHit(0) And TouchY() > SCREEN_HEIGHT2)
			LaunchEmail("test@testdomain.com", "TEST SUBJECT", "TEST TEXT!")
		End

		If KeyHit(KEY_ESCAPE)
			ExitApp()
		End
		if KeyHit(KEY_Q)
			Print"Setting graphics.. to 1024x768"
			SetGraphics(1024, 768)
		End
		if KeyHit(KEY_W)
			Print"Setting graphics.. to 800 x 600"
			SetGraphics(800, 600)
		End
		if KeyHit(KEY_E)
			Print"Setting graphics.. to 640 x 480"
			SetGraphics(640, 480)
		End
		' this will print "No FlushKeys 3 times
		For Local i:Int = 1 To 3
			If KeyHit(KEY_SPACE)
				Print "No FlushKeys"
			End
		Next
		' this will print "FlushKeys once
		For Local i:Int = 1 To 3
			If KeyHit(KEY_SPACE)
				Print "FlushKeys"
				' force the seed to change
				Rnd(0,1000)
				SetMouse(100, 100)
			End
			' clear the key hits
			FlushKeys()
		Next
		If Not mouseOn
			if (MouseX() < 0 or MouseX() > DEVICE_WIDTH or MouseY()< 0 or MouseY() > DEVICE_HEIGHT)
				ShowMouse()
				SetMouse(MouseX(), MouseY())
			Else
				HideMouse()
			End
		End

		Return 0
	End

	Method OnRender:Int()
		Cls
		DrawText "Starting Seed = "+startingSeed, 10, 10
		DrawText "Seed          = "+Seed, 10, 20
		DrawText "RealMillisecs = "+RealMillisecs(), 10, 30
		DrawText "Millisecs     = "+Millisecs(), 10, 40
		If mouseOn
			DrawText "Mouse On      = true (press enter to toggle)", 10, 50
		Else
			DrawText "Mouse On      = false (press enter to toggle)", 10, 50
		End
		DrawText "UpdateRate    = "+GetUpdateRate(), 10, 60
		DrawText "MouseX        = "+MouseX(), 10, 70
		DrawText "MouseY        = "+MouseY(), 10, 80
		DrawText "DEVICE_WIDTH  = "+DEVICE_WIDTH, 10, 90
		DrawText "DEVICE_HEIGHT = "+DEVICE_HEIGHT, 10, 100
		DrawText "Press F1 or click the top half of the screen to Launch Browser", 10, 120
		DrawText "Press F2 or click the bottom half of the screen to Launch Email", 10, 130
		DrawText "Press Q to Change Screen size to 1024x768", 10, 140
		DrawText "Press W to Change Screen size to 800x600", 10, 150
		DrawText "Press E to Change Screen size to 640x480", 10, 160
		Return 0
	End

End

' force strict coding standard
Strict

' import the diddy module
Import diddy

' the starting point for a Monkey app
Function Main:Int()
	' create the "game"
	New MyGame
	Return 0
End

' MyGame
Class MyGame Extends DiddyApp
	' save the starting seed
	Field startingSeed:Int
	Field mouseOn:Bool = true

	Method OnCreate:Int()
		Super.OnCreate()
		' set the seed to use the system time
		startingSeed = RealMillisecs()
		Seed = startingSeed
		ShowMouse()

		Return 0
	End

	Method OnUpdate:Int()
		If KeyHit(KEY_ENTER)
			If mouseOn
				HideMouse()
				mouseOn = False
			Else
				ShowMouse()
				mouseOn = True
			End
		End
		If KeyHit(KEY_F1) or (TouchHit(0) And TouchY() < SCREEN_HEIGHT2)
			LaunchBrowser("http://www.google.com")
		End
		If KeyHit(KEY_F2) or (TouchHit(0) And TouchY() > SCREEN_HEIGHT2)
			LaunchEmail("test@testdomain.com", "TEST SUBJECT", "TEST TEXT!")
		End

		If KeyHit(KEY_ESCAPE)
			ExitApp()
		End

		' this will print "No FlushKeys 3 times
		For Local i:Int = 1 To 3
			If KeyHit(KEY_SPACE)
				Print "No FlushKeys"
			End
		Next
		' this will print "FlushKeys once
		For Local i:Int = 1 To 3
			If KeyHit(KEY_SPACE)
				Print "FlushKeys"
				' force the seed to change
				Rnd(0,1000)
				SetMouse(100, 100)
			End
			' clear the key hits
			FlushKeys()
		Next
		If Not mouseOn
			if (MouseX() < 0 or MouseX() > DEVICE_WIDTH or MouseY()< 0 or MouseY() > DEVICE_HEIGHT)
				ShowMouse()
				SetMouse(MouseX(), MouseY())
			Else
				HideMouse()
			End
		End
		Return 0
	End

	Method OnRender:Int()
		Cls
		DrawText "Starting Seed = "+startingSeed, 10, 10
		DrawText "Seed          = "+Seed, 10, 20
		DrawText "RealMillisecs = "+RealMillisecs(), 10, 30
		DrawText "Millisecs     = "+Millisecs(), 10, 40
		If mouseOn
			DrawText "Mouse On      = true (press enter to toggle)", 10, 50
		Else
			DrawText "Mouse On      = false (press enter to toggle)", 10, 50
		End
		DrawText "UpdateRate    = "+GetUpdateRate(), 10, 60
		DrawText "MouseX        = "+MouseX(), 10, 70
		DrawText "MouseY        = "+MouseY(), 10, 80
		DrawText "DEVICE_WIDTH  = "+DEVICE_WIDTH, 10, 90
		DrawText "DEVICE_HEIGHT = "+DEVICE_HEIGHT, 10, 100
		DrawText "Press F1 or click the top half of the screen to Launch Browser", 10, 120
		DrawText "Press F2 or click the bottom half of the screen to Launch Email", 10, 130
		Return 0
	End

End


Updated Functions example.

Updated Functions example.

' force strict coding standard
Strict

' import the diddy module
Import diddy

' the starting point for a Monkey app
Function Main:Int()
	' create the "game"
	New MyGame
	Return 0
End

' MyGame
Class MyGame Extends DiddyApp
	' save the starting seed
	Field startingSeed:Int
	Field mouseOn:Bool = true

	Method OnCreate:Int()
		Super.OnCreate()
		' set the seed to use the system time
		startingSeed = RealMillisecs()
		Seed = startingSeed
		ShowMouse()

		Return 0
	End

	Method OnUpdate:Int()
		If KeyHit(KEY_ENTER)
			If mouseOn
				HideMouse()
				mouseOn = False
			Else
				ShowMouse()
				mouseOn = True
			End
		End
		If KeyHit(KEY_F1) or (TouchHit(0) And TouchY() < SCREEN_HEIGHT2)
			LaunchBrowser("http://www.google.com")
		End
		If KeyHit(KEY_F2) or (TouchHit(0) And TouchY() > SCREEN_HEIGHT2)
			LaunchEmail("test@testdomain.com", "TEST SUBJECT", "TEST TEXT!")
		End

		If KeyHit(KEY_ESCAPE)
			ExitApp()
		End

		' this will print "No FlushKeys 3 times
		For Local i:Int = 1 To 3
			If KeyHit(KEY_SPACE)
				Print "No FlushKeys"
			End
		Next
		' this will print "FlushKeys once
		For Local i:Int = 1 To 3
			If KeyHit(KEY_SPACE)
				Print "FlushKeys"
				' force the seed to change
				Rnd(0,1000)
				SetMouse(100, 100)
			End
			' clear the key hits
			FlushKeys()
		Next
		If Not mouseOn
			if (MouseX() < 0 or MouseX() > DEVICE_WIDTH or MouseY()< 0 or MouseY() > DEVICE_HEIGHT)
				ShowMouse()
				SetMouse(MouseX(), MouseY())
			Else
				HideMouse()
			End
		End
		Return 0
	End

	Method OnRender:Int()
		Cls
		DrawText "Starting Seed = "+startingSeed, 10, 10
		DrawText "Seed          = "+Seed, 10, 20
		DrawText "RealMillisecs = "+RealMillisecs(), 10, 30
		DrawText "Millisecs     = "+Millisecs(), 10, 40
		If mouseOn
			DrawText "Mouse On      = true (press enter to toggle)", 10, 50
		Else
			DrawText "Mouse On      = false (press enter to toggle)", 10, 50
		End
		DrawText "UpdateRate    = "+GetUpdateRate(), 10, 60
		DrawText "MouseX        = "+MouseX(), 10, 70
		DrawText "MouseY        = "+MouseY(), 10, 80
		DrawText "DEVICE_WIDTH  = "+DEVICE_WIDTH, 10, 90
		DrawText "DEVICE_HEIGHT = "+DEVICE_HEIGHT, 10, 100
		DrawText "Press F1 or click the top half of the screen to Launch Browser", 10, 120
		DrawText "Press F2 or click the bottom half of the screen to Launch Email", 10, 130
		Return 0
	End

End

' force strict coding standard
Strict

' import the diddy module
Import diddy

' the starting point for a Monkey app
Function Main:Int()
	' create the "game"
	New MyGame
	Return 0
End

' MyGame
Class MyGame Extends DiddyApp
	' save the starting seed
	Field startingSeed:Int
	Field mouseOn:Bool = true

	Method OnCreate:Int()
		Super.OnCreate()
		' set the seed to use the system time
		startingSeed = RealMillisecs()
		Seed = startingSeed
		ShowMouse()

		Return 0
	End

	Method OnUpdate:Int()
		If KeyHit(KEY_ENTER)
			If mouseOn
				HideMouse()
				mouseOn = False
			Else
				ShowMouse()
				mouseOn = True
			End
		End

		If KeyHit(KEY_ESCAPE)
			ExitApp()
		End

		' this will print "No FlushKeys 3 times
		For Local i:Int = 1 To 3
			If KeyHit(KEY_SPACE)
				Print "No FlushKeys"
			End
		Next
		' this will print "FlushKeys once
		For Local i:Int = 1 To 3
			If KeyHit(KEY_SPACE)
				Print "FlushKeys"
				' force the seed to change
				Rnd(0,1000)
				SetMouse(100, 100)
			End
			' clear the key hits
			FlushKeys()
		Next
		If Not mouseOn
			if (MouseX() < 0 or MouseX() > DEVICE_WIDTH or MouseY()< 0 or MouseY() > DEVICE_HEIGHT)
				ShowMouse()
				SetMouse(MouseX(), MouseY())
			Else
				HideMouse()
			End
		End
		Return 0
	End

	Method OnRender:Int()
		Cls
		DrawText "Starting Seed = "+startingSeed, 10, 10
		DrawText "Seed          = "+Seed, 10, 20
		DrawText "RealMillisecs = "+RealMillisecs(), 10, 30
		DrawText "Millisecs     = "+Millisecs(), 10, 40
		If mouseOn
			DrawText "Mouse On      = true (press enter to toggle)", 10, 50
		Else
			DrawText "Mouse On      = false (press enter to toggle)", 10, 50
		End
		DrawText "UpdateRate    = "+GetUpdateRate(), 10, 60
		DrawText "MouseX        = "+MouseX(), 10, 70
		DrawText "MouseY        = "+MouseY(), 10, 80
		DrawText "DEVICE_WIDTH  = "+DEVICE_WIDTH, 10, 90
		DrawText "DEVICE_HEIGHT = "+DEVICE_HEIGHT, 10, 100

		Return 0
	End

End


Added Zwer99 scrolling fix for particles


Removed stupid iOS wrapped code which cause MouseZInit to be added twice... WTF!

--HG--
branch : development


fix potential crash if OnSuspend is triggered before OnUpdate

Strict

Private

Import bono
Import brl.asyncevent
Import mojo.app
Import mojo.graphics

Public

Class App Extends app.App Abstract
    Private

    Field timer:DeltaTimer
    Field contentScaler:ContentScaler

    Public

    Const DEFAULT_FPS:Int = 60
    Field renderable:Renderable
    Field suspendable:Suspendable
    Field updateable:Updateable

    Method Run:Void() Abstract

    Method TranslateSpace:Vector2D(vec:Vector2D)
        If Not contentScaler Then Return vec.Copy()
        Return contentScaler.TranslateSpace(Self, vec.Copy())
    End

    Method GetDirector:Director()
        Return Director.Shared()
    End

    Method SetHandler:Void(obj:Object)
        renderable = Renderable(obj)
        suspendable = Suspendable(obj)
        updateable = Updateable(obj)
    End

    ' --- CONFIG

    Method GetTargetFps:Int()
        Return DEFAULT_FPS
    End

    Method GetContentScaler:ContentScaler()
        Return Null
    End

    Method GetVirtualSize:Vector2D()
        Return Device.GetSize()
    End

    ' --- MONKEY APP

    Method OnCreate:Int()
        Try
            SetUpdateRate(GetTargetFps())
            contentScaler = GetContentScaler()

            Director.Shared().SetApp(Self)
            Run()
        Catch ex:Exception
            Error ex
        End
        Return 0
    End

    Method OnRender:Int()
        Try
            Cls(0, 0, 0)
            UpdateAsyncEvents()
            If contentScaler Then contentScaler.OnRenderPre(Self)
            If renderable Then renderable.OnRender()
            If contentScaler Then contentScaler.OnRenderPost(Self)
        Catch ex:Exception
            Error ex
        End
        Return 0
    End

    Method OnUpdate:Int()
        If Not timer Then timer = New DeltaTimer(GetTargetFps())

        Try
            timer.OnUpdate()
            If timer.delta <> 0 And updateable Then updateable.OnUpdate(timer)
        Catch ex:Exception
            Error ex
        End
        Return 0
    End

    Method OnResume:Int()
        Try
            If timer Then timer.Play()
            If suspendable Then suspendable.OnResume()
        Catch ex:Exception
            Error ex
        End
        Return 0
    End

    Method OnSuspend:Int()
        Try
            If timer Then timer.Pause()
            If suspendable Then suspendable.OnSuspend()
        Catch ex:Exception
            Error ex
        End
        Return 0
    End
End

Strict

Private

Import bono
Import brl.asyncevent
Import mojo.app
Import mojo.graphics

Public

Class App Extends app.App Abstract
    Private

    Field timer:DeltaTimer
    Field contentScaler:ContentScaler

    Public

    Const DEFAULT_FPS:Int = 60
    Field renderable:Renderable
    Field suspendable:Suspendable
    Field updateable:Updateable

    Method Run:Void() Abstract

    Method TranslateSpace:Vector2D(vec:Vector2D)
        If Not contentScaler Then Return vec.Copy()
        Return contentScaler.TranslateSpace(Self, vec.Copy())
    End

    Method GetDirector:Director()
        Return Director.Shared()
    End

    Method SetHandler:Void(obj:Object)
        renderable = Renderable(obj)
        suspendable = Suspendable(obj)
        updateable = Updateable(obj)
    End

    ' --- CONFIG

    Method GetTargetFps:Int()
        Return DEFAULT_FPS
    End

    Method GetContentScaler:ContentScaler()
        Return Null
    End

    Method GetVirtualSize:Vector2D()
        Return Device.GetSize()
    End

    ' --- MONKEY APP

    Method OnCreate:Int()
        Try
            SetUpdateRate(GetTargetFps())
            contentScaler = GetContentScaler()

            Director.Shared().SetApp(Self)
            Run()
        Catch ex:Exception
            Error ex
        End
        Return 0
    End

    Method OnRender:Int()
        Try
            Cls(0, 0, 0)
            UpdateAsyncEvents()
            If contentScaler Then contentScaler.OnRenderPre(Self)
            If renderable Then renderable.OnRender()
            If contentScaler Then contentScaler.OnRenderPost(Self)
        Catch ex:Exception
            Error ex
        End
        Return 0
    End

    Method OnUpdate:Int()
        If Not timer Then timer = New DeltaTimer(GetTargetFps())

        Try
            timer.OnUpdate()
            If timer.delta <> 0 And updateable Then updateable.OnUpdate(timer)
        Catch ex:Exception
            Error ex
        End
        Return 0
    End

    Method OnResume:Int()
        Try
            timer.Play()
            If suspendable Then suspendable.OnResume()
        Catch ex:Exception
            Error ex
        End
        Return 0
    End

    Method OnSuspend:Int()
        Try
            timer.Pause()
            If suspendable Then suspendable.OnSuspend()
        Catch ex:Exception
            Error ex
        End
        Return 0
    End
End


Replaced AssertError with XMLParseException in xml.monkey.

' Diddy XML parser
' This is a very simple XML parser, and expects a document to be as close to well-formed as possible.
' The only real leniency it has is that attributes may be defined without surrounding quotes/double quotes.
' All XML characters should be escaped in strings. > < etc.
' Currently it supports regular tag and attribute definitions, processing instructions, and skipping comments.
' For now, exported indentation is hardcoded to 2 spaces.
' Written from scratch, using as little string manipulation as possible.

Import assert
Import functions
Import collections
Import stringbuilder

Class XMLParser
	Const TAG_DEFAULT:Int = 0
	Const TAG_COMMENT:Int = 1
	Const TAG_CDATA:Int = 2
	Const TAG_DOCTYPE:Int = 3

	Field str:String
	Field tags:Int[] ' tag character indexes, alternating < and >
	Field tagType:Int[]
	Field tagCount:Int = 0
	Field tagsLength:Int
	Field quotes:Int[] ' quote character indexes, alternating opening and closing
	Field quoteCount:Int = 0
	Field quotesLength:Int
	Field pis:Int[] ' pi character indexes, alternating <? And ?> (index is on <>)
	Field piCount:Int = 0
	Field pisLength:Int

	Method CacheControlCharacters:Void()
		tagsLength = 128
		quotesLength = 128
		pisLength = 128
		tags = New Int[tagsLength]
		tagType = New Int[tagsLength]
		quotes = New Int[quotesLength]
		pis = New Int[quotesLength]
		tagCount = 0
		quoteCount = 0
		piCount = 0
		Local inTag:Bool = False
		Local inQuote:Bool = False
		Local inComment:Bool = False
		Local inCdata:Bool = False
		Local inDoctype:Bool = False
		Local inPi:Bool = False
		Local strlen:Int = str.Length
		For Local i:Int = 0 Until strlen
			' if we're in a comment, we're only looking for -->
			If inComment Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_HYPHEN And str[i-2] = ASC_HYPHEN Then
					If tagCount+1 >= tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_COMMENT
					tagCount += 1
					inComment = False
				End
			' if we're in a cdata, we're only looking for ]]>
			ElseIf inCdata Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_CLOSE_BRACKET And str[i-2] = ASC_CLOSE_BRACKET Then
					If tagCount+1 >= tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_CDATA
					tagCount += 1
					inCdata = False
				End
			' if we're in a quoted string, we're only looking for "
			ElseIf inQuote Then
				If str[i] = ASC_DOUBLE_QUOTE Then
					If quoteCount+1 >= quotesLength Then
						quotesLength *= 2
						quotes = quotes.Resize(quotesLength)
					End
					quotes[quoteCount] = i
					quoteCount += 1
					inQuote = False
				End
			' check if we should start a new quoted string
			ElseIf str[i] = ASC_DOUBLE_QUOTE Then
				If quoteCount+1 >= quotesLength Then
					quotesLength *= 2
					quotes = quotes.Resize(quotesLength)
				End
				quotes[quoteCount] = i
				quoteCount += 1
				inQuote = True
			' if we're in a processing instruction, we're only looking for ?>
			ElseIf inPi Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_QUESTION Then
					If piCount+1 >= pisLength Then
						pisLength *= 2
						pis = pis.Resize(pisLength)
					End
					pis[piCount] = i
					piCount += 1
					inPi = False
				End
			' if we're in a doctype, we're only looking for >
			ElseIf inDoctype Then
				If str[i] = ASC_GREATER_THAN Then
					If tagCount+1 >= tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_DOCTYPE
					tagCount += 1
					inDoctype = False
				End
			' less than
			ElseIf str[i] = ASC_LESS_THAN Then
				' if we're in a tag, die
				If inTag Then Throw New XMLParseException("Invalid less than!")
				' check for prolog
				If str[i+1] = ASC_EXCLAMATION Then
					' comment?
					If str[i+2] = ASC_HYPHEN And str[i+3] = ASC_HYPHEN Then
						If tagCount+1 >= tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_COMMENT
						tagCount += 1
						inComment = True
					' cdata?
					ElseIf str[i+2] = ASC_OPEN_BRACKET And
							(str[i+3] = ASC_UPPER_C Or str[i+3] = ASC_LOWER_C) And
							(str[i+4] = ASC_UPPER_D Or str[i+4] = ASC_LOWER_D) And
							(str[i+5] = ASC_UPPER_A Or str[i+5] = ASC_LOWER_A) And
							(str[i+6] = ASC_UPPER_T Or str[i+6] = ASC_LOWER_T) And
							(str[i+7] = ASC_UPPER_A Or str[i+7] = ASC_LOWER_A) And
							str[i+8] = ASC_OPEN_BRACKET Then
						If tagCount+1 >= tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_CDATA
						tagCount += 1
						inCdata = True
					' doctype?
					ElseIf (str[i+2] = ASC_UPPER_D Or str[i+2] = ASC_LOWER_D) And
							(str[i+3] = ASC_UPPER_O Or str[i+3] = ASC_LOWER_O) And
							(str[i+4] = ASC_UPPER_C Or str[i+4] = ASC_LOWER_C) And
							(str[i+5] = ASC_UPPER_T Or str[i+5] = ASC_LOWER_T) And
							(str[i+6] = ASC_UPPER_Y Or str[i+6] = ASC_LOWER_Y) And
							(str[i+7] = ASC_UPPER_P Or str[i+7] = ASC_LOWER_P) And
							(str[i+8] = ASC_UPPER_E Or str[i+8] = ASC_LOWER_E) Then
						If tagCount+1 >= tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_DOCTYPE
						tagCount += 1
						inDoctype = True
					Else
						Throw New XMLParseException("Invalid prolog.")
					End
				' check for processing instruction
				ElseIf str[i+1] = ASC_QUESTION Then
					If piCount+1 >= pisLength Then
						pisLength *= 2
						pis = pis.Resize(pisLength)
					End
					pis[piCount] = i
					piCount += 1
					inPi = True
				' finally, it must just be opening a tag
				Else
					If tagCount+1 >= tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_DEFAULT
					tagCount += 1
					inTag = True
				End
			' greater than
			ElseIf str[i] = ASC_GREATER_THAN Then
				If Not inTag Then Throw New XMLParseException("Invalid greater than!")
				If tagCount+1 = tagsLength Then
					tagsLength *= 2
					tags = tags.Resize(tagsLength)
					tagType = tagType.Resize(tagsLength)
				End
				tags[tagCount] = i
				tagType[tagCount] = TAG_DEFAULT
				tagCount += 1
				inTag = False
			End
		Next
		If inQuote Then Throw New XMLParseException("Unclosed quote!")
		If inTag Then Throw New XMLParseException("Unclosed tag!")
		If inComment Then Throw New XMLParseException("Unclosed comment!")
		If inCdata Then Throw New XMLParseException("Unclosed cdata!")
		If inPi Then Throw New XMLParseException("Unclosed processing instruction!")
	End

	' get the contents of a tag (given start and end)
	Method GetTagContents:XMLElement(startIndex:Int, endIndex:Int)
		' die if empty tag
		If startIndex = endIndex Then Throw New XMLParseException("Empty tag detected.")
		' our element
		Local e:XMLElement = New XMLElement
		Local a:Int, singleQuoted:Bool, doubleQuoted:Bool, key:String, value:String

		' get the name
		a = startIndex
		While a < endIndex
			If str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR Or a = endIndex-1 Then
				If a = endIndex-1 Then
					e.name = str[startIndex..endIndex]
				Else
					e.name = str[startIndex..a]
				End
				a += 1
				Exit
			End
			a += 1
		End
		startIndex = a

		' TODO: validate tag name is alphanumeric
		' if no name, die
		If e.name = "" Then Throw New XMLParseException("Error reading tag name.")

		' loop on all tokens
		While startIndex < endIndex
			' trim leading whitespace
			While startIndex < endIndex And (str[startIndex]=ASC_SPACE Or str[startIndex]=ASC_TAB Or str[startIndex]=ASC_LF Or str[startIndex]=ASC_CR)
				startIndex += 1
			End

			' clear check variables
			singleQuoted = False
			doubleQuoted = False
			key = ""
			value = ""

			' find the key
			a = startIndex
			While a < endIndex
				If str[a] = ASC_EQUALS Or str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR Or a = endIndex-1 Then
					If a=endIndex-1 Then
						key = str[startIndex..endIndex]
					Else
						key = str[startIndex..a]
					End
					a += 1
					Exit
				End
				a += 1
			End
			startIndex = a

			' if the key is empty, there was an error (unless we've hit the end of the string)
			If key = "" Then
				If a < endIndex Then
					Throw New XMLParseException("Error reading attribute key.")
				Else
					Exit
				End
			End

			' if it stopped on an equals, get the value
			If str[a-1] = ASC_EQUALS Then
				singleQuoted = False
				doubleQuoted = False
				While a < endIndex
					' check if it's a single quote
					If str[a] = ASC_SINGLE_QUOTE And Not doubleQuoted Then
						' if this is the first index, mark it as quoted
						If a = startIndex Then
							singleQuoted = True
						' otherwise, if we're not quoted at all, die
						ElseIf Not singleQuoted And Not doubleQuoted Then
							Throw New XMLParseException("Unexpected single quote detected in attribute value.")
						Else
							' we must be ending the quote here, so grab it and break out
							singleQuoted = False
							value = str[startIndex+1..a]
							a += 1
							Exit
						End

					' check if it's a double quote
					ElseIf str[a] = ASC_DOUBLE_QUOTE And Not singleQuoted Then
						' if this is the first index, mark it as quoted
						If a = startIndex Then
							doubleQuoted = True
						' otherwise, if we're not quoted at all, die
						ElseIf Not singleQuoted And Not doubleQuoted Then
							Throw New XMLParseException("Unexpected double quote detected in attribute value.")
						Else
							' we must be ending the quote here, so break out
							doubleQuoted = False
							value = str[startIndex+1..a]
							a += 1
							Exit
						End

					' should we be ending the attribute?
					ElseIf a = endIndex-1 Or (Not singleQuoted And Not doubleQuoted And (str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR))
						If a=endIndex-1 Then
							value = str[startIndex..endIndex]
						Else
							value = str[startIndex..a]
						End
						a += 1
						Exit
					End
					a += 1
				End
				startIndex = a
				value = UnescapeXMLString(value)

				If singleQuoted Or doubleQuoted Then Throw New XMLParseException("Unclosed quote detected.")
			End

			' set the attribute
			e.SetAttribute(key, value)

			If a >= endIndex Then Exit
		End
		Return e
	End

	Method ParseFile:XMLDocument(filename:String)
		Local xmlString:String = LoadString(filename)
		If Not xmlString Then
			Throw New XMLParseException("Error: Cannot load " + filename)
		End
		Return ParseString(xmlString)
	End

	Method TrimString:Void(startIdx:Int, endIdx:Int, trimmed:Int[])
		Local trimStart:Int = startIdx, trimEnd:Int = endIdx
		While trimEnd > trimStart
			Local ch:Int = str[trimEnd-1]
			If ch = ASC_CR Or ch = ASC_LF Or ch = ASC_SPACE Or ch = ASC_TAB Then
				trimEnd -= 1
			Else
				Exit
			End
		End
		While trimStart < trimEnd
			Local ch:Int = str[trimStart]
			If ch = ASC_CR Or ch = ASC_LF Or ch = ASC_SPACE Or ch = ASC_TAB Then
				trimStart += 1
			Else
				Exit
			End
		End
		trimmed[0] = trimStart
		trimmed[1] = trimEnd
	End

	' parses an xml doc, currently doesn't support nested PI or prolog
	Method ParseString:XMLDocument(str:String)
		Self.str = str

		Local doc:XMLDocument = New XMLDocument
		Local elements:ArrayList<XMLElement> = New ArrayList<XMLElement>
		Local thisE:XMLElement = Null, newE:XMLElement = Null
		Local index:Int = 0, a:Int, b:Int, c:Int, nextIndex:Int
		Local trimmed:Int[] = New Int[2]

		' cache all the control characters
		CacheControlCharacters()

		' find first opening tag
		If tagCount = 0 Then Throw New XMLParseException("Something seriously wrong... no tags!")

		' parse processing instructions
		index = 0
		a = pis[index]+2
		b = pis[index+1]-1
		While index < piCount
			TrimString(a, b, trimmed)
			If trimmed[0] <> trimmed[1] Then
				newE = GetTagContents(trimmed[0], trimmed[1])
				newE.pi = True
				doc.pi.Add(newE)
				newE = Null
			Else
				Throw New XMLParseException("Empty processing instruction.")
			End
			index += 2
		End

		' loop on tags
		index = 0
		While index+1 < tagCount
			' we skip comments
			If tagType[index] = TAG_COMMENT Then
				' skip comments

			' if it's cdata
			ElseIf tagType[index] = TAG_CDATA Then
				' get the text between < and >
				a = tags[index]+9 ' "![CDATA[".Length
				b = tags[index+1]-2 ' "]]".Length

				' add a cdata element
				newE = New XMLElement
				newE.cdata = True
				newE.value = str[a..b]
				newE.parent = thisE
				thisE.AddChild(newE)
				newE = Null

			' otherwise we do normal tag stuff
			Else
				' get the text between < and >
				a = tags[index]+1
				b = tags[index+1]

				' trim the string
				TrimString(a, b, trimmed)

				' if it's a completely empty tag name, die
				If trimmed[0] = trimmed[1] Then Throw New XMLParseException("Empty tag.")

				' check if the first character is a slash (end tag)
				If str[trimmed[0]] = ASC_SLASH Then
					' if no current element, die
					If thisE = Null Then Throw New XMLParseException("Closing tag found outside main document tag.")

					' strip the slash
					trimmed[0] += 1

					' check that the tag name length matches
					If trimmed[1] - trimmed[0] <> thisE.name.Length Then Throw New XMLParseException("Closing tag ~q"+str[trimmed[0]..trimmed[1]]+"~q does not match opening tag ~q"+thisE.name+"~q")

					' check that the tag name matches (manually so that we don't create an entire string slice when the first character could be wrong!)
					For Local nameIdx:Int = 0 Until thisE.name.Length
						If str[trimmed[0]+nameIdx] <> thisE.name[nameIdx] Then Throw New XMLParseException("Closing tag ~q"+str[trimmed[0]..trimmed[1]]+"~q does not match opening tag ~q"+thisE.name+"~q")
					Next

					' pop the element from the stack, or set the document root
					If Not elements.IsEmpty() Then
						thisE = elements.RemoveLast()
					Else
						'doc.root = thisE
						Exit
					End

				' check if the last character is a slash (self closing tag)
				ElseIf str[trimmed[1]-1] = ASC_SLASH Then
					' strip the slash
					trimmed[1] -= 1

					' create an element from the tag
					newE = GetTagContents(trimmed[0], trimmed[1])

					' add as a child or set as the root
					If doc.root = Null Then doc.root = newE
					If thisE <> Null Then
						thisE.AddChild(newE)
					Else
						'doc.root = newE
						Exit
					End
					newE = Null

				' otherwise it's an opening tag
				Else
					' create an element from the tag
					newE = GetTagContents(trimmed[0], trimmed[1])

					If doc.root = Null Then doc.root = newE

					' add as a child if we already have an element
					If thisE <> Null Then
						thisE.AddChild(newE)
					End

					' push this element
					elements.AddLast(thisE)

					' and set as the current
					thisE = newE
					newE = Null
				End
			End

			' get any text between tags
			index += 1
			If index < tagCount Then
				a = tags[index]+1
				b = tags[index+1]
				TrimString(a, b, trimmed)
				If trimmed[0] <> trimmed[1] Then
					If thisE <> Null Then
						thisE.value += UnescapeXMLString(str[trimmed[0]..trimmed[1]])
					Else
						'AssertError("Loose text outside of any tag!") - Getting this for some reason, I'll fix it later :/
					End
				End
			End

			' next tag
			index += 1
		End
		If doc.root = Null Then Throw New XMLParseException("Error parsing XML: no document tag found.")
		Return doc
	End
End

Class XMLDocument
Private
	Field xmlVersion:String = "1.0"
	Field xmlEncoding:String = "UTF-8"
	Field root:XMLElement
	Field pi:ArrayList<XMLElement> = New ArrayList<XMLElement>
	Field prologs:ArrayList<XMLElement> = New ArrayList<XMLElement>

Public

' Constructors
	Method New(rootName:String="")
		If rootName <> "" Then root = New XMLElement(rootName)
	End

	Method New(root:XMLElement)
		Self.root = root
	End

' Methods
	Method ExportString:String(formatXML:Bool = True)
		' we start with the xml instruction
		Local output:String = "<?xml version=~q"+xmlVersion+"~q encoding=~q"+xmlEncoding+"~q?>"
		If formatXML Then output += "~n"
		' root node
		output += root.ToString(formatXML) + "~n"
		' done!
		Return output
	End

	Method SaveFile:Void(filename:String)
		' TODO when file IO is ready!
	End

' Properties
	Method Root:XMLElement() Property
		Return root
	End

	Method Prologs:ArrayList<XMLElement>() Property
		Return prologs
	End

	Method ProcessingInstructions:ArrayList<XMLElement>() Property
		Return pi
	End

	Method Version:String() Property
		Return xmlVersion
	End

	Method Encoding:String() Property
		Return xmlEncoding
	End
End

Class XMLAttribute
Private
	Field name:String
	Field value:String

' Constructors
	Method New(name:String, value:String)
		Self.name = name
		Self.value = value
	End
End

Class XMLElement
Private
	Field openTagStart:String = "<"
	Field openTagEnd:String = ">"
	Field selfCloseEnd:String = " />"
	Field closeTagStart:String = "</"
	Field closeTagEnd:String = ">"

	Field name:String
	Field attributes:ArrayList<XMLAttribute> = New ArrayList<XMLAttribute>
	Field children:ArrayList<XMLElement> = New ArrayList<XMLElement>
	Field value:String
	Field parent:XMLElement

	Field pi:Bool
	Field prolog:Bool
	Field cdata:Bool

Public
' Constructors
	Method New()
	End

	Method New(name:String, parent:XMLElement = Null)
		Self.parent = parent
		Self.name = name
		If parent <> Null Then parent.children.Add(Self)
	End

' Methods
	Method IsProcessingInstruction:Bool()
		Return pi
	End

	Method IsProlog:Bool()
		Return prolog
	End

	Method IsCharacterData:Bool()
		Return cdata
	End

	' avoid using this method if you can, because you should try not to have "floating" elements
	Method AddChild:Void(child:XMLElement)
		If children.Contains(child) Return
		children.Add(child)
		child.parent = Self
	End

	Method HasAttribute:Bool(name:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then Return True
		Next
		Return False
	End

	Method GetAttribute:String(name:String, defaultValue:String = "")
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then Return att.value
		Next
		Return defaultValue
	End

	Method SetAttribute:String(name:String, value:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then
				Local old:String = att.value
				att.value = value
				Return old
			End
		Next
		attributes.Add(New XMLAttribute(name, value))
		Return ""
	End

	Method ClearAttribute:String(name:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then
				attributes.Remove(att)
				Return att.value
			End
		Next
		Return ""
	End

	Method Dispose:Void(removeSelf:Bool = True)
		' dispose children
		Local en:IEnumerator<XMLElement> = children.Enumerator()
		While en.HasNext()
			Local element:XMLElement = en.NextObject()
			element.Dispose(False)
			en.Remove()
		End
		' remove self from parent if this is not recursing
		If removeSelf And parent <> Null Then parent.children.Remove(Self)
		' clear out the parent
		parent = Null
	End

	Method ToString:String(formatXML:Bool=True, indentation:Int=0)
		Local rv:String = ""
		If formatXML Then
			For Local i% = 0 Until indentation
				rv += "  "
			Next
		End
		rv += openTagStart + name
		If Not attributes.IsEmpty() Then
			For Local i% = 0 Until attributes.Size
				Local att:XMLAttribute = attributes.Get(i)
				rv += " " + att.name + "=~q" + EscapeXMLString(att.value) + "~q"
			Next
		End
		If children.IsEmpty() Then
			Local esc:String = EscapeXMLString(value.Trim())
			If esc.Length() = 0
				rv += selfCloseEnd
			Else
				rv += openTagEnd + value.Trim() + closeTagStart + name + closeTagEnd
			End
		Else
			rv += openTagEnd
			If formatXML Then
				rv += "~n"
			End
			For Local i% = 0 Until children.Size
				rv += children.Get(i).ToString(formatXML, indentation + 1)
			End
			Local esc:String = EscapeXMLString(value.Trim())
			If esc.Length() > 0 Then
				If Not formatXML Then
					rv += esc
				Else
					rv += "~n" + esc + "~n"
				End
			End
			If formatXML Then
				For Local i% = 0 Until indentation
					rv += "  "
				Next
			End
			rv += closeTagStart + name + closeTagEnd
		End
		If formatXML Then
			rv += "~n"
		End
		Return rv
	End

	Method GetChildrenByName:ArrayList<XMLElement>(findName:String)
		Local rv:ArrayList<XMLElement> = New ArrayList<XMLElement>
		For Local element:XMLElement = Eachin children
			If element.name = findName Then rv.Add(element)
		Next
		Return rv
	End

	Method GetFirstChildByName:XMLElement(findName:String)
		For Local element:XMLElement = Eachin children
			If element.name = findName Then Return element
		Next
		Return Null
	End

' Properties
	Method Children:ArrayList<XMLElement>() Property
		Return children
	End

	Method Parent:XMLElement() Property
		Return parent
	End

	Method Name:String() Property
		Return name
	End

	Method Name:Void(name:String) Property
		Self.name = name
	End

	Method Value:String() Property
		Return value
	End

	Method Value:Void(value:String) Property
		Self.value = value
	End
End

' hopefully this stringbuilder-based replace should be faster since it doesn't create intermediate string objects!
Function EscapeXMLString:String(str:String)
	xmlsb.Length = 0
	For Local i:Int = 0 Until str.Length
		Select str[i]
			Case ASC_AMPERSAND
				xmlsb.Append("&")
			Case ASC_LESS_THAN
				xmlsb.Append("<")
			Case ASC_GREATER_THAN
				xmlsb.Append(">")
			Case ASC_SINGLE_QUOTE
				xmlsb.Append("'")
			Case ASC_DOUBLE_QUOTE
				xmlsb.Append(""")
			Default
				xmlsb.AppendByte(str[i])
		End
	End
	Return xmlsb.ToString()
End

' unescaping is rare so we won't bother using a stringbuilder
Function UnescapeXMLString:String(str:String)
	str = str.Replace(""", "~q")
	str = str.Replace("'", "'")
	str = str.Replace(">", ">")
	str = str.Replace("<", "<")
	str = str.Replace("&", "&")
	Return str
End

Class XMLParseException Extends DiddyException
	Method New(message:String="", cause:Throwable=Null)
		Super.New(message, cause)
	End
End

Private
Global xmlsb:StringBuilder = New StringBuilder

' Diddy XML parser
' This is a very simple XML parser, and expects a document to be as close to well-formed as possible.
' The only real leniency it has is that attributes may be defined without surrounding quotes/double quotes.
' All XML characters should be escaped in strings. > < etc.
' Currently it supports regular tag and attribute definitions, processing instructions, and skipping comments.
' For now, exported indentation is hardcoded to 2 spaces.
' Written from scratch, using as little string manipulation as possible.

Import assert
Import functions
Import collections
Import stringbuilder

Class XMLParser
	Const TAG_DEFAULT:Int = 0
	Const TAG_COMMENT:Int = 1
	Const TAG_CDATA:Int = 2
	Const TAG_DOCTYPE:Int = 3

	Field str:String
	Field tags:Int[] ' tag character indexes, alternating < and >
	Field tagType:Int[]
	Field tagCount:Int = 0
	Field tagsLength:Int
	Field quotes:Int[] ' quote character indexes, alternating opening and closing
	Field quoteCount:Int = 0
	Field quotesLength:Int
	Field pis:Int[] ' pi character indexes, alternating <? And ?> (index is on <>)
	Field piCount:Int = 0
	Field pisLength:Int

	Method CacheControlCharacters:Void()
		tagsLength = 128
		quotesLength = 128
		pisLength = 128
		tags = New Int[tagsLength]
		tagType = New Int[tagsLength]
		quotes = New Int[quotesLength]
		pis = New Int[quotesLength]
		tagCount = 0
		quoteCount = 0
		piCount = 0
		Local inTag:Bool = False
		Local inQuote:Bool = False
		Local inComment:Bool = False
		Local inCdata:Bool = False
		Local inDoctype:Bool = False
		Local inPi:Bool = False
		Local strlen:Int = str.Length
		For Local i:Int = 0 Until strlen
			' if we're in a comment, we're only looking for -->
			If inComment Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_HYPHEN And str[i-2] = ASC_HYPHEN Then
					If tagCount+1 >= tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_COMMENT
					tagCount += 1
					inComment = False
				End
			' if we're in a cdata, we're only looking for ]]>
			ElseIf inCdata Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_CLOSE_BRACKET And str[i-2] = ASC_CLOSE_BRACKET Then
					If tagCount+1 >= tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_CDATA
					tagCount += 1
					inCdata = False
				End
			' if we're in a quoted string, we're only looking for "
			ElseIf inQuote Then
				If str[i] = ASC_DOUBLE_QUOTE Then
					If quoteCount+1 >= quotesLength Then
						quotesLength *= 2
						quotes = quotes.Resize(quotesLength)
					End
					quotes[quoteCount] = i
					quoteCount += 1
					inQuote = False
				End
			' check if we should start a new quoted string
			ElseIf str[i] = ASC_DOUBLE_QUOTE Then
				If quoteCount+1 >= quotesLength Then
					quotesLength *= 2
					quotes = quotes.Resize(quotesLength)
				End
				quotes[quoteCount] = i
				quoteCount += 1
				inQuote = True
			' if we're in a processing instruction, we're only looking for ?>
			ElseIf inPi Then
				If str[i] = ASC_GREATER_THAN And str[i-1] = ASC_QUESTION Then
					If piCount+1 >= pisLength Then
						pisLength *= 2
						pis = pis.Resize(pisLength)
					End
					pis[piCount] = i
					piCount += 1
					inPi = False
				End
			' if we're in a doctype, we're only looking for >
			ElseIf inDoctype Then
				If str[i] = ASC_GREATER_THAN Then
					If tagCount+1 >= tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_DOCTYPE
					tagCount += 1
					inDoctype = False
				End
			' less than
			ElseIf str[i] = ASC_LESS_THAN Then
				' if we're in a tag, die
				If inTag Then AssertError "Invalid less than!"
				' check for prolog
				If str[i+1] = ASC_EXCLAMATION Then
					' comment?
					If str[i+2] = ASC_HYPHEN And str[i+3] = ASC_HYPHEN Then
						If tagCount+1 >= tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_COMMENT
						tagCount += 1
						inComment = True
					' cdata?
					ElseIf str[i+2] = ASC_OPEN_BRACKET And
							(str[i+3] = ASC_UPPER_C Or str[i+3] = ASC_LOWER_C) And
							(str[i+4] = ASC_UPPER_D Or str[i+4] = ASC_LOWER_D) And
							(str[i+5] = ASC_UPPER_A Or str[i+5] = ASC_LOWER_A) And
							(str[i+6] = ASC_UPPER_T Or str[i+6] = ASC_LOWER_T) And
							(str[i+7] = ASC_UPPER_A Or str[i+7] = ASC_LOWER_A) And
							str[i+8] = ASC_OPEN_BRACKET Then
						If tagCount+1 >= tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_CDATA
						tagCount += 1
						inCdata = True
					' doctype?
					ElseIf (str[i+2] = ASC_UPPER_D Or str[i+2] = ASC_LOWER_D) And
							(str[i+3] = ASC_UPPER_O Or str[i+3] = ASC_LOWER_O) And
							(str[i+4] = ASC_UPPER_C Or str[i+4] = ASC_LOWER_C) And
							(str[i+5] = ASC_UPPER_T Or str[i+5] = ASC_LOWER_T) And
							(str[i+6] = ASC_UPPER_Y Or str[i+6] = ASC_LOWER_Y) And
							(str[i+7] = ASC_UPPER_P Or str[i+7] = ASC_LOWER_P) And
							(str[i+8] = ASC_UPPER_E Or str[i+8] = ASC_LOWER_E) Then
						If tagCount+1 >= tagsLength Then
							tagsLength *= 2
							tags = tags.Resize(tagsLength)
							tagType = tagType.Resize(tagsLength)
						End
						tags[tagCount] = i
						tagType[tagCount] = TAG_DOCTYPE
						tagCount += 1
						inDoctype = True
					Else
						AssertError "Invalid prolog."
					End
				' check for processing instruction
				ElseIf str[i+1] = ASC_QUESTION Then
					If piCount+1 >= pisLength Then
						pisLength *= 2
						pis = pis.Resize(pisLength)
					End
					pis[piCount] = i
					piCount += 1
					inPi = True
				' finally, it must just be opening a tag
				Else
					If tagCount+1 >= tagsLength Then
						tagsLength *= 2
						tags = tags.Resize(tagsLength)
						tagType = tagType.Resize(tagsLength)
					End
					tags[tagCount] = i
					tagType[tagCount] = TAG_DEFAULT
					tagCount += 1
					inTag = True
				End
			' greater than
			ElseIf str[i] = ASC_GREATER_THAN Then
				If Not inTag Then AssertError "Invalid greater than!"
				If tagCount+1 = tagsLength Then
					tagsLength *= 2
					tags = tags.Resize(tagsLength)
					tagType = tagType.Resize(tagsLength)
				End
				tags[tagCount] = i
				tagType[tagCount] = TAG_DEFAULT
				tagCount += 1
				inTag = False
			End
		Next
		If inQuote Then AssertError "Unclosed quote!"
		If inTag Then AssertError "Unclosed tag!"
		If inComment Then AssertError "Unclosed comment!"
		If inCdata Then AssertError "Unclosed cdata!"
		If inPi Then AssertError "Unclosed processing instruction!"
	End

	' get the contents of a tag (given start and end)
	Method GetTagContents:XMLElement(startIndex:Int, endIndex:Int)
		' die if empty tag
		If startIndex = endIndex Then AssertError("Empty tag detected.")
		' our element
		Local e:XMLElement = New XMLElement
		Local a:Int, singleQuoted:Bool, doubleQuoted:Bool, key:String, value:String

		' get the name
		a = startIndex
		While a < endIndex
			If str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR Or a = endIndex-1 Then
				If a = endIndex-1 Then
					e.name = str[startIndex..endIndex]
				Else
					e.name = str[startIndex..a]
				End
				a += 1
				Exit
			End
			a += 1
		End
		startIndex = a

		' TODO: validate tag name is alphanumeric
		' if no name, die
		If e.name = "" Then AssertError("Error reading tag name.")

		' loop on all tokens
		While startIndex < endIndex
			' trim leading whitespace
			While startIndex < endIndex And (str[startIndex]=ASC_SPACE Or str[startIndex]=ASC_TAB Or str[startIndex]=ASC_LF Or str[startIndex]=ASC_CR)
				startIndex += 1
			End

			' clear check variables
			singleQuoted = False
			doubleQuoted = False
			key = ""
			value = ""

			' find the key
			a = startIndex
			While a < endIndex
				If str[a] = ASC_EQUALS Or str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR Or a = endIndex-1 Then
					If a=endIndex-1 Then
						key = str[startIndex..endIndex]
					Else
						key = str[startIndex..a]
					End
					a += 1
					Exit
				End
				a += 1
			End
			startIndex = a

			' if the key is empty, there was an error (unless we've hit the end of the string)
			If key = "" Then
				If a < endIndex Then
					AssertError("Error reading attribute key.")
				Else
					Exit
				End
			End

			' if it stopped on an equals, get the value
			If str[a-1] = ASC_EQUALS Then
				singleQuoted = False
				doubleQuoted = False
				While a < endIndex
					' check if it's a single quote
					If str[a] = ASC_SINGLE_QUOTE And Not doubleQuoted Then
						' if this is the first index, mark it as quoted
						If a = startIndex Then
							singleQuoted = True
						' otherwise, if we're not quoted at all, die
						ElseIf Not singleQuoted And Not doubleQuoted Then
							AssertError("Unexpected single quote detected in attribute value.")
						Else
							' we must be ending the quote here, so grab it and break out
							singleQuoted = False
							value = str[startIndex+1..a]
							a += 1
							Exit
						End

					' check if it's a double quote
					ElseIf str[a] = ASC_DOUBLE_QUOTE And Not singleQuoted Then
						' if this is the first index, mark it as quoted
						If a = startIndex Then
							doubleQuoted = True
						' otherwise, if we're not quoted at all, die
						ElseIf Not singleQuoted And Not doubleQuoted Then
							AssertError("Unexpected double quote detected in attribute value.")
						Else
							' we must be ending the quote here, so break out
							doubleQuoted = False
							value = str[startIndex+1..a]
							a += 1
							Exit
						End

					' should we be ending the attribute?
					ElseIf a = endIndex-1 Or (Not singleQuoted And Not doubleQuoted And (str[a]=ASC_SPACE Or str[a]=ASC_TAB Or str[a]=ASC_LF Or str[a]=ASC_CR))
						If a=endIndex-1 Then
							value = str[startIndex..endIndex]
						Else
							value = str[startIndex..a]
						End
						a += 1
						Exit
					End
					a += 1
				End
				startIndex = a
				value = UnescapeXMLString(value)

				If singleQuoted Or doubleQuoted Then AssertError("Unclosed quote detected.")
			End

			' set the attribute
			e.SetAttribute(key, value)

			If a >= endIndex Then Exit
		End
		Return e
	End

	Method ParseFile:XMLDocument(filename:String)
		Local xmlString:String = LoadString(filename)
		If Not xmlString Then
			AssertError("Error: Cannot load " + filename)
		End
		Return ParseString(xmlString)
	End

	Method TrimString:Void(startIdx:Int, endIdx:Int, trimmed:Int[])
		Local trimStart:Int = startIdx, trimEnd:Int = endIdx
		While trimEnd > trimStart
			Local ch:Int = str[trimEnd-1]
			If ch = ASC_CR Or ch = ASC_LF Or ch = ASC_SPACE Or ch = ASC_TAB Then
				trimEnd -= 1
			Else
				Exit
			End
		End
		While trimStart < trimEnd
			Local ch:Int = str[trimStart]
			If ch = ASC_CR Or ch = ASC_LF Or ch = ASC_SPACE Or ch = ASC_TAB Then
				trimStart += 1
			Else
				Exit
			End
		End
		trimmed[0] = trimStart
		trimmed[1] = trimEnd
	End

	' parses an xml doc, currently doesn't support nested PI or prolog
	Method ParseString:XMLDocument(str:String)
		Self.str = str

		Local doc:XMLDocument = New XMLDocument
		Local elements:ArrayList<XMLElement> = New ArrayList<XMLElement>
		Local thisE:XMLElement = Null, newE:XMLElement = Null
		Local index:Int = 0, a:Int, b:Int, c:Int, nextIndex:Int
		Local trimmed:Int[] = New Int[2]

		' cache all the control characters
		CacheControlCharacters()

		' find first opening tag
		If tagCount = 0 Then AssertError "Something seriously wrong... no tags!"

		' parse processing instructions
		index = 0
		a = pis[index]+2
		b = pis[index+1]-1
		While index < piCount
			TrimString(a, b, trimmed)
			If trimmed[0] <> trimmed[1] Then
				newE = GetTagContents(trimmed[0], trimmed[1])
				newE.pi = True
				doc.pi.Add(newE)
				newE = Null
			Else
				AssertError "Empty processing instruction."
			End
			index += 2
		End

		' loop on tags
		index = 0
		While index+1 < tagCount
			' we skip comments
			If tagType[index] = TAG_COMMENT Then
				' skip comments

			' if it's cdata
			ElseIf tagType[index] = TAG_CDATA Then
				' get the text between < and >
				a = tags[index]+9 ' "![CDATA[".Length
				b = tags[index+1]-2 ' "]]".Length

				' add a cdata element
				newE = New XMLElement
				newE.cdata = True
				newE.value = str[a..b]
				newE.parent = thisE
				thisE.AddChild(newE)
				newE = Null

			' otherwise we do normal tag stuff
			Else
				' get the text between < and >
				a = tags[index]+1
				b = tags[index+1]

				' trim the string
				TrimString(a, b, trimmed)

				' if it's a completely empty tag name, die
				If trimmed[0] = trimmed[1] Then AssertError "Empty tag."

				' check if the first character is a slash (end tag)
				If str[trimmed[0]] = ASC_SLASH Then
					' if no current element, die
					If thisE = Null Then AssertError("Closing tag found outside main document tag.")

					' strip the slash
					trimmed[0] += 1

					' check that the tag name length matches
					If trimmed[1] - trimmed[0] <> thisE.name.Length Then AssertError("Closing tag ~q"+str[trimmed[0]..trimmed[1]]+"~q does not match opening tag ~q"+thisE.name+"~q")

					' check that the tag name matches (manually so that we don't create an entire string slice when the first character could be wrong!)
					For Local nameIdx:Int = 0 Until thisE.name.Length
						If str[trimmed[0]+nameIdx] <> thisE.name[nameIdx] Then AssertError("Closing tag ~q"+str[trimmed[0]..trimmed[1]]+"~q does not match opening tag ~q"+thisE.name+"~q")
					Next

					' pop the element from the stack, or set the document root
					If Not elements.IsEmpty() Then
						thisE = elements.RemoveLast()
					Else
						'doc.root = thisE
						Exit
					End

				' check if the last character is a slash (self closing tag)
				ElseIf str[trimmed[1]-1] = ASC_SLASH Then
					' strip the slash
					trimmed[1] -= 1

					' create an element from the tag
					newE = GetTagContents(trimmed[0], trimmed[1])

					' add as a child or set as the root
					If doc.root = Null Then doc.root = newE
					If thisE <> Null Then
						thisE.AddChild(newE)
					Else
						'doc.root = newE
						Exit
					End
					newE = Null

				' otherwise it's an opening tag
				Else
					' create an element from the tag
					newE = GetTagContents(trimmed[0], trimmed[1])

					If doc.root = Null Then doc.root = newE

					' add as a child if we already have an element
					If thisE <> Null Then
						thisE.AddChild(newE)
					End

					' push this element
					elements.AddLast(thisE)

					' and set as the current
					thisE = newE
					newE = Null
				End
			End

			' get any text between tags
			index += 1
			If index < tagCount Then
				a = tags[index]+1
				b = tags[index+1]
				TrimString(a, b, trimmed)
				If trimmed[0] <> trimmed[1] Then
					If thisE <> Null Then
						thisE.value += UnescapeXMLString(str[trimmed[0]..trimmed[1]])
					Else
						'AssertError("Loose text outside of any tag!") - Getting this for some reason, I'll fix it later :/
					End
				End
			End

			' next tag
			index += 1
		End
		If doc.root = Null Then AssertError("Error parsing XML: no document tag found.")
		Return doc
	End
End

Class XMLDocument
Private
	Field xmlVersion:String = "1.0"
	Field xmlEncoding:String = "UTF-8"
	Field root:XMLElement
	Field pi:ArrayList<XMLElement> = New ArrayList<XMLElement>
	Field prologs:ArrayList<XMLElement> = New ArrayList<XMLElement>

Public

' Constructors
	Method New(rootName:String="")
		If rootName <> "" Then root = New XMLElement(rootName)
	End

	Method New(root:XMLElement)
		Self.root = root
	End

' Methods
	Method ExportString:String(formatXML:Bool = True)
		' we start with the xml instruction
		Local output:String = "<?xml version=~q"+xmlVersion+"~q encoding=~q"+xmlEncoding+"~q?>"
		If formatXML Then output += "~n"
		' root node
		output += root.ToString(formatXML) + "~n"
		' done!
		Return output
	End

	Method SaveFile:Void(filename:String)
		' TODO when file IO is ready!
	End

' Properties
	Method Root:XMLElement() Property
		Return root
	End

	Method Prologs:ArrayList<XMLElement>() Property
		Return prologs
	End

	Method ProcessingInstructions:ArrayList<XMLElement>() Property
		Return pi
	End

	Method Version:String() Property
		Return xmlVersion
	End

	Method Encoding:String() Property
		Return xmlEncoding
	End
End

Class XMLAttribute
Private
	Field name:String
	Field value:String

' Constructors
	Method New(name:String, value:String)
		Self.name = name
		Self.value = value
	End
End

Class XMLElement
Private
	Field openTagStart:String = "<"
	Field openTagEnd:String = ">"
	Field selfCloseEnd:String = " />"
	Field closeTagStart:String = "</"
	Field closeTagEnd:String = ">"

	Field name:String
	Field attributes:ArrayList<XMLAttribute> = New ArrayList<XMLAttribute>
	Field children:ArrayList<XMLElement> = New ArrayList<XMLElement>
	Field value:String
	Field parent:XMLElement

	Field pi:Bool
	Field prolog:Bool
	Field cdata:Bool

Public
' Constructors
	Method New()
	End

	Method New(name:String, parent:XMLElement = Null)
		Self.parent = parent
		Self.name = name
		If parent <> Null Then parent.children.Add(Self)
	End

' Methods
	Method IsProcessingInstruction:Bool()
		Return pi
	End

	Method IsProlog:Bool()
		Return prolog
	End

	Method IsCharacterData:Bool()
		Return cdata
	End

	' avoid using this method if you can, because you should try not to have "floating" elements
	Method AddChild:Void(child:XMLElement)
		If children.Contains(child) Return
		children.Add(child)
		child.parent = Self
	End

	Method HasAttribute:Bool(name:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then Return True
		Next
		Return False
	End

	Method GetAttribute:String(name:String, defaultValue:String = "")
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then Return att.value
		Next
		Return defaultValue
	End

	Method SetAttribute:String(name:String, value:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then
				Local old:String = att.value
				att.value = value
				Return old
			End
		Next
		attributes.Add(New XMLAttribute(name, value))
		Return ""
	End

	Method ClearAttribute:String(name:String)
		For Local i% = 0 Until attributes.Size
			Local att:XMLAttribute = attributes.Get(i)
			If att.name = name Then
				attributes.Remove(att)
				Return att.value
			End
		Next
		Return ""
	End

	Method Dispose:Void(removeSelf:Bool = True)
		' dispose children
		Local en:IEnumerator<XMLElement> = children.Enumerator()
		While en.HasNext()
			Local element:XMLElement = en.NextObject()
			element.Dispose(False)
			en.Remove()
		End
		' remove self from parent if this is not recursing
		If removeSelf And parent <> Null Then parent.children.Remove(Self)
		' clear out the parent
		parent = Null
	End

	Method ToString:String(formatXML:Bool=True, indentation:Int=0)
		Local rv:String = ""
		If formatXML Then
			For Local i% = 0 Until indentation
				rv += "  "
			Next
		End
		rv += openTagStart + name
		If Not attributes.IsEmpty() Then
			For Local i% = 0 Until attributes.Size
				Local att:XMLAttribute = attributes.Get(i)
				rv += " " + att.name + "=~q" + EscapeXMLString(att.value) + "~q"
			Next
		End
		If children.IsEmpty() Then
			Local esc:String = EscapeXMLString(value.Trim())
			If esc.Length() = 0
				rv += selfCloseEnd
			Else
				rv += openTagEnd + value.Trim() + closeTagStart + name + closeTagEnd
			End
		Else
			rv += openTagEnd
			If formatXML Then
				rv += "~n"
			End
			For Local i% = 0 Until children.Size
				rv += children.Get(i).ToString(formatXML, indentation + 1)
			End
			Local esc:String = EscapeXMLString(value.Trim())
			If esc.Length() > 0 Then
				If Not formatXML Then
					rv += esc
				Else
					rv += "~n" + esc + "~n"
				End
			End
			If formatXML Then
				For Local i% = 0 Until indentation
					rv += "  "
				Next
			End
			rv += closeTagStart + name + closeTagEnd
		End
		If formatXML Then
			rv += "~n"
		End
		Return rv
	End

	Method GetChildrenByName:ArrayList<XMLElement>(findName:String)
		Local rv:ArrayList<XMLElement> = New ArrayList<XMLElement>
		For Local element:XMLElement = Eachin children
			If element.name = findName Then rv.Add(element)
		Next
		Return rv
	End

	Method GetFirstChildByName:XMLElement(findName:String)
		For Local element:XMLElement = Eachin children
			If element.name = findName Then Return element
		Next
		Return Null
	End

' Properties
	Method Children:ArrayList<XMLElement>() Property
		Return children
	End

	Method Parent:XMLElement() Property
		Return parent
	End

	Method Name:String() Property
		Return name
	End

	Method Name:Void(name:String) Property
		Self.name = name
	End

	Method Value:String() Property
		Return value
	End

	Method Value:Void(value:String) Property
		Self.value = value
	End
End

' hopefully this stringbuilder-based replace should be faster since it doesn't create intermediate string objects!
Function EscapeXMLString:String(str:String)
	xmlsb.Length = 0
	For Local i:Int = 0 Until str.Length
		Select str[i]
			Case ASC_AMPERSAND
				xmlsb.Append("&")
			Case ASC_LESS_THAN
				xmlsb.Append("<")
			Case ASC_GREATER_THAN
				xmlsb.Append(">")
			Case ASC_SINGLE_QUOTE
				xmlsb.Append("'")
			Case ASC_DOUBLE_QUOTE
				xmlsb.Append(""")
			Default
				xmlsb.AppendByte(str[i])
		End
	End
	Return xmlsb.ToString()
End

' unescaping is rare so we won't bother using a stringbuilder
Function UnescapeXMLString:String(str:String)
	str = str.Replace(""", "~q")
	str = str.Replace("'", "'")
	str = str.Replace(">", ">")
	str = str.Replace("<", "<")
	str = str.Replace("&", "&")
	Return str
End

Private
Global xmlsb:StringBuilder = New StringBuilder


Updated the main example.

Updated the main example.

Strict

Public

' Preprocessor related:
#If TARGET = "stdcpp"
	#USE_MOJOWRAPPER = True
#End

' Imports:
Import networking
Import stringutil

#If Not USE_MOJOWRAPPER
	Import mojo
#Else
	Import mojoemulator
#End

Import brl.asyncevent

' Classes:
Class TestApplication Extends App Implements NetworkListener Final
	' Constant variable(s):
	Const PORT:= 5029

	' Constructor(s):
	Method OnCreate:Int()
		#If Not USE_MOJOWRAPPER
			SetUpdateRate(60)
		#Else
			SetUpdateRate(4)
		#End

		Server = New NetworkEngine()

		Server.SetCallback(Self)

		Server.Host(PORT, True, NetworkEngine.SOCKET_TYPE_TCP)

		' Return the default response.
		Return 0
	End

	Method OnUpdate:Int()
		UpdateAsyncEvents()

		Server.Update()

		If (Client <> Null) Then
			Client.Update()

			#If Not USE_MOJOWRAPPER
				If (KeyHit(KEY_W)) Then
					If (Client.Open And Server.Open) Then
						Print("Sending...")
			#End
						Client.Send(New Packet("Message from the client."), 1)
			#If Not USE_MOJOWRAPPER
					Endif
				Endif
			#End
		Endif

		' Return the default response.
		Return 0
	End

	Method OnRender:Int()
		Cls(205.0, 205.0, 205.0)

		' Return the default response.
		Return 0
	End

	Method OnClose:Int()
		Server.Close()
		Client.Close()

		Return Super.OnClose()
	End

	' Call-backs:
	Method OnNetworkBind:Void(Network:NetworkEngine, Successful:Bool)
		If (Not Successful) Then
			#If CONFIG = "debug"
				DebugStop()
			#End

			OnClose()

			Return
		Else
			If (Network = Client) Then
				Print("Client socket bound.")
			Elseif (Network = Server) Then
				Print("Server socket bound.")

				Client = New NetworkEngine()

				Client.SetCallback(Self)

				Client.Connect("127.0.0.1", PORT, True, NetworkEngine.SOCKET_TYPE_TCP)
			Else
				Print("Unknown network bound.")
			Endif
		Endif

		Return
	End

	Method OnReceiveMessage:Void(Network:NetworkEngine, C:Client, Type:MessageType, Message:Packet, MessageSize:Int)
		Print(Message.ReadString(MessageSize))

		If (Network = Server) Then
			Server.Send(New Packet("Message from the host."), 1)
		Endif

		Return
	End

	Method OnSendComplete:Void(Network:NetworkEngine, P:Packet, Address:SocketAddress, BytesSent:Int)
		'Print("Sending operation complete.")

		Return
	End

	Method OnClientConnect:Bool(Network:NetworkEngine, Address:SocketAddress)
		Local Host:= Address.Host

		#Rem
			If (Host <> "127.0.0.1") Then
				DebugStop()

				Return False
			Endif
		#End

		' Return the default response.
		Return True
	End

	' This is called once, at any time after 'OnClientConnect'.
	Method OnClientAccepted:Void(Network:NetworkEngine, C:Client)
		Print("Client accepted: " + C.Address)

		Return
	End

	' Fields:
	Field Server:NetworkEngine
	Field Client:NetworkEngine
End

' Functions:
Function Main:Int()
	Local Test:= New TestApplication()

	' Return the default response.
	Return 0
End

Strict

Public

' Imports:
Import networking
Import stringutil

Import mojo

Import brl.asyncevent

' Classes:
Class TestApplication Extends App Implements NetworkListener Final
	' Constant variable(s):
	Const PORT:= 5029

	' Constructor(s):
	Method OnCreate:Int()
		SetUpdateRate(60)

		Server = New NetworkEngine()

		Server.SetCallback(Self)

		Server.Host(PORT, True, NetworkEngine.SOCKET_TYPE_TCP)

		' Return the default response.
		Return 0
	End

	Method OnUpdate:Int()
		UpdateAsyncEvents()

		Server.Update()
		Client.Update()

		If (KeyHit(KEY_W) And Client <> Null) Then
			If (Client.Open And Server.Open) Then
				Print("Sending...")

				Client.Send(New Packet("Message from the client."), 1)
			Else
				Print("Unable to send to server - Server: " + BoolToString(Server.Open) + ", Client: " + BoolToString(Client.Open))
			Endif
		Endif

		' Return the default response.
		Return 0
	End

	Method OnRender:Int()
		Cls(205.0, 205.0, 205.0)

		' Return the default response.
		Return 0
	End

	Method OnClose:Int()
		Server.Close()
		Client.Close()

		Return Super.OnClose()
	End

	' Call-backs:
	Method OnNetworkBind:Void(Network:NetworkEngine, Successful:Bool)
		If (Not Successful) Then
			#If CONFIG = "debug"
				DebugStop()
			#End

			OnClose()

			Return
		Else
			If (Network = Client) Then
				Print("Client socket bound.")
			Elseif (Network = Server) Then
				Print("Server socket bound.")

				Client = New NetworkEngine()

				Client.SetCallback(Self)

				Client.Connect("127.0.0.1", PORT, True, NetworkEngine.SOCKET_TYPE_TCP)
			Else
				Print("Unknown network bound.")
			Endif
		Endif

		Return
	End

	Method OnReceiveMessage:Void(Network:NetworkEngine, C:Client, Type:MessageType, Message:Packet, MessageSize:Int)
		Print(Message.ReadString(MessageSize))

		If (Network = Server) Then
			Server.Send(New Packet("Message from the host."), 1)
		Endif

		Return
	End

	Method OnSendComplete:Void(Network:NetworkEngine, P:Packet, Address:SocketAddress, BytesSent:Int)
		Print("Sending operation complete.")

		Return
	End

	Method OnClientConnect:Bool(Network:NetworkEngine, Address:SocketAddress)
		Local Host:= Address.Host

		#Rem
			If (Host <> "127.0.0.1") Then
				DebugStop()

				Return False
			Endif
		#End

		' Return the default response.
		Return True
	End

	' This is called once, at any time after 'OnClientConnect'.
	Method OnClientAccepted:Void(Network:NetworkEngine, C:Client)
		Print("Client accepted: " + C.Address)

		Return
	End

	' Fields:
	Field Server:NetworkEngine
	Field Client:NetworkEngine
End

' Functions:
Function Main:Int()
	Local Test:= New TestApplication()

	' Return the default response.
	Return 0
End


Added a class to handle caching and reusing random seeds.  Uses Monkey's Rnd() implementation, but works with an independent seed that will not affect regular calls to Rnd().

#Rem
Copyright (c) 2011 Steve Revill and Shane Woolcock
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#End

Function LinearTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	Return diff * t / d + b
End

Function QuadTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d / 2
	If t < 1 Return diff / 2 * t * t + b
	t -= 1
	Return -diff / 2 * (t * (t - 2) - 1) + b
End

Function QuinticTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d / 2
	If (t < 1) Return diff / 2 * t * t * t * t * t + b
	t -= 2
	Return diff / 2 * (t * t * t * t * t + 2) + b
End

Function TweenCalc:Float(p1:Float, p2:Float, t:Float)
	Return p1 + t * (p2 - p1)
End

Function TweenSmooth:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	Return p1 + v * (p2 - p1)
End

Function SmoothStep:Float(x:Float, interpSmooth:Int = 1)
	For Local i:Int = 0 Until interpSmooth
		x *= x * (3 - 2 * x)
	Next

	Return x
End

Function TweenUp:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	v = Pow(v, 2) 'power of 2.
	Return p1 + v * (p2 - p1)
End

Function TweenDown:Float(p1:Float, p2:Float, t:Float)
	Local v:Float = SmoothStep(t)
	v = 1 - Pow(1 - v, 2) 'InvSquared
	Return p1 + v * (p2 - p1)
End

Function Bezier:Float(p1:Float, p2:Float, cp1:Float, cp2:Float, t:Float)
	Local v:Float = p1 * Pow( (1 - t), 3) + 3 * cp1 * Pow( (1 - t), 2) * t + 3 * cp2 * (1 - t) * Pow(t, 2) + p2 * Pow(t, 3)
	Return v
End

Function EaseInBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	Return diff - EaseOutBounceTween(d - t, 0, diff, d) + b
End

Function EaseOutBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	t /= d
	If (t < (1 / 2.75))
		Return diff * (7.5625 * t * t) + b
	ElseIf(t < (2 / 2.75))
		t -= (1.5 / 2.75)
		Return diff * (7.5625 * t * t + 0.75) + b
	ElseIf(t < (2.5 / 2.75))
		t -= (2.25 / 2.75)
		Return diff * (7.5625 * t * t + 0.9375) + b
	Else
		t -= (2.625 / 2.75)
		Return diff * (7.5625 * t * t + 0.984375) + b
	End
End

Function EaseInOutBounceTween:Float(b:Float, c:Float, t:Float, d:Float = 1)
	Local diff:Float = c - b
	If (t < d / 2)
		Return EaseInBounceTween(t * 2, 0, diff, d) * 0.5 + b
	End
	Return EaseOutBounceTween(t * 2 - d, 0, diff, d) * 0.5 + diff * 0.5 + b
End

Function BackEaseInTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	t /= d
	Return diff * t * t * ( (s + 1) * t - s) + b
End

Function BackEaseOutTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	t = t / d - 1
	Return diff * (t * t * ( (s + 1) * t + s) + 1) + b
End

Function BackEaseInOutTween:Float(b:Float, c:Float, t:Float, d:Float = 1, s:Float = 2)
	Local diff:Float = c - b
	Local s2:Float = s
	t /= d / 2
	s2 *= 1.525
	If t < 1
		Return diff / 2 * (t * t * ( (s2 + 1) * t - s2)) + b
	End
	t -= 2
	Return diff / 2 * (t * t * ( (s2 + 1) * t + s2) + 2) + b
End

Class RandomSource
Private
	Field currentSeed:Float

	Method NextRnd:Float()
		' cache global seed
		Local lastSeed:Float = Seed
		' update it to be ours
		Seed = Self.currentSeed
		' call global Rnd function
		Local thisRnd:Float = Rnd()
		' update our seed
		Self.currentSeed = Seed
		' reset global seed
		Seed = lastSeed
		' return the random float
		Return thisRnd
	End

Public
	Method CurrentSeed:Float() Property Return currentSeed End
	Method CurrentSeed:Void(currentSeed:Float) Property Self.currentSeed = currentSeed End

	Method New()
		currentSeed = Seed
	End

	Method New(seed:Int)
		currentSeed = seed
	End

	Method New(other:RandomSource)
		currentSeed = other.CurrentSeed
	End

	Method NextInt:Int()
		Return (NextInt($10000) Shl 16) | NextInt($10000)
	End

	Method NextInt:Int(n:Int)
		Return Int(NextRnd() * n)
	End

	Method NextInt:Int(low:Int, high:Int)
		Return low + Int(NextRnd() * (high-low))
	End

	Method NextFloat:Float()
		Return NextRnd()
	End

	Method NextBool:Bool()
		Return NextRnd() >= 0.5
	End
End


Added support for strings, sizes in bits, and non-standard byte sizes. Fixed problems with constants using function calls. Not the best of methods, but it gets the job done.

Strict

Public

' Preprocessor related:
#SIZEOF_IMPLEMENTED = True

#If LANG = "cpp"
	#SIZEOF_NATIVE = True

	#If TARGET <> "win8"
		#SIZEOF_NORMAL_CPP_TARGET = True
	#End
#End

' Imports:
'Import vector

' Global & Constant variable(s):

' Standard type sizes (In bytes):
Const SizeOf_Octet:Int = 1
Const SizeOf_Octet_InBits:Int = 8

Const SizeOf_Byte:Int = SizeOf_Octet
Const SizeOf_Byte_InBits:Int = SizeOf_Octet_InBits

#If Not SIZEOF_NATIVE
	Const SizeOf_Integer:Int = 4 * SizeOf_Octet ' 8

	#If CPP_DOUBLE_PRECISION_FLOATS And SIZEOF_NORMAL_CPP_TARGET
		Const SizeOf_FloatingPoint:Int = 8 * SizeOf_Octet
	#Else
		Const SizeOf_FloatingPoint:Int = 4 * SizeOf_Octet
	#End

	Const SizeOf_Boolean:Int = SizeOf_Byte ' 1
#Else
	' Global variable(s) (External):

	' Modifying these values is considered non-standard, and should not be attempted:
	Extern

	Global SizeOf_Integer:Int = "(int)sizeof(int)"
	Global SizeOf_FloatingPoint:Int = "(int)sizeof(Float)"
	Global SizeOf_Boolean:Int = "(int)sizeof(bool)"

	Public
#End

#If SIZEOF_NATIVE And LANG = "cpp" And SIZEOF_NORMAL_CPP_TARGET
	' Global variable(s) (External):

	Extern

	' Modifying the value of this variable is considered non-standard, and should not be attempted.
	Global SizeOf_Char:Int = "(int)sizeof(Char)"

	Public
#Else
	Const SizeOf_Char:Int = SizeOf_Byte ' 1
#End

Global SizeOf_Integer_InBits:Int = SizeOf_Integer * SizeOf_Octet_InBits
Global SizeOf_FloatingPoint_InBits:Int = SizeOf_FloatingPoint * SizeOf_Octet_InBits
Global SizeOf_Boolean_InBits:Int = SizeOf_Boolean * SizeOf_Byte_InBits ' SizeOf_Octet_InBits

' External bindings:
#If SIZEOF_NATIVE
	Extern

	' Functions (Native):
	Function SizeOf:Int(I:Int)="(int)sizeof"
	Function SizeOf:Int(F:Float)="(int)sizeof"
	Function SizeOf:Int(B:Bool)="(int)sizeof"

	Public
#End

' Functions (Monkey):
Function IntSize:Int(Size:Int)
	If (Size > 1) Then
		If (Size > 4) Then
			Return 8
		Elseif (Size < 4) Then
			Return 2
		Else
			Return 4
		Endif
	Endif

	Return 1
End

#If Not SIZEOF_NATIVE
	Function SizeOf:Int(I:Int)
		Return SizeOf_Integer
	End

	Function SizeOf:Int(F:Float)
		Return SizeOf_FloatingPoint
	End

	Function SizeOf:Int(B:Bool)
		Return SizeOf_Boolean
	End
#End

Function SizeOf:Int(S:String, IsString:Bool=False)
	' Make the input-string upper-case.
	S = S.ToUpper()

	If (Not IsString) Then
		If (S.Find("INT") <> -1) Then
			Return SizeOf(0)
		Elseif (S.Find("FLOAT") <> -1) Then
			Return SizeOf(0.0)
		Elseif (S.Find("BOOL") <> -1) Then
			Return SizeOf(False)
		#Rem
			Elseif (S.Find("VEC") <> -1) Then
				Return Max(Int(S), 1)
		#End

		#Rem
			Elseif (S.Find("BYTE")) Then
				Return SizeOf_Byte
		#End
		Endif
	Endif

	' Return the default response.
	Return SizeOf_Char * S.Length()
End

Function InBits:Int(I:Int)
	Return SizeOf_Integer_InBits
End

Function InBits:Int(F:Float)
	Return SizeOf_FloatingPoint_InBits
End

Function InBits:Int(B:Bool)
	Return SizeOf_Boolean_InBits
End

Function InBits:Int(S:String, IsString:Bool=False)
	Local Output:= SizeOf(S, IsString)

	If (Not IsString) Then
		Return Output * SizeOf_Octet_InBits
	Endif

	Return Output * SizeOf_Char_InBits
End

Strict

Public

' Preprocessor related:
#SIZEOF_IMPLEMENTED = True

#If LANG = "cpp"
	#SIZEOF_NATIVE = True
#End

' Imports:
'Import vector

' Constant variable(s):

' Standard type sizes (In bytes):
#If Not SIZEOF_NATIVE
	Const SizeOf_Integer:Int = 4 ' 8

	#If CPP_DOUBLE_PRECISION_FLOATS And LANG = "cpp" And TARGET <> "win8"
		Const SizeOf_FloatingPoint:Int = 8
	#Else
		Const SizeOf_FloatingPoint:Int = 4
	#End

	Const SizeOf_Boolean:Int = 1
#Else
	Const SizeOf_Integer:Int = SizeOf(0)
	Const SizeOf_FloatingPoint:Int = SizeOf(0.0)
	Const SizeOf_Boolean:Int = SizeOf(False)
#End

' External bindings:
#If SIZEOF_NATIVE
	Extern

	' Functions (Native):
	Function SizeOf:Int(I:Int)="(int)sizeof"
	Function SizeOf:Int(F:Float)="(int)sizeof"
	Function SizeOf:Int(B:Bool)="(int)sizeof"

	Public
#End

' Functions (Monkey):
Function IntSize:Int(Size:Int)
	If (Size > 1) Then
		If (Size > 4) Then
			Return 8
		Elseif (Size < 4) Then
			Return 2
		Else
			Return 4
		Endif
	Endif

	Return 1
End

#If Not SIZEOF_NATIVE
	Function SizeOf:Int(I:Int)
		Return SizeOf_Integer
	End

	Function SizeOf:Int(F:Float)
		Return SizeOf_FloatingPoint
	End

	Function SizeOf:Int(B:Bool)
		Return SizeOf_Boolean
	End
#End

Function SizeOf:Int(S:String)
	' Make the input-string upper-case.
	S = S.ToUpper()

	If (S.Find("INT") <> -1) Then
		Return SizeOf(0)
	Elseif (S.Find("FLOAT") <> -1) Then
		Return SizeOf(0.0)
	Elseif (S.Find("BOOL") <> -1) Then
		Return SizeOf(False)
	#Rem
		Elseif (S.Find("VEC") <> -1) Then
			Return Max(Int(S), 1)
	#End
	Endif

	' Return the default response.
	Return 1
End


Added proper preprocessor flags for 'filestream' I/O.

Strict

Public

#Rem
	This acts as a compatibility-layer with the official 'brl' implementation.

	This API intends to be as accurate as possible, allowing
	users to build consistent codebases between platforms.

	ATTENTION: This sub-module does not follow the "standalone" behavioral
	rules applied to the other modules.

	To use this sub-module, you must have the 'regal.ioutil.publicdatastream'
	module installed, and any dependencies it requires.

	In addition, this uses both the standard 'brl.stream' module,
	as well as the internal file-system modules/extensions.
#End

' Preprocessor related:
#If VIRTUALOS_CREATEFILE_IMPLEMENTED And (VIRTUALOS_EXTENSION_LOADBUFFER Or VIRTUALOS_EXTENSION_SAVEBUFFER)
	#VIRTUALOS_FILESTREAM_IMPLEMENTED = True
#End

#If VIRTUALOS_EXTENSION_LOADBUFFER
	#VIRTUALOS_FILESTREAM_CAN_LOAD = True
#End

#If VIRTUALOS_EXTENSION_SAVEBUFFER
	VIRTUALOS_FILESTREAM_CAN_SAVE = True
#End

' Imports (Public):
Import brl.stream

' Imports (Private):
Private

#If VIRTUALOS_FILESTREAM_IMPLEMENTED
	Import regal.ioutil.publicdatastream

	Import filesystem
	Import legend
#End

Public

#If VIRTUALOS_FILESTREAM_IMPLEMENTED
	' Classes:
	Class FileStream Extends PublicDataStream
		' Constant variable(s):

		' Defaults:
		Const Default_Size:= 4096 ' 4KB

		' Functions:
		Function Open:FileStream(Path:String, Mode:String)
			Try
				Return New FileStream(Path, Mode)
			Catch E:StreamError
				' Nothing so far.
			End

			Return Null
		End

		' Constructor(s) (Public):
		Method New(Path:String, Mode:String, Size:Int=Default_Size, FixByteOrder:Bool=Default_BigEndianStorage, SizeLimit:Int=NOLIMIT)
			Super.New(Size, FixByteOrder, True, SizeLimit)

			EstablishStream(Path, Mode)
		End

		' Constructor(s) (Protected):
		Protected

		Method EstablishStream:Void(Path:String, Mode:String)
			Select Mode
				Case "r", "u"
					ReadFileData(RealPath(Path), True)
				Case "a"
					ReadFileData(RealPath(Path), False)
				Case "w"
					CreateFile(Path)
				Default
					Throw New FileStreamOpenException("Invalid file-mode detected.", Path, Self)
			End Select

			Self.Path = Path
			Self.Mode = Mode

			Return
		End

		Public

		' Methods (Public):
		Method Close:Void()
			' Push data to the file-system.
			WriteFileData(RealPath(Self.Path))

			' Close the stream properly.
			Super.Close()

			Return
		End

		' Methods (Protected):
		Protected

		Method ReadFileData:DataBuffer(RealPath:String, ResetPosition:Bool=True)
			Local Contents:= __OS_LoadBuffer(RealPath)

			If (Contents = Null) Then
				Return Null
			Endif

			Local Position:= Self.Position

			WriteAll(Contents, 0, Contents.Length)

			If (ResetPosition) Then
				Seek(Position)
			Endif

			Return Contents
		End

		Method WriteFileData:Bool(RealPath:String)
			Local Output:= New DataBuffer(Self.Length)

			Self.Data.CopyBytes(Offset, Output, 0, Length)

			If (Not __OS_SaveBuffer(RealPath, Output)) Then
				Return False
			Endif

			'Output.Discard()

			' Return the default response.
			Return True
		End

		Public

		' Fields (Protected):
		Protected

		Field Path:String, Mode:String

		Public
	End
#End

' Exceptions:
Class FileStreamOpenException Extends StreamError
	' Constructor(s):
	Method New(Message:String, Path:String, FS:FileStream)
		Super.New(FS)

		Self.Message = Message
	End

	' Methods:
	Method ToString:String() ' Property
		Return Message
	End

	' Fields (Protected):
	Protected

	Field Message:String

	Public
End

Strict

Public

#Rem
	This acts as a compatibility-layer with the official 'brl' implementation.

	This API intends to be as accurate as possible, allowing
	users to build consistent codebases between platforms.

	ATTENTION: This sub-module does not follow the "standalone" behavioral
	rules applied to the other modules.

	To use this sub-module, you must have the 'regal.ioutil.publicdatastream'
	module installed, and any dependencies it requires.

	In addition, this uses both the standard 'brl.stream' module,
	as well as the internal file-system modules/extensions.
#End

' Preprocessor related:
#VIRTUALOS_FILESTREAM_IMPLEMENTED = True

' Imports (Public):
Import brl.stream

' Imports (Private):
Private

#If VIRTUALOS_FILESTREAM_IMPLEMENTED
	Import regal.ioutil.publicdatastream

	Import filesystem
	Import legend
#End

Public

#If VIRTUALOS_FILESTREAM_IMPLEMENTED
	' Classes:
	Class FileStream Extends PublicDataStream
		' Constant variable(s):

		' Defaults:
		Const Default_Size:= 4096 ' 4KB

		' Functions:
		Function Open:FileStream(Path:String, Mode:String)
			Try
				Return New FileStream(Path, Mode)
			Catch E:StreamError
				' Nothing so far.
			End

			Return Null
		End

		' Constructor(s) (Public):
		Method New(Path:String, Mode:String, Size:Int=Default_Size, FixByteOrder:Bool=Default_BigEndianStorage, SizeLimit:Int=NOLIMIT)
			Super.New(Size, FixByteOrder, True, SizeLimit)

			EstablishStream(Path, Mode)
		End

		' Constructor(s) (Protected):
		Protected

		Method EstablishStream:Void(Path:String, Mode:String)
			Select Mode
				Case "r", "u"
					ReadFileData(RealPath(Path), True)
				Case "a"
					ReadFileData(RealPath(Path), False)
				Case "w"
					CreateFile(Path)
				Default
					Throw New FileStreamOpenException("Invalid file-mode detected.", Path, Self)
			End Select

			Self.Path = Path
			Self.Mode = Mode

			Return
		End

		Public

		' Methods (Public):
		Method Close:Void()
			' Push data to the file-system.
			WriteFileData(RealPath(Self.Path))

			' Close the stream properly.
			Super.Close()

			Return
		End

		' Methods (Protected):
		Protected

		Method ReadFileData:DataBuffer(RealPath:String, ResetPosition:Bool=True)
			Local Contents:= __OS_LoadBuffer(RealPath)

			If (Contents = Null) Then
				Return Null
			Endif

			Local Position:= Self.Position

			WriteAll(Contents, 0, Contents.Length)

			If (ResetPosition) Then
				Seek(Position)
			Endif

			Return Contents
		End

		Method WriteFileData:Bool(RealPath:String)
			Local Output:= New DataBuffer(Self.Length)

			Self.Data.CopyBytes(Offset, Output, 0, Length)

			If (Not __OS_SaveBuffer(RealPath, Output)) Then
				Return False
			Endif

			'Output.Discard()

			' Return the default response.
			Return True
		End

		Public

		' Fields (Protected):
		Protected

		Field Path:String, Mode:String

		Public
	End
#End

' Exceptions:
Class FileStreamOpenException Extends StreamError
	' Constructor(s):
	Method New(Message:String, Path:String, FS:FileStream)
		Super.New(FS)

		Self.Message = Message
	End

	' Methods:
	Method ToString:String() ' Property
		Return Message
	End

	' Fields (Protected):
	Protected

	Field Message:String

	Public
End