Jump to content



Photo
* * * * * 1 votes

ALP Nudging in VPX

ALP Nudge VPX Atgames Legends Pinball Nudging Accelerometer

  • Please log in to reply
13 replies to this topic

#1 popcorndog

popcorndog

    Hobbyist

  • Members
  • PipPip
  • 47 posts

  • Flag: United States of America

  • Favorite Pinball: Tiki Bob's Atomic Beach Party

Posted 05 March 2022 - 03:27 PM

Has anyone successfully configured the Atgames Legends Pinball accelerometer for nudging in VPX?  I have searched the web and forums for a guide, but all I can find is posts that people have tried it - most unsuccessful.  Appreciate help setting this up successfully, as I have my ALPs cabinet buttons configured for other purposes and occasionally I run into issues with the pinball getting stuck on a table feature.  I do not keep a keyboard connected to the computer and do not want the inconvenience of getting the keyboard out to nudge.



#2 theissdev

theissdev

    Hobbyist

  • Members
  • PipPip
  • 10 posts

  • Flag: United States of America

  • Favorite Pinball: Theater of Magic, Judge Dredd

Posted 21 March 2022 - 04:17 PM

So, I've been able to wire up the ALP accelerometer via x360ce to the left stick and the plunger on the right stick. Plunger works great. Nudging shows correctly in the bottom displays of the nudge test table - right side (red) showing correct raw and left side (black) showing muted raw (but showing). The nudge test table is also showing the nudge values in the sensor text.

The problem is that even though those are showing, there's no nudging appearing on the table itself. Balls just sit there, even though left side is showing nudging happening. Been fighting this for about a week now. Not sure what else to do. Trying renaming the NudgePlugIn_mjrAccelAndTilt.vbs to NudgePlugIn.vbs, all combinations of the 3 nudge checkboxes in prefs, and messing w/ values for max and gain (even to extremes).

 

It's frustrating because I can see the nudge inputs working in those bottom displays, I just can't get it to apply to the table for w/e reason. Not sure what else to try. I've exhausted the google. =(

 

Should probably mention I'm running 10.7 as well. =D


Edited by theissdev, 21 March 2022 - 04:30 PM.


#3 theissdev

theissdev

    Hobbyist

  • Members
  • PipPip
  • 10 posts

  • Flag: United States of America

  • Favorite Pinball: Theater of Magic, Judge Dredd

Posted 23 March 2022 - 02:29 AM

So, i've figured out if I add 

Nudge 0, Y * 5

~ line 419 of the nudge and calibration table, i get nudge when i shove the alp forward. Again, the sensor data is there, just not being applied to the table. This seems to at least get the balls to pop up (hardcoded to 0 (degrees) - up).

270 when wired to neg X values and 90 for pos also seems to nudge correctly (at least in raw x inputs).

 

Y is the raw input from the alp accelerometer. Definitely bumps more w/ more force. It's totally ignoring all the max and gain values though.

Need to marry X and Y to get true degree direction and expose to all tables, not just the test one. Looking into writing a custom NudgeInput vbs to do this. I'll post when I've got something working.


Edited by theissdev, 23 March 2022 - 02:33 AM.


#4 theissdev

theissdev

    Hobbyist

  • Members
  • PipPip
  • 10 posts

  • Flag: United States of America

  • Favorite Pinball: Theater of Magic, Judge Dredd

Posted 05 April 2022 - 03:43 PM

So, I tried to do this in a NudgePlugIn.vbs file, but it doesn't like that. Ended up modifying core.vbs (full code below). This will get you nudge on most of the SS tables. It doesn't work at all for all of the EM tables I've tested and some newer Stern SS tables don't accept it (ST Enterprise for one). (Doesn't accept = no nudge but table isn't broken or anything.)

It works w/ the gain in VP's settings. I've got the ALP sensor wired up to the left stick in x360ce w/ the VP nudge axes set to XAxis-XAxis 100 50, YAxis-YAxis 100 50 and a DeadZone of 15% (even though it's got a dead zone in x360ce as well).

This is definitely a hack, but it'll get you nudge w/ the ALP sensor for most SS tables. Still not sure why there's a broken connection somewhere between the input and the actual nudging. =/

Note that there's no nudge timeout or anything here, just forwarding the sensor data to the nudge call. This also seems to bypass the tilt, so you can pretty much nudge like crazy.

I've got a KL25Z inbound, so I'm not going to spend more time on this. I know it's more precise than the sensor in the ALP, so hopefully VPX handles it the right way and I'll just use that from then on.

Option Explicit

Const VPinMAMEDriverVer = 3.58

'======================
' VPinMAME driver core
'======================
'
'see Scripts.txt for details on how to use this, plus the actual functions below for more help in its comments

Dim Controller	 ' VPinMAME/B2S/etc Controller Object
Dim vpmTimer	 ' Timer Object
Dim vpmNudge	 ' Nudge handler Object
Dim Lights(260)	 ' Put all lamps in an array for easier handling
Dim XMax, YMax, XGain, YGain, DeadZone, TiltSens
' If more than one lamp is connected, fill this with an array of each light
Dim vpmMultiLights() : ReDim vpmMultiLights(0)
Private gNextMechNo : gNextMechNo = 0 ' keep track of created mech handlers (would be nice with static members)

' Callbacks
Dim SolCallback(68) ' Solenoids (parsed at Runtime)
Dim SolModCallback(68) ' Solenoid modulated callbacks (parsed at Runtime)
Dim SolPrevState(68) ' When modulating solenoids are in use, needed to keep positive value levels from changing boolean state
Dim LampCallback	' Called after lamps are updated
Dim PDLedCallback	' Called after leds are updated
Dim GICallback		' Called for each changed GI String
Dim GICallback2		' Called for each changed GI String
Dim MotorCallback	' Called after solenoids are updated
Dim vpmCreateBall	' Called whenever a vpm class needs to create a ball
Dim BSize:If IsEmpty(Eval("BallSize"))=true Then BSize=25 Else BSize = BallSize/2
Dim BMass:If IsEmpty(Eval("BallMass"))=true Then BMass=1 Else BMass = BallMass
Dim UseDMD:If IsEmpty(Eval("UseVPMDMD"))=true Then UseDMD=false Else UseDMD = UseVPMDMD
Dim UseModSol:If IsEmpty(Eval("UseVPMModSol"))=true Then UseModSol=false Else UseModSol = UseVPMModSol
Dim UseColoredDMD:If IsEmpty(Eval("UseVPMColoredDMD"))=true Then UseColoredDMD=false Else UseColoredDMD = UseVPMColoredDMD
Dim UseNVRAM:If IsEmpty(Eval("UseVPMNVRAM"))=true Then UseNVRAM=false Else UseNVRAM = UseVPMNVRAM
Dim NVRAMCallback

' Assign Null Default Sub so script won't error if only one is defined in a script (should redefine in your script)
Set GICallback = GetRef("NullSub")
Set GICallback2 = GetRef("NullSub")

' Game specific info
Dim ExtraKeyHelp ' Help string for game specific keys
Dim vpmShowDips  ' Show DIPs function

Private vpmVPVer : vpmVPVer = vpmCheckVPVer()

Private Function PinMAMEInterval
	If vpmVPVer >= 10200 Then
			PinMAMEInterval = -1 ' VP10.2 introduced special frame-sync'ed timers
	Else
		If vpmVPVer >= 10000 Then
			PinMAMEInterval = 3  ' as old VP9 timers pretended to run at 1000Hz but actually did only a max of 100Hz (e.g. corresponding nowadays to interval=10), we do something inbetween for VP10+ by default
		Else
			PinMAMEInterval = 1
		End If
	End If
End Function

Private Const conStackSw	= 8  ' Stack switches
Private Const conMaxBalls	= 13 ' Because of Apollo 13n
Private Const conMaxTimers	= 20 ' Spinners can generate a lot of timers
Private Const conTimerPulse = 40 ' Timer runs at 25Hz
Private Const conFastTicks	= 4  ' Fast is 4 times per timer pulse
Private Const conMaxSwHit	= 5  ' Don't stack up more than 5 events for each switch

' DEPRECATED Flipper constants:
Private Const conFlipRetStrength = 0.01  ' Flipper return strength
Private Const conFlipRetSpeed	 = 0.137 ' Flipper return speed

Function CheckScript(file) 'Checks Tables and Scripts directories for specified vbs file, and if it exitst, will load it.
	CheckScript = False
	On Error Resume Next
	Dim TablesDirectory:TablesDirectory = Left(UserDirectory,InStrRev(UserDirectory,"\",InStrRev(UserDirectory,"\")-1))&"Tables\"
	Dim ScriptsDirectory:ScriptsDirectory = Left(UserDirectory,InStrRev(UserDirectory,"\",InStrRev(UserDirectory,"\")-1))&"Scripts\"
	Dim check:Set check = CreateObject("Scripting.FileSystemObject")
	If check.FileExists(tablesdirectory & file) Or check.FileExists(scriptsdirectory & file) Or check.FileExists(file) Then CheckScript = True
	On Error Goto 0
End Function

Function LoadScript(file) 'Checks Tables and Scripts directories for specified vbs file, and if it exitst, will load it.
	LoadScript = False
	On Error Resume Next
	If CheckScript(file) Then ExecuteGlobal GetTextFile(file):LoadScript = True
	On Error Goto 0
End Function

' Dictionary
' At one point, Microsoft had made Scripting.Dictionary "unsafe for scripting", but it's
' been a long time since that was true. So now, to maintain compatibility with all tables
' and scripts that use cvpmDictionary, this class is now a simple wrapper around Microsoft's
' more efficient implementation.
Class cvpmDictionary
	Private mDict
	Private Sub Class_Initialize : Set mDict = CreateObject("Scripting.Dictionary") : End Sub

	' DEPRECATED: MS Dictionaries are not index-based. Use "Exists" method instead.
	Private Function FindKey(aKey)
		Dim ii, key : FindKey = -1
		If mDict.Count > 0 Then
			ii = 0
			For Each key In mDict.Keys
				If key = aKey Then FindKey = ii : Exit Function
			Next
		End If
	End Function

	Public Property Get Count : Count = mDict.Count : End Property

	Public Property Get Item(aKey)
		Item = Empty
		If mDict.Exists(aKey) Then
			If IsObject(mDict(aKey)) Then
				Set Item = mDict(aKey)
			Else
				Item = mDict(aKey)
			End If
		End If
	End Property

	Public Property Let Item(aKey, aData)
		If IsObject(aData) Then
			Set mDict(aKey) = aData
		Else
			mDict(aKey) = aData
		End If
	End Property

	Public Property Set Key(aKey)
		' This function is (and always has been) a no-op.  Previous definition
		' just looked up aKey in the keys list, and if found, set the key to itself.
	End Property

	Public Sub Add(aKey, aItem)
		If IsObject(aItem) Then
			Set mDict(aKey) = aItem
		Else
			mDict(aKey) = aItem
		End If
	End Sub

	Public Sub Remove(aKey)		 : mDict.Remove(aKey)		   : End Sub
	Public Sub		RemoveAll	 : mDict.RemoveAll			   : End Sub
	Public Function Exists(aKey) : Exists = mDict.Exists(aKey) : End Function
	Public Function Items		 : Items  = mDict.Items		   : End Function
	Public Function Keys		 : Keys	  = mDict.Keys		   : End Function
End Class

'--------------------
'		Timer
'--------------------
Class cvpmTimer
	Private mQue, mNow, mTimers
	Private mSlowUpdates, mFastUpdates, mResets, mFastTimer

	Private Sub Class_Initialize
		ReDim mQue(conMaxTimers) : mNow = 0 : mTimers = 0
		Set mSlowUpdates = New cvpmDictionary
		Set mFastUpdates = New cvpmDictionary
		Set mResets		 = New cvpmDictionary
	End Sub

	Public Sub InitTimer(aTimerObj, aFast)
		If aFast Then
			Set mFastTimer = aTimerObj
			aTimerObj.TimerInterval = conTimerPulse \ conFastTicks
			aTimerObj.TimerEnabled = False
			vpmBuildEvent aTimerObj, "Timer", "vpmTimer.FastUpdate"
		Else
			aTimerObj.Interval = conTimerPulse : aTimerObj.Enabled = True
			vpmBuildEvent aTimerObj, "Timer", "vpmTimer.Update"
		End If
	End Sub

	Sub EnableUpdate(aClass, aFast, aEnabled)
		On Error Resume Next
		If aFast Then
			If aEnabled Then mFastUpdates.Add aClass, 0 : Else mFastUpdates.Remove aClass
			mFastTimer.TimerEnabled = mFastUpdates.Count > 0
		Else
			If aEnabled Then mSlowUpdates.Add aClass, 0 : Else mSlowUpdates.Remove aClass
		End If
	End Sub

	Public Sub Reset
		Dim obj : For Each obj In mResets.Keys : obj.Reset : Next
	End Sub


	Public Sub FastUpdate
		Dim obj : For Each obj In mFastUpdates.Keys : obj.Update : Next
	End Sub

	Public Sub Update
		Dim ii, jj, sw, obj, mQuecopy

		Dim X, Y
		NudgeSensorStatus X, Y
		If abs(Y) > 0 OR abs(X) > 0 Then
			If abs(X) = 0 Then
				Nudge 0, Y * YGain
			ElseIf abs(Y) = 0 Then
				If X < 0 Then
					Nudge 270, X * XGain
				Else
					Nudge 90, X * XGain
				End If
			Else
				Nudge Atn(Y/X) * 57.295779513, Sqr(X * X * XGain + Y * Y * YGain)
			End If
		End If

		For Each obj In mSlowUpdates.Keys : obj.Update : Next

		If mTimers = 0 Then Exit Sub
		mNow = mNow + 1 : ii = 1

		Do While ii <= mTimers
			If mQue(ii)(0) <= mNow Then
				If mQue(ii)(1) = 0 Then
					If isObject(mQue(ii)(3)) Then
						Call mQue(ii)(3)(mQue(ii)(2))
					ElseIf varType(mQue(ii)(3)) = vbString Then
						If mQue(ii)(3) > "" Then Execute mQue(ii)(3) & " " & mQue(ii)(2) & " "
					End If
					mTimers = mTimers - 1
					For jj = ii To mTimers : mQue(jj) = mQue(jj+1) : Next : ii = ii - 1
				ElseIf mQue(ii)(1) = 1 Then
					mQuecopy = mQue(ii)(2)
					Controller.Switch(mQuecopy) = False
					mQue(ii)(0) = mNow + mQue(ii)(4) : mQue(ii)(1) = 0
				Else '2
					mQuecopy = mQue(ii)(2)
					Controller.Switch(mQuecopy) = True
					mQue(ii)(1) = 1
				End If
			End If
			ii = ii + 1
		Loop
	End Sub

	Public Sub AddResetObj(aObj) : mResets.Add aObj, 0 : End Sub

	Public Sub PulseSw(aSwNo) : PulseSwitch aSwNo, 0, 0 : End Sub

	Public Default Sub PulseSwitch(aSwNo, aDelay, aCallback)
		Dim ii, count, last
		count = 0
		For ii = 1 To mTimers
			If mQue(ii)(1) > 0 And mQue(ii)(2) = aSwNo Then count = count + 1 : last = ii
		Next
		If count >= conMaxSwHit Or mTimers = conMaxTimers Then Exit Sub
		mTimers = mTimers + 1 : mQue(mTimers) = Array(mNow, 2, aSwNo, aCallback, aDelay\conTimerPulse)
		If count Then mQue(mTimers)(0) = mQue(last)(0) + mQue(last)(1)
	End Sub

	Public Sub AddTimer(aDelay, aCallback)
		If mTimers = conMaxTimers Then Exit Sub
		mTimers = mTimers + 1
		mQue(mTimers) = Array(mNow + aDelay \ conTimerPulse, 0, 0, aCallback)
	End Sub

	Public Sub AddTimer2(aDelay, aCallback, aID)
		If mTimers = conMaxTimers Then Exit Sub
		mTimers = mTimers + 1
		mQue(mTimers) = Array(mNow + aDelay \ conTimerPulse, 0, aID, aCallback)
	End Sub
End Class

'--------------------
'	  Trough
'--------------------
Class cvpmTrough
	' Takes over for older cvpmBallStack in "trough mode". Theory of operation:
	' A trough can hold up to N balls, and has N*2 "slots". A ball effectively takes
	' up two slots, so no two adjacent slots (0 and 1) can be occupied at the same time.
	' Switches are assigned to even slots only, which means that as balls move through
	' the trough, each switch is allowed to flip between open and closed.
	' Slot 0 is the exit, and can have additional balls "stacked" on it, simulating balls
	' falling onto the exit kicker instead of coming in from the entrance. Extra balls
	' can be queued up at the entrance, and will enter the trough only if there's room
	' for them.

	Private mSlot(), mSw(), mEntrySw
	Private mBallsInEntry, mMaxBallsPerKick, mStackExitBalls
	Private mExitKicker, mExitDir, mExitForce, mDirVar, mForceVar
	Private mSounds

	' If you want to see what the trough is doing internally, add a TextBox to your table
	' named "DebugBox" (recommend Courier New or FixedSys at a small font size) and set
	' this variable to true via .isDebug = True.
	Private mDebug

	Private Sub Class_Initialize
		Dim ii

		ReDim mSw(conMaxBalls), mSlot(conMaxBalls * 2)
		For ii = 0 to UBound(mSlot) : mSlot(ii) = 0 : Next ' All slots empty to start
		For ii = 0 to UBound(mSw)	: mSw(ii) = 0	: Next ' All switches unassigned to start.
		mEntrySw = 0

		Set mExitKicker = Nothing
		mExitDir = 0 : mExitForce = 1 : mDirVar = 0 : mForceVar = 0
		mBallsInEntry = 0 : mMaxBallsPerKick = 1 : mStackExitBalls = 1

		Set mSounds = New cvpmDictionary

		mDebug = False

		If Not IsObject(vpmTrough) Then Set vpmTrough = Me
	End Sub

	Public Property Let IsTrough(aYes)
		If aYes Then
			Set vpmTrough = Me
		ElseIf Me Is vpmTrough Then
			Set vpmTrough = Nothing
		End If
	End Property

	Public Property Get IsTrough
		IsTrough = (Me Is vpmTrough)
	End Property

	' Initialization

	Public Property Let isDebug(enabled) : mDebug = enabled : End Property

	Public Property Let Size(aSize)
		Dim oldSize, newSize, ii
		oldSize = UBound(mSw)
		newSize = vpMax(1, aSize)

		ReDim Preserve mSlot(newSize * 2)
		ReDim Preserve mSw(newSize)
		For ii = oldSize+1 To newSize : mSw(ii) = 0 : Next
		For ii = (oldSize*2) + 1 to (newSize*2) : mSlot(ii) = 0 : Next
	End Property
	Public Property Get Size : Size = UBound(mSw) : End Property

	' Set EntrySw = 0 if you want balls to just fall into the trough automatically.
	' Set it to a real switch number to indicate that a ball is occupying an entry kicker.
	' The ROM in the controller is then responsible for kicking the ball into the trough.
	Public Property Let EntrySw(swNo) : mEntrySw = swNo : End Property

	' Assign switches, starting from slot 0 and going to entrance.
	' This sub allows you to pass in as many switches as you wish.
	Public Sub InitSwitches(switchArray)
		If Not IsArray(switchArray) Then
			Err.Raise 17, "cvpmTrough.InitSwitches: Input must be an array."
		End If

		Dim ii
		For ii = 0 to UBound(mSw)
			If ii > UBound(switchArray) Then
				mSw(ii) = 0
			Else
				mSw(ii) = switchArray(ii)
			End If
		Next
	End Sub

	' Alternative: Assign a switch to a specific slot.
	Public Sub AddSw(slotNo, swNo)
		If slotNo < 0 OR slotNo > UBound(mSw) Then Exit Sub
		mSw(slotNo) = swNo
	End Sub

	' MaxBallsPerKick: Kick up to N balls total per exit kick. Balls are only kicked from Slot 0.
	' StackExitBalls: Automatically stack up to N balls in Slot 0 regardless of where they came from.

	' Example: Subway where exit kicker is on the same level as the trough and a ball can
	' come in from the exit: StackExitBalls = 1, MaxBallsPerKick = 2. If Slot 0 has 1
	' ball and Slot 1 is occupied, only one ball will be kicked. If Slot 0 has 2 or more
	' balls, it'll kick out 2 balls.

	' Example: Twilight Zone Slot Kicker: Kicker is below trough, so if a ball is in the
	' exit chute, another ball can fall into the chute as well whether it came in from the
	' exit (Slot Machine) or any other entrance (Piano, Camera). In both cases, the kicker
	' will eject 2 balls at once. Set StackExitBalls = 2, maxBallsPerKick = 2 to simulate.

	Public Property Let MaxBallsPerKick(n) : mMaxBallsPerKick = vpMax(1, n) : End Property
	Public Property Let StackExitBalls(n) : mStackExitBalls = vpMax(1, n) : End Property

	Public Sub InitExit(aKicker, aDir, aForce)
		If TypeName(aKicker) <> "Kicker" Then
			Err.Raise 17, "cvpmTrough.InitExit: Cannot use object of type '" & TypeName(aKicker) & "'."
		End If

		Set mExitKicker = aKicker
		mExitDir = aDir
		mExitForce = vpMax(1, aForce)
	End Sub

	Public Sub InitExitVariance(aDirVar, aForceVar)
		mDirVar = aDirVar
		mForceVar = aForceVar
	End Sub

	' Setup sounds
	Public Sub InitEntrySounds(addSound, entrySoundEmpty, entrySoundBall)
		mSounds.Item("add") = addSound
		mSounds.Item("entry") = entrySoundEmpty
		mSounds.Item("entryBall") = entrySoundBall
	End Sub

	Public Sub InitExitSounds(exitSoundEmpty, exitSoundBall)
		mSounds.Item("exit") = exitSoundEmpty
		mSounds.Item("exitBall") = exitSoundBall
	End Sub

	' Start trough with this many balls
	Public Property Let Balls(numBalls)
		Dim ii, ballsAdded

		' First clear all slots.
		For ii = 0 to UBound(mSlot) : mSlot(ii) = 0 : Next

		' Now put a ball in each even-numbered slot up to the number requested.
		' First, stack exit slot. (Note, we may get a negative number. vpMin/vpMax prevent that.)
		mSlot(0) = vpMax(0, vpMin(mStackExitBalls, numBalls))
		ballsAdded = mSlot(0)

		' Fill remaining slots.
		For ii = 1 to vpMin(numBalls - mSlot(0), UBound(mSw))
			mSlot(ii*2) = 1
			ballsAdded = ballsAdded + 1
		Next

		' If we asked to put more balls in the trough than it can handle, queue up the rest.
		mBallsInEntry = vpMax(0, numBalls-ballsAdded)

		UpdateTroughSwitches
	End Property

	Public Property Get Balls
		Balls = 0
		Dim ii : For ii = 0 to UBound(mSlot) : Balls = Balls + mSlot(ii) : Next
	End Property

	Public Property Get BallsPending : BallsPending = mBallsInEntry : End Property

	' Auto-generate events for any entry kickers (eg. outhole, TZ Camera and Piano, etc.)
	' Accepts a single kicker, an Array, or a Collection.
	Public Sub CreateEvents(aName, aKicker)
		Dim obj, tmp
		If Not vpmCheckEvent(aName, Me) Then Exit Sub
		vpmSetArray tmp, aKicker
		For Each obj In tmp
			If isObject(obj) Then
				vpmBuildEvent obj, "Hit", aName & ".AddBall Me"
			Else
				vpmBuildEvent mKicker, "Hit", aName & ".AddBall Me"
			End If
		Next
	End Sub

	' VPM Update management

	Private Property Let NeedUpdate(aEnabled) : vpmTimer.EnableUpdate Me, False, aEnabled : End Property

	Public Sub Reset
		Dim mEntrySwcopy
		UpdateTroughSwitches
		If mEntrySw Then
			mEntrySwcopy = mEntrySw
			Controller.Switch(mEntrySwcopy) = (mBallsInEntry > 0)
		End If
	End Sub

	Public Sub Update
		NeedUpdate = AdvanceBalls
		UpdateTroughSwitches
	End Sub

	' Switch and slot management

	Private Sub setSw(slotNo, enabled)
		Dim mSwcopy
		If mSw(slotNo) Then
			mSwcopy = mSw(slotNo)
			Controller.Switch(mSwcopy) = enabled
		End If
	End Sub

	Private Sub UpdateTroughSwitches
		Dim ii, mSwcopy
		For ii = 0 to UBound(mSw)
			If mSw(ii) Then
				mSwcopy = mSw(ii)
				Controller.Switch(mSwcopy) = (mSlot(ii*2) > 0)
			End If
		Next
		If mDebug Then UpdateDebugBox
	End Sub

	Private Sub UpdateDebugBox ' Requires a textbox named DebugBox
		Dim str, ii, mSwcopy
		str = "Entry: " & mBallsInEntry & " (sw" & mEntrySw & " = "
		If mEntrySw > 0 Then
			mSwcopy = mEntrySw
			str = str & Controller.Switch(mSwcopy)
		Else
			str = str & "n/a"
		End If
		str = str & ")" & vbNewLine

		str = str & "["
		For ii = UBound(mSlot) To 0 Step -1 : str = str & mSlot(ii) : Next
		str = str & "]" & vbNewLine

		str = str & "["
		For ii = UBound(mSlot) To 0 Step -1
			If ii Mod 2 = 0 Then
				If mSw(ii\2) Then
					mSwcopy = mSw(ii\2)
					If Controller.Switch(mSwcopy) Then
						str = str & "1"
					Else
						str = str & "0"
					End If
				Else
					str = str & "-"
				End If
			Else
				str = str & " "
			End If
		Next
		str = str & "]"

		DebugBox.Text = str
	End Sub

	Private Function AdvanceBalls
		Dim ii, canMove, maxSlot
		maxSlot = UBound(mSlot)
		AdvanceBalls = False

		' Move balls through slots, one slot at a time.
		For ii = 0 to maxSlot
			If mSlot(ii) Then ' Ball in this slot.
				canMove = False

				' Can this ball move?  (Slot 0 = no)
				If ii = 0 Then
					' Slot 0 never moves (except when ejected)
					canMove = False
				ElseIf ii = 1 Then
					' Slot 1 automatically moves to Slot 0
					canMove = True
				ElseIf ii = 2 Then
					' Slot 2 moves if the number of balls in slot 0 is less than the stack target.
					canMove = (mSlot(0) < mStackExitBalls)
				Else
					' Only move if there is no ball in ii-1 or ii-2.
					canMove = (mSlot(ii-2) = 0) AND (mSlot(ii-1) = 0)
				End If

				If canMove Then
					mSlot(ii) = mSlot(ii) - 1
					mSlot(ii-1) = mSlot(ii-1) + 1
					AdvanceBalls = True ' Mark balls as having moved.
				End If
			End If
		Next

		' If balls are supposed to fall into the trough without going through a kicker,
		' see if any balls are pending and try to add one automatically if so.
		If mBallsInEntry > 0 AND mEntrySw <= 0 Then
			AdvanceBalls = AddBallAtEntrance OR AdvanceBalls
		End If
	End Function

	' Ball management

	Private Function AddBallAtEntrance
		Dim mSwcopy
		Dim maxSlot : maxSlot = UBound(mSlot)
		AddBallAtEntrance = False

		' Only add a ball if there's room for it at the entrance.
		' If the trough is full (or the entrance is occupied), the ball will remain
		' in the entry queue. In a kicker-gated trough, the entry switch will remain
		' pressed down, usually resulting in the machine retrying the load. In a fall-in
		' trough, the ball will just remain queued until the entrance opens up.
		If mSlot(maxSlot) = 0 AND mSlot(maxSlot-1) = 0 Then
			mSlot(maxSlot) = 1
			mBallsInEntry = vpMax(0, mBallsInEntry - 1)
			If mBallsInEntry = 0 AND mEntrySw Then
				mSwcopy = mEntrySw
				Controller.Switch(mSwcopy) = False
			End If
			AddBallAtEntrance = True
		End If
	End Function

	Public Sub AddBall(aKicker)
		Dim mSwcopy
		Dim addDone : addDone = False
		If IsObject(aKicker) Then
			aKicker.DestroyBall
			If aKicker Is mExitKicker Then
				' Ball fell in from exit. Stack it up on Slot 0.
				mSlot(0) = mSlot(0) + 1
				NeedUpdate = True
				UpdateTroughSwitches
				addDone = True
			End If
		End If

		If Not addDone Then
			' Ball came in from entrance. Queue it up for entry.
			mBallsInEntry = mBallsInEntry + 1
		If mEntrySw > 0 Then
			mSwcopy = mEntrySw
			' Trough has an entry kicker. Ball will not enter trough
			' until the entry solenoid is fired.
			Controller.Switch(mSwcopy) = True
			End If
			NeedUpdate = True
		End If

	If isObject(aKicker) Then
		If VP8sound then
			PlaySound mSounds.Item("add")
		ElseIf VP9sound then
			PlaySound mSounds.Item("add"), 1, 1, CoreAudioPan(aKicker.x), 0
		Else
			PlaySound mSounds.Item("add"), 1, 1, CoreAudioPan(aKicker.x), 0, 0, False, False, CoreAudioFade(aKicker.y)
		End If
	Else
		PlaySound mSounds.Item("add")
	End If
	End Sub

	' Use solCallback(solNo) on the trough entry kicker solenoid.
	Public Sub solIn(aEnabled)
		If aEnabled Then
			If mBallsInEntry > 0 Then
				NeedUpdate = AddBallAtEntrance
				PlaySound mSounds.Item("entryBall")
			Else
				PlaySound mSounds.Item("entry")
			End If
		End If
	End Sub
	Public Sub EntrySol_On : solIn(true) : End Sub

	' Use solCallback(solNo) on the trough exit kicker solenoid.
	Public Sub solOut(aEnabled)
		Dim iiBall, kDir, kForce, kBaseDir, ballsEjected
		ballsEjected = 0

		If aEnabled Then
			For iiBall = 0 to (mMaxBallsPerKick - 1)
				kDir = (mExitDir + (Rnd - 0.5) * mDirVar)
				kForce = vpMax(1, mExitForce + (Rnd - 0.5) * mForceVar * (0.8 * iiBall)) ' Dampen force a bit on subsequent balls.

				If mSlot(0) > 0 Then
					' Remove ball from this slot.
					mSlot(0) = mSlot(0) - 1
					If isObject(mExitKicker) Then
						vpmTimer.AddTimer ballsEjected*200, "vpmCreateBall(" & mExitKicker.Name & ").Kick " &_
							CInt(kDir) & "," & Replace(kForce,",",".") & ", 0 '"
					End If

					ballsEjected = ballsEjected + 1
				End If
			Next

			If ballsEjected > 0 Then
				If isObject(mExitKicker) Then
					If VP8sound then
						PlaySound mSounds.Item("exitBall")
					ElseIf VP9sound then
						PlaySound mSounds.Item("exitBall"), 1, 1, CoreAudioPan(mExitKicker.x), 0
					Else
						PlaySound mSounds.Item("exitBall"), 1, 1, CoreAudioPan(mExitKicker.x), 0, 0, False, False, CoreAudioFade(mExitKicker.y)
					End If
				Else
					PlaySound mSounds.Item("exitBall")
				End If
				UpdateTroughSwitches
				NeedUpdate = True
			Else
				If isObject(mExitKicker) Then
					If VP8sound then
						PlaySound mSounds.Item("exit")
					ElseIf VP9sound then
						PlaySound mSounds.Item("exit"), 1, 1, CoreAudioPan(mExitKicker.x), 0
					Else
						PlaySound mSounds.Item("exit"), 1, 1, CoreAudioPan(mExitKicker.x), 0, 0, False, False, CoreAudioFade(mExitKicker.y)
					End If
				Else
					PlaySound mSounds.Item("exit")
				End If
			End If
		End If
	End Sub
	Public Sub ExitSol_On : solOut(true) : End Sub
End Class

'--------------------
'	  Saucer
'--------------------
Class cvpmSaucer
	' Takes over for older cvpmBallStack in "saucer mode".

	Private mSw, mKicker, mExternalKicker
	Private mDir(1), mForce(1), mZForce(1), mDirVar, mForceVar
	Private mSounds

	Private Sub Class_Initialize
		mSw = 0

		mKicker = 0
		mExternalKicker = 0
		mDir(0) = 0 : mForce(0) = 1 : mZForce(0) = 0
		mDir(1) = 0 : mForce(1) = 1 : mZForce(1) = 0
		mDirVar = 0 : mForceVar = 0

		Set mSounds = New cvpmDictionary
	End Sub

	' Initialization

	Public Sub InitKicker(aKicker, aSw, aDir, aForce, aZForce)
		If TypeName(aKicker) <> "Kicker" Then
			Err.Raise 17, "cvpmSaucer.InitKicker: Cannot use object of type '" & TypeName(aKicker) & "'."
		End If

		Set mKicker = aKicker
		mSw = aSw
		mDir(0) = aDir
		mForce(0) = vpMax(1, aForce)
		mZForce(0) = vpMax(0, aZForce)
	End Sub

	Public Sub InitExitVariance(aDirVar, aForceVar)
		mDirVar = aDirVar
		mForceVar = aForceVar
	End Sub

	' Alternate kick params (simulates a saucer with two kickers)
	Public Sub InitAltKick(aDir, aForce, aZForce)
		mDir(1) = aDir
		mForce(1) = vpMax(1, aForce)
		mZForce(1) = vpMax(0, aZForce)
	End Sub

	' Setup sounds
	Public Sub InitSounds(addSound, exitSoundEmpty, exitSoundBall)
		mSounds.Item("add") = addSound
		mSounds.Item("exit") = exitSoundEmpty
		mSounds.Item("exitBall") = exitSoundBall
	End Sub

	' Generate hit event for the kicker(s) associated with this saucer.
	' Accepts a single kicker, an Array, or a Collection.
	Public Sub CreateEvents(aName, aKicker)
		Dim obj, tmp
		If Not vpmCheckEvent(aName, Me) Then Exit Sub
		vpmSetArray tmp, aKicker
		For Each obj In tmp
			If isObject(obj) Then
				vpmBuildEvent obj, "Hit", aName & ".AddBall Me"
			Else
				vpmBuildEvent mKicker, "Hit", aName & ".AddBall Me"
			End If
		Next
	End Sub

	' Ball management

	Public Sub AddBall(aKicker)
		Dim mSwcopy
		If isObject(aKicker) Then
			If aKicker Is mKicker Then
				mKicker.Enabled = False
				mExternalKicker = 0
			Else
				aKicker.Enabled = False
				Set mExternalKicker = aKicker
			End If
		Else
			mKicker.Enabled = False
			mExternalKicker = 0
		End If

		If mSw Then
			mSwcopy = mSw
			Controller.Switch(mSwcopy) = True
		End If
		If isObject(mKicker) Then
			If VP8sound then
				PlaySound mSounds.Item("add")
			ElseIf VP9sound then
				PlaySound mSounds.Item("add"), 1, 1, CoreAudioPan(mKicker.x), 0
			Else
				PlaySound mSounds.Item("add"), 1, 1, CoreAudioPan(mKicker.x), 0, 0, False, False, CoreAudioFade(mKicker.y)
			End If
		Else
			PlaySound mSounds.Item("add")
		End If
	End Sub

	Public Property Get HasBall
		HasBall = False
		If IsObject(mExternalKicker) Then
			HasBall = True
		Else
			HasBall = Not mKicker.Enabled
		End If
	End Property

	' SolCallback solNo, "mySaucer.solOut"
	Public Sub solOut(aEnabled) : If aEnabled Then KickOut 0 : End If : End Sub
	Public Sub ExitSol_On : KickOut 0 : End Sub

	' SolCallback solNo, "mySaucer.solOutAlt"
	Public Sub solOutAlt(aEnabled) : If aEnabled Then KickOut 1 : End If : End Sub
	Public Sub ExitAltSol_On : KickOut 1 : End Sub

	Private Sub KickOut(kickIndex)
		Dim mSwcopy
		If HasBall Then
			Dim kDir, kForce, kZForce

			kDir = mDir(kickIndex) + (Rnd - 0.5)*mDirVar
			kForce = vpMax(1, mForce(kickIndex) + (Rnd - 0.5)*mForceVar)
			kZForce = mZForce(kickIndex)

			If IsObject(mExternalKicker) Then
				' Transfer ball to internal kicker and remove relationship
				vpmCreateBall mKicker
				mExternalKicker.DestroyBall
				mExternalKicker.Enabled = True
			Else
				mKicker.Enabled = True
			End If

			mKicker.Kick kDir, kForce, kZForce
			If mSw Then
				mSwcopy = mSw
				Controller.Switch(mSwcopy) = False
			End If
			If isObject(mKicker) Then
				If VP8sound then
					PlaySound mSounds.Item("exitBall")
				ElseIf VP9sound then
					PlaySound mSounds.Item("exitBall"), 1, 1, CoreAudioPan(mKicker.x), 0
				Else
					PlaySound mSounds.Item("exitBall"), 1, 1, CoreAudioPan(mKicker.x), 0, 0, False, False, CoreAudioFade(mKicker.y)
				End If
			Else
				PlaySound mSounds.Item("exitBall")
			End If
		Else
			If isObject(mKicker) Then
				If VP8sound then
					PlaySound mSounds.Item("exit")
				ElseIf VP9sound then
					PlaySound mSounds.Item("exit"), 1, 1, CoreAudioPan(mKicker.x), 0
				Else
					PlaySound mSounds.Item("exit"), 1, 1, CoreAudioPan(mKicker.x), 0, 0, False, False, CoreAudioFade(mKicker.y)
				End If
			Else
				PlaySound mSounds.Item("exit")
			End If
		End If
	End Sub
End Class

'--------------------
'	  BallStack (DEPRECATED/LEGACY)
'	  Known issues:
'	  - Adding more balls than conMaxBalls will crash the script.
'	  - If there are more balls in trough than are ever used in a game (e.g. Bride of Pinbot),
'		one or more trough switches will be permanently stuck down and may result in a ROM test report.
'	  - Trough does not handle stacking balls at exit.
'	  - Saucer mode is essentially a hack on top of the trough logic.
'--------------------
Class cvpmBallStack
	Private mSw(), mEntrySw, mBalls, mBallIn, mBallPos(), mSaucer, mBallsMoving
	Private mInitKicker, mExitKicker, mExitDir, mExitForce
	Private mExitDir2, mExitForce2
	Private mEntrySnd, mEntrySndBall, mExitSnd, mExitSndBall, mAddSnd
	Private mSoundKicker
	Public KickZ, KickBalls, KickForceVar, KickAngleVar

	Private Sub Class_Initialize
		ReDim mSw(conStackSw), mBallPos(conMaxBalls)
		mBallIn = 0 : mBalls = 0 : mExitKicker = 0 : mInitKicker = 0 : mBallsMoving = False
		KickBalls = 1 : mSaucer = False : mExitDir = 0 : mExitForce = 0
		mExitDir2 = 0 : mExitForce2 = 0 : KickZ = 0 : KickForceVar = 0 : KickAngleVar = 0
		mAddSnd = 0 : mEntrySnd = 0 : mEntrySndBall = 0 : mExitSnd = 0 : mExitSndBall = 0 : mSoundKicker = 0
		vpmTimer.AddResetObj Me
	End Sub

	Private Property Let NeedUpdate(aEnabled) : vpmTimer.EnableUpdate Me, False, aEnabled : End Property

	Private Function SetSw(aNo, aStatus)
		Dim mSwcopy
		SetSw = False
		If HasSw(aNo) Then
			mSwcopy = mSw(aNo)
			Controller.Switch(mSwcopy) = aStatus
			SetSw = True
		End If
	End Function

	Private Function HasSw(aNo)
		HasSw = False : If aNo <= conStackSw Then If mSw(aNo) Then HasSw = True
	End Function

	Public Sub Reset
		Dim mSwcopy
		Dim ii : If mBalls Then For ii = 1 to mBalls : SetSw mBallPos(ii), True : Next
		If mEntrySw And mBallIn > 0 Then
			mSwcopy = mEntrySw
			Controller.Switch(mSwcopy) = True
		End If
	End Sub

	Public Sub Update
		Dim BallQue, ii, mSwcopy
		NeedUpdate = False : BallQue = 1
		For ii = 1 To mBalls
			If mBallpos(ii) > BallQue Then ' next slot available
				NeedUpdate = True
				If HasSw(mBallPos(ii)) Then ' has switch
					mSwcopy = mSw(mBallPos(ii))
					If Controller.Switch(mSwcopy) Then
						SetSw mBallPos(ii), False
					Else
						mBallPos(ii) = mBallPos(ii) - 1
						SetSw mBallPos(ii), True
					End If
				Else ' no switch. Move ball to first switch or occupied slot
					Do
						mBallPos(ii) = mBallPos(ii) - 1
					Loop Until SetSw(mBallPos(ii), True) Or mBallPos(ii) = BallQue
				End If
			End If
			BallQue = mBallPos(ii) + 1
		Next
	End Sub

	Public Sub AddBall(aKicker)
		Dim mSwcopy
		If isObject(aKicker) Then
			If mSaucer Then
				If aKicker Is mExitKicker Then
					mExitKicker.Enabled = False : mInitKicker = 0
				Else
					aKicker.Enabled = False : Set mInitKicker = aKicker
				End If
			Else
				aKicker.DestroyBall
			End If
		ElseIf mSaucer Then
			mExitKicker.Enabled = False : mInitKicker = 0
		End If
		If mEntrySw Then
			mSwcopy = mEntrySw
			Controller.Switch(mSwcopy) = True : mBallIn = mBallIn + 1
		Else
			mBalls = mBalls + 1 : mBallPos(mBalls) = conStackSw + 1 : NeedUpdate = True
		End If
		If isObject(mSoundKicker) Then
			If VP8sound then
				PlaySound mAddSnd
			ElseIf VP9sound then
				PlaySound mAddSnd, 1, 1, CoreAudioPan(mSoundKicker.x), 0
			Else
				PlaySound mAddSnd, 1, 1, CoreAudioPan(mSoundKicker.x), 0, 0, False, False, CoreAudioFade(mSoundKicker.y)
			End If
		Else
			PlaySound mAddSnd
		End If
	End Sub

	' A bug in the script engine forces the "End If" at the end
	Public Sub SolIn(aEnabled)	   : If aEnabled Then KickIn		: End If : End Sub
	Public Sub SolOut(aEnabled)	   : If aEnabled Then KickOut False : End If : End Sub
	Public Sub SolOutAlt(aEnabled) : If aEnabled Then KickOut True	: End If : End Sub
	Public Sub EntrySol_On	 : KickIn		 : End Sub
	Public Sub ExitSol_On	 : KickOut False : End Sub
	Public Sub ExitAltSol_On : KickOut True	 : End Sub

	Private Sub KickIn
		Dim mSwcopy
		If mBallIn Then PlaySound mEntrySndBall Else PlaySound mEntrySnd : Exit Sub
		mBalls = mBalls + 1 : mBallIn = mBallIn - 1 : mBallPos(mBalls) = conStackSw + 1 : NeedUpdate = True
		If mEntrySw And mBallIn = 0 Then
			mSwcopy = mEntrySw
			Controller.Switch(mSwcopy) = False
		End If
	End Sub

	Private Sub KickOut(aAltSol)
		Dim ii,jj, kForce, kDir, kBaseDir
		If mBalls Then
			If isObject(mSoundKicker) Then
				If VP8sound then
					PlaySound mExitSndBall
				ElseIf VP9sound then
					PlaySound mExitSndBall, 1, 1, CoreAudioPan(mSoundKicker.x), 0
				Else
					PlaySound mExitSndBall, 1, 1, CoreAudioPan(mSoundKicker.x), 0, 0, False, False, CoreAudioFade(mSoundKicker.y)
				End If
			Else
				PlaySound mExitSndBall
			End If
		Else
			If isObject(mSoundKicker) Then
				If VP8sound then
					PlaySound mExitSnd
				ElseIf VP9sound then
					PlaySound mExitSnd, 1, 1, CoreAudioPan(mSoundKicker.x), 0
				Else
					PlaySound mExitSnd, 1, 1, CoreAudioPan(mSoundKicker.x), 0, 0, False, False, CoreAudioFade(mSoundKicker.y)
				End If
			Else
				PlaySound mExitSnd
			End If
			Exit Sub
		End If
		If aAltSol Then kForce = mExitForce2 : kBaseDir = mExitDir2 Else kForce = mExitForce : kBaseDir = mExitDir
		kForce = kForce + (Rnd - 0.5)*KickForceVar
		If mSaucer Then
			SetSw 1, False : mBalls = 0 : kDir = kBaseDir + (Rnd - 0.5)*KickAngleVar
			If isObject(mInitKicker) Then
				vpmCreateBall mExitKicker : mInitKicker.Destroyball : mInitKicker.Enabled = True
			Else
				mExitKicker.Enabled = True
			End If
			mExitKicker.Kick kDir, kForce, KickZ
		Else
			For ii = 1 To kickballs
				If mBalls = 0 Or mBallPos(1) <> ii Then Exit For ' No more balls
				For jj = 2 To mBalls ' Move balls in array
					mBallPos(jj-1) = mBallPos(jj)
				Next
				mBallPos(mBalls) = 0 : mBalls = mBalls - 1 : NeedUpdate = True
				SetSw ii, False
				If isObject(mExitKicker) Then
					If kForce < 1 Then kForce = 1
					kDir = kBaseDir + (Rnd - 0.5)*KickAngleVar
					vpmTimer.AddTimer (ii-1)*200, "vpmCreateBall(" & mExitKicker.Name & ").Kick " &_
					  CInt(kDir) & "," & Replace(kForce,",",".") & "," & Replace(KickZ,",",".") & " '"
				End If
				kForce = kForce * 0.8
			Next
		End If
	End Sub

	Public Sub InitSaucer(aKicker, aSw, aDir, aPower)
		InitKick aKicker, aDir, aPower : mSaucer = True
		If aSw Then mSw(1) = aSw Else mSw(1) = aKicker.TimerInterval
	End Sub

	Public Sub InitNoTrough(aKicker, aSw, aDir, aPower)
		InitKick aKicker, aDir, aPower : Balls = 1
		If aSw Then mSw(1) = aSw Else mSw(1) = aKicker.TimerInterval
		If Not IsObject(vpmTrough) Then Set vpmTrough = Me
	End Sub

	Public Sub InitSw(aEntry, aSw1, aSw2, aSw3, aSw4, aSw5, aSw6, aSw7)
		mEntrySw = aEntry : mSw(1) = aSw1 : mSw(2) = aSw2 : mSw(3) = aSw3 : mSw(4) = aSw4
		mSw(5) = aSw5 : mSw(6) = aSw6 : mSw(7) = aSw7 : mSw(8) = 0
		If Not IsObject(vpmTrough) Then Set vpmTrough = Me
	End Sub

	Public Sub InitSw8(aEntry, aSw1, aSw2, aSw3, aSw4, aSw5, aSw6, aSw7, aSw8)
		mEntrySw = aEntry : mSw(1) = aSw1 : mSw(2) = aSw2 : mSw(3) = aSw3 : mSw(4) = aSw4
		mSw(5) = aSw5 : mSw(6) = aSw6 : mSw(7) = aSw7 : mSw(8) = aSw8
		If Not IsObject(vpmTrough) Then Set vpmTrough = Me
	End Sub

	Public Sub InitKick(aKicker, aDir, aForce)
		Set mExitKicker = aKicker : mExitDir = aDir : mExitForce = aForce
		Set mSoundKicker = aKicker
	End Sub

	Public Sub CreateEvents(aName, aKicker)
		Dim obj, tmp
		If Not vpmCheckEvent(aName, Me) Then Exit Sub
		vpmSetArray tmp, aKicker
		For Each obj In tmp
			If isObject(obj) Then
				vpmBuildEvent obj, "Hit", aName & ".AddBall Me"
			Else
				vpmBuildEvent mExitKicker, "Hit", aName & ".AddBall Me"
			End If
		Next
	End Sub

	Public Property Let IsTrough(aIsTrough)
		If aIsTrough Then
			Set vpmTrough = Me
		ElseIf IsObject(vpmTrough) Then
			If vpmTrough Is Me Then vpmTrough = 0
		End If
	End Property

	Public Property Get IsTrough : IsTrough = vpmTrough Is Me : End Property

	Public Sub InitAltKick(aDir, aForce)
		mExitDir2 = aDir : mExitForce2 = aForce
	End Sub

	Public Sub InitEntrySnd(aBall, aNoBall) : mEntrySndBall = aBall : mEntrySnd = aNoBall : End Sub
	Public Sub InitExitSnd(aBall, aNoBall)	: mExitSndBall = aBall	: mExitSnd = aNoBall  : End Sub
	Public Sub InitAddSnd(aSnd) : mAddSnd = aSnd : End Sub

	Public Property Let Balls(aBalls)
		Dim ii
		For ii = 1 To conStackSw
			SetSw ii, False : mBallPos(ii) = conStackSw + 1
		Next
		If mSaucer And aBalls > 0 And mBalls = 0 Then vpmCreateBall mExitKicker
		mBalls = aBalls : NeedUpdate = True
	End Property

	Public Default Property Get Balls : Balls = mBalls		   : End Property
	Public Property Get BallsPending  : BallsPending = mBallIn : End Property

	' Obsolete stuff
	Public Sub SolEntry(aSnd1, aSnd2, aEnabled)
		If aEnabled Then mEntrySndBall = aSnd1 : mEntrySnd = aSnd2 : KickIn
	End Sub
	Public Sub SolExit(aSnd1, aSnd2, aEnabled)
		If aEnabled Then mExitSndBall = aSnd1 : mExitSnd = aSnd2 : KickOut False
	End Sub
	Public Sub InitProxy(aProxyPos, aSwNo) : End Sub
	Public TempBallColour, TempBallImage, BallColour
	Public Property Let BallImage(aImage) : vpmBallImage = aImage : End Property
End Class

'--------------------
'		Nudge
'--------------------
class cvpmNudge
	Private mCount, mSensitivity, mNudgeTimer, mSlingBump, mForce
	Public TiltSwitch

	Private Sub Class_Initialize
		mCount = 0 : TiltSwitch = 0 : mSensitivity = 5 : vpmTimer.AddResetObj Me
	End sub

	Private Property Let NeedUpdate(aEnabled) : vpmTimer.EnableUpdate Me, False, aEnabled : End Property

	Public Property Let TiltObj(aSlingBump)
		Dim ii
		ReDim mForce(vpmSetArray(mSlingBump, aSlingBump))
		For ii = 0 To UBound(mForce)
			If TypeName(mSlingBump(ii)) = "Bumper" Then mForce(ii) = mSlingBump(ii).Threshold
			If vpmVPVer >= 9000 and TypeName(mSlingBump(ii)) = "Wall" Then mForce(ii) = mSlingBump(ii).SlingshotThreshold
		Next
	End Property

	Public Property Let Sensitivity(aSens) : mSensitivity = (10-aSens)+1 : End property

	Public Sub DoNudge(ByVal aDir, ByVal aForce)
		'aDir = aDir + (Rnd-0.5)*15*aForce : aForce = (0.6+Rnd*0.8)*aForce
		'Nudge aDir, aForce
		'If TiltSwitch = 0 Then Exit Sub ' If no switch why care
		'mCount = mCount + aForce * 1.2
		'If mCount > mSensitivity + 10 Then mCount = mSensitivity + 10
		'If mCount >= mSensitivity Then vpmTimer.PulseSw TiltSwitch
		'NeedUpdate = True
	End sub

	Public Sub Update
		If mCount > 0 Then
			mNudgeTimer = mNudgeTimer + 1
			If mNudgeTimer > 1000\conTimerPulse Then
				If mCount > mSensitivity+1 Then mCount = mCount - 1 : vpmTimer.PulseSw TiltSwitch
				mCount = mCount - 1 : mNudgeTimer = 0
			End If
		Else
			mCount = 0 : NeedUpdate = False
		End If
	End Sub

	Public Sub Reset : mCount = 0 : End Sub

	Sub NudgeRead_Timer
		Dim X, Y
		NudgeSensorStatus X, Y

		Nudge 0,Y * 5

	End Sub

	Public Sub SolGameOn(aEnabled)
		if IsEmpty(mForce) then exit sub 'prevent errors if vpmNudge.TiltObj isn't set
		Dim obj, ii
		If aEnabled Then
			ii = 0
			For Each obj In mSlingBump
				If TypeName(obj) = "Bumper" Then obj.Threshold = mForce(ii)
				If vpmVPVer >= 9000 and TypeName(obj) = "Wall" Then obj.SlingshotThreshold = mForce(ii)
				ii = ii + 1
			Next
		Else
			For Each obj In mSlingBump
				If TypeName(obj) = "Bumper" Then obj.Threshold = 100
				If vpmVPVer >= 9000 and TypeName(obj) = "Wall" Then obj.SlingshotThreshold = 100
			Next
		End If
	End Sub
End Class

'--------------------
'	 DropTarget
'--------------------
Class cvpmDropTarget
	Private mDropObj, mDropSw(), mDropSnd, mRaiseSnd, mSwAnyUp, mSwAllDn, mAllDn, mLink

	Private Sub Class_Initialize
		mDropSnd = 0 : mRaiseSnd = 0 : mSwAnyUp = 0 : mSwAllDn = 0 : mAllDn = False : mLink = Empty
	End sub

	Private Sub CheckAllDn(ByVal aStatus)
		Dim obj
		If Not IsEmpty(mLink) Then
			If aStatus Then
				For Each obj In mLink : aStatus = aStatus And obj.AllDown : Next
			End If
			For Each obj In mLink: obj.SetAllDn aStatus : Next
		End If
		SetAllDn aStatus
	End Sub

	Public Sub SetAllDn(aStatus)
		Dim mSwcopy
		If mSwAllDn Then
			mSwcopy = mSwAllDn
			Controller.Switch(mSwcopy) = aStatus
		End If
		If mSwAnyUp Then
			mSwcopy = mSwAnyUp
			Controller.Switch(mSwcopy) = Not aStatus
		End If
	End Sub

	Public Sub InitDrop(aWalls, aSw)
		Dim obj, obj2, ii
		' Fill in switch number
		On Error Resume Next : ReDim mDropSw(0)
		If IsArray(aSw) Then
			ReDim mDropSw(UBound(aSw))
			For ii = 0 To UBound(aSw) : mDropSw(ii) = aSw(ii) : Next
		ElseIf aSw = 0 Or Err Then
			On Error Goto 0
			If vpmIsArray(aWalls) Then
				ii = 0 : If IsArray(aWalls) Then ReDim mDropSw(UBound(aWalls)) Else ReDim mDropSw(aWalls.Count-1)
				For Each obj In aWalls
					If vpmIsArray(obj) Then
						For Each obj2 In obj
							If obj2.HasHitEvent Then mDropSw(ii) = obj2.TimerInterval : Exit For
						Next
					Else
						mDropSw(ii) = obj.TimerInterval
					End If
					ii = ii + 1
				Next
			Else
				mDropSw(0) = aWalls.TimerInterval
			End If
		Else
			mDropSw(0) = aSw
		End If
		' Copy walls
		vpmSetArray mDropObj, aWalls
	End Sub

	Public Sub CreateEvents(aName)
		Dim ii, obj1, obj2
		If Not vpmCheckEvent(aName, Me) Then Exit Sub
		ii = 1
		For Each obj1 In mDropObj
			If vpmIsArray(obj1) Then
				For Each obj2 In obj1
					if TypeName(obj2) = "HitTarget" Then 'if object in array is a Target, use .Dropped
						vpmBuildEvent obj2, "Dropped", aName & ".Hit " & ii	'Droptarget_Dropped : DTbank.Hit 1 : End Sub
					else
						If obj2.HasHitEvent Then vpmBuildEvent obj2, "Hit", aName & ".Hit " & ii
					End If
				Next
			Else
				if TypeName(obj1) = "HitTarget" Then 'if object in array is a Target, use .Dropped
					vpmBuildEvent obj1, "Dropped", aName & ".Hit " & ii
				else
					vpmBuildEvent obj1, "Hit", aName & ".Hit " & ii
				End If
			End If
			ii = ii + 1
		Next
	End Sub

	Public Property Let AnyUpSw(aSwAnyUp)
		Dim mSwcopy
		mSwAnyUp = aSwAnyUp
		mSwcopy = mSwAnyUp
		Controller.Switch(mSwcopy) = True
	End Property
	Public Property Let AllDownSw(aSwAllDn) : mSwAllDn = aSwAllDn : End Property
	Public Property Get AllDown : AllDown = mAllDn : End Property
	Public Sub InitSnd(aDrop, aRaise) : mDropSnd = aDrop : mRaiseSnd = aRaise : End Sub
	Public Property Let LinkedTo(aLink)
		If IsArray(aLink) Then mLink = aLink Else mLink = Array(aLink)
	End Property

	Public Sub Hit(aNo)
		Dim ii, mSwcopy

'		vpmSolWall mDropObj(aNo-1), mDropSnd, True

		If TypeName(mDropObj(aNo-1)) = "HitTarget" Then
			If VP8sound then
				PlaySound mDropSnd
			ElseIf VP9sound then
				PlaySound mDropSnd, 1, 1, CoreAudioPan(mDropObj(aNo-1).x), 0
			Else
				PlaySound mDropSnd, 1, 1, CoreAudioPan(mDropObj(aNo-1).x), 0, 0, False, False, CoreAudioFade(mDropObj(aNo-1).y)
			End If
		Else
			PlaySound mDropSnd
		End If
		vpmSolWall mDropObj(aNo-1), False, True

		mSwcopy = mDropSw(aNo-1)
		Controller.Switch(mSwcopy) = True
		For Each ii In mDropSw
			mSwcopy = ii
			If Not Controller.Switch(mSwcopy) Then Exit Sub
		Next
		mAllDn = True : CheckAllDn True
	End Sub

	Public Sub SolHit(aNo, aEnabled) : If aEnabled Then Hit aNo : End If : End Sub

	Public Sub SolUnhit(aNo, aEnabled)
		Dim mSwcopy
		Dim ii : If Not aEnabled Then Exit Sub
		If TypeName(mDropObj(aNo-1)) = "HitTarget" Then
			If VP8sound then
				PlaySound mRaiseSnd
			ElseIf VP9sound then
				PlaySound mRaiseSnd, 1, 1, CoreAudioPan(mDropObj(aNo-1).x), 0
			Else
				PlaySound mRaiseSnd, 1, 1, CoreAudioPan(mDropObj(aNo-1).x), 0, 0, False, False, CoreAudioFade(mDropObj(aNo-1).y): vpmSolWall mDropObj(aNo-1), False, False
			End If
		Else
			PlaySound mRaiseSnd
		End If

		mSwcopy = mDropSw(aNo-1)
		Controller.Switch(mSwcopy) = False
		mAllDn = False : CheckAllDn False
	End Sub

	Public Sub SolDropDown(aEnabled)
		Dim mSwcopy
		Dim ii : If Not aEnabled Then Exit Sub
		If TypeName(mDropObj(0)) = "HitTarget" Then
			If VP8sound then
				PlaySound mDropSnd
			ElseIf VP9sound then
				PlaySound mDropSnd, 1, 1, CoreAudioPan(mDropObj(0).x), 0
			Else
				PlaySound mDropSnd, 1, 1, CoreAudioPan(mDropObj(0).x), 0, 0, False, False, CoreAudioFade(mDropObj(0).y)
			End If
		Else
			PlaySound mDropSnd
		End If

		For Each ii In mDropObj : vpmSolWall ii, False, True : Next
		For Each ii In mDropSw	: mSwcopy = ii : Controller.Switch(mSwcopy) = True : Next
		mAllDn = True : CheckAllDn True
	End Sub

	Public Sub SolDropUp(aEnabled)
		Dim mSwcopy
		Dim ii : If Not aEnabled Then Exit Sub
		If TypeName(mDropObj(0)) = "HitTarget" Then
			If VP8sound then
				PlaySound mRaiseSnd
			ElseIf VP9sound then
				PlaySound mRaiseSnd, 1, 1, CoreAudioPan(mDropObj(0).x), 0
			Else
				PlaySound mRaiseSnd, 1, 1, CoreAudioPan(mDropObj(0).x), 0, 0, False, False, CoreAudioFade(mDropObj(0).y)
			End If
		Else
			PlaySound mRaiseSnd
		End If

		For Each ii In mDropObj : vpmSolWall ii, False, False : Next
		For Each ii In mDropSw	: mSwcopy = ii : Controller.Switch(mSwcopy) = False : Next
		mAllDn = False : CheckAllDn False
	End Sub

	Public Sub DropSol_On : SolDropUp True : End Sub
End Class

'--------------------
'		Magnet
'--------------------
Class cvpmMagnet
	Private mEnabled, mBalls, mTrigger
	Public X, Y, Strength, Size, GrabCenter, Solenoid

	Private Sub Class_Initialize
		Size = 1 : Strength = 0 : Solenoid = 0 : mEnabled = False
		Set mBalls = New cvpmDictionary
	End Sub

	Private Property Let NeedUpdate(aEnabled) : vpmTimer.EnableUpdate Me, True, aEnabled : End Property

	Public Sub InitMagnet(aTrigger, aStrength)
		Dim tmp
		If vpmIsArray(aTrigger) Then Set tmp = aTrigger(0) Else Set tmp = aTrigger
		X = tmp.X : Y = tmp.Y : Size = tmp.Radius : vpmTimer.InitTimer tmp, True
		If IsArray(aTrigger) Then mTrigger = aTrigger Else Set mTrigger = aTrigger
		Strength = aStrength : GrabCenter = aStrength > 14
	End Sub

	Public Sub CreateEvents(aName)
		If vpmCheckEvent(aName, Me) Then
			vpmBuildEvent mTrigger, "Hit", aName & ".AddBall ActiveBall"
			vpmBuildEvent mTrigger, "UnHit", aName & ".RemoveBall ActiveBall"
		End If
	End Sub

	Public Property Let MagnetOn(aEnabled) : mEnabled = aEnabled : End Property
	Public Property Get MagnetOn
		If Solenoid > 0 Then MagnetOn = Controller.Solenoid(Solenoid) Else MagnetOn = mEnabled
	End Property

	Public Sub AddBall(aBall)
		With mBalls
			If .Exists(aBall) Then .Item(aBall) = .Item(aBall) + 1 Else .Add aBall, 1 : NeedUpdate = True
		End With
	End Sub

	Public Sub RemoveBall(aBall)
		With mBalls
			If .Exists(aBall) Then .Item(aBall) = .Item(aBall) - 1 : If .Item(aBall) <= 0 Then .Remove aBall
			NeedUpdate = (.Count > 0)
		End With
	End Sub

	Public Property Get Balls : Balls = mBalls.Keys : End Property

	Public Sub Update
		Dim obj
		If MagnetOn Then
			On Error Resume Next
			For Each obj In mBalls.Keys
				If obj.X < 0 Or Err Then mBalls.Remove obj Else AttractBall obj
			Next
			On Error Goto 0
		End If
	End Sub

	Public Sub AttractBall(aBall)
		Dim dX, dY, dist, force, ratio
		dX = aBall.X - X : dY = aBall.Y - Y : dist = Sqr(dX*dX + dY*dY)
		If dist > Size Or dist < 1 Then Exit Sub 'Just to be safe
		If GrabCenter And dist < 20 Then
			aBall.VelX = 0 : aBall.VelY = 0 : aBall.X = X : aBall.Y = Y
		Else
			ratio = dist / (1.5*Size)
			force = Strength * exp(-0.2/ratio)/(ratio*ratio*56) * 1.5
			aBall.VelX = (aBall.VelX - dX * force / dist) * 0.985
			aBall.VelY = (aBall.VelY - dY * force / dist) * 0.985
		End if
	End Sub
	' obsolete
	Public Property Let Range(aSize) : Size = aSize : End Property
	Public Property Get Range		 : Range = Size : End Property
End Class

'--------------------
'	  Turntable
'--------------------
Class cvpmTurntable
	Private mX, mY, mSize, mTrigger, mBalls, mSpinUp, mSpinDown
	Private mMotorOn, mSpinCW
	Private mMaxSpeed, mTargetSpeed, mCurrentAccel
	Public Speed

	Private Sub Class_Initialize
		Set mBalls = New cvpmDictionary
		mMotorOn = False : mSpinCW = True : Speed = 0 : mSpinUp = 10 : mSpinDown = 4
		AdjustTargets
	End Sub

	Private Property Let NeedUpdate(aEnabled) : vpmTimer.EnableUpdate Me, True, aEnabled : End Property

	Public Sub InitTurntable(aTrigger, aMaxSpeed)
		mX = aTrigger.X : mY = aTrigger.Y : mSize = aTrigger.Radius : vpmTimer.InitTimer aTrigger, True
		mMaxSpeed = aMaxSpeed : Set mTrigger = aTrigger
		AdjustTargets
	End Sub

	Public Sub CreateEvents(aName)
		If vpmCheckEvent(aName, Me) Then
			vpmBuildEvent mTrigger, "Hit", aName & ".AddBall ActiveBall"
			vpmBuildEvent mTrigger, "UnHit", aName & ".RemoveBall ActiveBall"
		End If
	End Sub

	Public Sub SolMotorState(aCW, aMotorOn)
		mSpinCW = aCW
		mMotorOn = aMotorOn
		AdjustTargets
	End Sub

	Private Sub AdjustTargets
		If mMotorOn Then
			mTargetSpeed = MaxSpeed
			mCurrentAccel = SpinUp
			If Not mSpinCW Then mTargetSpeed = -MaxSpeed
		Else
			mTargetSpeed = 0
			mCurrentAccel = SpinDown
		End If

		NeedUpdate = mBalls.Count Or SpinUp Or SpinDown
	End Sub

	Public Property Let MaxSpeed(newSpeed) : mMaxSpeed = newSpeed : AdjustTargets : End Property
	Public Property Let SpinUp(newRate) : mSpinUp = newRate : AdjustTargets : End Property
	Public Property Let SpinDown(newRate) : mSpinDown = newRate : AdjustTargets : End Property

	Public Property Get MaxSpeed : MaxSpeed = mMaxSpeed : End Property
	Public Property Get SpinUp : SpinUp = mSpinup : End Property
	Public Property Get SpinDown : SpinDown = mSpinDown : End Property

	Public Property Let MotorOn(aEnabled) : SolMotorState mSpinCW, aEnabled : End Property
	Public Property Let SpinCW(aCW) : SolMotorState aCW, mMotorOn : End Property

	Public Property Get MotorOn : MotorOn = mMotorOn : End Property
	Public Property Get SpinCW : SpinCW = mSpinCW : End Property

	Public Sub AddBall(aBall)
		On Error Resume Next : mBalls.Add aBall,0 : NeedUpdate = True
	End Sub
	Public Sub RemoveBall(aBall)
		On Error Resume Next
		mBalls.Remove aBall : NeedUpdate = mBalls.Count Or SpinUp Or SpinDown
	End Sub
	Public Property Get Balls : Balls = mBalls.Keys : End Property

	Public Sub Update
		If Speed > mTargetSpeed Then
			Speed = Speed - mCurrentAccel/100
			If Speed < mTargetSpeed Then Speed = mTargetSpeed : NeedUpdate = mBalls.Count
		ElseIf Speed < mTargetSpeed Then
			Speed = Speed + mCurrentAccel/100
			If Speed > mTargetSpeed Then Speed = mTargetSpeed : NeedUpdate = mBalls.Count
		End If

		If Speed Then
			Dim obj
			On Error Resume Next
			For Each obj In mBalls.Keys
				If obj.X < 0 Or Err Then mBalls.Remove obj Else AffectBall obj
			Next
			On Error Goto 0
		End If
	End Sub

	Public Sub AffectBall(aBall)
		Dim dX, dY, dist
		dX = aBall.X - mX : dY = aBall.Y - mY : dist = Sqr(dX*dX + dY*dY)
		If dist > mSize Or dist < 1 Or Speed = 0 Then Exit Sub
		aBall.VelX = aBall.VelX - (dY * Speed / 8000)
		aBall.VelY = aBall.VelY + (dX * Speed / 8000)
	End Sub
End Class

'--------------------
'	  Mech
'--------------------
Const vpmMechLinear	   = &H00
Const vpmMechNonLinear = &H01
Const vpmMechCircle	   = &H00
Const vpmMechStopEnd   = &H02
Const vpmMechReverse   = &H04
Const vpmMechOneSol	   = &H00
Const vpmMechOneDirSol = &H10
Const vpmMechTwoDirSol = &H20
Const vpmMechStepSol   = &H40
Const vpmFourStepSol   = &H60
Const vpmMechSlow	   = &H00
Const vpmMechFast	   = &H80
Const vpmMechStepSw	   = &H00
Const vpmMechLengthSw  = &H100

Class cvpmMech
	Public Sol1, Sol2, MType, Length, Steps, Acc, Ret
	Private mMechNo, mNextSw, mSw(), mLastPos, mLastSpeed, mCallback

	Private Sub Class_Initialize
		ReDim mSw(10)
		gNextMechNo = gNextMechNo + 1 : mMechNo = gNextMechNo : mNextSw = 0 : mLastPos = 0 : mLastSpeed = 0
		MType = 0 : Length = 0 : Steps = 0 : Acc = 0 : Ret = 0 : vpmTimer.addResetObj Me
	End Sub

	Public Sub AddSw(aSwNo, aStart, aEnd)
		mSw(mNextSw) = Array(aSwNo, aStart, aEnd, 0)
		mNextSw = mNextSw + 1
	End Sub

	Public Sub AddPulseSwNew(aSwNo, aInterval, aStart, aEnd)
		If Controller.Version >= "01200000" Then
			mSw(mNextSw) = Array(aSwNo, aStart, aEnd, aInterval)
		Else
			mSw(mNextSw) = Array(aSwNo, -aInterval, aEnd - aStart + 1, 0)
		End If
		mNextSw = mNextSw + 1
	End Sub

	Public Sub Start
		Dim sw, ii
		With Controller
			.Mech(1) = Sol1 : .Mech(2) = Sol2 : .Mech(3) = Length
			.Mech(4) = Steps : .Mech(5) = MType : .Mech(6) = Acc : .Mech(7) = Ret
			ii = 10
			For Each sw In mSw
				If IsArray(sw) Then
					.Mech(ii) = sw(0) : .Mech(ii+1) = sw(1)
					.Mech(ii+2) = sw(2) : .Mech(ii+3) = sw(3)
					ii = ii + 10
				End If
			Next
			.Mech(0) = mMechNo
		End With
		If IsObject(mCallback) Then mCallBack 0, 0, 0 : mLastPos = 0 : vpmTimer.EnableUpdate Me, False, True
	End Sub

	Public Property Get Position : Position = Controller.GetMech(mMechNo) : End Property
	Public Property Get Speed	 : Speed = Controller.GetMech(-mMechNo)	  : End Property
	Public Property Let Callback(aCallBack) : Set mCallback = aCallBack : End Property

	Public Sub Update
		Dim currPos, speed
		currPos = Controller.GetMech(mMechNo)
		speed = Controller.GetMech(-mMechNo)
		If currPos < 0 Or (mLastPos = currPos And mLastSpeed = speed) Then Exit Sub
		mCallBack currPos, speed, mLastPos : mLastPos = currPos : mLastSpeed = speed
	End Sub

	Public Sub Reset : Start : End Sub
	' Obsolete
	Public Sub AddPulseSw(aSwNo, aInterval, aLength) : AddSw aSwNo, -aInterval, aLength : End Sub
End Class

'--------------------
'	Captive Ball
'--------------------
Class cvpmCaptiveBall
	Private mBallKicked, mBallDir, mBallCos, mBallSin, mTrigHit
	Private mTrig, mWall, mKickers, mVelX, mVelY, mKickNo
	Public ForceTrans, MinForce, RestSwitch, NailedBalls

	Private Sub Class_Initialize
		mBallKicked = False : ForceTrans = 0.5 : mTrigHit = False : MinForce = 3 : NailedBalls = 0
		vpmTimer.addResetObj Me
	End Sub

	Public Sub InitCaptive(aTrig, aWall, aKickers, aBallDir)
		Set mTrig = aTrig : Set mWall = aWall
		mKickNo = vpmSetArray(mKickers, aKickers)
		mBallDir = aBallDir : mBallCos = Cos(aBallDir * (3.14159265358979/180)) : mBallSin = Sin(aBallDir * (3.14159265358979/180))
	End Sub

	Public Sub Start
		Dim mSwcopy
		vpmCreateBall mKickers(mKickNo + (mKickNo <> NailedBalls))
		If RestSwitch Then
			mSwcopy = RestSwitch
			Controller.Switch(mSwcopy) = True
		End If
	End Sub

	Public Sub TrigHit(aBall)
		mTrigHit = IsObject(aBall) : If mTrigHit Then mVelX = aBall.VelX : mVelY = aBall.VelY
	End Sub

	Public Sub Reset
		Dim mSwcopy
		If RestSwitch Then
			mSwcopy = RestSwitch
			Controller.Switch(mSwcopy) = True
		End If
	End Sub

	Public Sub BallHit(aBall)
		Dim dX, dY, force, mSwcopy
		If mBallKicked Then Exit Sub ' Ball is not here
		If mTrigHit Then mTrigHit = False Else mVelX = aBall.VelX : mVelY = aBall.VelY
		dX = aBall.X - mKickers(0).X : dY = aBall.Y - mKickers(0).Y
		force = -ForceTrans * (dY * mVelY + dX * mVelX) * (dY * mBallCos + dX * mBallSin) / (dX*dX + dY*dY)
		If force < 1 Then Exit Sub
		If force < MinForce Then force = MinForce
		If mKickNo <> NailedBalls Then
			vpmCreateBall mKickers(mKickNo)
			mKickers(mKickNo-1).DestroyBall
		End If
		mKickers(mKickNo).Kick mBallDir, force : mBallKicked = True
		If RestSwitch Then
			mSwcopy = RestSwitch
			Controller.Switch(mSwcopy) = False
		End If
	End Sub

	Public Sub BallReturn(aKicker)
		Dim mSwcopy
		If mKickNo <> NailedBalls Then vpmCreateBall mKickers(mKickNo-1) : aKicker.DestroyBall
		mBallKicked = False
		If RestSwitch Then
			mSwcopy = RestSwitch
			Controller.Switch(mSwcopy) = True
		End If
	End Sub

	Public Sub CreateEvents(aName)
		If vpmCheckEvent(aName, Me) Then
			If Not mTrig Is Nothing Then
				vpmBuildEvent mTrig, "Hit", aName & ".TrigHit ActiveBall"
				vpmBuildEvent mTrig, "UnHit", aName & ".TrigHit 0"
			End If
			vpmBuildEvent mWall, "Hit", aName & ".BallHit ActiveBall"
			vpmBuildEvent mKickers(mKickNo), "Hit", aName & ".BallReturn Me"
		End If
	End Sub
	' Obsolete
	Public BallImage, BallColour
End Class

'--------------------
'	Visible Locks
'--------------------
Class cvpmVLock
	Private mTrig, mKick, mSw(), mSize, mBalls, mGateOpen, mRealForce, mBallSnd, mNoBallSnd
	Public ExitDir, ExitForce, KickForceVar

	Private Sub Class_Initialize
		mBalls = 0 : ExitDir = 0 : ExitForce = 0 : KickForceVar = 0 : mGateOpen = False
		vpmTimer.addResetObj Me
	End Sub

	Public Sub InitVLock(aTrig, aKick, aSw)
		Dim ii
		mSize = vpmSetArray(mTrig, aTrig)
		If vpmSetArray(mKick, aKick) <> mSize Then MsgBox "cvpmVLock: Unmatched kick+trig" : Exit Sub
		On Error Resume Next
		ReDim mSw(mSize)
		If IsArray(aSw) Then
			For ii = 0 To UBound(aSw) : mSw(ii) = aSw(ii) : Next
		ElseIf aSw = 0 Or Err Then
			For ii = 0 To mSize: mSw(ii) = mTrig(ii).TimerInterval : Next
		Else
			mSw(0) = aSw
		End If
	End Sub

	Public Sub InitSnd(aBall, aNoBall) : mBallSnd = aBall : mNoBallSnd = aNoBall : End Sub
	Public Sub CreateEvents(aName)
		Dim ii
		If Not vpmCheckEvent(aName, Me) Then Exit Sub
		For ii = 0 To mSize
			vpmBuildEvent mTrig(ii), "Hit", aName & ".TrigHit ActiveBall," & ii+1
			vpmBuildEvent mTrig(ii), "Unhit", aName & ".TrigUnhit ActiveBall," & ii+1
			vpmBuildEvent mKick(ii), "Hit", aName & ".KickHit " & ii+1
		Next
	End Sub

	Public Sub SolExit(aEnabled)
		Dim ii, mSwcopy
		mGateOpen = aEnabled
		If Not aEnabled Then Exit Sub
		If mBalls > 0 Then
			If isObject(mKick(0)) Then
				If VP8sound then
					PlaySound mBallSnd
				ElseIf VP9sound then
					PlaySound mBallSnd, 1, 1, CoreAudioPan(mKick(0).x), 0
				Else
					PlaySound mBallSnd, 1, 1, CoreAudioPan(mKick(0).x), 0, 0, False, False, CoreAudioFade(mKick(0).y)
				End If
			Else
				PlaySound mBallSnd
			End If
		Else
			If isObject(mKick(0)) Then
				If VP8sound then
					PlaySound mNoBallSnd
				ElseIf VP9sound then
					PlaySound mNoBallSnd, 1, 1, CoreAudioPan(mKick(0).x), 0
				Else
					PlaySound mNoBallSnd, 1, 1, CoreAudioPan(mKick(0).x), 0, 0, False, False, CoreAudioFade(mKick(0).y)
				End If
			Else
				PlaySound mNoBallSnd
			End If
			Exit Sub
		End If
		For ii = 0 To mBalls-1
			mKick(ii).Enabled = False
			If mSw(ii) Then
				mSwcopy = mSw(ii)
				Controller.Switch(mSwcopy) = False
			End If
		Next
		If ExitForce > 0 Then ' Up
			mRealForce = ExitForce + (Rnd - 0.5)*KickForceVar : mKick(mBalls-1).Kick ExitDir, mRealForce
		Else ' Down
			mKick(0).Kick 0, 0
		End If
	End Sub

	Public Sub Reset
		Dim mSwcopy
		Dim ii : If mBalls = 0 Then Exit Sub
		For ii = 0 To mBalls-1
			If mSw(ii) Then
				mSwcopy = mSw(ii)
				Controller.Switch(mSwcopy) = True
			End If
		Next
	End Sub

	Public Property Get Balls : Balls = mBalls : End Property

	Public Property Let Balls(aBalls)
		Dim mSwcopy
		Dim ii : mBalls = aBalls
		For ii = 0 To mSize
			mSwcopy = mSw(ii)
			If ii >= aBalls Then
				mKick(ii).DestroyBall : If mSwcopy Then Controller.Switch(mSwcopy) = False
			Else
				vpmCreateBall mKick(ii) : If mSwcopy Then Controller.Switch(mSwcopy) = True
			End If
		Next
	End Property

	Public Sub TrigHit(aBall, aNo)
		Dim mSwcopy
		aNo = aNo - 1
		If mSw(aNo) Then
			mSwcopy = mSw(aNo)
			Controller.Switch(mSwcopy) = True
		End If
		If aBall.VelY < -1 Then Exit Sub ' Allow small upwards speed
		If aNo = mSize Then mBalls = mBalls + 1
		If mBalls > aNo Then mKick(aNo).Enabled = Not mGateOpen
	End Sub

	Public Sub TrigUnhit(aBall, aNo)
		Dim mSwcopy
		aNo = aNo - 1
		If mSw(aNo) Then
			mSwcopy = mSw(aNo)
			Controller.Switch(mSwcopy) = False
		End If
		If aBall.VelY > -1 Then
			If aNo = 0 Then mBalls = mBalls - 1
			If aNo < mSize Then mKick(aNo+1).Kick 0, 0
		Else
			If aNo = mSize Then mBalls = mBalls - 1
			If aNo > 0 Then mKick(aNo-1).Kick ExitDir, mRealForce
		End If
	End Sub

	Public Sub KickHit(aNo) : mKick(aNo-1).Enabled = False : End Sub
End Class

'--------------------
'	View Dips
'--------------------
Class cvpmDips
	Private mLWF, mChkCount, mOptCount, mItems()

	Private Sub Class_Initialize
		ReDim mItems(100)
	End Sub

	Private Sub addChkBox(aType, aLeft, aTop, aWidth, aNames)
		Dim ii, obj
		If Not isObject(mLWF) Then Exit Sub
		For ii = 0 To UBound(aNames) Step 2
			Set obj = mLWF.AddCtrl("chkBox", 10+aLeft, 5+aTop+ii*7, aWidth, 14, aNames(ii))
			mChkCount = mChkCount + 1 : mItems(mChkCount+mOptCount) = Array(aType, obj, mChkCount, aNames(ii+1), aNames(ii+1))
		Next
	End Sub

	Private Sub addOptBox(aType, aLeft, aTop, aWidth, aHeading, aMask, aNames)
		Dim ii, obj
		If Not isObject(mLWF) Then Exit Sub
		mLWF.AddCtrl "Frame", 10+aLeft, 5+aTop, 10+aWidth, 7*UBound(aNames)+25, aHeading
		If aMask Then
			For ii = 0 To UBound(aNames) Step 2
				Set obj = mLWF.AddCtrl("OptBtn", 10+aLeft+5, 5+aTop+ii*7+14, aWidth, 14, aNames(ii))
				mOptCount = mOptCount + 1 : mItems(mChkCount+mOptCount) = Array(aType+2,obj,mOptCount,aNames(ii+1),aMask)
			Next
		Else
			addChkBox aType, 5+aLeft, 15+aTop, aWidth, aNames
		End If
	End Sub

	Public Sub addForm(ByVal aWidth, aHeight, aName)
		If aWidth < 80 Then aWidth = 80
		On Error Resume Next
		Set mLWF = CreateObject("VPinMAME.WSHDlg") : If Err Then Exit Sub
		With mLWF
			.x = -1 : .y = -1 ' : .w = aWidth : .h = aHeight+60
			.Title = aName : .AddCtrl "OKBtn", -1, -1, 70, 25, "&Ok"
		End With
		mChkCount = 0 : mOptCount = 0
	End Sub

	Public Sub addChk(aLeft, aTop, aWidth, aNames)
		addChkBox 0, aLeft, aTop, aWidth, aNames
	End Sub
	Public Sub addChkExtra(aLeft, aTop, aWidth, aNames)
		addChkBox 1, aLeft, aTop, aWidth, aNames
	End Sub
	Public Sub addFrame(aLeft, aTop, aWidth, aHeading, aMask, aNames)
		addOptBox 0, aLeft, aTop, aWidth, aHeading, aMask, aNames
	End Sub
	Public Sub addFrameExtra(aLeft, aTop, aWidth, aHeading, aMask, aNames)
		addOptBox 1, aLeft, aTop, aWidth, aHeading, aMask, aNames
	End Sub

	Public Sub addLabel(aLeft, aTop, aWidth, aHeight, aCaption)
		If Not isObject(mLWF) Then Exit Sub
		mLWF.AddCtrl "Label", 10+aLeft, 5+aTop, aWidth, aHeight, aCaption
	End Sub

	Public Sub viewDips : viewDipsExtra 0 : End Sub
	Public Function viewDipsExtra(aExtra)
		Dim dips(1), ii, useDip
		If Not isObject(mLWF) Then Exit Function
		With Controller
			dips(0) = .Dip(0) + .Dip(1)*256 + .Dip(2)*65536 + (.Dip(3) And &H7f)*&H1000000
			If .Dip(3) And &H80 Then dips(0) = dips(0) Or &H80000000 'workaround for overflow error
		End With
		useDip = False : dips(1) = aExtra
		For ii = 1 To mChkCount + mOptCount
			mItems(ii)(1).Value = -((dips(mItems(ii)(0) And &H01) And mItems(ii)(4)) = mItems(ii)(3))
			If (mItems(ii)(0) And &H01) = 0 Then useDip = True
		Next
		mLWF.Show GetPlayerHWnd
		dips(0) = 0 : dips(1) = 0
		For ii = 1 To mChkCount + mOptCount
			If mItems(ii)(1).Value Then dips(mItems(ii)(0) And &H01) = dips(mItems(ii)(0) And &H01) Or mItems(ii)(3)
		Next
		If useDip Then
			With Controller
				.Dip(0) =  (dips(0) And 255)
				.Dip(1) = ((dips(0) And 65280)\256) And 255
				.Dip(2) = ((dips(0) And &H00ff0000)\65536) And 255
				.Dip(3) = ((dips(0) And &Hff000000)\&H01000000) And 255
			End With
		End If
		viewDipsExtra = dips(1)
	End Function
End Class

'--------------------
'	Impulse Plunger
'--------------------
Class cvpmImpulseP
	Private mEnabled, mBalls, mTrigger, mEntrySnd, mExitSnd, MExitSndBall
	Public X, Y, Strength, Res, Size, Solenoid, IMPowerOut, Time, mCount, Pull, IMPowerTrans, cFactor, Auto, RandomOut, SwitchNum, SwitchOn, BallOn

	Private Sub Class_Initialize
		Size = 1 : Strength = 0 : Solenoid = 0 : Res = 1 : IMPowerOut = 0 : Time = 0 : mCount = 0 : mEnabled = False
		Pull = 0 : IMPowerTrans = 0 : Auto = False : RandomOut = 0 : SwitchOn = 0 : SwitchNum = 0 : BallOn = 0
		Set mBalls = New cvpmDictionary
	End Sub

	Private Property Let NeedUpdate(aEnabled) : vpmTimer.EnableUpdate Me, True, aEnabled : End Property

	Public Sub InitImpulseP(aTrigger, aStrength, aTime)
		Dim tmp
		If vpmIsArray(aTrigger) Then Set tmp = aTrigger(0) Else Set tmp = aTrigger
		X = tmp.X : Y = tmp.Y : Size = tmp.Radius : vpmTimer.InitTimer tmp, True
		If IsArray(aTrigger) Then mTrigger = aTrigger Else Set mTrigger = aTrigger
		Strength = aStrength
		Res = 500
		Time = aTime
		If aTime = 0 Then
			Auto = True
		Else
			cFactor = (Res / Time) / 100
			Auto = False
		End If
	End Sub

	Public Sub CreateEvents(aName)
		If vpmCheckEvent(aName, Me) Then
			vpmBuildEvent mTrigger, "Hit", aName & ".AddBall ActiveBall"
			vpmBuildEvent mTrigger, "UnHit", aName & ".RemoveBall ActiveBall"
		End If
	End Sub


	Public Property Let PlungeOn(aEnabled) : mEnabled = aEnabled : End Property
	Public Property Get PlungeOn
		If Solenoid > 0 Then PlungeOn = Controller.Solenoid(Solenoid) Else PlungeOn = mEnabled
	End Property

	Public Sub AddBall(aBall)
		Dim mSwcopy
		With mBalls
			If .Exists(aBall) Then .Item(aBall) = .Item(aBall) + 1 Else .Add aBall, 1 : NeedUpdate = True
		End With
		If SwitchOn = True Then
			mSwcopy = SwitchNum
			Controller.Switch(mSwcopy) = 1
		End If
		BallOn = 1
	End Sub

	Public Sub RemoveBall(aBall)
		Dim mSwcopy
		With mBalls
			If .Exists(aBall) Then .Item(aBall) = .Item(aBall) - 1 : If .Item(aBall) <= 0 Then .Remove aBall
			NeedUpdate = (.Count > 0)
		End With
		If SwitchOn = True Then
			mSwcopy = SwitchNum
			Controller.Switch(mSwcopy) = 0
		End If
		BallOn = 0
	End Sub

	Public Property Get Balls : Balls = mBalls.Keys : End Property

	Public Sub Update
		Dim obj
		If pull = 1 and mCount < Res Then
			mCount = mCount + cFactor
			IMPowerTrans = mCount
			NeedUpdate = True
		Else
			IMPowerTrans = mCount
			NeedUpdate = False
		End If
		If PlungeOn Then
			On Error Resume Next
			For Each obj In mBalls.Keys
				If obj.X < 0 Or Err Then : mBalls.Remove obj : Else : PlungeBall obj : End If
			Next
			On Error Goto 0
		End If
	End Sub

	Public Sub PlungeBall(aBall)
		aBall.VelY = IMPowerOut
	End Sub

	Public Sub Random(aInput) ' Random Output Varience
		RandomOut = aInput
	End Sub

	Public Sub Fire	' Resets System and Transfer Power Value
		If Auto = True Then
		IMPowerOut = -Strength + ((Rnd) * RandomOut)
		Else
		IMPowerOut = -Strength * (IMPowerTrans + ((Rnd-0.5) * cFactor * RandomOut)) / Res
		End If
		PlungeOn = True
		Update
		PlungeOn = False
		Pull = 0 : IMPowerOut = 0 : IMPowerTrans = 0 : mCount = 0
		If BallOn = 1 Then : PlaySound mExitSndBall : Else : PlaySound mExitSnd : End If
	End Sub

	Public Sub AutoFire	' Auto-Fire Specific Call (so you don't have to change timing)
		IMPowerOut = -Strength + ((Rnd) * RandomOut)
		PlungeOn = True
		Update
		PlungeOn = False
		Pull = 0 : IMPowerOut = 0 : IMPowerTrans = 0 : mCount = 0
		If BallOn = 1 Then : PlaySound mExitSndBall : Else : PlaySound mExitSnd : End If
	End Sub

	Public Sub Pullback ' Pull Plunger
		Pull = 0 : IMPowerOut = 0 : IMPowerTrans = 0 : mCount = 0 ' reinitialize to be sure
		Pull = 1 : NeedUpdate = True
		PlaySound mEntrySnd
	End Sub

	Public Sub PullbackandRetract ' Pull Plunger and retract
		Pull = 0 : IMPowerOut = 0 : IMPowerTrans = 0 : mCount = 0 ' reinitialize to be sure
		Pull = 1 : NeedUpdate = True
		PlaySound mEntrySnd
	End Sub

	Public Sub Switch(aSw)
		SwitchOn = True
		SwitchNum = aSw
	End Sub

	Public Sub InitEntrySnd(aNoBall) : mEntrySnd = aNoBall : End Sub
	Public Sub InitExitSnd(aBall, aNoBall) : mExitSndBall = aBall : mExitSnd = aNoBall : End Sub
End Class

Set vpmTimer = New cvpmTimer
If LoadScript("NudgePlugIn.vbs") Then Set vpmNudge = New cvpmNudge2 Else Set vpmNudge = New cvpmNudge

'-------------
'cvpmFlips (FastFlips) 2 Beta 1
'-------------

'Redesigned to better support games from the solid-state flipper era, including previously unsupported games thanks to DJrobX and Stumblor!
'New Features:
'- Switches from script to rom control after a delay (100ms default, vpmflips.RomControlDelay)
'	-This works independently for each flipper, ex. the Thing Flip will not interfere even briefly with lower flippers
'- New Feature - vpmFlips.Enabled. vpmFlips.Enabled = True / False will enable / disable fastflips. (This does the same thing as vpmflipsSAM.RomControl)
'	-May be necessary to manually disable flippers for video mode on some games
'- New method to disable upper flippers without getting rom errors from the double action cab switches: call helper subs NoUpperLeftFlipper, NoUpperRightFlipper
'	-On SS games that reuse upper flipper COILS, call the appropriate helper subs from the table script: NoUpperLeftFlipper, NoUpperRightFlipper (for example AFM)
'	-On SS games that reuse upper flipper SWITCHES, you will still need the cSingleLFlip/cSingleRFlip lines!
'	-CSinglexFlip automatically disables flippers to retain legacy behavior
'- Initializes using pulsetimer. VpmInit Me line no longer necessary (? may change)

'Todo
'test delay - works okay but a little weird if longer than romcontroldelay
'test more tables
'test Red & Ted with the left side flippers
'test region safety
'update SAM.vbs ?

dim vpmFlips : set vpmFlips = New cvpmFlips2 : vpmFlips.Name = "vpmFlips"

Sub NoUpperLeftFlipper() : vpmFlips.FlipperSolNumber(2) = 0 : End Sub
Sub NoUpperRightFlipper() : vpmFlips.FlipperSolNumber(3) = 0 : End Sub

Function NullFunction(a) : End Function

vpmtimer.addtimer 40, "vpmFlips.Init'" 'this might be a dumb idea but it would replace the requirement for vpminit me

Class cvpmFlips2   'test fastflips switches to rom control after 100ms or so delay
	Public Name, Delay, TiltObjects, Sol, DebugOn
	Public LagCompensation 'flag for solenoid jitter (may not be a problem anymore) set private

	Public FlipperSolNumber(3)	'Flipper Solenoid Number. By default these are set to use the Core constants. 0=left 1=right 2=Uleft 3=URight
	Public ButtonState(3)		'Key Flip State 'set private
	Public SolState(3)			'Rom Flip State	'set private

	'Public SubL, SubUL, SubR, SubUR 'may restore these to reduce nested calls. For now the script is compressed a bit.
	Public FlipperSub(3)	'Set to the flipper subs by .init

	Public FlippersEnabled	'Flipper Circuit State (from the ROM)
	Public OnOff			'FastFlips Enabled. Separate from FlippersEnabled, which is the flipper circuit state	'private 'todo rename

	Public FlipAt(3)		'Flip Time in gametime	'private
	Public RomControlDelay	'Delay after flipping that Rom Controlled Flips are accepted (default 100ms)


	Private Sub Class_Initialize()
		dim idx :for idx = 0 to 3 :FlipperSub(idx) = "NullFunction" : OnOff=True: ButtonState(idx)=0:SolState(idx)=0: Next
		Delay=0: FlippersEnabled=0: DebugOn=0 : LagCompensation=0 : Sol=0 : TiltObjects=1
		RomControlDelay = 100	'RomControlDelay MS between switching to rom controlled flippers
		FlipperSolNumber(0)=sLLFlipper :FlipperSolNumber(1)=sLRFlipper :FlipperSolNumber(2)=sULFlipper : FlipperSolNumber(3)=sURFlipper
	End Sub

	Sub Init()	'called by a timer, but previously was called by vpminit sub
		On Error Resume Next 'If there's no usesolenoids variable present, exit
			call eval(UseSolenoids) : if err then exit Sub
		On Error Goto 0
		err.clear

		'Set Solenoid
		if not UseSolenoids > 1 then exit sub
		On Error Resume Next
			'For some WPC games (IJ) that reuse upper flipper
			'switch numbers, and legacy fast flip code, disable
			'flippers if cSinglexFlip is set.
			If not cSingleLFlip Then
				if err.number = 0 then NoUpperLeftFlipper
			End If
			err.clear
			If not cSingleRFlip Then
				if err.number = 0 then NoUpperRightFlipper
			End If
			err.clear
			If UseSolenoids > 2 Then
				Solenoid = UseSolenoids
			Else
				err.clear
				if IsEmpty(GameOnSolenoid) or Err then msgbox "VPMflips error: " & err.description
				if err = 500 then 'Error 500 - Variable not defined
					msgbox "UseSolenoids = 2 error!" & vbnewline & vbnewline & "GameOnSolenoid is not defined!" & vbnewline & _
					"System may be incompatible (Check the compatibility list) or your system scripts may be out of date"
				End If
				Solenoid = GameOnSolenoid
			End If
		On Error Goto 0

		'Set callbacks
		dim idx : for idx = 0 to 3
			If IsNumeric(FlipperSolNumber(idx)) then
				Callback(idx) = SolCallback(abs(FlipperSolNumber(idx)))
			end If
		Next

		'dim str
		'for idx = 0 to 3 : str = str & "Callback" & idx & ":" & Callback(idx) &vbnewline : Next
		'str = "init successful" &vbnewline& _
		'	"Sol=" & Solenoid & " " & sol &vbnewline& str
		'msgbox str
	'vpmFlips.DebugTestInit = True	'removed debug stuff for the moment
	End Sub

	'Index based callbacks...
	Public Property Let Callback(aIdx, aInput)
		if Not IsEmpty(aInput) then
			FlipperSub(aIDX) = aInput 'hold old flipper subs
			SolCallback(FlipperSolNumber(aIdx)) = name & ".RomFlip(" & aIdx & ")="
		end if
	End Property
	Public Property Get Callback(aIdx) : CallBack = FlipperSub(aIDX) : End Property

	Public Property Let Enabled(ByVal aEnabled) 'improving choreography
		aEnabled = cBool(aEnabled)
		if aEnabled <> OnOff then 'disregard redundant updates
			OnOff = aEnabled
			dim idx
			If aEnabled then 'Switch to ROM solenoid states or button states immediately
				for idx = 0 to 3
					if SolState(idx) <> ButtonState(idx) And FlippersEnabled Then Execute FlipperSub(idx) &" "& ButtonState(idx) end If
				Next
			Else
				for idx = 0 to 3 : if ButtonState(idx) <> SolState(idx) Then Execute FlipperSub(idx) &" "& SolState(idx) end if : Next
			End If
		end If
	End Property
	Public Property Get Enabled : Enabled = OnOff : End Property

	Public Property Let Solenoid(aInput) : if isnumeric(aInput) then Sol = abs(aInput) : end if : End Property 'set solenoid
	Public Property Get Solenoid : Solenoid = sol : End Property

	Public Property Let Flip(aIdx, ByVal aEnabled) 'Key Flip: Indexed base flip... may keep may not
		aEnabled = abs(aEnabled) 'True / False is not region safe with execute. Convert to 1 or 0 instead.
		ButtonState(aIDX) = aEnabled 'track flipper button states: the game-on sol flips immediately if the button is held down
		'debug.print "Key Flip " & aIdx &" @ " & gametime  & " FF ON: " & OnOff & " Circuit On? " & FlippersEnabled
		If OnOff and FlippersEnabled or DebugOn then
			execute FlipperSub(aIdx) & " " & aEnabled
			FlipAt(aIDX) = GameTime
		end If
	End Property

	'call callbacks 'legacy
	Public Sub FlipL(aEnabled) : Flip(0)=aEnabled :End Sub : Public Sub FlipR(aEnabled) : Flip(1)=aEnabled :End Sub
	Public Sub FlipUL(aEnabled): Flip(2)=aEnabled :End Sub : Public Sub FlipUR(aEnabled): Flip(3)=aEnabled :End Sub

	Public Property Let RomFlip(aIdx, ByVal aEnabled)
		aEnabled = abs(aEnabled)
		SolState(aIdx) = aEnabled

		If Not OnOff OR GameTime >= FlipAt(aIdx) + RomControlDelay Then
			Execute FlipperSub(aIDX) & " " & aEnabled
			'tb.text = "Rom Flip " & aIdx & " state:" & aEnabled &vbnewline&_
			'GameTime & " >= " & FlipAt(aIdx) & "+" & RomControlDelay
			'debug.print "rom flip @ " & gametime & "solenoid:" & sol & ": " & FlippersEnabled
		end if
	End property

	Public Sub TiltSol(ByVal aEnabled) 'Handle solenoid / Delay (if delayinit)
		aEnabled = cBool(aEnabled)
		If delay > 0 and not aEnabled then 'handle delay
			vpmtimer.addtimer Delay, Name & ".FireDelay" & "'"
			LagCompensation = 1
		else
			If Delay > 0 then LagCompensation = 0
			EnableFlippers(aEnabled)
		end If
	End Sub

	Sub FireDelay() : If LagCompensation then EnableFlippers False End If : End Sub

	Public Sub EnableFlippers(ByVal aEnabled) 'private
		aEnabled = abs(aEnabled) 'Might fix TMNT issue with vpmnudge.solgameon?
		dim idx
		'If aEnabled then execute SubL &" "& ButtonState(0) :execute SubR &" "& ButtonState(1) :execute subUL &" "& ButtonState(2): execute subUR &" "& ButtonState(3)':end if
		If aEnabled then : for idx = 0 to 3 : execute FlipperSub(idx) &" "& ButtonState(idx) : next : end If
		FlippersEnabled = aEnabled
		If TiltObjects then vpmnudge.solgameon aEnabled
		If Not aEnabled then
'			 execute subL & " " & 0 : execute subR & " " & 0
'			 execute subUL & " " & 0 : execute subUR & " " & 0
			for idx = 0 to 3 : execute FlipperSub(idx) &" "& 0 : Next
		End If
	End Sub

	'debug for finding sols
	Public Sub PrintSols() : Dim x, sols: sols=controller.solenoids: for x= 0 to uBound(sols) : if sols(x) then debug.print x & ":" & sols(x) end if : Next : End Sub

End Class

'---------------------------
' Check VP version running
'---------------------------
Private Function vpmCheckVPVer
	On Error Resume Next
	' a bug in VBS?: Err object is not cleared on Exit Function
	If VPBuildVersion < 0 Or Err Then vpmCheckVPVer = 5000 : Err.Clear : Exit Function
	If VPBuildVersion > 2806 and VPBuildVersion < 9999 Then
		vpmCheckVPVer = 6300
	ElseIf VPBuildVersion > 2721 and VPBuildVersion < 9999 Then
		vpmCheckVPVer = 6100
	ElseIf VPBuildVersion >= 920 and VPBuildVersion <= 999 Then
		vpmCheckVPVer = 9200
	ElseIf VPBuildVersion > 909 and VPBuildVersion <= 999 Then
		vpmCheckVPVer = 9100
	ElseIf VPBuildVersion >= 900 and VPBuildVersion <= 999 Then
		vpmCheckVPVer = 9000
	ElseIf VPBuildVersion >= 10000 Then
		vpmCheckVPVer = VPBuildVersion
	Else
		vpmCheckVPVer = 6000
	End If
End Function

'--------------------
' Initialise timers
'--------------------
Sub PulseTimer_Init	  : vpmTimer.InitTimer Me, False : End Sub
Sub PinMAMETimer_Init : Me.Interval = PinMAMEInterval : Me.Enabled = True : End Sub

'---------------------------------------------
' Init function called from Table_Init event
'---------------------------------------------
Public Sub vpmInit(aTable)
	Set vpmTable = aTable
	If vpmVPVer >= 6000 Then
	  On Error Resume Next
		If Not IsObject(GetRef(aTable.name & "_Paused")) Or Err Then Err.Clear : vpmBuildEvent aTable, "Paused", "Controller.Pause = True"
		If Not IsObject(GetRef(aTable.name & "_UnPaused")) Or Err Then Err.Clear : vpmBuildEvent aTable, "UnPaused", "Controller.Pause = False"
		If Not IsObject(GetRef(aTable.name & "_Exit")) Or Err Then Err.Clear : vpmBuildEvent aTable, "Exit", "Controller.Pause = False:Controller.Stop"
	End If
	if UseModSol Then
		If Controller.Version >= 02080000 Then
		  Controller.SolMask(2)=1
		Else
		  MsgBox "Modulated Flashers/Solenoids not supported with this Visual PinMAME version (2.8 or newer is required)"
		End If
	End If
	'InitVpmFlips	'have vpmtimer doing this atm

	NudgeGetCalibration XMax, YMax, XGain, YGain, DeadZone, TiltSens
End Sub

' Exit function called in Table_Exit event
Public Sub vpmExit : End Sub
'------------------------------------------------------
' All classes call this function to create a ball
' Assign vpmCreateBall if you want a custom function
'------------------------------------------------------
Private Function vpmDefCreateBall(aKicker)
	If Not IsEmpty(vpmBallImage) Then aKicker.Createball.Image = vpmBallImage Else aKicker.Createball : End If
	Set vpmDefCreateBall = aKicker
End Function

Private Function vpmDefCreateBall2(aKicker)
	If Not IsEmpty(vpmBallImage) Then aKicker.Createsizedball(BSize).Image = vpmBallImage Else aKicker.Createsizedball(BSize) : End If
	Set vpmDefCreateBall2 = aKicker
End Function

Private Function vpmDefCreateBall3(aKicker)
	If Not IsEmpty(vpmBallImage) Then
		aKicker.CreateSizedBallWithMass(BSize,BMass).Image = vpmBallImage
	Else
		aKicker.CreateSizedBallWithMass BSize,BMass 'for whatever reason it doesn't work if using ()
	End If
	Set vpmDefCreateBall3 = aKicker
End Function

If vpmVPVer >= 10000 Then
	Set vpmCreateBall = GetRef("vpmDefCreateBall3")
ElseIf vpmVPVer >= 9100 Then
	Set vpmCreateBall = GetRef("vpmDefCreateBall2")
Else
	Set vpmCreateBall = GetRef("vpmDefCreateBall")
End If

Private vpmTrough ' Default Trough. Used to clear up missing balls
Private vpmTable  ' Table object

'-------------------
' Main Loop
'------------------
Private Const CHGNO = 0
Private Const CHGSTATE = 1
Private vpmTrueFalse : vpmTrueFalse = Array(" True", " False"," True")

Sub vpmDoSolCallback(aNo, aEnabled)
	If SolCallback(aNo) <> "" Then Execute SolCallback(aNo) & vpmTrueFalse(aEnabled+1)
End Sub

Sub vpmDoLampUpdate(aNo, aEnabled)
	On Error Resume Next : Lights(aNo).State = Abs(aEnabled)
End Sub

Sub PinMAMETimer_Timer
	Dim ChgLamp,ChgSol,ChgGI, ii, tmp, idx, nsol, solon, ChgLed
	Dim DMDp
	Dim ChgNVRAM

	'Me.Enabled = False 'this was supposed to be some kind of weird mutex, disable it

	On Error Resume Next
		If UseDMD Then
			DMDp = Controller.RawDmdPixels
			If Not IsEmpty(DMDp) Then
				DMDWidth = Controller.RawDmdWidth
				DMDHeight = Controller.RawDmdHeight
				DMDPixels = DMDp
			End If
		ElseIf UseColoredDMD Then
			DMDp = Controller.RawDmdColoredPixels
			If Not IsEmpty(DMDp) Then
				DMDWidth = Controller.RawDmdWidth
				DMDHeight = Controller.RawDmdHeight
				DMDColoredPixels = DMDp
			End If
		End If
		If UseNVRAM Then
			If isObject(NVRAMCallback) Then
				ChgNVRAM = Controller.ChangedNVRAM 'Controller.NVRAM would deliver everything of the NVRAM all the time as 1D array
				If(Not IsEmpty(ChgNVRAM)) Then NVRAMCallback ChgNVRAM
			End If
		End If
		If UseLamps Then ChgLamp = Controller.ChangedLamps Else LampCallback
		If UsePdbLeds Then ChgLed = Controller.ChangedPDLeds Else PDLedCallback
		If UseSolenoids Then ChgSol = Controller.ChangedSolenoids
		If isObject(GICallback) or isObject(GICallback2) Then ChgGI = Controller.ChangedGIStrings
		MotorCallback
	On Error Goto 0
	If Not IsEmpty(ChgLamp) Then
		On Error Resume Next
			For ii = 0 To UBound(ChgLamp)
				idx = ChgLamp(ii, 0)
				If IsArray(Lights(idx)) Then
					For Each tmp In Lights(idx) : tmp.State = ChgLamp(ii, 1) : Next
				Else
					Lights(idx).State = ChgLamp(ii, 1)
				End If
			Next
			For Each tmp In vpmMultiLights
				For ii = 1 To UBound(tmp) : tmp(ii).State = tmp(0).State : Next
			Next
			LampCallback
		On Error Goto 0
	End If
	If Not IsEmpty(ChgSol) Then
		For ii = 0 To UBound(ChgSol)
			nsol = ChgSol(ii, 0)
			tmp = SolCallback(nsol)
			solon = ChgSol(ii, 1)
			If solon > 1 Then solon = 1
			If UseModSol Then
				If solon <> SolPrevState(nsol) Then
					SolPrevState(nsol) = solon
					If tmp <> "" Then Execute tmp & vpmTrueFalse(solon+1)
				End If
				tmp = SolModCallback(nsol)
				If tmp <> "" Then Execute tmp & " " & ChgSol(ii, 1)
			Else
				If tmp <> "" Then Execute tmp & vpmTrueFalse(solon+1)
			End If
		if UseSolenoids > 1 then if nsol = vpmFlips.Solenoid then vpmFlips.TiltSol solon ': msgbox solon
		Next
	End If
	If Not IsEmpty(ChgGI) Then
		For ii = 0 To UBound(ChgGI)
			GICallback ChgGI(ii, 0), CBool(ChgGI(ii, 1))
			GICallback2 ChgGI(ii, 0), ChgGI(ii, 1)
		Next
	End If
	If Not IsEmpty(ChgLed) Then
		On Error Resume Next
			For ii = 0 To UBound(ChgLed)
				Dim color,ledstate
				idx = ChgLed(ii, 0)
				color = ChgLed(ii, 1)
				if color = 0 Then ledstate = 0 : Else ledstate = 1: End If

				If IsArray(Lights(idx)) Then
					For Each tmp In Lights(idx) : tmp.Color = color : tmp.State = ledstate : Next
				Else
					Lights(idx).Color = color : Lights(idx).State = ledstate
				End If
			Next
			For Each tmp In vpmMultiLights
				For ii = 1 To UBound(tmp) : tmp(ii).Color = tmp(0).Color : tmp(ii).State = tmp(0).State : Next
			Next
			PDLedCallback
		On Error Goto 0
	End If
	'Me.Enabled = True 'this was supposed to be some kind of weird mutex, disable it
End Sub

'
' Private helper functions
'
Private Sub vpmPlaySound(aEnabled, aSound)
	If VarType(aSound) = vbString Then
		If aEnabled Then StopSound aSound : PlaySound aSound
	ElseIf aSound Then
		If aEnabled Then PlaySound SSolenoidOn Else PlaySound SSolenoidOff
	End If
End Sub

Private Sub vpmToggleObj(aObj, aEnabled)
	Dim mSwcopy
	Select Case TypeName(aObj)
		Case "Wall", "HitTarget"		   aObj.IsDropped = aEnabled
		Case "Bumper", "Light"			   aObj.State	  = Abs(aEnabled)
		Case "Kicker", "Trigger", "Timer"  aObj.Enabled	  = aEnabled
		Case "Gate"						   aObj.Open	  = aEnabled
		Case "Primitive", "Ramp", "Rubber", "Flasher" aObj.Visible = aEnabled
		Case "Integer"					   mSwcopy = aObj : Controller.Switch(mSwcopy) = aEnabled
		Case Else MsgBox "vpmToggleObj: Unhandled Object " & TypeName(aObj)
	End Select
End Sub

Private Function vpmCheckEvent(aName, aObj)
	vpmCheckEvent = True
	On Error Resume Next
	If Not Eval(aName) Is aObj Or Err Then MsgBox "CreateEvents: Wrong name " & aName : vpmCheckEvent = False
End Function

Private Sub vpmBuildEvent(aObj, aEvent, aTask)
	Dim obj, str
	str = "_" & aEvent & " : " & aTask & " : End Sub"
	If vpmIsArray(aObj) Then
		For Each obj In aObj : ExecuteGlobal "Sub " & obj.Name & str : Next
	Else
		ExecuteGlobal "Sub " & aObj.Name & str
	End If
End Sub

Private Function vpmIsCollection(aObj)
	vpmIsCollection =  TypeName(aObj) = "Collection" Or TypeName(aObj) = "ICollection"
End Function
Private Function vpmIsArray(aObj)
	vpmIsArray = IsArray(aObj) Or vpmIsCollection(aObj)
End Function

Private Function vpmSetArray(aTo, aFrom)
	If IsArray(aFrom) Then
		aTo = aFrom : vpmSetArray = UBound(aFrom)
	ElseIf vpmIsCollection(aFrom) Then
		Set aTo = aFrom : vpmSetArray = aFrom.Count - 1
	Else
		aTo = Array(aFrom) : vpmSetArray = 0
	End If
End Function

Sub vpmCreateEvents(aHitObjs)
	Dim obj
	For Each obj In aHitObjs
		Select Case TypeName(obj)
			Case "Trigger"
				vpmBuildEvent obj, "Hit", "Controller.Switch(" & Obj.TimerInterval & ") = True"
				vpmBuildEvent obj, "UnHit", "Controller.Switch(" & Obj.TimerInterval & ") = False"
			Case "Wall"
				If obj.HasHitEvent Then
					vpmBuildEvent obj, "Hit", "vpmTimer.PulseSw " & Obj.TimerInterval
				Else
					vpmBuildEvent obj, "SlingShot", "vpmTimer.PulseSw " & Obj.TimerInterval
				End If
			Case "Bumper", "Gate", "Primitive", "HitTarget", "Rubber"
				vpmBuildEvent obj, "Hit", "vpmTimer.PulseSw " & Obj.TimerInterval
			Case "Spinner"
				vpmBuildEvent obj, "Spin", "vpmTimer.PulseSw " & Obj.TimerInterval
		End Select
	Next
End Sub

Sub vpmMapLights(aLights)
	Dim obj, str, ii, idx
	For Each obj In aLights
		idx = obj.TimerInterval
		If IsArray(Lights(idx)) Then
			str = "Lights(" & idx & ") = Array("
			For Each ii In Lights(idx) : str = str & ii.Name & "," : Next
			ExecuteGlobal str & obj.Name & ")"
		ElseIf IsObject(Lights(idx)) Then
			Lights(idx) = Array(Lights(idx),obj)
		Else
			Set Lights(idx) = obj
		End If
	Next
End Sub

Function vpmMoveBall(aBall, aFromKick, aToKick)
	With aToKick.CreateBall
		If TypeName(aBall) = "IBall" Then
			.Color = aBall.Color   : .Image = aBall.Image
			If vpmVPVer >= 6000 Then
				.FrontDecal = aBall.FrontDecal : .BackDecal = aBall.BackDecal
'				.UserValue = aBall.UserValue
			End If
		End If
	End With
	aFromKick.DestroyBall : Set vpmMoveBall = aToKick
End Function

Sub vpmAddBall
	Dim Answer
	If IsObject(vpmTrough) Then
			Answer=MsgBox("Click YES to Add a ball to the Trough, NO Removes a ball from the Trough",vbYesNoCancel + vbQuestion)
		If Answer = vbYes Then vpmTrough.AddBall 0
		If Answer = vbNo Then vpmTrough.Balls=vpmTrough.Balls-1
	End If
End Sub

'----------------------------
' Generic solenoid handlers
'----------------------------
' ----- Flippers ------
Sub vpmSolFlipper(aFlip1, aFlip2, aEnabled)
	Dim oldStrength, oldSpeed ' only for pre-VP10
	If aEnabled Then
		PlaySound SFlipperOn : aFlip1.RotateToEnd : If Not aFlip2 Is Nothing Then aFlip2.RotateToEnd
	Else
		PlaySound SFlipperOff
		If vpmVPVer < 10000 Then
			oldStrength = aFlip1.Strength : aFlip1.Strength = conFlipRetStrength
			oldSpeed = aFlip1.Speed : aFlip1.Speed = conFlipRetSpeed
		End If
		aFlip1.RotateToStart
		If vpmVPVer < 10000 Then
			aFlip1.Strength = oldStrength
			aFlip1.Speed = oldSpeed
		End If
		If Not aFlip2 Is Nothing Then
			If vpmVPVer < 10000 Then
				oldStrength = aFlip2.Strength : aFlip2.Strength = conFlipRetStrength
				oldSpeed = aFlip2.Speed : aFlip2.Speed = conFlipRetSpeed
			End If
			aFlip2.RotateToStart
			If vpmVPVer < 10000 Then
				aFlip2.Strength = oldStrength
				aFlip2.Speed = oldSpeed
			End If
		End If
	End If
End Sub

' ----- Flippers With Speed Control ------
Sub vpmSolFlip2(aFlip1, aFlip2, aFlipSpeedUp, aFlipSpeedDn, aSnd, aEnabled) ' DEPRECATED, as VP10 does not feature speed on flippers anymore
	Dim oldStrength, oldSpeed
	If aEnabled Then
		If aSnd = true then : PlaySound SFlipperOn : End If
		If Not aFlipSpeedUp = 0 Then
			aFlip1.Speed = aFlipSpeedUp
			aFlip1.RotateToEnd
		Else
			aFlip1.RotateToEnd
		End If
		If Not aFlip2 Is Nothing Then
			If Not aFlipSpeedUp = 0 Then
				aFlip2.Speed = aFlipSpeedUp
				aFlip2.RotateToEnd
			Else
				aFlip2.RotateToEnd
			End If
		End If
	Else
		If aSnd = true then : PlaySound SFlipperOff : End If
		oldStrength = aFlip1.Strength
		aFlip1.Strength = conFlipRetStrength
		oldSpeed = aFlip1.Speed
		If Not aFlipSpeedDn = 0 Then
			aFlip1.Speed = aFlipSpeedDn
		Else
			aFlip1.Speed = conFlipRetSpeed
		End If
		aFlip1.RotateToStart : aFlip1.Strength = oldStrength : aFlip1.Speed = oldSpeed
		If Not aFlip2 Is Nothing Then
			oldStrength = aFlip2.Strength
			oldSpeed = aFlip2.Speed
			If Not aFlipSpeedDn = 0 Then
				aFlip2.Speed = aFlipSpeedDn
			Else
				aFlip2.Speed = conFlipRetSpeed
			End If
			aFlip2.Strength = conFlipRetStrength
			aFlip2.RotateToStart : aFlip2.Strength = oldStrength : aFlip2.Speed = oldSpeed
		End If
	End If
End Sub

' ------ Diverters ------
Sub vpmSolDiverter(aDiv, aSound, aEnabled)
	If aEnabled Then aDiv.RotateToEnd : Else aDiv.RotateToStart

'	vpmPlaySound aEnabled, aSound

	If VarType(aSound) = vbString Then
		If aEnabled Then
			StopSound aSound
			If isObject(aDiv) Then
				If VP8sound then
					PlaySound aSound
				ElseIf VP9sound then
					PlaySound aSound, 1, 1, CoreAudioPan(aDiv.x), 0
				Else
					PlaySound aSound, 1, 1, CoreAudioPan(aDiv.x), 0, 0, False, False, CoreAudioFade(aDiv.y)
				End If
			Else
				PlaySound aSound
			End If
		End If
	ElseIf aSound Then
		If aEnabled Then
			If isObject(aDiv) Then
				If VP8sound then
					PlaySound SSolenoidOn
				ElseIf VP9sound then
					PlaySound SSolenoidOn, 1, 1, CoreAudioPan(aDiv.x), 0
				Else
					PlaySound SSolenoidOn, 1, 1, CoreAudioPan(aDiv.x), 0, 0, False, False, CoreAudioFade(aDiv.y)
				End If
			Else
				PlaySound SSolenoidOn
			End If
		Else
			If isObject(aDiv) Then
				If VP8sound then
					PlaySound SSolenoidOff
				ElseIf VP9sound then
					PlaySound SSolenoidOff, 1, 1, CoreAudioPan(aDiv.x), 0
				Else
					PlaySound SSolenoidOff, 1, 1, CoreAudioPan(aDiv.x), 0, 0, False, False, CoreAudioFade(aDiv.y)
				End If
			Else
				PlaySound SSolenoidOff
			End If
		End If
	End If
End sub

' ------ Walls ------
Sub vpmSolWall(aWall, aSound, aEnabled)
	Dim obj
	If vpmIsArray(aWall) Then
		For Each obj In aWall : obj.IsDropped = aEnabled : Next
	Else
		aWall.IsDropped = aEnabled
	End If
	vpmPlaySound aEnabled, aSound
End Sub

Sub vpmSolToggleWall(aWall1, aWall2, aSound, aEnabled)
	Dim obj
	If vpmIsArray(aWall1) Then
		For Each obj In aWall1 : obj.IsDropped = aEnabled : Next
	Else
		aWall1.IsDropped = aEnabled
	End If
	If vpmIsArray(aWall2) Then
		For Each obj In aWall2 : obj.IsDropped = Not aEnabled : Next
	Else
		aWall2.IsDropped = Not aEnabled
	End If
	vpmPlaySound aEnabled, aSound
End Sub

' ------- Autoplunger ------
Sub vpmSolAutoPlunger(aPlung, aVar, aEnabled)
	Dim oldFire
	If aEnabled Then
		oldFire = aPlung.FireSpeed : aPlung.FireSpeed = oldFire * (100-aVar*(2*Rnd-1))/100
		PlaySound SSolenoidOn : aPlung.Fire : aPlung.FireSpeed = oldFire
	Else
		aPlung.Pullback
	End If
End Sub

' --------Autoplunger with Specified Sound To Play ---------
Sub vpmSolAutoPlungeS(aPlung, aSound, aVar, aEnabled)
	Dim oldFire
	If aEnabled Then
		oldFire = aPlung.FireSpeed : aPlung.FireSpeed = oldFire * (100-aVar*(2*Rnd-1))/100
		PlaySound aSound : aPlung.Fire : aPlung.FireSpeed = oldFire
	Else
		aPlung.Pullback
	End If
End Sub

' --------- Gate -----------
Sub vpmSolGate(aGate, aSound, aEnabled)
	Dim obj
	If vpmIsArray(aGate) Then
		For Each obj In aGate : obj.Open = aEnabled : Next
	Else
		aGate.Open = aEnabled
	End If
	vpmPlaySound aEnabled, aSound
End Sub

' ------ Sound Only -------
Sub vpmSolSound(aSound, aEnabled)
	If aEnabled Then StopSound aSound : PlaySound aSound
End Sub

' ------- Flashers --------
Sub vpmFlasher(aFlash, aEnabled)
	Dim obj
	If vpmIsArray(aFlash) Then
		For Each obj In aFlash : obj.State = Abs(aEnabled) : Next
	Else
		aFlash.State = Abs(aEnabled)
	End If
End Sub

'---- Generic object toggle ----
Sub vpmSolToggleObj(aObj1, aObj2, aSound, aEnabled)
	Dim obj
	If vpmIsArray(aObj1) Then
		If IsArray(aObj1(0)) Then
			For Each obj In aObj1(0) : vpmToggleObj obj, aEnabled	  : Next
			For Each obj In aObj1(1) : vpmToggleObj obj, Not aEnabled : Next
		Else
			For Each obj In aObj1	 : vpmToggleObj obj, aEnabled	  : Next
		End If
	ElseIf Not aObj1 Is Nothing Then
		vpmToggleObj aObj1, aEnabled
	End If
	If vpmIsArray(aObj2) Then
		If IsArray(aObj2(0)) Then
			For Each obj In aObj2(0) : vpmToggleObj obj, Not aEnabled : Next
			For Each obj In aObj2(1) : vpmToggleObj obj, aEnabled	  : Next
		Else
			For Each obj In aObj2	 : vpmToggleObj obj, Not aEnabled : Next
		End If
	ElseIf Not aObj2 Is Nothing Then
		vpmToggleObj aObj2, Not aEnabled
	End If
	vpmPlaySound aEnabled, aSound
End Sub

'
' Stubs to allow older games to still work
' These will be removed one day
'
Sub SolFlipper(f1,f2,e) : vpmSolFlipper f1,f2,e : End Sub
Sub SolDiverter(d,s,e) : vpmSolDiverter d,s,e : End Sub
Sub SolSound(s,e) : vpmSolSound s,e : End Sub
Sub Flasher(f,e) : vpmFlasher f,e : End Sub
Sub SolMagnet(m,e) : vpmSolMagnet m,e : End Sub
Sub SolAutoPlunger(p,e) : vpmSolAutoPlunger p,0,e : End Sub
Function KeyDownHandler(ByVal k) : KeyDownHandler = vpmKeyDown(k) : End Function
Function KeyUpHandler(ByVal k) : KeyUpHandler = vpmKeyUp(k) : End Function
Function KeyName(ByVal k) : KeyName = vpmKeyName(k) : End Function
Sub vpmSolMagnet(m,e) : m.Enabled = e : If Not e Then m.Kick 180,1 : End If : End Sub
Dim vpmBallImage : vpmBallImage = Empty ' Default ball properties
Dim vpmBallColour

'-- Flipper solenoids (all games)
Const sLRFlipper = 46
Const sLLFlipper = 48
Const sURFlipper = 34
Const sULFlipper = 36

' Convert keycode to readable string
Private keyNames1, keyNames2
keyNames1 = Array("Escape","1","2","3","4","5","6","7","8","9","0","Minus '-'",_
"Equals '='","Backspace","Tab","Q","W","E","R","T","Y","U","I","O","P","[","]",_
"Enter","Left Ctrl","A","S","D","F","G","H","J","K","L",";","'","`","Left Shift",_
"\","Z","X","C","V","B","N","M",",",".","/","Right Shift","*","Left Menu","Space",_
"Caps Lock","F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","NumLock","ScrlLock",_
"Numpad 7","Numpad 8","Numpad 9","Numpad -","Numpad 4","Numpad 5","Numpad 6",_
"Numpad +","Numpad 1","Numpad 2","Numpad 3","Numpad 0","Numpad .","?","?","?",_
"F11","F12","F13","F14","F15")
keyNames2 = Array("Pause","?","Home","Up","PageUp","?","Left","?","Right","?",_
"End","Down","PageDown","Insert","Delete")

Function vpmKeyName(ByVal aKeycode)
	If aKeyCode-1 <= UBound(keyNames1) Then
		vpmKeyName = keyNames1(aKeyCode-1)
	ElseIf aKeyCode >= 197 And aKeyCode <= 211 Then
		vpmKeyName = keyNames2(aKeyCode-197)
	ElseIf aKeyCode = 184 Then
		vpmKeyName = "R.Alt"
	Else
		vpmKeyName = "?"
	End If
End Function

Private vpmSystemHelp
Private Sub vpmShowHelp
	Dim szKeyMsg
	szKeyMsg = "The following keys are defined: "				   & vbNewLine &_
			   "(American keyboard layout)"						   & vbNewLine &_
		vbNewLine & "Visual PinMAME keys:"						   & vbNewLine &_
		vpmKeyName(keyShowOpts)	  & vbTab & "Game options..."	   & vbNewLine &_
		vpmKeyName(keyShowKeys)	  & vbTab & "Keyboard settings..." & vbNewLine &_
		vpmKeyName(keyReset)	  & vbTab & "Reset emulation"	   & vbNewLine &_
		vpmKeyName(keyFrame)	  & vbTab & "Toggle Display lock"  & vbNewLine &_
		vpmKeyName(keyDoubleSize) & vbTab & "Toggle Display size"  & vbNewLine
	If IsObject(vpmShowDips) Then
			szKeyMsg = szKeyMsg & vpmKeyName(keyShowDips)	& vbTab & "Show DIP Switch / Option Menu" & vbNewLine
		End If
	If IsObject(vpmTrough) Then
		szKeyMsg = szKeyMsg & vpmKeyName(keyAddBall) & vbTab & "Add / Remove Ball From Table" & vbNewLine
	End If
	szKeyMsg = szKeyMsg & vpmKeyName(keyBangBack) & vbTab & "Bang Back" & vbNewLine &_
		vbNewLine & vpmSystemHelp & vbNewLine
	If ExtraKeyHelp <> "" Then
		szKeyMsg = szKeyMsg & vbNewLine & "Game Specific keys:" &_
			vbNewLine & ExtraKeyHelp & vbNewLine
	End If
	szKeyMsg = szKeyMsg & vbNewLine & "Visual Pinball keys:"	 & vbNewLine &_
		vpmKeyName(LeftFlipperKey)	& vbTab & "Left Flipper"	 & vbNewLine &_
		vpmKeyName(RightFlipperKey) & vbTab & "Right Flipper"	 & vbNewLine &_
		vpmKeyName(LeftMagnaSave)	& vbTab & "Left Magna Save"	 & vbNewLine &_
		vpmKeyName(RightMagnaSave)	& vbTab & "Right Magna Save" & vbNewLine &_
		vpmKeyName(PlungerKey)		& vbTab & "Launch Ball"		 & vbNewLine &_
		vpmKeyName(StartGameKey)	& vbTab & "Start Button"	 & vbNewLine &_
		vpmKeyName(AddCreditKey)	& vbTab & "Insert Coin 1"	 & vbNewLine &_
		vpmKeyName(AddCreditKey2)	& vbTab & "Insert Coin 2"	 & vbNewLine &_
		vpmKeyName(ExitGame)		& vbTab & "Exit Game"		 & vbNewLine &_
		vpmKeyName(MechanicalTilt)	& vbTab & "Mechanical Tilt"	 & vbNewLine &_
		vpmKeyName(LeftTiltKey)		& vbTab & "Nudge from Left"	 & vbNewLine &_
		vpmKeyName(RightTiltKey)	& vbTab & "Nudge from Right" & vbNewLine &_
		vpmKeyName(CenterTiltKey)	& vbTab & "Nudge forward"	 & vbNewLine
	MsgBox szKeyMsg,vbOkOnly,"Keyboard Settings..."
End Sub

Private Sub NullSub(no,enabled)
'Place Holder Sub
End Sub

'added thanks to Koadic
Sub NVOffset(version) ' version 2 for dB2S compatibility
	Dim check,nvcheck,v,vv,nvpath,rom
	Set check = CreateObject("Scripting.FileSystemObject")
	Set nvcheck = CreateObject("WScript.Shell")
	nvpath = nvcheck.RegRead("HKCU\Software\Freeware\Visual PinMame\globals\nvram_directory") & "\"
	rom = controller.gamename
	For v=1 to 32 'check up to 32 possible versions using same rom, it's overkill, but could be changed to a lower number (requested for 32 NFL variations)
		If check.FileExists(nvpath & rom & " v" & v & ".txt") Then vv=v : exit For : End If
		vv=0
	Next
	If vv=version or version = 0 Then
		Exit Sub
	ElseIf vv=0 Then
		check.CreateTextFile nvpath & rom & " v" & version & ".txt", True
		Exit Sub
	Else
		check.moveFile nvpath & rom & " v" & vv & ".txt", nvpath & rom & " v" & version & ".txt"
		If check.FileExists(nvpath & rom & ".nv") Then
			check.copyFile nvpath & rom & ".nv", nvpath & rom & " v" & vv & ".nv", True
		End If
		If check.FileExists(nvpath & rom & " v" & version & ".nv") Then
			check.copyFile nvpath & rom & " v" & version & ".nv", nvpath & rom & ".nv", True
		End If
	End If
End Sub

Sub VPMVol
	Dim VolPM,VolPMNew
	VolPM = Controller.Games(controller.GameName).Settings.Value("volume")
	VolPMNew = InputBox ("Enter desired VPinMAME Volume Level (-32 to 0)","VPinMAME Volume",VolPM)
	If VolPMNew = "" Then Exit Sub
	If VolPMNew <=0 and VolPMNew >= -32 Then
		Controller.Games(controller.GameName).Settings.Value("volume")= round(VolPMNew)
		msgbox "The Visual PinMAME Global Volume is now set to " & round(VolPMNew) & "db." & VbNewLine & VbNewLine & "Please reset Visual PinMAME (F3) to apply."
	Else
		msgbox "Entered value is out of range. Entry must be in the range of negative 32 to 0." & VbNewLine & VbNewLine & "Visual PinMAME Global Volume will remain set at " & VolPM & "."
	End If
End Sub

' Simple min/max functions
Function vpMin(a, b) : If a < b Then vpMin = a Else vpMin = b : End If : End Function
Function vpMax(a, b) : If a > b Then vpMax = a Else vpMax = b : End If : End Function

LoadScript("ledcontrol.vbs"):Err.Clear	' Checks for existance of ledcontrol.vbs and loads it if found, if found but no ledwiz installed, clear error to allow loading of table

LoadScript("GlobalPlugIn.vbs")			' Checks for existance of GlobalPlugIn.vbs and loads it if found, useful for adding
										' custom scripting that can be used for all tables instead of altering the core.vbs

Dim swidth, sheight, VP8sound, VP9sound
swidth = 950
sheight = 2100
VP8sound = False
VP9sound = False

On Error Resume Next
Err.Clear
If Version > 0 then
	If Version >= 10700 then 'no ActiveTable call until 10.7
		Dim soundtable
		Set soundtable = ActiveTable
		swidth = soundtable.Width
		sheight = soundtable.Height
	End If
	If Version < 10400 then 'no front/rear fade until 10.4
		VP9sound = True
	End If
End If

If Err.Number <> 0 then
	If VPBuildVersion >= 920 and VPBuildVersion <= 999 then 'no pan parameter in PlaySound until 9.2
		VP9sound = True
	Else
		VP8sound = True
	End If
	swidth = 950
	sheight = 2100
	Err.Clear
End If
On Error Goto 0

Private Function CoreAudioPan(xpar) 'calculates the audio pan of an table object using the actual table width or 950 for older versions
	If xpar < 0 then
		CoreAudioPan = -1.
	Else
		If xpar > swidth then
			CoreAudioPan = 1.
		Else
			CoreAudioPan = (xpar*2./swidth)-1.
		End If
	End If
End Function

Private Function CoreAudioFade(ypar) 'calculates the audio fade of an table object using the actual table length or 2100 for older versions
	If ypar < 0 then
		CoreAudioFade = -1.
	Else
		If ypar > sheight then
			CoreAudioFade = 1.
		Else
			CoreAudioFade = (ypar*2./sheight)-1.
		End If
	End If
End Function

Hope this helps some. Again, it's a hack, but even so, it's a decent temp fix to get some nudge w/o having to use the buttons. =/


Edited by theissdev, 05 April 2022 - 03:45 PM.


#5 theissdev

theissdev

    Hobbyist

  • Members
  • PipPip
  • 10 posts

  • Flag: United States of America

  • Favorite Pinball: Theater of Magic, Judge Dredd

Posted 06 April 2022 - 01:59 AM

Ok. The KL25Z came in. Set it up w/ Pinscape. Taped it under the bottom of the cab w/ some shims and industrial double-sided tape on the corners so it's upright, but not flush on the bottom. Removed x360ce. Have Joy2Key wired up for all buttons. VPX is set up to use the KL25Z X and Y axes for nudge, the plunger is set up w/ the ALP plunger (which is Z axis).

Everything works great. All EM tables I checked and the Stern games that didn't work w/ the hack are working great. Like seriously great. Feels way better as well. Nudge buttons are just for magnasave now (w/ right one doubling as launch for the non-plunger launch games).

This is the guy I got: https://smile.amazon...49210165&sr=8-2

Honestly, it's worth it not just for avoiding the fighting w/ the ALP accelerometer, but it's waaay better as well. Stumbled upon a few hits googling where peeps have said while the plunger hooks up nicely, the ALP accelerometer just doesn't work for w/e reason.



#6 hcj13

hcj13

    Hobbyist

  • Members
  • PipPip
  • 11 posts

  • Flag: ---------

  • Favorite Pinball: batman

Posted 08 April 2022 - 06:05 PM

Quick question. How did you separate the accelerometer and plunger of ALP on mapping in VPX. And how do I map the KLZ board accelerometer to VPX? Thanks!!!

Edited by hcj13, 08 April 2022 - 06:07 PM.


#7 theissdev

theissdev

    Hobbyist

  • Members
  • PipPip
  • 10 posts

  • Flag: United States of America

  • Favorite Pinball: Theater of Magic, Judge Dredd

Posted 13 April 2022 - 04:48 PM

The Z axis for the ALP plunger just seems to come in naturally. I don't think I have any remnants from the x360ce experiment.

The X and Y axes are set up via Pinscape, which I'm pretty sure is needed for the KL25Z board set up (not sure if you can just get info off it w/o it). You probably want to use Pinscape regardless, b/c it also lets you make setting changes.

 

Mine are:
System Type: Stand-alone KL25Z
USB Identification: LedWiz Unit 1
Pincape ID: Pinscape Unit 1
Reset on Disconnect: Enabled / 10s
TV On Switch: Disabled
IR Remote Controller: Not Connected / Not Connected
Joystick Input: Enabled / X/Y/Z / 8.333ms / 1
Accelerometer Orientation: Ports facing rear (as I mounted)
Dynamic Range: +-1G
Auto Centering: Auto w/ default 5s
Plunger Sensor Setup: None
All the remaining stuff default

I'm assuming since the Plunger Sensor Setup is None, it's passing the ALP plunger through the Z axis, which works for me. I was wondering if I was going to have to hardwire the ALP plunger into the KL25Z, but it seems like I didn't have to do that.

I also changed the plunger key in VPX to U and set that to my forward nudge button for the ball launch button. Position is a little wonky, but I no longer need nudge buttons, so eh. Left/Right nudge buttons are now magna save/special as well.



#8 apollothesun

apollothesun

    Neophyte

  • Members
  • Pip
  • 4 posts

  • Flag: Philippines

  • Favorite Pinball: fx3 indiana jones

Posted 28 July 2022 - 05:01 AM

Hi, I’ve been searching for this mod for the alp. By any chance you have fx3 steam pinball games and tried with the new setup?
Do you have pictures of the mounted kl25z?

#9 MarkPinHead

MarkPinHead

    Neophyte

  • Members
  • Pip
  • 9 posts

  • Flag: United Kingdom

  • Favorite Pinball: Addams Family

Posted 01 December 2022 - 10:15 PM

These nudge settings work for me, the tilt still needs some fiddling though:

 

  • X Axis rX Axis x-max:100 X-Gain:100%
  • Y Axis rY Axis y-max:100 Y-Gain:100%
  • Deadzone 45%
  • Enable nudge filter
  • Tilt Sens. 100

The Nudge table you get with 10.7.2 is your friend.


Edited by MarkPinHead, 01 December 2022 - 10:20 PM.


#10 strells

strells

    Enthusiast

  • Silver Supporter
  • 421 posts

  • Flag: United States of America

  • Favorite Pinball: Addams Family

Posted 06 December 2022 - 05:28 PM

These nudge settings work for me, the tilt still needs some fiddling though:

 

  • X Axis rX Axis x-max:100 X-Gain:100%
  • Y Axis rY Axis y-max:100 Y-Gain:100%
  • Deadzone 45%
  • Enable nudge filter
  • Tilt Sens. 100

The Nudge table you get with 10.7.2 is your friend.

45% deadzone?  Does nudging even do anything with it that high?  Mine is usually around 3-5%.



#11 MarkPinHead

MarkPinHead

    Neophyte

  • Members
  • Pip
  • 9 posts

  • Flag: United Kingdom

  • Favorite Pinball: Addams Family

Posted 06 December 2022 - 06:29 PM

When using the nudge table I can see both axes jumping around constantly and having it that high covers it ok, nudging works ok for me.

I haven't got the tilt sorted though, have you?

#12 strells

strells

    Enthusiast

  • Silver Supporter
  • 421 posts

  • Flag: United States of America

  • Favorite Pinball: Addams Family

Posted 06 December 2022 - 08:14 PM

When using the nudge table I can see both axes jumping around constantly and having it that high covers it ok, nudging works ok for me.

I haven't got the tilt sorted though, have you?

I have a real tilt bob so I don't have issues like this (other than hearing the bob hit the side of its ring when I tilt).



#13 wryker

wryker

    Enthusiast

  • Members
  • PipPipPip
  • 160 posts

  • Flag: ---------

  • Favorite Pinball: Black Knight

Posted 21 January 2023 - 06:03 PM

When using the nudge table I can see both axes jumping around constantly and having it that high covers it ok, nudging works ok for me.

I haven't got the tilt sorted though, have you?

I'm in the same boat as you. My axes jump CRAZY and I've been using J2K for nudging but wanted to have everything in VPX. I've been toying around with the nudge settings last night for 3 hours (posted a question about this very issue in 2-3 FB groups too). I just tried your settings you posted above and it appears to work.

I'm gonna try the LOTR table and see how the ball moves (my last settings before coming across your post left the ball 'floating' at times!).

Ok. Played LOTR and the ball occasionally floated <sigh>.

This is so frustrating.


Ok. The KL25Z came in. Set it up w/ Pinscape. Taped it under the bottom of the cab w/ some shims and industrial double-sided tape on the corners so it's upright, but not flush on the bottom. Removed x360ce. Have Joy2Key wired up for all buttons. VPX is set up to use the KL25Z X and Y axes for nudge, the plunger is set up w/ the ALP plunger (which is Z axis).

Everything works great. All EM tables I checked and the Stern games that didn't work w/ the hack are working great. Like seriously great. Feels way better as well. Nudge buttons are just for magnasave now (w/ right one doubling as launch for the non-plunger launch games).

This is the guy I got: https://smile.amazon...49210165&sr=8-2

Honestly, it's worth it not just for avoiding the fighting w/ the ALP accelerometer, but it's waaay better as well. Stumbled upon a few hits googling where peeps have said while the plunger hooks up nicely, the ALP accelerometer just doesn't work for w/e reason.

I trust you removed your playfield or something to get access to the inside of the ALP? If so - do you have any pics of the wiring you did and where/how you affixed it to the ALP?



#14 1mikeg

1mikeg

    Neophyte

  • Members
  • Pip
  • 2 posts

  • Flag: ---------

  • Favorite Pinball: Godzilla

Posted 11 July 2023 - 08:22 PM

 

When using the nudge table I can see both axes jumping around constantly and having it that high covers it ok, nudging works ok for me.

I haven't got the tilt sorted though, have you?

I'm in the same boat as you. My axes jump CRAZY and I've been using J2K for nudging but wanted to have everything in VPX. I've been toying around with the nudge settings last night for 3 hours (posted a question about this very issue in 2-3 FB groups too). I just tried your settings you posted above and it appears to work.

I'm gonna try the LOTR table and see how the ball moves (my last settings before coming across your post left the ball 'floating' at times!).

Ok. Played LOTR and the ball occasionally floated <sigh>.

This is so frustrating.


Ok. The KL25Z came in. Set it up w/ Pinscape. Taped it under the bottom of the cab w/ some shims and industrial double-sided tape on the corners so it's upright, but not flush on the bottom. Removed x360ce. Have Joy2Key wired up for all buttons. VPX is set up to use the KL25Z X and Y axes for nudge, the plunger is set up w/ the ALP plunger (which is Z axis).

Everything works great. All EM tables I checked and the Stern games that didn't work w/ the hack are working great. Like seriously great. Feels way better as well. Nudge buttons are just for magnasave now (w/ right one doubling as launch for the non-plunger launch games).

This is the guy I got: https://smile.amazon...49210165&sr=8-2

Honestly, it's worth it not just for avoiding the fighting w/ the ALP accelerometer, but it's waaay better as well. Stumbled upon a few hits googling where peeps have said while the plunger hooks up nicely, the ALP accelerometer just doesn't work for w/e reason.

I trust you removed your playfield or something to get access to the inside of the ALP? If so - do you have any pics of the wiring you did and where/how you affixed it to the ALP?

 

I found that running the calibration wizard in windows as a controller killed the crazy shake for me.

 

Type "Game Controllers" in start search > Set Up USB Game Controller > Select the HID device (turn off any other game controllers if you're having trouble finding it) > Properties > Calibrate







Also tagged with one or more of these keywords: ALP, Nudge, VPX, Atgames Legends Pinball, Nudging, Accelerometer