Cleaned up MoveTo for SimpleDialog

Cleaned up MoveTo for SimpleDialog

#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

#Rem
Header: A collection of very simple GUI elements, usually for a basic menu system.
#End

Import diddy

Const VERTICAL:Int = 0
Const HORIZONTAL:Int = 1

#Rem
Summary: Provides a group of buttons that automatically handle mouseover images and sounds.
The buttons are automatically layed out horizontally or vertically, so only the menu itself needs to be positioned.
#End
Class SimpleMenu Extends List<SimpleButton>
	Field x:Float, y:Float
	Field buttonGap:Int = 0
	Field mouseOverName:String = ""
	Field clickedName:String = ""
	Field clearClickedName:Int = 1
	Field nextX:Int = 0
	Field nextY:Int = 0
	Field w:Int, h:Int
	Field addGap:Int = 0
	Field soundMouseOver:GameSound
	Field soundClick:GameSound
	Field useVirtualRes:Bool = False
	Field orientation:Int = VERTICAL

	Method New()
		Error "Please use a different constructor"
	End

#Rem
Summary: Creates a new SimpleMenu with the specified configuration.
#End
	Method New(soundMouseOverFile$, soundClickFile$, x:Int, y:Int, gap:Int, useVirtualRes:Bool, orientation:Int = VERTICAL)
		Init(soundMouseOverFile, soundClickFile, x, y, gap, useVirtualRes, orientation)
	End

#Rem
Summary: Initialises the menu (internal method).
Developers do not need to call this.
#End
	Method Init:Void(soundMouseOverFile:String="", soundClickFile:String="", x:Float, y:Float, gap:Int, useVirtualRes:Bool, orientation:Int)
		Self.Clear()
		Self.useVirtualRes = useVirtualRes
		Self.orientation = orientation
		Self.x = x
		Self.y = y
		nextX = x
		nextY = y
		Self.buttonGap = gap
		w = 0
		h = 0
		mouseOverName = ""
		clickedName = ""
		addGap = 0
		If soundMouseOverFile<>"" Then
			soundMouseOver = New GameSound
			soundMouseOver.Load(soundMouseOverFile)
		End
		If soundClickFile<>"" Then
			soundClick = New GameSound
			soundClick.Load(soundClickFile)
		End
	End

#Rem
Summary: Calculates and returns the minimum width of the menu required to fit all buttons at their current positions.
See [[CalcHeight]].
Developers do not need to call this.
#End
	Method CalcWidth:Int()
		Local left:Int=10000
		Local right:Int=-10000
		Local b:SimpleButton
		For b = EachIn Self
			If b.x < left Then left = b.x
			If b.x+b.image.w > right Then right = b.x + b.image.w
		Next
		w = right - left
		Return w
	End

#Rem
Summary: Calculates and returns the minimum height of the menu required to fit all buttons at their current positions.
See [[CalcWidth]].
Developers do not need to call this.
#End
	Method CalcHeight:Int()
		Local top:Int=10000
		Local bot:Int=-10000
		Local b:SimpleButton
		For b = EachIn Self
			If b.y < top Then top = b.y
			If b.y+b.image.h > bot Then bot = b.y + b.image.h
		Next
		h = bot - top
		Return h
	End

#Rem
Summary: Sets the X coordinate of the menu, moving all the buttons with it.
#End
	Method SetX:Void(thex#)
		CalcLeft()
		Local oldx# = x
		x = thex
		Local diff# = x - oldx
		Local b:SimpleButton
		For b = EachIn Self
			b.MoveBy(diff,0)
		Next
	End

#Rem
Summary: Sets the Y coordinate of the menu, moving all the buttons with it.
#End
	Method SetY:Void(they#)
		CalcTop()
		Local oldy# = y
		y = they
		Local diff# = y-oldy
		Local b:SimpleButton
		For b = EachIn Self
			b.MoveBy(0,diff)
		Next
	End

#Rem
Summary: Sets the x coordinate of the menu to be that of the leftmost button.
Developers do not need to call this.
#End
	Method CalcLeft:Void()
		x = 10000
		Local b:SimpleButton
		For b = EachIn Self
			If b.x <x Then x = b.x
		Next
	End

#Rem
Summary: Sets the y coordinate of the menu to be that of the topmost button.
Developers do not need to call this.
#End
	Method CalcTop:Void()
		y = 10000
		Local b:SimpleButton
		For b = EachIn Self
			If b.y < y Then y = b.y
		Next
	End

#Rem
Summary: Sets the x coordinate of the menu to be centred horizontally on the screen.
#End
	Method CentreHoriz:Void()
		CalcWidth()
		if useVirtualRes
			SetX((SCREEN_WIDTH-w)/2)
		Else
			SetX((DEVICE_WIDTH-w)/2)
		End
	End

#Rem
Summary: Sets the y coordinate of the menu to be centred vertically on the screen.
#End
	Method CentreVert:Void()
		CalcHeight()
		if useVirtualRes
			SetY((SCREEN_HEIGHT-h)/2)
		Else
			SetY((DEVICE_HEIGHT-h)/2)
		End
	End

#Rem
Summary: Sets the x and y coordinates of the menu to be centred both horizontally and vertically on the screen.
#End
	Method Centre:Void()
		CentreHoriz()
		CentreVert()
	End

#Rem
Summary: Sets the alpha for each button to the passed value.
#End
	Method SetMenuAlpha:Void(alpha:Float)
		Local b:SimpleButton
		For b = EachIn Self
			b.alpha = alpha
		Next
	End

#Rem
Summary: Creates a new [[SimpleButton]] and initialises it with the specified image, mouseover image, and name.
The button is added to the end of the menu and is positioned to fit the current menu orientation.
[code]
Local sb:SimpleButton = menu.AddButton(diddyGame.images.Find("newgame"), diddyGame.images.Find("newgameMO"), "New Game")
[/code]
#End
	Method AddButton:SimpleButton(buttonImage:GameImage, mouseOverFile:GameImage, name:String = "", drawText:Bool = False)
		Local b:SimpleButton = ProcessAddButton(buttonImage, mouseOverFile, name, drawText)
		If orientation = VERTICAL
			IncreaseHeight(b)
		Else
			IncreaseWidth(b)
		End
		Return b
	End

#Rem
Summary: Creates a new [[SimpleButton]] and initialises it with the specified image, mouseover image, and name.
The button is added to the end of the menu and is positioned to fit the current menu orientation.
[code]
Local sb:SimpleButton = menu.AddButton("newgame", "newgameMO", "New Game")
[/code]
#End
	Method AddButton:SimpleButton(buttonImageFile:String, mouseOverFile:String, name:String = "", drawText:Bool = False)
		Local b:SimpleButton = ProcessAddButton(buttonImageFile, mouseOverFile, name, drawText)
		If orientation = VERTICAL
			IncreaseHeight(b)
		Else
			IncreaseWidth(b)
		End
		Return b
	End

#Rem
Summary: Increases the height of the menu to fit the passed SimpleButton.
Developers do not need to call this.
#End
	Method IncreaseHeight:Void(b:SimpleButton)
		nextY = nextY + b.image.h + buttonGap

		h = h + b.image.h
		If addGap Then
			h = h + buttonGap
		Else
			addGap = 1
		End
	End

#Rem
Summary: Increases the width of the menu to fit the passed SimpleButton.
Developers do not need to call this.
#End
	Method IncreaseWidth:Void(b:SimpleButton)
		nextX = nextX + b.image.w + buttonGap

		w = w + b.image.w
		If addGap Then
			w = w + buttonGap
		Else
			addGap = 1
		End
	End

#Rem
Summary: Internal method to add a button to the menu.
Developers do not need to call this.
#End
	Method ProcessAddButton:SimpleButton(buttonImage:GameImage, mouseOver:GameImage, name:String, drawText:Bool = False)
		Local b:SimpleButton = New SimpleButton
		b.drawText = drawText
		b.name = StripAll(buttonImage.name.ToUpper())
		b.image = buttonImage
		b.image.SetHandle(0, 0)
		If mouseOver <> Null
			b.imageMouseOver = mouseOver
			b.imageMouseOver.SetHandle(0, 0)
		End
		b = ProcessButton(b, name)
		Return b
	End

#Rem
Summary: Internal method to add a button to the menu.
Developers do not need to call this.
#End
	Method ProcessAddButton:SimpleButton(buttonImageFile:String, mouseOverFile:String, name:String, drawText:Bool = False)
		Local b:SimpleButton = New SimpleButton
		b.drawText = drawText
		b.Load(buttonImageFile, mouseOverFile)
		b = ProcessButton(b, name)
		Return b
	End

#Rem
Summary: Internal method to add a button to the menu.
Developers do not need to call this.
#End
	Method ProcessButton:SimpleButton(b:SimpleButton, name:String)
		b.useVirtualRes = Self.useVirtualRes
		b.orientation = Self.orientation
		b.text = name
		If name <> "" Then b.name = name.ToUpper()
		If orientation = VERTICAL
			b.CentreX(nextY)
		Else
			b.MoveTo(nextX, nextY)
		End

		b.soundMouseOver = soundMouseOver
		b.soundClick = soundClick
		AddLast(b)
		Return b
	End

#Rem
Summary: Finds the button in the menu with the given name.
[code]
Local sb:SimpleButton = menu.Find("New Game")
[/code]
#End
	Method FindButton:SimpleButton(name:String)
		name = name.ToUpper()
		Local b:SimpleButton
		For b = EachIn Self
			If b.name = name Then Return b
		Next
		Return Null
	End

#Rem
Summary: Returns 1 if the button with the specified name was clicked since the last call to [[Update]].
[code]
menu.Update()
...
If menu.Clicked("New Game") Then
	' start a new game
ElseIf menu.Clicked("Quit") Then
	' exit the game
End
[/code]
#End
	Method Clicked:Int(name:String)
		name = name.ToUpper()
		If name = clickedName
			If clearClickedName Then clickedName = ""
			Return 1
		Else
			Return 0
		End
	End

#Rem
Summary: Determines whether the user clicked on a button, such that the next call to [[Clicked]] can test for it.
This should be called only once, towards the start of your [[Screen.Update]] implementation, or [[App.OnUpdate]].
#End
	Method Update:Int()
		If diddyGame.screenFade.active
			Return 0
		EndIf
		clickedName = ""
		Local b:SimpleButton
		For b = EachIn Self
			b.Update()
			If b.mouseOver Then mouseOverName = b.name
			If b.clicked Then clickedName = b.name
		Next
		Return 1
	End

#Rem
Summary: Delegates to [[Sprite.Precache]] on each of the buttons.
#End
	Method Precache:Void()
		For Local b:SimpleButton = EachIn Self
			b.Precache()
		Next
	End

#Rem
Summary: Delegates to [[SimpleButton.Draw]] on each of the buttons.
#End
	Method Draw:Void()
		For Local b:SimpleButton = EachIn Self
			b.Draw()
		Next
	End
End

#Rem
Summary: A Delegate so that the developer can override the drawing of text on widgets
#END
Class SimpleTextDrawDelegate Abstract
	Method Draw:Void(text:String, x:Float, y:Float)
	End
End

#Rem
Summary: Represents a button in a [[SimpleMenu]].
#End
Class SimpleButton Extends Sprite
	Field active:Int = 1
	Field clicked:Int = 0
	Field selected:Int = 0
	Field mouseOver:Int = 0
	Field disabled:Bool = False
	Field soundMouseOver:GameSound
	Field soundClick:GameSound
	Field imageMouseOver:GameImage
	Field imageSelected:GameImage
	Field imageSelectedMO:GameImage
	Field useVirtualRes:Bool = False
	Field orientation:Int = VERTICAL
	Field drawText:Bool
	Field text:String
	Field textDrawDelegate:SimpleTextDrawDelegate

#Rem
Summary: Delegates to [[Sprite.Precache]] if the button has a valid image.
Developers do not need to call this.
#End
	Method Precache:Void()
		If image<>null
			Super.Precache()
		End
	End

#Rem
Summary: Renders the button.
Developers do not need to call this.
#End
	Method Draw:Void()
		If active = 0 Then Return
		SetAlpha Self.alpha
		if mouseOver
			if selected And imageSelectedMO <> null Then
				DrawImage Self.imageSelectedMO.image, x, y
			Else
				DrawImage Self.imageMouseOver.image, x, y
			End
		ElseIf selected And imageSelected <> null Then
			DrawImage Self.imageSelected.image, x, y
		Else
			DrawImage Self.image.image, x, y
		EndIf
		If drawText
			If textDrawDelegate <> Null
				textDrawDelegate.Draw(text, x + Self.image.w2, y)
			Else
				DrawText(text, x + Self.image.w2, y + Self.image.h2, 0.5, 0.5)
			End
		End
		SetAlpha 1
	End

#Rem
Summary: Fires a button click.
Developers do not need to call this.
#End
	Method Click:Void()
		If clicked = 0
			clicked = 1
			If soundClick <> null
				soundClick.Play()
			End
		End
	End

#Rem
Summary: Centres the button horizontally within the screen, also setting the Y position to the passed value.
#End
	Method CentreX:Void(yCoord:Int)
		if useVirtualRes
			MoveTo((SCREEN_WIDTH-image.w)/2, yCoord)
		Else
			MoveTo((DEVICE_WIDTH-image.w)/2, yCoord)
		End

	End

#Rem
Summary: Centres the button vertically within the screen, also setting the X position to the passed value.
#End
	Method CentreY:Void(xCoord:Int)
		If useVirtualRes
			MoveTo((SCREEN_HEIGHT-image.h)/2, xCoord)
		Else
			MoveTo((DEVICE_HEIGHT-image.h)/2, xCoord)
		End
	End

#Rem
Summary: Moves the button by the passed number of pixels.
#End
	Method MoveBy:Void(dx:Float,dy:Float)
		x += dx
		y += dy
	End

#Rem
Summary: Moves the button to the exact specified location.
#End
	Method MoveTo:Void(dx:Float,dy:Float)
		x = dx
		y = dy
	End

#Rem
Summary: Loads the required images and sounds for the button.
Developers do not need to call this.
#End
	Method Load:Void(buttonImage:String, mouseOverImage:String = "", soundMouseOverFile:String="", soundClickFile:String="")
		Self.image = New GameImage
		image.Load(diddyGame.images.path + buttonImage, False)

		If mouseOverImage <> ""
			imageMouseOver = New GameImage
			imageMouseOver.Load(diddyGame.images.path + mouseOverImage, False)
		End

		name = StripAll(buttonImage.ToUpper())

		If soundMouseOverFile<>"" Then
			soundMouseOver = New GameSound
			soundMouseOver.Load(soundMouseOverFile)
		End
		If soundClickFile<>"" Then
			soundClick = New GameSound
			soundClick.Load(soundClickFile)
		End
	End

#Rem
Summary: Sets additional images so that the button can be used as a toggle button.
#End
	Method SetSelectedImage:Void(buttonImage:String, buttonImageMO:String = "")
		imageSelected = New GameImage
		imageSelected.Load(diddyGame.images.path + buttonImage, False)

		If buttonImageMO <> ""
			imageSelectedMO = New GameImage
			imageSelectedMO.Load(diddyGame.images.path + buttonImageMO, False)
		End
	End

#Rem
Summary: Updates the clicked status of the button, and plays mouseover sounds.
Developers only need to call this if they are using the button outside of a [[SimpleMenu]].
#End
	Method Update:Void()
		If active = 0 or disabled Then Return
		Local mx:Int = diddyGame.mouseX
		Local my:Int = diddyGame.mouseY
		If not useVirtualRes
			mx = MouseX()
			my = MouseY()
		End
		If mx >= x And mx < x+image.w And my >= y And my < y+image.h Then
			If mouseOver = 0
				if soundMouseOver <> null
					soundMouseOver.Play()
				End
			End
			mouseOver = 1
			If MouseHit() Then
				Click()
			Else
				clicked = 0
			End
		Else
			mouseOver = 0
			clicked = 0
		End
	End
End

#Rem
Summary: Represents a slider control, often used for volume levels.
#End
Class SimpleSlider Extends Sprite
	Field active:Int
	Field dotImage:GameImage
	Field dotX:Int, dotY:Int
	Field value:Int
	Field border:Int=0
	Field borderY:Int=5
	Field useVirtualRes:Bool = False

#Rem
Summary: Creates a new [[SimpleSlider]] with the specified configuration.
#End
	Method New(barFile:String, dotFile:String, x:Int, y:int, border:int = 0, name:String="", borderY:int=5, useVirtualRes:Bool = True)
		Self.image = New GameImage
		Self.useVirtualRes = useVirtualRes

		image.Load(diddyGame.images.path + barFile, False)
		name = StripAll(barFile.ToUpper())

		dotImage = New GameImage
		dotImage.Load(diddyGame.images.path + dotFile, False)
		dotImage.name = StripAll(dotFile.ToUpper())

		Self.x = x
		Self.y = y
		Self.border = border
		Self.borderY = borderY

		Self.SetValue(50)
		Self.dotY = y-3
		Self.active = 1
	End

#Rem
Summary: Sets the value of the slider (between 0 and 100 inclusive) and updates the position of the bar.
#End
	Method SetValue:Void(toSet:int)
		value = toSet
		If toSet < 0 Then value = 0
		If toSet > 100 Then value = 100
		Local percent:Float = value/100.0
		dotX = x + border + (percent * (image.w - (border * 2))) - dotImage.w2
	End

#Rem
Summary: Updates the value of the slider if the user has dragged somewhere on it.
This should be called only once per slider, towards the start of your [[Screen.Update]] implementation, or [[App.OnUpdate]].
Returns 1 if the slider changed value since the last frame, otherwise 0.
#End
	Method Update:Int()
		Local change:Int=0
		If active
			Local buffer:int = 10
			Local mx:Int = diddyGame.mouseX
			Local my:Int = diddyGame.mouseY
			if not useVirtualRes
				mx = MouseX()
				my = MouseY()
			End
			If mx >= x - buffer And mx < x + image.w + buffer And my >= y - borderY And my < y + image.h + borderY
				If MouseDown(MOUSE_LEFT)
					If mx <= x+border
						SetValue(0)
						change = 1
					ElseIf mx >= x+image.w-border
						SetValue(100)
						change = 1
					Else
						Local d:Float = mx - x - border
						Local p:Float = d/(image.w-(border*2))
						SetValue(Round(p*100))
						change = 1
					End
				End
			End
		End
		Return change
	End

#Rem
Summary: Renders the slider.
#End
	Method Draw:Void()
		If active
			DrawImage(image.image,x,y)
			DrawImage(dotImage.image,dotX,dotY)
		End
	End
End

#Rem
Summary: Provides a dialog which a menu can be added to
#End
Class SimpleDialog
	Field alpha:Float = 0
	Field menu:SimpleMenu
	Field title:String
	Field image:GameImage
	Field show:Bool
	Field titleX:Float
	Field titleY:Float
	Field x:Float
	Field y:Float
	Field fadeInSpeed:Float
	Field fadeOutSpeed:Float
	Field titleDrawDelegate:SimpleTextDrawDelegate
	Field alphaControl:Float
	Field text:String
	Field textX:Float
	Field textY:Float
	Field textDrawDelegate:SimpleTextDrawDelegate
	Field textColor:Int[3]

#Rem
Summary: Creates a new [[SimpleDialog]] with the specified configuration.
#End
	Method New(menu:SimpleMenu, image:GameImage)
		Self.alpha = 0
		Self.menu = menu
		Self.image = image
		x = SCREEN_WIDTH2
		y = SCREEN_HEIGHT2
		titleX = SCREEN_WIDTH2
		titleY = 40
		fadeInSpeed = 0.05
		fadeOutSpeed = 0.08
		alphaControl = 0.4
		textX = SCREEN_WIDTH2
		textY = titleY + 50
		textColor[0] = 255
		textColor[1] = 255
		textColor[2] = 255
	End

#Rem
Summary: Updates the dialog, controls the alpha and menu (menu is only usable if alpha is greater than alphaControl
#End
	Method Update:Void()
		If show
			If alpha < 1
				alpha += fadeInSpeed * dt.delta
			Else
				alpha = 1
			End
			If menu
				menu.SetMenuAlpha(alpha)
				If alpha > alphaControl Then
					menu.Update()
				End
			End
		Else
			If alpha > 0
				alpha -= fadeOutSpeed * dt.delta
				If alpha < 0 Then alpha = 0
				If menu Then menu.SetMenuAlpha(alpha)
			Else
				alpha = 0
				show = False
			End
		End
	End

#Rem
Summary: Set the colour of the text
#END
	Method SetTextColor:Void(r:Int, g:Int, b:Int)
		textColor[0] = r
		textColor[1] = g
		textColor[2] = b
	End

#Rem
Summary: Renders the dialog.
#End
	Method Draw:Void()
		If alpha > 0
			SetAlpha alpha
			image.Draw(x, y)
			If titleDrawDelegate <> Null Then
				titleDrawDelegate.Draw(title, titleX, titleY)
			Else
				DrawText(title, titleX, titleY, 0.5, 0.5)
			End
			SetColor(textColor[0], textColor[1], textColor[2])
			If textDrawDelegate <> Null Then
				textDrawDelegate.Draw(text, textX, textY)
			Else
				DrawText(text, textX, textY, 0.5, 0.5)
			End
			SetColor(255, 255, 255)
			If menu Then menu.Draw()

			SetAlpha 1
		End
	End

#Rem
Summary: Moves the dialog by the set amounts along with text and menus
#End
	Method MoveBy:Void(x:Float, y:Float, moveButtons:Bool = True)
		Self.x += x
		Self.y += y
		Self.textX += x
		Self.textY += y
		Self.titleX += x
		Self.titleY += y

		If moveButtons
			If menu
				For Local b:SimpleButton = EachIn Self.menu
					b.MoveBy(x, y)
				Next
			End
		End
	End

#Rem
Summary: Moves the dialog to the set coords along with text and menus
#End
	Method MoveTo:Void(newX:Float, newY:Float, moveButtons:Bool = True)
		MoveBy(newX - x, newY - y, moveButtons)
	End
End

#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

#Rem
Header: A collection of very simple GUI elements, usually for a basic menu system.
#End

Import diddy

Const VERTICAL:Int = 0
Const HORIZONTAL:Int = 1

#Rem
Summary: Provides a group of buttons that automatically handle mouseover images and sounds.
The buttons are automatically layed out horizontally or vertically, so only the menu itself needs to be positioned.
#End
Class SimpleMenu Extends List<SimpleButton>
	Field x:Float, y:Float
	Field buttonGap:Int = 0
	Field mouseOverName:String = ""
	Field clickedName:String = ""
	Field clearClickedName:Int = 1
	Field nextX:Int = 0
	Field nextY:Int = 0
	Field w:Int, h:Int
	Field addGap:Int = 0
	Field soundMouseOver:GameSound
	Field soundClick:GameSound
	Field useVirtualRes:Bool = False
	Field orientation:Int = VERTICAL

	Method New()
		Error "Please use a different constructor"
	End

#Rem
Summary: Creates a new SimpleMenu with the specified configuration.
#End
	Method New(soundMouseOverFile$, soundClickFile$, x:Int, y:Int, gap:Int, useVirtualRes:Bool, orientation:Int = VERTICAL)
		Init(soundMouseOverFile, soundClickFile, x, y, gap, useVirtualRes, orientation)
	End

#Rem
Summary: Initialises the menu (internal method).
Developers do not need to call this.
#End
	Method Init:Void(soundMouseOverFile:String="", soundClickFile:String="", x:Float, y:Float, gap:Int, useVirtualRes:Bool, orientation:Int)
		Self.Clear()
		Self.useVirtualRes = useVirtualRes
		Self.orientation = orientation
		Self.x = x
		Self.y = y
		nextX = x
		nextY = y
		Self.buttonGap = gap
		w = 0
		h = 0
		mouseOverName = ""
		clickedName = ""
		addGap = 0
		If soundMouseOverFile<>"" Then
			soundMouseOver = New GameSound
			soundMouseOver.Load(soundMouseOverFile)
		End
		If soundClickFile<>"" Then
			soundClick = New GameSound
			soundClick.Load(soundClickFile)
		End
	End

#Rem
Summary: Calculates and returns the minimum width of the menu required to fit all buttons at their current positions.
See [[CalcHeight]].
Developers do not need to call this.
#End
	Method CalcWidth:Int()
		Local left:Int=10000
		Local right:Int=-10000
		Local b:SimpleButton
		For b = EachIn Self
			If b.x < left Then left = b.x
			If b.x+b.image.w > right Then right = b.x + b.image.w
		Next
		w = right - left
		Return w
	End

#Rem
Summary: Calculates and returns the minimum height of the menu required to fit all buttons at their current positions.
See [[CalcWidth]].
Developers do not need to call this.
#End
	Method CalcHeight:Int()
		Local top:Int=10000
		Local bot:Int=-10000
		Local b:SimpleButton
		For b = EachIn Self
			If b.y < top Then top = b.y
			If b.y+b.image.h > bot Then bot = b.y + b.image.h
		Next
		h = bot - top
		Return h
	End

#Rem
Summary: Sets the X coordinate of the menu, moving all the buttons with it.
#End
	Method SetX:Void(thex#)
		CalcLeft()
		Local oldx# = x
		x = thex
		Local diff# = x - oldx
		Local b:SimpleButton
		For b = EachIn Self
			b.MoveBy(diff,0)
		Next
	End

#Rem
Summary: Sets the Y coordinate of the menu, moving all the buttons with it.
#End
	Method SetY:Void(they#)
		CalcTop()
		Local oldy# = y
		y = they
		Local diff# = y-oldy
		Local b:SimpleButton
		For b = EachIn Self
			b.MoveBy(0,diff)
		Next
	End

#Rem
Summary: Sets the x coordinate of the menu to be that of the leftmost button.
Developers do not need to call this.
#End
	Method CalcLeft:Void()
		x = 10000
		Local b:SimpleButton
		For b = EachIn Self
			If b.x <x Then x = b.x
		Next
	End

#Rem
Summary: Sets the y coordinate of the menu to be that of the topmost button.
Developers do not need to call this.
#End
	Method CalcTop:Void()
		y = 10000
		Local b:SimpleButton
		For b = EachIn Self
			If b.y < y Then y = b.y
		Next
	End

#Rem
Summary: Sets the x coordinate of the menu to be centred horizontally on the screen.
#End
	Method CentreHoriz:Void()
		CalcWidth()
		if useVirtualRes
			SetX((SCREEN_WIDTH-w)/2)
		Else
			SetX((DEVICE_WIDTH-w)/2)
		End
	End

#Rem
Summary: Sets the y coordinate of the menu to be centred vertically on the screen.
#End
	Method CentreVert:Void()
		CalcHeight()
		if useVirtualRes
			SetY((SCREEN_HEIGHT-h)/2)
		Else
			SetY((DEVICE_HEIGHT-h)/2)
		End
	End

#Rem
Summary: Sets the x and y coordinates of the menu to be centred both horizontally and vertically on the screen.
#End
	Method Centre:Void()
		CentreHoriz()
		CentreVert()
	End

#Rem
Summary: Sets the alpha for each button to the passed value.
#End
	Method SetMenuAlpha:Void(alpha:Float)
		Local b:SimpleButton
		For b = EachIn Self
			b.alpha = alpha
		Next
	End

#Rem
Summary: Creates a new [[SimpleButton]] and initialises it with the specified image, mouseover image, and name.
The button is added to the end of the menu and is positioned to fit the current menu orientation.
[code]
Local sb:SimpleButton = menu.AddButton(diddyGame.images.Find("newgame"), diddyGame.images.Find("newgameMO"), "New Game")
[/code]
#End
	Method AddButton:SimpleButton(buttonImage:GameImage, mouseOverFile:GameImage, name:String = "", drawText:Bool = False)
		Local b:SimpleButton = ProcessAddButton(buttonImage, mouseOverFile, name, drawText)
		If orientation = VERTICAL
			IncreaseHeight(b)
		Else
			IncreaseWidth(b)
		End
		Return b
	End

#Rem
Summary: Creates a new [[SimpleButton]] and initialises it with the specified image, mouseover image, and name.
The button is added to the end of the menu and is positioned to fit the current menu orientation.
[code]
Local sb:SimpleButton = menu.AddButton("newgame", "newgameMO", "New Game")
[/code]
#End
	Method AddButton:SimpleButton(buttonImageFile:String, mouseOverFile:String, name:String = "", drawText:Bool = False)
		Local b:SimpleButton = ProcessAddButton(buttonImageFile, mouseOverFile, name, drawText)
		If orientation = VERTICAL
			IncreaseHeight(b)
		Else
			IncreaseWidth(b)
		End
		Return b
	End

#Rem
Summary: Increases the height of the menu to fit the passed SimpleButton.
Developers do not need to call this.
#End
	Method IncreaseHeight:Void(b:SimpleButton)
		nextY = nextY + b.image.h + buttonGap

		h = h + b.image.h
		If addGap Then
			h = h + buttonGap
		Else
			addGap = 1
		End
	End

#Rem
Summary: Increases the width of the menu to fit the passed SimpleButton.
Developers do not need to call this.
#End
	Method IncreaseWidth:Void(b:SimpleButton)
		nextX = nextX + b.image.w + buttonGap

		w = w + b.image.w
		If addGap Then
			w = w + buttonGap
		Else
			addGap = 1
		End
	End

#Rem
Summary: Internal method to add a button to the menu.
Developers do not need to call this.
#End
	Method ProcessAddButton:SimpleButton(buttonImage:GameImage, mouseOver:GameImage, name:String, drawText:Bool = False)
		Local b:SimpleButton = New SimpleButton
		b.drawText = drawText
		b.name = StripAll(buttonImage.name.ToUpper())
		b.image = buttonImage
		b.image.SetHandle(0, 0)
		If mouseOver <> Null
			b.imageMouseOver = mouseOver
			b.imageMouseOver.SetHandle(0, 0)
		End
		b = ProcessButton(b, name)
		Return b
	End

#Rem
Summary: Internal method to add a button to the menu.
Developers do not need to call this.
#End
	Method ProcessAddButton:SimpleButton(buttonImageFile:String, mouseOverFile:String, name:String, drawText:Bool = False)
		Local b:SimpleButton = New SimpleButton
		b.drawText = drawText
		b.Load(buttonImageFile, mouseOverFile)
		b = ProcessButton(b, name)
		Return b
	End

#Rem
Summary: Internal method to add a button to the menu.
Developers do not need to call this.
#End
	Method ProcessButton:SimpleButton(b:SimpleButton, name:String)
		b.useVirtualRes = Self.useVirtualRes
		b.orientation = Self.orientation
		b.text = name
		If name <> "" Then b.name = name.ToUpper()
		If orientation = VERTICAL
			b.CentreX(nextY)
		Else
			b.MoveTo(nextX, nextY)
		End

		b.soundMouseOver = soundMouseOver
		b.soundClick = soundClick
		AddLast(b)
		Return b
	End

#Rem
Summary: Finds the button in the menu with the given name.
[code]
Local sb:SimpleButton = menu.Find("New Game")
[/code]
#End
	Method FindButton:SimpleButton(name:String)
		name = name.ToUpper()
		Local b:SimpleButton
		For b = EachIn Self
			If b.name = name Then Return b
		Next
		Return Null
	End

#Rem
Summary: Returns 1 if the button with the specified name was clicked since the last call to [[Update]].
[code]
menu.Update()
...
If menu.Clicked("New Game") Then
	' start a new game
ElseIf menu.Clicked("Quit") Then
	' exit the game
End
[/code]
#End
	Method Clicked:Int(name:String)
		name = name.ToUpper()
		If name = clickedName
			If clearClickedName Then clickedName = ""
			Return 1
		Else
			Return 0
		End
	End

#Rem
Summary: Determines whether the user clicked on a button, such that the next call to [[Clicked]] can test for it.
This should be called only once, towards the start of your [[Screen.Update]] implementation, or [[App.OnUpdate]].
#End
	Method Update:Int()
		If diddyGame.screenFade.active
			Return 0
		EndIf
		clickedName = ""
		Local b:SimpleButton
		For b = EachIn Self
			b.Update()
			If b.mouseOver Then mouseOverName = b.name
			If b.clicked Then clickedName = b.name
		Next
		Return 1
	End

#Rem
Summary: Delegates to [[Sprite.Precache]] on each of the buttons.
#End
	Method Precache:Void()
		For Local b:SimpleButton = EachIn Self
			b.Precache()
		Next
	End

#Rem
Summary: Delegates to [[SimpleButton.Draw]] on each of the buttons.
#End
	Method Draw:Void()
		For Local b:SimpleButton = EachIn Self
			b.Draw()
		Next
	End
End

#Rem
Summary: A Delegate so that the developer can override the drawing of text on widgets
#END
Class SimpleTextDrawDelegate Abstract
	Method Draw:Void(text:String, x:Float, y:Float)
	End
End

#Rem
Summary: Represents a button in a [[SimpleMenu]].
#End
Class SimpleButton Extends Sprite
	Field active:Int = 1
	Field clicked:Int = 0
	Field selected:Int = 0
	Field mouseOver:Int = 0
	Field disabled:Bool = False
	Field soundMouseOver:GameSound
	Field soundClick:GameSound
	Field imageMouseOver:GameImage
	Field imageSelected:GameImage
	Field imageSelectedMO:GameImage
	Field useVirtualRes:Bool = False
	Field orientation:Int = VERTICAL
	Field drawText:Bool
	Field text:String
	Field textDrawDelegate:SimpleTextDrawDelegate

#Rem
Summary: Delegates to [[Sprite.Precache]] if the button has a valid image.
Developers do not need to call this.
#End
	Method Precache:Void()
		If image<>null
			Super.Precache()
		End
	End

#Rem
Summary: Renders the button.
Developers do not need to call this.
#End
	Method Draw:Void()
		If active = 0 Then Return
		SetAlpha Self.alpha
		if mouseOver
			if selected And imageSelectedMO <> null Then
				DrawImage Self.imageSelectedMO.image, x, y
			Else
				DrawImage Self.imageMouseOver.image, x, y
			End
		ElseIf selected And imageSelected <> null Then
			DrawImage Self.imageSelected.image, x, y
		Else
			DrawImage Self.image.image, x, y
		EndIf
		If drawText
			If textDrawDelegate <> Null
				textDrawDelegate.Draw(text, x + Self.image.w2, y)
			Else
				DrawText(text, x + Self.image.w2, y + Self.image.h2, 0.5, 0.5)
			End
		End
		SetAlpha 1
	End

#Rem
Summary: Fires a button click.
Developers do not need to call this.
#End
	Method Click:Void()
		If clicked = 0
			clicked = 1
			If soundClick <> null
				soundClick.Play()
			End
		End
	End

#Rem
Summary: Centres the button horizontally within the screen, also setting the Y position to the passed value.
#End
	Method CentreX:Void(yCoord:Int)
		if useVirtualRes
			MoveTo((SCREEN_WIDTH-image.w)/2, yCoord)
		Else
			MoveTo((DEVICE_WIDTH-image.w)/2, yCoord)
		End

	End

#Rem
Summary: Centres the button vertically within the screen, also setting the X position to the passed value.
#End
	Method CentreY:Void(xCoord:Int)
		If useVirtualRes
			MoveTo((SCREEN_HEIGHT-image.h)/2, xCoord)
		Else
			MoveTo((DEVICE_HEIGHT-image.h)/2, xCoord)
		End
	End

#Rem
Summary: Moves the button by the passed number of pixels.
#End
	Method MoveBy:Void(dx:Float,dy:Float)
		x += dx
		y += dy
	End

#Rem
Summary: Moves the button to the exact specified location.
#End
	Method MoveTo:Void(dx:Float,dy:Float)
		x = dx
		y = dy
	End

#Rem
Summary: Loads the required images and sounds for the button.
Developers do not need to call this.
#End
	Method Load:Void(buttonImage:String, mouseOverImage:String = "", soundMouseOverFile:String="", soundClickFile:String="")
		Self.image = New GameImage
		image.Load(diddyGame.images.path + buttonImage, False)

		If mouseOverImage <> ""
			imageMouseOver = New GameImage
			imageMouseOver.Load(diddyGame.images.path + mouseOverImage, False)
		End

		name = StripAll(buttonImage.ToUpper())

		If soundMouseOverFile<>"" Then
			soundMouseOver = New GameSound
			soundMouseOver.Load(soundMouseOverFile)
		End
		If soundClickFile<>"" Then
			soundClick = New GameSound
			soundClick.Load(soundClickFile)
		End
	End

#Rem
Summary: Sets additional images so that the button can be used as a toggle button.
#End
	Method SetSelectedImage:Void(buttonImage:String, buttonImageMO:String = "")
		imageSelected = New GameImage
		imageSelected.Load(diddyGame.images.path + buttonImage, False)

		If buttonImageMO <> ""
			imageSelectedMO = New GameImage
			imageSelectedMO.Load(diddyGame.images.path + buttonImageMO, False)
		End
	End

#Rem
Summary: Updates the clicked status of the button, and plays mouseover sounds.
Developers only need to call this if they are using the button outside of a [[SimpleMenu]].
#End
	Method Update:Void()
		If active = 0 or disabled Then Return
		Local mx:Int = diddyGame.mouseX
		Local my:Int = diddyGame.mouseY
		If not useVirtualRes
			mx = MouseX()
			my = MouseY()
		End
		If mx >= x And mx < x+image.w And my >= y And my < y+image.h Then
			If mouseOver = 0
				if soundMouseOver <> null
					soundMouseOver.Play()
				End
			End
			mouseOver = 1
			If MouseHit() Then
				Click()
			Else
				clicked = 0
			End
		Else
			mouseOver = 0
			clicked = 0
		End
	End
End

#Rem
Summary: Represents a slider control, often used for volume levels.
#End
Class SimpleSlider Extends Sprite
	Field active:Int
	Field dotImage:GameImage
	Field dotX:Int, dotY:Int
	Field value:Int
	Field border:Int=0
	Field borderY:Int=5
	Field useVirtualRes:Bool = False

#Rem
Summary: Creates a new [[SimpleSlider]] with the specified configuration.
#End
	Method New(barFile:String, dotFile:String, x:Int, y:int, border:int = 0, name:String="", borderY:int=5, useVirtualRes:Bool = True)
		Self.image = New GameImage
		Self.useVirtualRes = useVirtualRes

		image.Load(diddyGame.images.path + barFile, False)
		name = StripAll(barFile.ToUpper())

		dotImage = New GameImage
		dotImage.Load(diddyGame.images.path + dotFile, False)
		dotImage.name = StripAll(dotFile.ToUpper())

		Self.x = x
		Self.y = y
		Self.border = border
		Self.borderY = borderY

		Self.SetValue(50)
		Self.dotY = y-3
		Self.active = 1
	End

#Rem
Summary: Sets the value of the slider (between 0 and 100 inclusive) and updates the position of the bar.
#End
	Method SetValue:Void(toSet:int)
		value = toSet
		If toSet < 0 Then value = 0
		If toSet > 100 Then value = 100
		Local percent:Float = value/100.0
		dotX = x + border + (percent * (image.w - (border * 2))) - dotImage.w2
	End

#Rem
Summary: Updates the value of the slider if the user has dragged somewhere on it.
This should be called only once per slider, towards the start of your [[Screen.Update]] implementation, or [[App.OnUpdate]].
Returns 1 if the slider changed value since the last frame, otherwise 0.
#End
	Method Update:Int()
		Local change:Int=0
		If active
			Local buffer:int = 10
			Local mx:Int = diddyGame.mouseX
			Local my:Int = diddyGame.mouseY
			if not useVirtualRes
				mx = MouseX()
				my = MouseY()
			End
			If mx >= x - buffer And mx < x + image.w + buffer And my >= y - borderY And my < y + image.h + borderY
				If MouseDown(MOUSE_LEFT)
					If mx <= x+border
						SetValue(0)
						change = 1
					ElseIf mx >= x+image.w-border
						SetValue(100)
						change = 1
					Else
						Local d:Float = mx - x - border
						Local p:Float = d/(image.w-(border*2))
						SetValue(Round(p*100))
						change = 1
					End
				End
			End
		End
		Return change
	End

#Rem
Summary: Renders the slider.
#End
	Method Draw:Void()
		If active
			DrawImage(image.image,x,y)
			DrawImage(dotImage.image,dotX,dotY)
		End
	End
End

#Rem
Summary: Provides a dialog which a menu can be added to
#End
Class SimpleDialog
	Field alpha:Float = 0
	Field menu:SimpleMenu
	Field title:String
	Field image:GameImage
	Field show:Bool
	Field titleX:Float
	Field titleY:Float
	Field x:Float
	Field y:Float
	Field fadeInSpeed:Float
	Field fadeOutSpeed:Float
	Field titleDrawDelegate:SimpleTextDrawDelegate
	Field alphaControl:Float
	Field text:String
	Field textX:Float
	Field textY:Float
	Field textDrawDelegate:SimpleTextDrawDelegate
	Field textColor:Int[3]

#Rem
Summary: Creates a new [[SimpleDialog]] with the specified configuration.
#End
	Method New(menu:SimpleMenu, image:GameImage)
		Self.alpha = 0
		Self.menu = menu
		Self.image = image
		x = SCREEN_WIDTH2
		y = SCREEN_HEIGHT2
		titleX = SCREEN_WIDTH2
		titleY = 40
		fadeInSpeed = 0.05
		fadeOutSpeed = 0.08
		alphaControl = 0.4
		textX = SCREEN_WIDTH2
		textY = titleY + 50
		textColor[0] = 255
		textColor[1] = 255
		textColor[2] = 255
	End

#Rem
Summary: Updates the dialog, controls the alpha and menu (menu is only usable if alpha is greater than alphaControl
#End
	Method Update:Void()
		If show
			If alpha < 1
				alpha += fadeInSpeed * dt.delta
			Else
				alpha = 1
			End
			If menu
				menu.SetMenuAlpha(alpha)
				If alpha > alphaControl Then
					menu.Update()
				End
			End
		Else
			If alpha > 0
				alpha -= fadeOutSpeed * dt.delta
				If alpha < 0 Then alpha = 0
				If menu Then menu.SetMenuAlpha(alpha)
			Else
				alpha = 0
				show = False
			End
		End
	End

#Rem
Summary: Set the colour of the text
#END
	Method SetTextColor:Void(r:Int, g:Int, b:Int)
		textColor[0] = r
		textColor[1] = g
		textColor[2] = b
	End

#Rem
Summary: Renders the dialog.
#End
	Method Draw:Void()
		If alpha > 0
			SetAlpha alpha
			image.Draw(x, y)
			If titleDrawDelegate <> Null Then
				titleDrawDelegate.Draw(title, titleX, titleY)
			Else
				DrawText(title, titleX, titleY, 0.5, 0.5)
			End
			SetColor(textColor[0], textColor[1], textColor[2])
			If textDrawDelegate <> Null Then
				textDrawDelegate.Draw(text, textX, textY)
			Else
				DrawText(text, textX, textY, 0.5, 0.5)
			End
			SetColor(255, 255, 255)
			If menu Then menu.Draw()

			SetAlpha 1
		End
	End

#Rem
Summary: Moves the dialong by the set amounts along with text and menus
#End
	Method MoveBy:Void(x:Float, y:Float, moveButtons:Bool = True)
		Self.x += x
		Self.y += y
		Self.textX += x
		Self.textY += y
		Self.titleX += x
		Self.titleY += y

		If moveButtons
			If menu
				For Local b:SimpleButton = EachIn Self.menu
					b.MoveBy(x, y)
				Next
			End
		End
	End

	Method MoveTo:Void(newX:Float, newY:Float, moveButtons:Bool = True)
		Local oldx:Float = x
		Local oldy:Float = y

		Self.x = newX
		Self.y = newY

		Local diffX:Float = x - oldx
		Local diffY:Float = y - oldy

		Self.textX += diffX
		Self.textY += diffY
		Self.titleX += diffX
		Self.titleY += diffY

		If moveButtons
			If menu
				For Local b:SimpleButton = EachIn Self.menu
					b.MoveBy(diffX, diffY)
				Next
			End
		End
	End
End


Forgot to add the email parameter to the cpp function.

#if HOST="macos" And TARGET="glfw"
	Import "native/diddy.${TARGET}.mac.${LANG}"
#else
	Import "native/diddy.${TARGET}.${LANG}"
#end
Import mojo
Import assert

Extern

	#If LANG="cpp" Then
		Function RealMillisecs:Int() = "diddy::systemMillisecs"
		Function FlushKeys:Void() = "diddy::flushKeys"
		Function HideMouse:Void() = "diddy::hideMouse"
		Function ShowMouse:Void() = "diddy::showMouse"
		Function GetUpdateRate:Int() = "diddy::getUpdateRate"
		Function SetMouse:Void(x:Int, y:Int) = "diddy::setMouse"
		Function ShowKeyboard:Void() = "diddy::showKeyboard"
		Function LaunchBrowser:Void(address:String) = "diddy::launchBrowser"
		Function LaunchEmail:Void(email:String, subject:String, text:String) = "diddy::launchEmail"
	#Else
		Function RealMillisecs:Int() = "diddy.systemMillisecs"
		Function FlushKeys:Void() = "diddy.flushKeys"
		Function HideMouse:Void() = "diddy.hideMouse"
		Function ShowMouse:Void() = "diddy.showMouse"
		Function GetUpdateRate:Int() = "diddy.getUpdateRate"
		Function SetMouse:Void(x:Int, y:Int) = "diddy.setMouse"
		Function ShowKeyboard:Void() = "diddy.showKeyboard"
		Function LaunchBrowser:Void(address:String) = "diddy.launchBrowser"
		Function LaunchEmail:Void(email:String, subject:String, text:String) = "diddy.launchEmail"
	#End
Public

Function ExitApp:Void()
	Error ""
End

Function RectsOverlap:Int(x0:Float, y0:Float, w0:Float, h0:Float, x2:Float, y2:Float, w2:Float, h2:Float)
	If x0 > (x2 + w2) Or (x0 + w0) < x2 Then Return False
	If y0 > (y2 + h2) Or (y0 + h0) < y2 Then Return False
	Return True
End

Function DrawRectOutline:Void(x:Int, y:Int, w:Int, h:Int)
	w -= 1
	h -= 1
	DrawLine(x,y,x+w,y)
	DrawLine(x+w,y,x+w,y+h)
	DrawLine(x+w,y+h,x,y+h)
	DrawLine(x,y+h,x,y)
End

Function LoadBitmap:Image(path$, flags%=0)
	Local pointer:Image = LoadImage(path, 1, flags)

	AssertNotNull(pointer, "Error loading bitmap "+path)

   	Return pointer
End

Function LoadAnimBitmap:Image(path$, w%, h%, count%, tmpImage:Image)
	'tmpImage = loadBitmap(path) <-- This creates another image, decided to just copy the code here
	tmpImage = LoadImage(path)

	AssertNotNull(tmpImage, "Error loading bitmap "+path)

	Local pointer:Image = tmpImage.GrabImage( 0, 0, w, h, count, Image.MidHandle)

   	Return pointer
End

Function LoadSoundSample:Sound(path$)
	Local pointer:Sound = LoadSound(path)
	AssertNotNull(pointer, "Error loading sound "+path)
	Return pointer
End

Function FormatNumeric:String(value:Float)
	Local i:Int,s:String,ns:String,k:Int
	Local os:String
	s=String(value)
	os=s
	Local pos:Int=s.Length()
	If s.Find(".")>0 pos=s.Find(".") Else os=""
	For i=pos To 1 Step -1
		If k>2 ns+="." k=0
		k+=1
		ns=ns+Mid(s,i,1)
	Next
	s=""
	For i= ns.Length() To 1 Step -1
		s+=Mid(ns,i,1)
	Next
	If os<>"" s=s+","+os[pos+1..]
	Return s
End

Function Left$( str$,n:Int )
	If n>str.Length() n=str.Length()
	Return str[..n]
End

Function Right$( str$,n:Int )
	If n>str.Length() n=str.Length()
	Return str[str.Length()-n..]
End

Function LSet$( str$,n:Int,char:String=" " )
	Local rep:String
	For Local i:Int=1 To n
		rep=rep+char
	Next
	str=str+rep
	Return str[..n]
End

Function RSet$( str$,n:Int,char:String=" " )
	Local rep:String
	For Local i:Int=1 To n
		rep=rep+char
	Next
	str=rep+str
	Return str[str.Length()-n..]
End

Function Mid$( str$,pos:Int,size:Int=-1 )
	If pos>str.Length() Return ""
	pos-=1
	If( size<0 ) Return str[pos..]
	If pos<0 size=size+pos pos=0
	If pos+size>str.Length() size=str.Length()-pos
	Return str[pos..pos+size]
End

Function StripDir$( path$ )
	Local i:=path.FindLast( "/" )
	If i<>-1 Return path[i+1..]
	Return path
End

Function StripExt$( path$ )
	Local i:=path.FindLast( "." )
	If i<>-1 And path.Find( "/",i+1 )=-1 Return path[..i]
	Return path
End

Function StripAll$( path$ )
	Return StripDir( StripExt( path ) )
End

Function Round%(flot#)
	Return Floor(flot+0.5)
End

Function PointInSpot:Int(x1:Float, y1:Float, x2:Float, y2:Float, radius:Float)
	Local dx:Float = x2 - x1
	Local dy:Float = y2 - y1
	Return dx * dx + dy * dy <= radius * radius
End

Function AnyInputPressed:Bool()
	For Local i:Int = 0 To 511
		If KeyHit(i) Then Return True
	Next
	Return False
End

Function FormatNumber:String(number:Float, decimal:Int=4, comma:Int=0, padleft:Int=0 )
	Assert(decimal > -1 And comma > -1 And padleft > -1, "Negative numbers not allowed in FormatNumber()")

	Local str:String = number
	Local dl:Int = str.Find(".")
	If decimal = 0 Then decimal = -1
	str = str[..dl+decimal+1]

	If comma
		While dl>comma
			str = str[..dl-comma] + "," + str[dl-comma..]
			dl -= comma
		Wend
	End

	If padleft
		Local paddedLength:Int = padleft+decimal+1
		If paddedLength < str.Length Then str = "Error"
		str = RSet(str,paddedLength)
	End
	Return str
End

Function IsWhitespace:Bool(str:String)
	Return str = "~t" Or str = "~n" Or str = "~r" Or str = " "
End

Function IsWhitespace:Bool(val:Int)
	Return val = ASC_TAB Or val = ASC_LF Or val = ASC_CR Or val = ASC_SPACE
End

' TODO: allow encoding of Int[]
' TODO: enabling lineWrap will wrap the string every lineWrapWidth characters (defaults to 80)
' enabling padOutput will pad the ending of the output with equals characters (=) to next 4-byte boundary
Function EncodeBase64:String(src:String, padOutput:Bool=False, lineWrap:Bool=False, lineWrapWidth:Int=80)
	Local rv:String = ""
	Local s1:Int, s2:Int, s3:Int, a:Int, b:Int, c:Int, d:Int, i:Int
	If src.Length = 0 Then Return ""
	Repeat
		s1 = src[i]
		If i+1 < src.Length Then s2 = src[i+1] Else s2 = 0
		If i+2 < src.Length Then s3 = src[i+2] Else s3 = 0
		a = s1 Shr 2
		b = ((s1 & 3) Shl 4) | (s2 Shr 4)
		c = ((s2 & 15) Shl 2) | (s3 Shr 6)
		d = s3 & 63
		If i+1 >= src.Length Then c = 64
		If i+2 >= src.Length Then d = 64
		rv += BASE64_CHARS[a..a+1] + BASE64_CHARS[b..b+1]
		If c < 64 Or padOutput Then rv += BASE64_CHARS[c..c+1]
		If d < 64 Or padOutput Then rv += BASE64_CHARS[d..d+1]
		i += 3
	Until i >= src.Length
	Return rv
End

' TODO: convert this to use BASE64_ARRAY
' I'll do this once Monkey has a decent string builder class.
Function DecodeBase64:String(src:String)
	Local rv:String = "", src2:String = ""
	Local s1:Int, s2:Int, s3:Int, a:Int, b:Int, c:Int, d:Int, i:Int
	' remove any non-base64 characters
	For i = 0 Until src.Length
		If BASE64_CHARS.Find(src[i..i+1]) >= 0 Then src2 += src[i..i+1]
	Next
	If src2.Length = 0 Then Return ""
	i = 0
	Repeat
		a = BASE64_CHARS.Find(src2[i..i+1])
		If i+1 > src2.Length Then Exit ' This shouldn't happen with base64, so something's wrong!
		b = BASE64_CHARS.Find(src2[i+1..i+2])
		If i+2 < src2.Length Then c = BASE64_CHARS.Find(src2[i+2..i+3]) Else c = 64
		If i+3 < src2.Length Then d = BASE64_CHARS.Find(src2[i+3..i+4]) Else d = 64
		s1 = (a Shl 2) | (b Shr 4)
		s2 = ((b & 15) Shl 4) | (c Shr 2)
		s3 = ((c & 3) Shl 6) | d
		rv += String.FromChar(s1)
		If c <> 64 Then rv += String.FromChar(s2)
		If d <> 64 Then rv += String.FromChar(s3)
		i += 4
	Until i >= src2.Length
	Return rv
End

Function DecodeBase64Bytes:Int[](src:String)
	InitBase64()
	Local a:Int, b:Int, c:Int, d:Int, i:Int, j:Int
	Local src2:Int[] = New Int[src.Length]

	' find out how many base64 characters
	Local srclen:Int = 0
	For i = 0 Until src.Length
		If BASE64_ARRAY[src[i]] >= 0 Then
			src2[srclen] = src[i]
			srclen += 1
		End
	Next

	' die if there are no base64 chars
	If srclen = 0 Then Return []

	' get the target length and create the array
	Local len:Int = 3*srclen/4
	If srclen Mod 4 = 1 Or srclen Mod 4 = 2 Then
		len += 1
	ElseIf srclen Mod 4 = 3 Then
		len += 2
	End
	Local rv:Int[] = New Int[len]

	i = 0
	j = 0
	Repeat
		a = BASE64_ARRAY[src2[i]]
		If i+1 > srclen Then Exit ' This shouldn't happen with base64, so something's wrong!
		b = BASE64_ARRAY[src2[i+1]]
		If i+2 < srclen Then c = BASE64_ARRAY[src2[i+2]] Else c = 64
		If i+3 < srclen Then d = BASE64_ARRAY[src2[i+3]] Else d = 64
		rv[j] = (a Shl 2) | (b Shr 4)
		If j+1 < len Then rv[j+1] = ((b & 15) Shl 4) | (c Shr 2)
		If j+2 < len Then rv[j+2] = ((c & 3) Shl 6) | d
		i += 4
		j += 3
	Until i >= srclen
	Return rv
End

Function InitBase64:Void()
	If BASE64_ARRAY.Length = 0 Then
		BASE64_ARRAY = New Int[256]
		Local i% = 0
		For i = 0 Until BASE64_ARRAY.Length
			BASE64_ARRAY[i] = -1
		Next
		For i = 0 Until BASE64_CHARS.Length
			BASE64_ARRAY[BASE64_CHARS[i]] = i
		Next
	End
End

' constants

Const BASE64_CHARS:String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
Global BASE64_ARRAY:Int[] = []

' control characters
Const ASC_NUL:Int = 0       ' Null character
Const ASC_SOH:Int = 1       ' Start of Heading
Const ASC_STX:Int = 2       ' Start of Text
Const ASC_ETX:Int = 3       ' End of Text
Const ASC_EOT:Int = 4       ' End of Transmission
Const ASC_ENQ:Int = 5       ' Enquiry
Const ASC_ACK:Int = 6       ' Acknowledgment
Const ASC_BEL:Int = 7       ' Bell
Const ASC_BACKSPACE:Int = 8 ' Backspace
Const ASC_TAB:Int = 9       ' Horizontal tab
Const ASC_LF:Int = 10       ' Linefeed
Const ASC_VTAB:Int = 11     ' Vertical tab
Const ASC_FF:Int = 12       ' Form feed
Const ASC_CR:Int = 13       ' Carriage return
Const ASC_SO:Int = 14       ' Shift Out
Const ASC_SI:Int = 15       ' Shift In
Const ASC_DLE:Int = 16      ' Data Line Escape
Const ASC_DC1:Int = 17      ' Device Control 1
Const ASC_DC2:Int = 18      ' Device Control 2
Const ASC_DC3:Int = 19      ' Device Control 3
Const ASC_DC4:Int = 20      ' Device Control 4
Const ASC_NAK:Int = 21      ' Negative Acknowledgment
Const ASC_SYN:Int = 22      ' Synchronous Idle
Const ASC_ETB:Int = 23      ' End of Transmit Block
Const ASC_CAN:Int = 24      ' Cancel
Const ASC_EM:Int = 25       ' End of Medium
Const ASC_SUB:Int = 26      ' Substitute
Const ASC_ESCAPE:Int = 27   ' Escape
Const ASC_FS:Int = 28       ' File separator
Const ASC_GS:Int = 29       ' Group separator
Const ASC_RS:Int = 30       ' Record separator
Const ASC_US:Int = 31       ' Unit separator

' visible characters
Const ASC_SPACE:Int = 32                ' '
Const ASC_EXCLAMATION:Int = 33          '!'
Const ASC_DOUBLE_QUOTE:Int = 34         '"'
Const ASC_HASH:Int = 35                 '#'
Const ASC_DOLLAR:Int = 36               '$'
Const ASC_PERCENT:Int = 37              '%'
Const ASC_AMPERSAND:Int = 38            '&'
Const ASC_SINGLE_QUOTE:Int = 39         '''
Const ASC_OPEN_PARENTHESIS:Int = 40     '('
Const ASC_CLOSE_PARENTHESIS:Int = 41    ')'
Const ASC_ASTERISK:Int = 42             '*'
Const ASC_PLUS:Int = 43                 '+'
Const ASC_COMMA:Int = 44                ','
Const ASC_HYPHEN:Int = 45               '-'
Const ASC_PERIOD:Int = 46               '.'
Const ASC_SLASH:Int = 47                '/'
Const ASC_0:Int = 48
Const ASC_1:Int = 49
Const ASC_2:Int = 50
Const ASC_3:Int = 51
Const ASC_4:Int = 52
Const ASC_5:Int = 53
Const ASC_6:Int = 54
Const ASC_7:Int = 55
Const ASC_8:Int = 56
Const ASC_9:Int = 57
Const ASC_COLON:Int = 58        ':'
Const ASC_SEMICOLON:Int = 59    ';'
Const ASC_LESS_THAN:Int = 60    '<'
Const ASC_EQUALS:Int = 61       '='
Const ASC_GREATER_THAN:Int = 62 '>'
Const ASC_QUESTION:Int = 63     '?'
Const ASC_AT:Int = 64           '@'
Const ASC_UPPER_A:Int = 65
Const ASC_UPPER_B:Int = 66
Const ASC_UPPER_C:Int = 67
Const ASC_UPPER_D:Int = 68
Const ASC_UPPER_E:Int = 69
Const ASC_UPPER_F:Int = 70
Const ASC_UPPER_G:Int = 71
Const ASC_UPPER_H:Int = 72
Const ASC_UPPER_I:Int = 73
Const ASC_UPPER_J:Int = 74
Const ASC_UPPER_K:Int = 75
Const ASC_UPPER_L:Int = 76
Const ASC_UPPER_M:Int = 77
Const ASC_UPPER_N:Int = 78
Const ASC_UPPER_O:Int = 79
Const ASC_UPPER_P:Int = 80
Const ASC_UPPER_Q:Int = 81
Const ASC_UPPER_R:Int = 82
Const ASC_UPPER_S:Int = 83
Const ASC_UPPER_T:Int = 84
Const ASC_UPPER_U:Int = 85
Const ASC_UPPER_V:Int = 86
Const ASC_UPPER_W:Int = 87
Const ASC_UPPER_X:Int = 88
Const ASC_UPPER_Y:Int = 89
Const ASC_UPPER_Z:Int = 90
Const ASC_OPEN_BRACKET:Int = 91     '['
Const ASC_BACKSLASH:Int = 92        '\'
Const ASC_CLOSE_BRACKET:Int = 93    ']'
Const ASC_CIRCUMFLEX:Int = 94       '^'
Const ASC_UNDERSCORE:Int = 95       '_'
Const ASC_BACKTICK:Int = 96         '`'
Const ASC_LOWER_A:Int = 97
Const ASC_LOWER_B:Int = 98
Const ASC_LOWER_C:Int = 99
Const ASC_LOWER_D:Int = 100
Const ASC_LOWER_E:Int = 101
Const ASC_LOWER_F:Int = 102
Const ASC_LOWER_G:Int = 103
Const ASC_LOWER_H:Int = 104
Const ASC_LOWER_I:Int = 105
Const ASC_LOWER_J:Int = 106
Const ASC_LOWER_K:Int = 107
Const ASC_LOWER_L:Int = 108
Const ASC_LOWER_M:Int = 109
Const ASC_LOWER_N:Int = 110
Const ASC_LOWER_O:Int = 111
Const ASC_LOWER_P:Int = 112
Const ASC_LOWER_Q:Int = 113
Const ASC_LOWER_R:Int = 114
Const ASC_LOWER_S:Int = 115
Const ASC_LOWER_T:Int = 116
Const ASC_LOWER_U:Int = 117
Const ASC_LOWER_V:Int = 118
Const ASC_LOWER_W:Int = 119
Const ASC_LOWER_X:Int = 120
Const ASC_LOWER_Y:Int = 121
Const ASC_LOWER_Z:Int = 122
Const ASC_OPEN_BRACE:Int = 123  '{'
Const ASC_PIPE:Int = 124        '|'
Const ASC_CLOSE_BRACE:Int = 125 '}'
Const ASC_TILDE:Int = 126       '~'
Const ASC_DELETE:Int = 127

#if HOST="macos" And TARGET="glfw"
	Import "native/diddy.${TARGET}.mac.${LANG}"
#else
	Import "native/diddy.${TARGET}.${LANG}"
#end
Import mojo
Import assert

Extern

	#If LANG="cpp" Then
		Function RealMillisecs:Int() = "diddy::systemMillisecs"
		Function FlushKeys:Void() = "diddy::flushKeys"
		Function HideMouse:Void() = "diddy::hideMouse"
		Function ShowMouse:Void() = "diddy::showMouse"
		Function GetUpdateRate:Int() = "diddy::getUpdateRate"
		Function SetMouse:Void(x:Int, y:Int) = "diddy::setMouse"
		Function ShowKeyboard:Void() = "diddy::showKeyboard"
		Function LaunchBrowser:Void(address:String) = "diddy::launchBrowser"
		Function LaunchEmail:Void(subject:String, text:String) = "diddy::launchEmail"
	#Else
		Function RealMillisecs:Int() = "diddy.systemMillisecs"
		Function FlushKeys:Void() = "diddy.flushKeys"
		Function HideMouse:Void() = "diddy.hideMouse"
		Function ShowMouse:Void() = "diddy.showMouse"
		Function GetUpdateRate:Int() = "diddy.getUpdateRate"
		Function SetMouse:Void(x:Int, y:Int) = "diddy.setMouse"
		Function ShowKeyboard:Void() = "diddy.showKeyboard"
		Function LaunchBrowser:Void(address:String) = "diddy.launchBrowser"
		Function LaunchEmail:Void(email:String, subject:String, text:String) = "diddy.launchEmail"
	#End
Public

Function ExitApp:Void()
	Error ""
End

Function RectsOverlap:Int(x0:Float, y0:Float, w0:Float, h0:Float, x2:Float, y2:Float, w2:Float, h2:Float)
	If x0 > (x2 + w2) Or (x0 + w0) < x2 Then Return False
	If y0 > (y2 + h2) Or (y0 + h0) < y2 Then Return False
	Return True
End

Function DrawRectOutline:Void(x:Int, y:Int, w:Int, h:Int)
	w -= 1
	h -= 1
	DrawLine(x,y,x+w,y)
	DrawLine(x+w,y,x+w,y+h)
	DrawLine(x+w,y+h,x,y+h)
	DrawLine(x,y+h,x,y)
End

Function LoadBitmap:Image(path$, flags%=0)
	Local pointer:Image = LoadImage(path, 1, flags)

	AssertNotNull(pointer, "Error loading bitmap "+path)

   	Return pointer
End

Function LoadAnimBitmap:Image(path$, w%, h%, count%, tmpImage:Image)
	'tmpImage = loadBitmap(path) <-- This creates another image, decided to just copy the code here
	tmpImage = LoadImage(path)

	AssertNotNull(tmpImage, "Error loading bitmap "+path)

	Local pointer:Image = tmpImage.GrabImage( 0, 0, w, h, count, Image.MidHandle)

   	Return pointer
End

Function LoadSoundSample:Sound(path$)
	Local pointer:Sound = LoadSound(path)
	AssertNotNull(pointer, "Error loading sound "+path)
	Return pointer
End

Function FormatNumeric:String(value:Float)
	Local i:Int,s:String,ns:String,k:Int
	Local os:String
	s=String(value)
	os=s
	Local pos:Int=s.Length()
	If s.Find(".")>0 pos=s.Find(".") Else os=""
	For i=pos To 1 Step -1
		If k>2 ns+="." k=0
		k+=1
		ns=ns+Mid(s,i,1)
	Next
	s=""
	For i= ns.Length() To 1 Step -1
		s+=Mid(ns,i,1)
	Next
	If os<>"" s=s+","+os[pos+1..]
	Return s
End

Function Left$( str$,n:Int )
	If n>str.Length() n=str.Length()
	Return str[..n]
End

Function Right$( str$,n:Int )
	If n>str.Length() n=str.Length()
	Return str[str.Length()-n..]
End

Function LSet$( str$,n:Int,char:String=" " )
	Local rep:String
	For Local i:Int=1 To n
		rep=rep+char
	Next
	str=str+rep
	Return str[..n]
End

Function RSet$( str$,n:Int,char:String=" " )
	Local rep:String
	For Local i:Int=1 To n
		rep=rep+char
	Next
	str=rep+str
	Return str[str.Length()-n..]
End

Function Mid$( str$,pos:Int,size:Int=-1 )
	If pos>str.Length() Return ""
	pos-=1
	If( size<0 ) Return str[pos..]
	If pos<0 size=size+pos pos=0
	If pos+size>str.Length() size=str.Length()-pos
	Return str[pos..pos+size]
End

Function StripDir$( path$ )
	Local i:=path.FindLast( "/" )
	If i<>-1 Return path[i+1..]
	Return path
End

Function StripExt$( path$ )
	Local i:=path.FindLast( "." )
	If i<>-1 And path.Find( "/",i+1 )=-1 Return path[..i]
	Return path
End

Function StripAll$( path$ )
	Return StripDir( StripExt( path ) )
End

Function Round%(flot#)
	Return Floor(flot+0.5)
End

Function PointInSpot:Int(x1:Float, y1:Float, x2:Float, y2:Float, radius:Float)
	Local dx:Float = x2 - x1
	Local dy:Float = y2 - y1
	Return dx * dx + dy * dy <= radius * radius
End

Function AnyInputPressed:Bool()
	For Local i:Int = 0 To 511
		If KeyHit(i) Then Return True
	Next
	Return False
End

Function FormatNumber:String(number:Float, decimal:Int=4, comma:Int=0, padleft:Int=0 )
	Assert(decimal > -1 And comma > -1 And padleft > -1, "Negative numbers not allowed in FormatNumber()")

	Local str:String = number
	Local dl:Int = str.Find(".")
	If decimal = 0 Then decimal = -1
	str = str[..dl+decimal+1]

	If comma
		While dl>comma
			str = str[..dl-comma] + "," + str[dl-comma..]
			dl -= comma
		Wend
	End

	If padleft
		Local paddedLength:Int = padleft+decimal+1
		If paddedLength < str.Length Then str = "Error"
		str = RSet(str,paddedLength)
	End
	Return str
End

Function IsWhitespace:Bool(str:String)
	Return str = "~t" Or str = "~n" Or str = "~r" Or str = " "
End

Function IsWhitespace:Bool(val:Int)
	Return val = ASC_TAB Or val = ASC_LF Or val = ASC_CR Or val = ASC_SPACE
End

' TODO: allow encoding of Int[]
' TODO: enabling lineWrap will wrap the string every lineWrapWidth characters (defaults to 80)
' enabling padOutput will pad the ending of the output with equals characters (=) to next 4-byte boundary
Function EncodeBase64:String(src:String, padOutput:Bool=False, lineWrap:Bool=False, lineWrapWidth:Int=80)
	Local rv:String = ""
	Local s1:Int, s2:Int, s3:Int, a:Int, b:Int, c:Int, d:Int, i:Int
	If src.Length = 0 Then Return ""
	Repeat
		s1 = src[i]
		If i+1 < src.Length Then s2 = src[i+1] Else s2 = 0
		If i+2 < src.Length Then s3 = src[i+2] Else s3 = 0
		a = s1 Shr 2
		b = ((s1 & 3) Shl 4) | (s2 Shr 4)
		c = ((s2 & 15) Shl 2) | (s3 Shr 6)
		d = s3 & 63
		If i+1 >= src.Length Then c = 64
		If i+2 >= src.Length Then d = 64
		rv += BASE64_CHARS[a..a+1] + BASE64_CHARS[b..b+1]
		If c < 64 Or padOutput Then rv += BASE64_CHARS[c..c+1]
		If d < 64 Or padOutput Then rv += BASE64_CHARS[d..d+1]
		i += 3
	Until i >= src.Length
	Return rv
End

' TODO: convert this to use BASE64_ARRAY
' I'll do this once Monkey has a decent string builder class.
Function DecodeBase64:String(src:String)
	Local rv:String = "", src2:String = ""
	Local s1:Int, s2:Int, s3:Int, a:Int, b:Int, c:Int, d:Int, i:Int
	' remove any non-base64 characters
	For i = 0 Until src.Length
		If BASE64_CHARS.Find(src[i..i+1]) >= 0 Then src2 += src[i..i+1]
	Next
	If src2.Length = 0 Then Return ""
	i = 0
	Repeat
		a = BASE64_CHARS.Find(src2[i..i+1])
		If i+1 > src2.Length Then Exit ' This shouldn't happen with base64, so something's wrong!
		b = BASE64_CHARS.Find(src2[i+1..i+2])
		If i+2 < src2.Length Then c = BASE64_CHARS.Find(src2[i+2..i+3]) Else c = 64
		If i+3 < src2.Length Then d = BASE64_CHARS.Find(src2[i+3..i+4]) Else d = 64
		s1 = (a Shl 2) | (b Shr 4)
		s2 = ((b & 15) Shl 4) | (c Shr 2)
		s3 = ((c & 3) Shl 6) | d
		rv += String.FromChar(s1)
		If c <> 64 Then rv += String.FromChar(s2)
		If d <> 64 Then rv += String.FromChar(s3)
		i += 4
	Until i >= src2.Length
	Return rv
End

Function DecodeBase64Bytes:Int[](src:String)
	InitBase64()
	Local a:Int, b:Int, c:Int, d:Int, i:Int, j:Int
	Local src2:Int[] = New Int[src.Length]

	' find out how many base64 characters
	Local srclen:Int = 0
	For i = 0 Until src.Length
		If BASE64_ARRAY[src[i]] >= 0 Then
			src2[srclen] = src[i]
			srclen += 1
		End
	Next

	' die if there are no base64 chars
	If srclen = 0 Then Return []

	' get the target length and create the array
	Local len:Int = 3*srclen/4
	If srclen Mod 4 = 1 Or srclen Mod 4 = 2 Then
		len += 1
	ElseIf srclen Mod 4 = 3 Then
		len += 2
	End
	Local rv:Int[] = New Int[len]

	i = 0
	j = 0
	Repeat
		a = BASE64_ARRAY[src2[i]]
		If i+1 > srclen Then Exit ' This shouldn't happen with base64, so something's wrong!
		b = BASE64_ARRAY[src2[i+1]]
		If i+2 < srclen Then c = BASE64_ARRAY[src2[i+2]] Else c = 64
		If i+3 < srclen Then d = BASE64_ARRAY[src2[i+3]] Else d = 64
		rv[j] = (a Shl 2) | (b Shr 4)
		If j+1 < len Then rv[j+1] = ((b & 15) Shl 4) | (c Shr 2)
		If j+2 < len Then rv[j+2] = ((c & 3) Shl 6) | d
		i += 4
		j += 3
	Until i >= srclen
	Return rv
End

Function InitBase64:Void()
	If BASE64_ARRAY.Length = 0 Then
		BASE64_ARRAY = New Int[256]
		Local i% = 0
		For i = 0 Until BASE64_ARRAY.Length
			BASE64_ARRAY[i] = -1
		Next
		For i = 0 Until BASE64_CHARS.Length
			BASE64_ARRAY[BASE64_CHARS[i]] = i
		Next
	End
End

' constants

Const BASE64_CHARS:String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
Global BASE64_ARRAY:Int[] = []

' control characters
Const ASC_NUL:Int = 0       ' Null character
Const ASC_SOH:Int = 1       ' Start of Heading
Const ASC_STX:Int = 2       ' Start of Text
Const ASC_ETX:Int = 3       ' End of Text
Const ASC_EOT:Int = 4       ' End of Transmission
Const ASC_ENQ:Int = 5       ' Enquiry
Const ASC_ACK:Int = 6       ' Acknowledgment
Const ASC_BEL:Int = 7       ' Bell
Const ASC_BACKSPACE:Int = 8 ' Backspace
Const ASC_TAB:Int = 9       ' Horizontal tab
Const ASC_LF:Int = 10       ' Linefeed
Const ASC_VTAB:Int = 11     ' Vertical tab
Const ASC_FF:Int = 12       ' Form feed
Const ASC_CR:Int = 13       ' Carriage return
Const ASC_SO:Int = 14       ' Shift Out
Const ASC_SI:Int = 15       ' Shift In
Const ASC_DLE:Int = 16      ' Data Line Escape
Const ASC_DC1:Int = 17      ' Device Control 1
Const ASC_DC2:Int = 18      ' Device Control 2
Const ASC_DC3:Int = 19      ' Device Control 3
Const ASC_DC4:Int = 20      ' Device Control 4
Const ASC_NAK:Int = 21      ' Negative Acknowledgment
Const ASC_SYN:Int = 22      ' Synchronous Idle
Const ASC_ETB:Int = 23      ' End of Transmit Block
Const ASC_CAN:Int = 24      ' Cancel
Const ASC_EM:Int = 25       ' End of Medium
Const ASC_SUB:Int = 26      ' Substitute
Const ASC_ESCAPE:Int = 27   ' Escape
Const ASC_FS:Int = 28       ' File separator
Const ASC_GS:Int = 29       ' Group separator
Const ASC_RS:Int = 30       ' Record separator
Const ASC_US:Int = 31       ' Unit separator

' visible characters
Const ASC_SPACE:Int = 32                ' '
Const ASC_EXCLAMATION:Int = 33          '!'
Const ASC_DOUBLE_QUOTE:Int = 34         '"'
Const ASC_HASH:Int = 35                 '#'
Const ASC_DOLLAR:Int = 36               '$'
Const ASC_PERCENT:Int = 37              '%'
Const ASC_AMPERSAND:Int = 38            '&'
Const ASC_SINGLE_QUOTE:Int = 39         '''
Const ASC_OPEN_PARENTHESIS:Int = 40     '('
Const ASC_CLOSE_PARENTHESIS:Int = 41    ')'
Const ASC_ASTERISK:Int = 42             '*'
Const ASC_PLUS:Int = 43                 '+'
Const ASC_COMMA:Int = 44                ','
Const ASC_HYPHEN:Int = 45               '-'
Const ASC_PERIOD:Int = 46               '.'
Const ASC_SLASH:Int = 47                '/'
Const ASC_0:Int = 48
Const ASC_1:Int = 49
Const ASC_2:Int = 50
Const ASC_3:Int = 51
Const ASC_4:Int = 52
Const ASC_5:Int = 53
Const ASC_6:Int = 54
Const ASC_7:Int = 55
Const ASC_8:Int = 56
Const ASC_9:Int = 57
Const ASC_COLON:Int = 58        ':'
Const ASC_SEMICOLON:Int = 59    ';'
Const ASC_LESS_THAN:Int = 60    '<'
Const ASC_EQUALS:Int = 61       '='
Const ASC_GREATER_THAN:Int = 62 '>'
Const ASC_QUESTION:Int = 63     '?'
Const ASC_AT:Int = 64           '@'
Const ASC_UPPER_A:Int = 65
Const ASC_UPPER_B:Int = 66
Const ASC_UPPER_C:Int = 67
Const ASC_UPPER_D:Int = 68
Const ASC_UPPER_E:Int = 69
Const ASC_UPPER_F:Int = 70
Const ASC_UPPER_G:Int = 71
Const ASC_UPPER_H:Int = 72
Const ASC_UPPER_I:Int = 73
Const ASC_UPPER_J:Int = 74
Const ASC_UPPER_K:Int = 75
Const ASC_UPPER_L:Int = 76
Const ASC_UPPER_M:Int = 77
Const ASC_UPPER_N:Int = 78
Const ASC_UPPER_O:Int = 79
Const ASC_UPPER_P:Int = 80
Const ASC_UPPER_Q:Int = 81
Const ASC_UPPER_R:Int = 82
Const ASC_UPPER_S:Int = 83
Const ASC_UPPER_T:Int = 84
Const ASC_UPPER_U:Int = 85
Const ASC_UPPER_V:Int = 86
Const ASC_UPPER_W:Int = 87
Const ASC_UPPER_X:Int = 88
Const ASC_UPPER_Y:Int = 89
Const ASC_UPPER_Z:Int = 90
Const ASC_OPEN_BRACKET:Int = 91     '['
Const ASC_BACKSLASH:Int = 92        '\'
Const ASC_CLOSE_BRACKET:Int = 93    ']'
Const ASC_CIRCUMFLEX:Int = 94       '^'
Const ASC_UNDERSCORE:Int = 95       '_'
Const ASC_BACKTICK:Int = 96         '`'
Const ASC_LOWER_A:Int = 97
Const ASC_LOWER_B:Int = 98
Const ASC_LOWER_C:Int = 99
Const ASC_LOWER_D:Int = 100
Const ASC_LOWER_E:Int = 101
Const ASC_LOWER_F:Int = 102
Const ASC_LOWER_G:Int = 103
Const ASC_LOWER_H:Int = 104
Const ASC_LOWER_I:Int = 105
Const ASC_LOWER_J:Int = 106
Const ASC_LOWER_K:Int = 107
Const ASC_LOWER_L:Int = 108
Const ASC_LOWER_M:Int = 109
Const ASC_LOWER_N:Int = 110
Const ASC_LOWER_O:Int = 111
Const ASC_LOWER_P:Int = 112
Const ASC_LOWER_Q:Int = 113
Const ASC_LOWER_R:Int = 114
Const ASC_LOWER_S:Int = 115
Const ASC_LOWER_T:Int = 116
Const ASC_LOWER_U:Int = 117
Const ASC_LOWER_V:Int = 118
Const ASC_LOWER_W:Int = 119
Const ASC_LOWER_X:Int = 120
Const ASC_LOWER_Y:Int = 121
Const ASC_LOWER_Z:Int = 122
Const ASC_OPEN_BRACE:Int = 123  '{'
Const ASC_PIPE:Int = 124        '|'
Const ASC_CLOSE_BRACE:Int = 125 '}'
Const ASC_TILDE:Int = 126       '~'
Const ASC_DELETE:Int = 127


Added a minor tweak to the 'chat' demo.

Added a minor tweak to the 'chat' demo.

#Rem
	DISCLAIMER:
		* This example is unfinished and likely contains bugs, use at your own risk.
#End

Strict

Public

' Preprocessor related:
#MOJO_AUTO_SUSPEND_ENABLED = False

#GLFW_WINDOW_RESIZABLE = True

' Imports:
Import regal.networking

Import brl.asyncevent
Import brl.stream
Import mojo

' Classes:
Class Application Extends App Implements CoreNetworkListener, MetaNetworkListener, ClientNetworkListener Final
	' Constant variable(s):
	Const PORT:= 27015 ' 27016 ' 5029
	Const PROTOCOL:= NetworkEngine.SOCKET_TYPE_TCP ' NetworkEngine.SOCKET_TYPE_UDP

	Const NOTIFICATION_HEADER:= "{NOTIFICATION}: "

	' This is used to offset user-identifiers.
	Const USER_ID_ORIGIN:= 1

	' States:
	Const STATE_IDLE:= 0
	Const STATE_CHAT:= 1

	' Message types:
	Const MSG_TYPE_USER:= NetworkEngine.MSG_TYPE_CUSTOM ' + 1
	Const MSG_TYPE_DISCONNECT:= (MSG_TYPE_USER+1)
	Const MSG_TYPE_TEXT:= (MSG_TYPE_DISCONNECT+1)
	Const MSG_TYPE_RAW_LINES:= (MSG_TYPE_TEXT+1)
	Const MSG_TYPE_NOTIFICATION:= (MSG_TYPE_RAW_LINES+1)

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

		Users = New List<User>()
		ChatLog = New StringDeque()

		LocalInfo = Null

		State = STATE_IDLE
		NextUserID = USER_ID_ORIGIN

		UsernameInputComplete = False

		' Return the default response.
		Return 0
	End

	' Methods:
	Method InitNetwork:Void()
		Network = New NetworkEngine()

		Network.SetCoreCallback(Self)
		Network.SetMetaCallback(Self)
		Network.SetClientCallback(Self)

		Return
	End

	Method Host:Void(Port:Int=PORT)
		InitNetwork()

		Network.Host(Port, True, PROTOCOL)

		Print("Hosting server on port " + Port + " using " + NetworkEngine.ProtocolToString(PROTOCOL) + "...")

		Return
	End

	Method Connect:Void(Hostname:String, Port:Int=PORT)
		InitNetwork()

		Network.Connect(Hostname, Port, True, PROTOCOL)

		Print("Connecting to " + Hostname + ":" + Port + " using " + NetworkEngine.ProtocolToString(PROTOCOL) + "...")

		Return
	End

	Method OnRender:Int()
		Select State
			Case STATE_CHAT
				Render_Chat()
			Default ' STATE_IDLE
				Render_Idle()
		End Select

		' Return the default response.
		Return 0
	End

	Method OnUpdate:Int()
		UpdateAsyncEvents()

		Select State
			Case STATE_IDLE
				If (Update_Idle()) Then
					State = STATE_CHAT
				Endif
			Case STATE_CHAT
				Update_Chat()
		End Select

		' Return the default response.
		Return 0
	End

	Method OnResize:Int()
		MaintainChatLog()

		Return Super.OnResize()
	End

	Method Render_Idle:Void()
		Cls(205.0, 205.0, 205.0)

		Return
	End

	Method Render_Chat:Void()
		Const PREVIEW_USER_COUNT:= 4

		Cls(127.5, Cos(Float(Millisecs() / 10)) * 255.0, 200.0)

		Local UserCount:= Users.Count()

		If (UserCount > 0) Then
			If (UserCount > PREVIEW_USER_COUNT) Then
				DrawText("Users connected: " + UserCount, 8.0, 8.0)
			Else
				Local Names:String

				Local Users_Node:= Users.FirstNode()

				Local Iterations:= Min(PREVIEW_USER_COUNT, UserCount)

				For Local I:= 1 To Iterations
					Local U:= Users_Node.Value()

					Names += U.Name

					If (I < Iterations) Then
						Names += ", "
					Endif

					Users_Node = Users_Node.NextNode()
				Next

				DrawText("Users connected: " + Names, 8.0, 8.0)
			Endif
		Endif

		If (Not Network.IsClient) Then
			DrawText("Clients connected overall: " + Network.ClientCount, 8.0, 24.0)
		Endif

		DrawChatLog(8.0, ChatOffset)
		DrawUserInput()

		Return
	End

	Method DrawChatLog:Void(X:Float=8.0, Y:Float=8.0)
		PushMatrix()

		Translate(X, Y)

		For Local Line:= Eachin ChatLog
			DrawText(Line, 0.0, 0.0)

			Translate(0.0, 16.0)
		Next

		PopMatrix()

		Return
	End

	Method DrawUserInput:Void()
		Local X:= 8.0
		Local Y:= (Float(DeviceHeight()) - 24.0)

		If (LocalInfo = Null) Then ' Network.IsClient
			If (Not UsernameInputComplete) Then
				DrawText("Please input a username: " + UserInput, X, Y)
			Else
				If (Network.IsClient) Then
					DrawText("Waiting for authentication...", X, Y)
				Endif
			Endif
		Else
			DrawText(LocalInfo.Name + ": " + UserInput, X, Y)
		Endif

		Return
	End

	Method Update_Idle:Bool()
		If (KeyHit(KEY_1)) Then
			Host()

			ChatOffset = 64.0

			Return True
		Elseif (KeyHit(KEY_0)) Then
			Connect("127.0.0.1") ' "localhost"

			ChatOffset = 48.0

			Return True
		Endif

		' Return the default response.
		Return False
	End

	Method Update_Chat:Void()
		Network.Update()

		#Rem
			If (KeyHit(KEY_W)) Then
				SendText(String(Millisecs()))
			Endif
		#End

		UpdateUserInput()

		Return
	End

	Method UpdateUserInput:Void()
		Repeat
			Local KeyCode:= GetChar()

			If (Not KeyCode) Then
				Exit
			Elseif (KeyCode = 13) Then ' Enter
				If (UserInput.Length > 0) Then
					If (UsernameInputComplete) Then
						If (Network.HasClient) Then
							SendText(UserInput)
						Endif
					Else
						If (Network.IsClient) Then
							If (LocalInfo = Null) Then
								DisplayLine("Asking the server to authenticate us...")

								' Tell the host our username.
								AskForUser(UserInput)

								UsernameInputComplete = True
							Endif
						Else
							LocalInfo = New User(UserInput, GetNextUserID())

							'BringClientUpToSpeed(Null)
							NotifyAboutUser(LocalInfo, Null, False, True)

							UsernameInputComplete = True
						Endif
					Endif

					UserInput = ""
				Endif
			Elseif (KeyCode = 8) Then
				UserInput = UserInput[..UserInput.Length-1]
			Elseif (KeyCode >= 32) Then
				UserInput += String.FromChar(KeyCode)
			Endif
		Forever

		Return
	End

	Method MaintainChatLog:Void()
		' Calculate the number of entries allowed in the chat-log:
		MaxChatLogEntries = ((DeviceHeight() / 16) - (4 + (Int(ChatOffset) / 16)))

		' Reduce the chat-log size.
		While (ChatLog.Length >= MaxChatLogEntries)
			ChatLog.PopFirst()
		Wend

		Return
	End

	Method DisplayLine:Void(RawText:String)
		' Output to the console.
		Print("{CHAT}: " + RawText)

		MaintainChatLog()

		' Add 'RawText' to the chat-log.
		ChatLog.PushLast(RawText)

		Return
	End

	Method DisplayLine:Void(U:User, Data:String)
		DisplayLine(ProcessLine(U, Data))

		Return
	End

	Method ProcessLine:String(U:User, Data:String)
		Return (U.Name + ": " + Data)
	End

	Method GetUser:User(C:Client)
		If (C = Null) Then
			Return Null
		Endif

		For Local U:= Eachin Users
			If (U.Handle = C) Then
				Return U
			Endif
		Next

		' Return the default response.
		Return Null
	End

	Method GetUser:User(Name:String)
		For Local U:= Eachin Users
			If (U.Name = Name) Then
				Return U
			Endif
		Next

		' Return the default response.
		Return Null
	End

	Method GetUser:User(ID:Int)
		For Local U:= Eachin Users
			If (U.ID = ID) Then
				Return U
			Endif
		Next

		' Return the default response.
		Return Null
	End

	' This repeats text to every 'Client', excluding 'Origin'.
	Method RepeatText:Void(RawText:String, Origin:Client)
		Local P:= Network.AllocatePacket()

		P.WriteLine(RawText)

		For Local C:= Eachin Network
			If (C = Origin) Then
				Continue
			Endif

			Network.Send(P, C, MSG_TYPE_RAW_LINES)
		Next

		Network.ReleasePacket(P)

		Return
	End

	Method GetNextUserID:Int() ' UShort
		Local ID:= NextUserID

		NextUserID += 1

		Return ID
	End

	Method AcceptUser:Bool(Name:String, Origin:Client)
		' Only accept the user if we don't already know them:
		If (GetUser(Origin) = Null) Then
			Local Num:Int = 0 ' Long ' Short

			For Local ExistingUser:= Eachin Users
				If (ExistingUser.Name.StartsWith(Name)) Then
					Local SuffixNum:= Int(ExistingUser.Name[Name.Length..])

					If (SuffixNum > 0) Then
						Num = (SuffixNum + 1)
					Else
						If (ExistingUser.Name = Name) Then
							Num += 1
						Endif
					Endif
				Endif
			Next

			If (Num > 0) Then
				Name += String(Num)
			Endif

			Local U:= New User(Name, GetNextUserID(), Origin)

			Users.AddLast(U)

			SendNotification(U.Name + " has connected.")

			' Tell 'Origin' what we're calling them.
			NotifyAboutUser(U, Origin, True)

			' Tell everyone else that they exist.
			For Local C:= Eachin Network
				If (C = Origin) Then
					Continue
				Endif

				NotifyAboutUser(U, C, False)
			Next

			Return True
		Endif

		' Return the default response.
		Return False
	End

	' This is used by servers to notify users about someone disconnecting.
	Method ReleaseUser:Bool(U:User)
		' Tell everyone that 'U' has disconnected.
		NotifyAboutUserDisconnection(U)

		' Remove this 'User' from our collection.
		Users.RemoveEach(U)

		' Notify everyone.
		SendNotification(U.Name + " has disconnected.")

		' Return the default response.
		Return True
	End

	Method SendText:Void(Text:String, C:Client=Null)
		Local P:= Network.AllocatePacket()

		P.WriteLine(Text)

		Network.Send(P, C, MSG_TYPE_TEXT, True)

		Network.ReleasePacket(P)

		If (C = Null And LocalInfo <> Null) Then
			DisplayLine(LocalInfo, Text)
		Endif

		Return
	End

	Method SendNotification:Void(Text:String, C:Client=Null)
		Local P:= Network.AllocatePacket()

		P.WriteLine(Text)

		Network.Send(P, C, MSG_TYPE_NOTIFICATION, True)

		Network.ReleasePacket(P)

		If (C = Null And LocalInfo <> Null) Then
			DisplayLine(NOTIFICATION_HEADER + Text)
		Endif

		Return
	End

	Method NotifyAboutUserDisconnection:Void(U:User, C:Client=Null)
		Local P:= Network.AllocatePacket()

		P.WriteShort(U.ID)

		Network.Send(P, C, MSG_TYPE_DISCONNECT, True)

		Network.ReleasePacket(P)

		Return
	End

	' These two are effectively the same, they're just not a unified codebase (Laziness):
	Method NotifyAboutUser:Void(U:User, C:Client, IsTheirInfo:Bool, IsServer:Bool=False)
		Local P:= Network.AllocatePacket()

		NetworkSerial.WriteBool(P, IsTheirInfo)
		NetworkSerial.WriteBool(P, IsServer)

		P.WriteShort(U.ID)

		P.WriteLine(U.Name)

		Network.Send(P, C, MSG_TYPE_USER, True, False)

		Network.ReleasePacket(P)

		Return
	End

	Method AskForUser:Void(Name:String, C:Client=Null, IsTheirInfo:Bool=False, IsServer:Bool=False)
		Local P:= Network.AllocatePacket()

		NetworkSerial.WriteBool(P, IsTheirInfo)
		NetworkSerial.WriteBool(P, IsServer)

		P.WriteShort(User.ID_RESERVED)

		P.WriteLine(Name)

		Network.Send(P, C, MSG_TYPE_USER, True, False)

		Network.ReleasePacket(P)

		Return
	End

	Method BringClientUpToSpeed:Void(Incoming:Client)
		For Local U:= Eachin Users
			If (U.Handle <> Incoming) Then
				NotifyAboutUser(U, Incoming, False)
			Endif
		Next

		If (LocalInfo <> Null) Then
			' Tell 'Incoming' about us.
			NotifyAboutUser(LocalInfo, Incoming, False, True)
		Endif

		Return
	End

	' Call-backs:
	Method OnReceiveMessage:Void(Network:NetworkEngine, Origin:Client, Type:MessageType, Message:Stream, MessageSize:Int)
		Print("Message received of type " + Type + ", and size " + MessageSize)
		Print("Incoming Address: " + Origin.Address)

		Local U:= GetUser(Origin)

		If (Not Network.IsClient) Then
			If (U = Null) Then
				Select Type
					Case MSG_TYPE_USER
						Local IsMyInfo:= NetworkSerial.ReadBool(Message)
						Local Reserved:= NetworkSerial.ReadBool(Message)
						Local ID:= Message.ReadShort()

						If (Not IsMyInfo And ID = User.ID_RESERVED) Then
							If (GetUser(ID) = Null) Then
								AcceptUser(Message.ReadLine(), Origin)
							Else
								Return
							Endif
						Else
							' Nothing so far.
						Endif
				End Select

				Return
			Endif
		Else
			Local HasLocalInfo:Bool = (LocalInfo <> Null)

			Select Type
				Case MSG_TYPE_USER
					Local IsMyInfo:= NetworkSerial.ReadBool(Message)
					Local IsServerInfo:= NetworkSerial.ReadBool(Message)
					Local ID:= Message.ReadShort()
					Local Name:= Message.ReadLine()

					Print("NAME: " + Name)
					Print("IsServer: " + Int(IsServerInfo))
					Print("IsMyInfo: " + Int(IsMyInfo))
					Print("ID: " + ID)

					' Check if it's our information:
					If (IsMyInfo And Not IsServerInfo) Then
						If (Not HasLocalInfo) Then
							LocalInfo = New User(Name, ID)

							DisplayLine("User information established at: " + Origin.Address)
							DisplayLine("Obtained username: " + Name)
						Else
							' Invalid message; already have local information.

							Return
						Endif
					Else
						Local U:User

						U = GetUser(ID)

						If (U = Null) Then
							If (IsServerInfo And ID <> User.ID_RESERVED) Then
								U = New User(Name, ID, Origin)

								DisplayLine("Creating user-entry for " + Name + " at " + Origin.Address)
							Else
								U = New User(Name)

								DisplayLine("Creating user-entry for " + Name)
							Endif

							Users.AddLast(U)
						Endif
					Endif
				Case MSG_TYPE_DISCONNECT
					Local ID:= Message.ReadShort()

					Local U:= GetUser(ID)

					If (U <> Null) Then
						Users.RemoveEach(U)
					Else
						' Nothing so far.
					Endif
				Default
					' Safety net (Allows us to make assumptions later):
					If (Not HasLocalInfo) Then
						Return
					Endif
			End Select
		Endif

		Select Type
			Case MSG_TYPE_TEXT
				If (U = Null) Then
					' Unable to find a user to attach to this message.

					Return
				Endif

				Local Processed:= ProcessLine(U, Message.ReadLine())

				DisplayLine(Processed)

				If (Not Network.IsClient) Then
					' Repeat the message to everyone else.
					RepeatText(Processed, Origin)
				Endif
			Case MSG_TYPE_NOTIFICATION
				If (Not Network.IsClient) Then
					Return
				Endif

				While (Not Message.Eof)
					DisplayLine(NOTIFICATION_HEADER + Message.ReadLine())
				Wend
			Case MSG_TYPE_RAW_LINES
				If (Not Network.IsClient) Then
					Return
				Endif

				While (Not Message.Eof)
					DisplayLine(Message.ReadLine())
				Wend
			Default
				' Nothing so far.
		End Select

		Return
	End

	Method OnDisconnected:Void(Network:NetworkEngine)
		DisplayLine("Disconnected from server.")

		Return
	End

	Method OnClientConnect:Bool(Network:NetworkEngine, Address:NetworkAddress)
		Print("Accepting client at: " + Address)

		' Return the default response. (Accept the new connection)
		Return True
	End

	Method OnClientAccepted:Void(Network:NetworkEngine, C:Client)
		Print("Client accepted: " + C.Address)

		BringClientUpToSpeed(C)

		Return
	End

	Method OnClientDisconnected:Void(Network:NetworkEngine, C:Client)
		Local U:= GetUser(C)

		If (U <> Null) Then
			ReleaseUser(U)
		Endif

		Print("Client disconnected: " + C.Address)

		Return
	End

	Method OnNetworkBind:Void(Network:NetworkEngine, Successful:Bool)
		If (Not Successful) Then
			Print("Unable to bind socket.")

			Network.Close()

			Return
		Endif

		Print("Socket bound.")

		Return
	End

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

		Return
	End

	' Properties:
	Method LocalInfo:User() Property
		Return Self._LocalInfo
	End

	Method LocalInfo:Void(Input:User) Property
		Local HasCollection:Bool = (Users <> Null)

		If (Input = Null And HasCollection) Then
			Users.RemoveEach(Self._LocalInfo)
		Endif

		Self._LocalInfo = Input

		If (Input <> Null And HasCollection) Then
			Users.AddLast(Input)
		Endif

		Return
	End

	' Fields (Public):

	' The active network, used for communication.
	Field Network:NetworkEngine

	' This contains other connected users. (Only the host for clients; could be changed easily)
	Field Users:List<User>

	' A log containing a limited number of messages output to the screen.
	Field ChatLog:StringDeque

	' Fields (Protected):
	Protected

	' This holds information about us. (Name, etc; backed by the 'LocalInfo' property)
	Field _LocalInfo:User

	' The user's current input.
	Field UserInput:String

	' This is actively updated with the number of log entries possible.
	' (Used to reduce log sizes, and keep things on screen)
	Field MaxChatLogEntries:Int

	' This is used to offset the chat, so we can render debug text.
	Field ChatOffset:Float

	' The state of the application.
	Field State:Int

	' This is used to keep track of user identifiers.
	Field NextUserID:Int ' UShort

	' Booleans / Flags:

	' This is used to request a username.
	Field UsernameInputComplete:Bool

	Public
End

Class User Final
	' Constant variable(s):
	Const ID_RESERVED:= 0 ' 65536

	' Constructor(s):
	Method New(Name:String, ID:Int=ID_RESERVED, Handle:Client=Null)
		Self.Name = Name
		Self.ID = ID
		Self.Handle = Handle
	End

	Method New(Name:String, Handle:Client)
		Self.Name = Name
		Self.Handle = Handle
	End

	' Fields:
	Field Handle:Client

	Field Name:String
	Field ID:Int = ID_RESERVED ' UShort
End

' Functions:
Function Main:Int()
	' Start the application.
	New Application()

	' Return the default response.
	Return 0
End

#Rem
	DISCLAIMER:
		* This example is unfinished and likely contains bugs, use at your own risk.
#End

Strict

Public

' Preprocessor related:
#MOJO_AUTO_SUSPEND_ENABLED = False

#GLFW_WINDOW_RESIZABLE = True

' Imports:
Import regal.networking

Import brl.asyncevent
Import brl.stream
Import mojo

' Classes:
Class Application Extends App Implements CoreNetworkListener, MetaNetworkListener, ClientNetworkListener Final
	' Constant variable(s):
	Const PORT:= 27015 ' 27016 ' 5029
	Const PROTOCOL:= NetworkEngine.SOCKET_TYPE_TCP ' NetworkEngine.SOCKET_TYPE_UDP

	Const NOTIFICATION_HEADER:= "{NOTIFICATION}: "

	' This is used to offset user-identifiers.
	Const USER_ID_ORIGIN:= 1

	' States:
	Const STATE_IDLE:= 0
	Const STATE_CHAT:= 1

	' Message types:
	Const MSG_TYPE_USER:= NetworkEngine.MSG_TYPE_CUSTOM ' + 1
	Const MSG_TYPE_DISCONNECT:= (MSG_TYPE_USER+1)
	Const MSG_TYPE_TEXT:= (MSG_TYPE_DISCONNECT+1)
	Const MSG_TYPE_RAW_LINES:= (MSG_TYPE_TEXT+1)
	Const MSG_TYPE_NOTIFICATION:= (MSG_TYPE_RAW_LINES+1)

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

		Users = New List<User>()
		ChatLog = New StringDeque()

		LocalInfo = Null

		State = STATE_IDLE
		NextUserID = USER_ID_ORIGIN

		UsernameInputComplete = False

		' Return the default response.
		Return 0
	End

	' Methods:
	Method InitNetwork:Void()
		Network = New NetworkEngine()

		Network.SetCoreCallback(Self)
		Network.SetMetaCallback(Self)
		Network.SetClientCallback(Self)

		Return
	End

	Method Host:Void(Port:Int=PORT)
		InitNetwork()

		Network.Host(Port, True, PROTOCOL)

		Print("Hosting server on port " + Port + " using " + NetworkEngine.ProtocolToString(PROTOCOL) + "...")

		Return
	End

	Method Connect:Void(Hostname:String, Port:Int=PORT)
		InitNetwork()

		Network.Connect(Hostname, Port, True, PROTOCOL)

		Print("Connecting to " + Hostname + ":" + Port + " using " + NetworkEngine.ProtocolToString(PROTOCOL) + "...")

		Return
	End

	Method OnRender:Int()
		Select State
			Case STATE_CHAT
				Render_Chat()
			Default ' STATE_IDLE
				Render_Idle()
		End Select

		' Return the default response.
		Return 0
	End

	Method OnUpdate:Int()
		UpdateAsyncEvents()

		Select State
			Case STATE_IDLE
				If (Update_Idle()) Then
					State = STATE_CHAT
				Endif
			Case STATE_CHAT
				Update_Chat()
		End Select

		' Return the default response.
		Return 0
	End

	Method OnResize:Int()
		MaintainChatLog()

		Return Super.OnResize()
	End

	Method Render_Idle:Void()
		Cls(205.0, 205.0, 205.0)

		Return
	End

	Method Render_Chat:Void()
		Const PREVIEW_USER_COUNT:= 4

		Cls(127.5, Cos(Float(Millisecs() / 10)) * 255.0, 200.0)

		Local UserCount:= Users.Count()

		If (UserCount > 0) Then
			If (UserCount > PREVIEW_USER_COUNT) Then
				DrawText("Users connected: " + UserCount, 8.0, 8.0)
			Else
				Local Names:String

				Local Users_Node:= Users.FirstNode()

				Local Iterations:= Min(PREVIEW_USER_COUNT, UserCount)

				For Local I:= 1 To Iterations
					Local U:= Users_Node.Value()

					Names += U.Name

					If (I < Iterations) Then
						Names += ", "
					Endif

					Users_Node = Users_Node.NextNode()
				Next

				DrawText("Users connected: " + Names, 8.0, 8.0)
			Endif
		Endif

		If (Not Network.IsClient) Then
			DrawText("Clients connected overall: " + Network.ClientCount, 8.0, 24.0)
		Endif

		DrawChatLog(8.0, ChatOffset)
		DrawUserInput()

		Return
	End

	Method DrawChatLog:Void(X:Float=8.0, Y:Float=8.0)
		PushMatrix()

		Translate(X, Y)

		For Local Line:= Eachin ChatLog
			DrawText(Line, 0.0, 0.0)

			Translate(0.0, 16.0)
		Next

		PopMatrix()

		Return
	End

	Method DrawUserInput:Void()
		Local X:= 8.0
		Local Y:= (Float(DeviceHeight()) - 24.0)

		If (LocalInfo = Null) Then ' Network.IsClient
			If (Not UsernameInputComplete) Then
				DrawText("Please input a username: " + UserInput, X, Y)
			Else
				If (Network.IsClient) Then
					DrawText("Waiting for authentication...", X, Y)
				Endif
			Endif
		Else
			DrawText(LocalInfo.Name + ": " + UserInput, X, Y)
		Endif

		Return
	End

	Method Update_Idle:Bool()
		If (KeyHit(KEY_1)) Then
			Host()

			ChatOffset = 64.0

			Return True
		Elseif (KeyHit(KEY_0)) Then
			Connect("127.0.0.1") ' "localhost"

			ChatOffset = 48.0

			Return True
		Endif

		' Return the default response.
		Return False
	End

	Method Update_Chat:Void()
		Network.Update()

		#Rem
			If (KeyHit(KEY_W)) Then
				SendText(String(Millisecs()))
			Endif
		#End

		UpdateUserInput()

		Return
	End

	Method UpdateUserInput:Void()
		Repeat
			Local KeyCode:= GetChar()

			If (Not KeyCode) Then
				Exit
			Elseif (KeyCode = 13) Then ' Enter
				If (UserInput.Length > 0) Then
					If (UsernameInputComplete) Then
						If (Network.HasClient) Then
							SendText(UserInput)
						Endif
					Else
						If (Network.IsClient) Then
							If (LocalInfo = Null) Then
								DisplayLine("Asking the server to authenticate us...")

								' Tell the host our username.
								AskForUser(UserInput)

								UsernameInputComplete = True
							Endif
						Else
							LocalInfo = New User(UserInput, GetNextUserID())

							BringClientUpToSpeed(Null)
							'NotifyAboutUser(LocalInfo, Null, False, True)

							UsernameInputComplete = True
						Endif
					Endif

					UserInput = ""
				Endif
			Elseif (KeyCode = 8) Then
				UserInput = UserInput[..UserInput.Length-1]
			Elseif (KeyCode >= 32) Then
				UserInput += String.FromChar(KeyCode)
			Endif
		Forever

		Return
	End

	Method MaintainChatLog:Void()
		' Calculate the number of entries allowed in the chat-log:
		MaxChatLogEntries = ((DeviceHeight() / 16) - (4 + (Int(ChatOffset) / 16)))

		' Reduce the chat-log size.
		While (ChatLog.Length >= MaxChatLogEntries)
			ChatLog.PopFirst()
		Wend

		Return
	End

	Method DisplayLine:Void(RawText:String)
		' Output to the console.
		Print("{CHAT}: " + RawText)

		MaintainChatLog()

		' Add 'RawText' to the chat-log.
		ChatLog.PushLast(RawText)

		Return
	End

	Method DisplayLine:Void(U:User, Data:String)
		DisplayLine(ProcessLine(U, Data))

		Return
	End

	Method ProcessLine:String(U:User, Data:String)
		Return (U.Name + ": " + Data)
	End

	Method GetUser:User(C:Client)
		If (C = Null) Then
			Return Null
		Endif

		For Local U:= Eachin Users
			If (U.Handle = C) Then
				Return U
			Endif
		Next

		' Return the default response.
		Return Null
	End

	Method GetUser:User(Name:String)
		For Local U:= Eachin Users
			If (U.Name = Name) Then
				Return U
			Endif
		Next

		' Return the default response.
		Return Null
	End

	Method GetUser:User(ID:Int)
		For Local U:= Eachin Users
			If (U.ID = ID) Then
				Return U
			Endif
		Next

		' Return the default response.
		Return Null
	End

	' This repeats text to every 'Client', excluding 'Origin'.
	Method RepeatText:Void(RawText:String, Origin:Client)
		Local P:= Network.AllocatePacket()

		P.WriteLine(RawText)

		For Local C:= Eachin Network
			If (C = Origin) Then
				Continue
			Endif

			Network.Send(P, C, MSG_TYPE_RAW_LINES)
		Next

		Network.ReleasePacket(P)

		Return
	End

	Method GetNextUserID:Int() ' UShort
		Local ID:= NextUserID

		NextUserID += 1

		Return ID
	End

	Method AcceptUser:Bool(Name:String, Origin:Client)
		' Only accept the user if we don't already know them:
		If (GetUser(Origin) = Null) Then
			Local Num:Int = 0 ' Long ' Short

			For Local ExistingUser:= Eachin Users
				If (ExistingUser.Name.StartsWith(Name)) Then
					Local SuffixNum:= Int(ExistingUser.Name[Name.Length..])

					If (SuffixNum > 0) Then
						Num = (SuffixNum + 1)
					Else
						If (ExistingUser.Name = Name) Then
							Num += 1
						Endif
					Endif
				Endif
			Next

			If (Num > 0) Then
				Name += String(Num)
			Endif

			Local U:= New User(Name, GetNextUserID(), Origin)

			Users.AddLast(U)

			SendNotification(U.Name + " has connected.")

			' Tell 'Origin' what we're calling them.
			NotifyAboutUser(U, Origin, True)

			' Tell everyone else that they exist.
			For Local C:= Eachin Network
				If (C = Origin) Then
					Continue
				Endif

				NotifyAboutUser(U, C, False)
			Next

			Return True
		Endif

		' Return the default response.
		Return False
	End

	' This is used by servers to notify users about someone disconnecting.
	Method ReleaseUser:Bool(U:User)
		' Tell everyone that 'U' has disconnected.
		NotifyAboutUserDisconnection(U)

		' Remove this 'User' from our collection.
		Users.RemoveEach(U)

		' Notify everyone.
		SendNotification(U.Name + " has disconnected.")

		' Return the default response.
		Return True
	End

	Method SendText:Void(Text:String, C:Client=Null)
		Local P:= Network.AllocatePacket()

		P.WriteLine(Text)

		Network.Send(P, C, MSG_TYPE_TEXT, True)

		Network.ReleasePacket(P)

		If (C = Null And LocalInfo <> Null) Then
			DisplayLine(LocalInfo, Text)
		Endif

		Return
	End

	Method SendNotification:Void(Text:String, C:Client=Null)
		Local P:= Network.AllocatePacket()

		P.WriteLine(Text)

		Network.Send(P, C, MSG_TYPE_NOTIFICATION, True)

		Network.ReleasePacket(P)

		If (C = Null And LocalInfo <> Null) Then
			DisplayLine(NOTIFICATION_HEADER + Text)
		Endif

		Return
	End

	Method NotifyAboutUserDisconnection:Void(U:User, C:Client=Null)
		Local P:= Network.AllocatePacket()

		P.WriteShort(U.ID)

		Network.Send(P, C, MSG_TYPE_DISCONNECT, True)

		Network.ReleasePacket(P)

		Return
	End

	' These two are effectively the same, they're just not a unified codebase (Laziness):
	Method NotifyAboutUser:Void(U:User, C:Client, IsTheirInfo:Bool, IsServer:Bool=False)
		Local P:= Network.AllocatePacket()

		NetworkSerial.WriteBool(P, IsTheirInfo)
		NetworkSerial.WriteBool(P, IsServer)

		P.WriteShort(U.ID)

		P.WriteLine(U.Name)

		Network.Send(P, C, MSG_TYPE_USER, True, False)

		Network.ReleasePacket(P)

		Return
	End

	Method AskForUser:Void(Name:String, C:Client=Null, IsTheirInfo:Bool=False, IsServer:Bool=False)
		Local P:= Network.AllocatePacket()

		NetworkSerial.WriteBool(P, IsTheirInfo)
		NetworkSerial.WriteBool(P, IsServer)

		P.WriteShort(User.ID_RESERVED)

		P.WriteLine(Name)

		Network.Send(P, C, MSG_TYPE_USER, True, False)

		Network.ReleasePacket(P)

		Return
	End

	Method BringClientUpToSpeed:Void(Incoming:Client)
		For Local U:= Eachin Users
			If (U.Handle <> Incoming) Then
				NotifyAboutUser(U, Incoming, False)
			Endif
		Next

		If (LocalInfo <> Null) Then
			' Tell 'Incoming' about us.
			NotifyAboutUser(LocalInfo, Incoming, False, True)
		Endif

		Return
	End

	' Call-backs:
	Method OnReceiveMessage:Void(Network:NetworkEngine, Origin:Client, Type:MessageType, Message:Stream, MessageSize:Int)
		Print("Message received of type " + Type + ", and size " + MessageSize)
		Print("Incoming Address: " + Origin.Address)

		Local U:= GetUser(Origin)

		If (Not Network.IsClient) Then
			If (U = Null) Then
				Select Type
					Case MSG_TYPE_USER
						Local IsMyInfo:= NetworkSerial.ReadBool(Message)
						Local Reserved:= NetworkSerial.ReadBool(Message)
						Local ID:= Message.ReadShort()

						If (Not IsMyInfo And ID = User.ID_RESERVED) Then
							If (GetUser(ID) = Null) Then
								AcceptUser(Message.ReadLine(), Origin)
							Else
								Return
							Endif
						Else
							' Nothing so far.
						Endif
				End Select

				Return
			Endif
		Else
			Local HasLocalInfo:Bool = (LocalInfo <> Null)

			Select Type
				Case MSG_TYPE_USER
					Local IsMyInfo:= NetworkSerial.ReadBool(Message)
					Local IsServerInfo:= NetworkSerial.ReadBool(Message)
					Local ID:= Message.ReadShort()
					Local Name:= Message.ReadLine()

					Print("NAME: " + Name)
					Print("IsServer: " + Int(IsServerInfo))
					Print("IsMyInfo: " + Int(IsMyInfo))
					Print("ID: " + ID)

					' Check if it's our information:
					If (IsMyInfo And Not IsServerInfo) Then
						If (Not HasLocalInfo) Then
							LocalInfo = New User(Name, ID)

							DisplayLine("User information established at: " + Origin.Address)
							DisplayLine("Obtained username: " + Name)
						Else
							' Invalid message; already have local information.

							Return
						Endif
					Else
						Local U:User

						U = GetUser(ID)

						If (U = Null) Then
							If (IsServerInfo And ID <> User.ID_RESERVED) Then
								U = New User(Name, ID, Origin)

								DisplayLine("Creating user-entry for " + Name + " at " + Origin.Address)
							Else
								U = New User(Name)

								DisplayLine("Creating user-entry for " + Name)
							Endif

							Users.AddLast(U)
						Endif
					Endif
				Case MSG_TYPE_DISCONNECT
					Local ID:= Message.ReadShort()

					Local U:= GetUser(ID)

					If (U <> Null) Then
						Users.RemoveEach(U)
					Else
						' Nothing so far.
					Endif
				Default
					' Safety net (Allows us to make assumptions later):
					If (Not HasLocalInfo) Then
						Return
					Endif
			End Select
		Endif

		Select Type
			Case MSG_TYPE_TEXT
				If (U = Null) Then
					' Unable to find a user to attach to this message.

					Return
				Endif

				Local Processed:= ProcessLine(U, Message.ReadLine())

				DisplayLine(Processed)

				If (Not Network.IsClient) Then
					' Repeat the message to everyone else.
					RepeatText(Processed, Origin)
				Endif
			Case MSG_TYPE_NOTIFICATION
				If (Not Network.IsClient) Then
					Return
				Endif

				While (Not Message.Eof)
					DisplayLine(NOTIFICATION_HEADER + Message.ReadLine())
				Wend
			Case MSG_TYPE_RAW_LINES
				If (Not Network.IsClient) Then
					Return
				Endif

				While (Not Message.Eof)
					DisplayLine(Message.ReadLine())
				Wend
			Default
				' Nothing so far.
		End Select

		Return
	End

	Method OnDisconnected:Void(Network:NetworkEngine)
		DisplayLine("Disconnected from server.")

		Return
	End

	Method OnClientConnect:Bool(Network:NetworkEngine, Address:NetworkAddress)
		Print("Accepting client at: " + Address)

		' Return the default response. (Accept the new connection)
		Return True
	End

	Method OnClientAccepted:Void(Network:NetworkEngine, C:Client)
		Print("Client accepted: " + C.Address)

		BringClientUpToSpeed(C)

		Return
	End

	Method OnClientDisconnected:Void(Network:NetworkEngine, C:Client)
		Local U:= GetUser(C)

		If (U <> Null) Then
			ReleaseUser(U)
		Endif

		Print("Client disconnected: " + C.Address)

		Return
	End

	Method OnNetworkBind:Void(Network:NetworkEngine, Successful:Bool)
		If (Not Successful) Then
			Print("Unable to bind socket.")

			Network.Close()

			Return
		Endif

		Print("Socket bound.")

		Return
	End

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

		Return
	End

	' Properties:
	Method LocalInfo:User() Property
		Return Self._LocalInfo
	End

	Method LocalInfo:Void(Input:User) Property
		Local HasCollection:Bool = (Users <> Null)

		If (Input = Null And HasCollection) Then
			Users.RemoveEach(Self._LocalInfo)
		Endif

		Self._LocalInfo = Input

		If (Input <> Null And HasCollection) Then
			Users.AddLast(Input)
		Endif

		Return
	End

	' Fields (Public):

	' The active network, used for communication.
	Field Network:NetworkEngine

	' This contains other connected users. (Only the host for clients; could be changed easily)
	Field Users:List<User>

	' A log containing a limited number of messages output to the screen.
	Field ChatLog:StringDeque

	' Fields (Protected):
	Protected

	' This holds information about us. (Name, etc; backed by the 'LocalInfo' property)
	Field _LocalInfo:User

	' The user's current input.
	Field UserInput:String

	' This is actively updated with the number of log entries possible.
	' (Used to reduce log sizes, and keep things on screen)
	Field MaxChatLogEntries:Int

	' This is used to offset the chat, so we can render debug text.
	Field ChatOffset:Float

	' The state of the application.
	Field State:Int

	' This is used to keep track of user identifiers.
	Field NextUserID:Int ' UShort

	' Booleans / Flags:

	' This is used to request a username.
	Field UsernameInputComplete:Bool

	Public
End

Class User Final
	' Constant variable(s):
	Const ID_RESERVED:= 0 ' 65536

	' Constructor(s):
	Method New(Name:String, ID:Int=ID_RESERVED, Handle:Client=Null)
		Self.Name = Name
		Self.ID = ID
		Self.Handle = Handle
	End

	Method New(Name:String, Handle:Client)
		Self.Name = Name
		Self.Handle = Handle
	End

	' Fields:
	Field Handle:Client

	Field Name:String
	Field ID:Int = ID_RESERVED ' UShort
End

' Functions:
Function Main:Int()
	' Start the application.
	New Application()

	' Return the default response.
	Return 0
End