diff --git a/APC.ino b/APC.ino index ce3d627..07e057f 100755 --- a/APC.ino +++ b/APC.ino @@ -17,7 +17,7 @@ SdFat SD; #define AllData 510 #define HwExtStackPosMax 20 // size of the HwExtBuffer -const char APC_Version[6] = "00.31"; // Current APC version - includes the other INO files also +const char APC_Version[6] = "01.00"; // Current APC version - includes the other INO files also void HandleBoolSetting(bool change); void HandleTextSetting(bool change); @@ -26,6 +26,8 @@ void HandleNumSetting(bool change); void HandleVolumeSetting(bool change); void RestoreDefaults(bool change); void ExitSettings(bool change); +void watchdogSetup(void){ } + byte (*PinMameException)(byte, byte); const byte AlphaUpper[128] = {0,0,0,0,0,0,0,0,107,21,0,0,0,0,0,0,0,0,0,0,64,191,64,21,0,0,64,4,0,0,0,40, // Blank $ * + - / for upper row alphanumeric displays @@ -102,7 +104,6 @@ unsigned int BlinkPeriod[65]; // blink period for this t byte BlinkingLamps[65][65]; // [BlinkTimer] used by these [lamps] byte SolMax = 24; // maximum number of solenoids bool SolChange = false; // Indicates that the state of a solenoid has to be changed -byte SolLatch = 0; // Indicates which solenoid latches must be updated byte SolRecycleTime[22]; // recycle time for each solenoid byte SolRecycleTimers[22]; // stores the numbers of the recycle timers for each solenoid bool C_BankActive = false; // A/C relay currently doing C bank? @@ -312,6 +313,7 @@ void setup() { TC2->TC_CHANNEL[1].TC_IDR=~TC_IER_CPCS; // IDR = interrupt disable register NVIC_EnableIRQ(TC7_IRQn); // enable interrupt delay(1000); + watchdogEnable(2000); // set watchdog to 2s DispPattern1 = AlphaUpper; // use character definitions for alphanumeric displays DispPattern2 = AlphaLower; DispRow1 = DisplayUpper; // use the standard display buffer @@ -436,6 +438,7 @@ void EnterAttractMode(byte Event) { // Enter the attract mode void TC7_Handler() { // interrupt routine - runs every ms TC_GetStatus(TC2, 1); // clear status + watchdogReset(); // reset system watchdog static byte SwDrv = 0; // switch driver being accessed at the moment static byte DispCol = 0; // display column being illuminated at the moment static byte LampCol = 0; // lamp column being illuminated at the moment @@ -575,22 +578,18 @@ void TC7_Handler() { // interrupt routine - run if (SolChange) { // is there a solenoid state to be changed? REG_PIOC_CODR = AllSelects - HwExtSels + AllData; // clear all select signals and the data bus - if (SolLatch & 1) { - c = SolBuffer[0]; - REG_PIOC_SODR = c<<1; - REG_PIOC_SODR = 16777216;} // select first latch - if (SolLatch & 2) { - REG_PIOC_CODR = AllSelects - HwExtSels + AllData; // clear all select signals and the data bus - c = SolBuffer[1]; - REG_PIOC_SODR = c<<1; - REG_PIOC_SODR = 8388608;} // select second latch - if (SolLatch & 4) { - REG_PIOC_CODR = AllSelects - HwExtSels + AllData; // clear all select signals and the data bus - c = SolBuffer[2]; - REG_PIOC_SODR = c<<1; - REG_PIOC_SODR = 4194304;} // select third latch - SolLatch = 0; - SolChange = false;} // reset flag + c = SolBuffer[0]; + REG_PIOC_SODR = c<<1; + REG_PIOC_SODR = 16777216; // select first latch + REG_PIOC_CODR = AllSelects - HwExtSels + AllData; // clear all select signals and the data bus + c = SolBuffer[1]; + REG_PIOC_SODR = c<<1; + REG_PIOC_SODR = 8388608; // select second latch + REG_PIOC_CODR = AllSelects - HwExtSels + AllData; // clear all select signals and the data bus + c = SolBuffer[2]; + REG_PIOC_SODR = c<<1; + REG_PIOC_SODR = 4194304;} // select third latch + REG_PIOC_CODR = AllSelects - HwExtSels + AllData; // clear all select signals and the data bus @@ -1572,20 +1571,17 @@ void ShowMessage(byte Seconds) { // switch to the second di void ActivateSolenoid(unsigned int Duration, byte Solenoid) { if ((Solenoid < 23 && !SolRecycleTimers[Solenoid-1]) || (Solenoid < 25 && Solenoid > 22)) { // consider recycling time for A bank solenoids only - SolChange = false; // block IRQ solenoid handling + SolChange = false; // block IRQ solenoid handling if (Solenoid > 8) { // does the solenoid not belong to the first latch? if (Solenoid < 17) { // does it belong to the second latch? - SolBuffer[1] |= 1<<(Solenoid-9); // latch counts from 0 - SolLatch |= 2;} // select second latch + SolBuffer[1] |= 1<<(Solenoid-9);} // latch counts from 0 else { - SolBuffer[2] |= 1<<(Solenoid-17); - SolLatch |= 4;}} // select third latch + SolBuffer[2] |= 1<<(Solenoid-17);}} else { - SolBuffer[0] |= 1<<(Solenoid-1); - SolLatch |= 1;} // select first latch + SolBuffer[0] |= 1<<(Solenoid-1);} if (!Duration) { Duration = *(GameDefinition.SolTimes+Solenoid-1);} // if no duration is specified use the solenoid specific default - if (Duration) { // duration = 0 means solenoid is permanently on + if (Duration) { // duration = 0 means solenoid is permanently on ActivateTimer(Duration, Solenoid, ReleaseSolenoid);} // otherwise use a timer to turn it off again SolChange = true;}} @@ -1606,19 +1602,16 @@ void ReleaseSolenoid(byte Solenoid) { SolChange = false; // block IRQ solenoid handling if (Solenoid > 8) { // does the solenoid not belong to the first latch? if (Solenoid < 17) { // does it belong to the second latch? - SolBuffer[1] &= 255-(1<<(Solenoid-9)); // latch counts from 0 - SolLatch |= 2;} // select second latch + SolBuffer[1] &= 255-(1<<(Solenoid-9));} // latch counts from 0 else { - SolBuffer[2] &= 255-(1<<(Solenoid-17)); - SolLatch |= 4;}} // select third latch + SolBuffer[2] &= 255-(1<<(Solenoid-17));}} else { - SolBuffer[0] &= 255-(1<<(Solenoid-1)); - SolLatch |= 1;} // select first latch + SolBuffer[0] &= 255-(1<<(Solenoid-1));} SolChange = true; // remove IRQ block if (Solenoid < 23 && SolRecycleTime[Solenoid-1]) { // is a recycling time specified? SolRecycleTimers[Solenoid-1] = ActivateTimer((unsigned int) SolRecycleTime[Solenoid-1], Solenoid, ReleaseSolBlock);}} // start the release timer -void ReleaseSolBlock(byte Coil) { // release the coil block when the recycling time is over +void ReleaseSolBlock(byte Coil) { // release the coil block when the recycling time is over SolRecycleTimers[Coil-1] = 0;} bool QuerySolenoid(byte Solenoid) { // determine the current state of a solenoid @@ -2100,7 +2093,7 @@ void AddBlinkLamp(byte Lamp, unsigned int Period) { BlinkTimers++; // increase the number of blink timers BlinkTimer[x] = ActivateTimer(Period, x, BlinkLamps);}}} // start a timer and store it's number -void RemoveBlinkLamp(byte LampNo) { // stop the lamp from blinking +void RemoveBlinkLamp(byte LampNo) { // stop the blinking of LampNo byte a = 0; byte b = 0; byte x = 0; @@ -2110,7 +2103,7 @@ void RemoveBlinkLamp(byte LampNo) { // stop the lamp from blin y = 0; if (BlinkTimer[x]) { // blink timer in use found? a++; // increase the number of active blink timers found - while (b < BlinkingNo[x]){ // check the list of lamps controlled by this timer + while (b < BlinkingNo[x]) { // check the list of lamps controlled by this timer if (BlinkingLamps[x][y]) { // active lamp found? b++; // increase the number of active lamps found for this blink timer if (BlinkingLamps[x][y] == LampNo) { // is it the lamp to be removed? @@ -2129,6 +2122,28 @@ void RemoveBlinkLamp(byte LampNo) { // stop the lamp from blin if (x > 64) { // max 64 blink timers possible (starting from 1) ErrorHandler(8,0,LampNo);}}} // show error 8 +void StopAllBlinkLamps() { // stop the blinking of all lamps + byte x = 0; + byte y = 0; + while (BlinkTimers) { // search all active blink timers + y = 0; + if (BlinkTimer[x]) { // blink timer in use found? + while (BlinkingNo[x]) { // check the list of lamps controlled by this timer + if (BlinkingLamps[x][y]) { // active lamp found? + TurnOffLamp(BlinkingLamps[x][y]); // turn it off + BlinkingLamps[x][y] = 0; // delete it from the list + BlinkingNo[x]--; // decrease the number of lamps controlled by this timer + if (!BlinkingNo[x]) { // = 0? + KillTimer(BlinkTimer[x]); // kill the timer + BlinkTimer[x] = 0; // delete it from the list + BlinkTimers--;}} // decrease the number of blink timers + y++; // increase the counter for the list of this blink timer + if (y > 64) { // max 64 lamps existing (starting from 1) + ErrorHandler(12,BlinkTimer[x],0);}}} // show error 7 + x++; // increase the counter for the list of active blink timers + if (x > 64) { // max 64 blink timers possible (starting from 1) + ErrorHandler(13,0,0);}}} // show error 8 + void ErrorHandler(unsigned int Error, unsigned int Number2, unsigned int Number3) { WriteUpper2("ERROR "); // Show Error Message WriteLower2(" "); @@ -2237,7 +2252,9 @@ void PlayMusic(byte Priority, const char* Filename) { if ((Priority > 99 && Priority > MusicPriority) || (Priority < 100 && Priority >= MusicPriority)) { MusicFile.close(); // close the previous file StartMusic = 0; // cancel the startup - MBP = 0;}} // and neglect its data + MBP = 0;} // and neglect its data + else { + return;}} if (!PlayingMusic) { // no music in play at the moment? MusicFile = SD.open(Filename); // open file if (!MusicFile) { @@ -2334,7 +2351,9 @@ void PlaySound(byte Priority, const char* Filename) { if ((Priority > 99 && Priority > SoundPriority) || (Priority < 100 && Priority >= SoundPriority)) { SoundFile.close(); StartSound = 0; - SBP = 0;}} + SBP = 0;} + else { + return;}} if (!PlayingSound) { // no sound in play at the moment? SoundFile = SD.open(Filename); // open file if (!SoundFile) { diff --git a/BaseCode.ino b/BaseCode.ino index eb49d2f..9cbdb2d 100755 --- a/BaseCode.ino +++ b/BaseCode.ino @@ -301,7 +301,8 @@ void BC_AttractModeSW(byte Button) { // Attract Mode switch beh Settings_Enter();} break; case 3: // start game - if (BC_CountBallsInTrunk() == game_settings[BCset_InstalledBalls] || (BC_CountBallsInTrunk() == game_settings[BCset_InstalledBalls]-1 && QuerySwitch(game_settings[BCset_PlungerLaneSwitch]))) { // Ball missing? + if ((game_settings[BCset_InstalledBalls == 1] && QuerySwitch(game_settings[BCset_OutholeSwitch])) || // single ball game + BC_CountBallsInTrunk() == game_settings[BCset_InstalledBalls] || (BC_CountBallsInTrunk() == game_settings[BCset_InstalledBalls]-1 && QuerySwitch(game_settings[BCset_PlungerLaneSwitch]))) { // Ball missing? Switch_Pressed = DummyProcess; // Switches do nothing ShowLampPatterns(0); // stop lamp animations BC_AttractDisplayCycle(0); // stop display animations @@ -326,7 +327,7 @@ void BC_AttractModeSW(byte Button) { // Attract Mode switch beh for (i=1; i < 5; i++) { LockedBalls[i] = 0; Points[i] = 0;} - BC_NewBall(game_settings[BCset_InstalledBalls]); // release a new ball (3 expected balls in the trunk) + BC_NewBall(game_settings[BCset_InstalledBalls]); // release a new ball ActivateSolenoid(0, 23); // enable flipper fingers ActivateSolenoid(0, 24);}}} @@ -338,9 +339,10 @@ void BC_AddPlayer() { void BC_CheckForLockedBalls(byte Event) { // check if balls are locked and release them UNUSED(Event); - if (QuerySwitch(game_settings[BCset_OutholeSwitch])) { // for the outhole - ActA_BankSol(game_settings[BCset_OutholeKicker]);} -} // add the locks of your game here + if (game_settings[BCset_InstalledBalls] > 1) { // single ball game -> no ball through ramp + if (QuerySwitch(game_settings[BCset_OutholeSwitch])) { // for the outhole + ActA_BankSol(game_settings[BCset_OutholeKicker]);} + }} // add the locks of your game here void BC_NewBall(byte Balls) { // release ball (Event = expected balls on ramp) ShowAllPoints(0); @@ -458,10 +460,6 @@ void BC_GameMain(byte Event) { // game switch events case 2: // ball roll tilt case 7: // slam tilt break; - case 46: // playfield tilt - WriteUpper(" TILT WARNING "); - ActivateTimer(3000, 0, ShowAllPoints); - break; case 3: // credit button BC_AddPlayer(); break; @@ -475,8 +473,11 @@ void BC_ClearOuthole(byte Event) { if (QuerySwitch(game_settings[BCset_OutholeSwitch])) { // outhole switch still active? if (!BlockOuthole && !C_BankActive) { // outhole blocked? BlockOuthole = true; // block outhole until this ball has been processed - ActivateSolenoid(30, game_settings[BCset_OutholeKicker]); // put ball in trunk - ActivateTimer(2000, 0, BC_BallEnd);} + if (game_settings[BCset_InstalledBalls] == 1) { // single ball game -> no ball through ramp + BC_BallEnd(0);} + else { + ActivateSolenoid(30, game_settings[BCset_OutholeKicker]); // put ball in trunk + ActivateTimer(2000, 0, BC_BallEnd);}} else { ActivateTimer(2000, 0, BC_ClearOuthole);}}} // come back in 2s if outhole is blocked @@ -486,53 +487,57 @@ void BC_HandleLock(byte Balls) { void BC_BallEnd(byte Event) { byte BallsInTrunk = BC_CountBallsInTrunk(); - if ((BallsInTrunk == 5)||(BallsInTrunk < game_settings[BCset_InstalledBalls]+1-Multiballs-InLock)) { - InLock = 0; -// if (Multiballs == 1) { -// for (i=0; i<3; i++) { // Count your locked balls here -// if (Switch[41+i]) { -// InLock++;}}} - WriteLower(" BALL ERROR "); - if (QuerySwitch(game_settings[BCset_OutholeSwitch])) { // ball still in outhole? - ActA_BankSol(game_settings[BCset_OutholeKicker]); // make the coil a bit stronger - ActivateTimer(2000, Event, BC_BallEnd);} // and come back in 2s - else { - if (Event < 11) { // have I been here already? - Event++; - ActivateTimer(1000, Event, BC_BallEnd);} // if not try again in 1s - else { // ball may be still in outhole - BlockOuthole = false; - Event = 0; - BC_ClearOuthole(0);}}} + if (game_settings[BCset_InstalledBalls] == 1) { // single ball game -> no ball through ramp + BlinkScore(0); // stop score blinking + BC_BallEnd2(1);} // add bonus count here and start BallEnd2 afterwards else { - switch (Multiballs) { - case 3: // goto 2 ball multiball - Multiballs = 2; - if (BallsInTrunk != 1) { // not 1 ball in trunk - ActivateTimer(1000, 0, BC_BallEnd);} // check again later - else { - BlockOuthole = false;} // remove outhole block - break; - case 2: // end multiball - Multiballs = 1; - if (BallsInTrunk == game_settings[BCset_InstalledBalls]) { // all balls in trunk? - ActivateTimer(1000, 0, BC_BallEnd);} - else { - BlockOuthole = false;} // remove outhole block - break; - case 1: // end of ball - if (BallsInTrunk + InLock != game_settings[BCset_InstalledBalls]) { - WriteUpper(" COUNT ERROR "); - InLock = 0; -// for (i=0; i<3; i++) { // check how many balls are on the ball ramp -// if (Switch[41+i]) { -// InLock++;}} - ActivateTimer(1000, 0, BC_BallEnd);} + if ((BallsInTrunk == 5)||(BallsInTrunk < game_settings[BCset_InstalledBalls]+1-Multiballs-InLock)) { + InLock = 0; + // if (Multiballs == 1) { + // for (i=0; i<3; i++) { // Count your locked balls here + // if (Switch[41+i]) { + // InLock++;}}} + WriteLower(" BALL ERROR "); + if (QuerySwitch(game_settings[BCset_OutholeSwitch])) { // ball still in outhole? + ActA_BankSol(game_settings[BCset_OutholeKicker]); // make the coil a bit stronger + ActivateTimer(2000, Event, BC_BallEnd);} // and come back in 2s else { - LockedBalls[Player] = 0; - BlinkScore(0); // stop score blinking - BC_BallEnd2(BallsInTrunk); // add bonus count here and start BallEnd2 afterwards - }}}} + if (Event < 11) { // have I been here already? + Event++; + ActivateTimer(1000, Event, BC_BallEnd);} // if not try again in 1s + else { // ball may be still in outhole + BlockOuthole = false; + Event = 0; + BC_ClearOuthole(0);}}} + else { + switch (Multiballs) { + case 3: // goto 2 ball multiball + Multiballs = 2; + if (BallsInTrunk != 1) { // not 1 ball in trunk + ActivateTimer(1000, 0, BC_BallEnd);} // check again later + else { + BlockOuthole = false;} // remove outhole block + break; + case 2: // end multiball + Multiballs = 1; + if (BallsInTrunk == game_settings[BCset_InstalledBalls]) { // all balls in trunk? + ActivateTimer(1000, 0, BC_BallEnd);} + else { + BlockOuthole = false;} // remove outhole block + break; + case 1: // end of ball + if (BallsInTrunk + InLock != game_settings[BCset_InstalledBalls]) { + WriteUpper(" COUNT ERROR "); + InLock = 0; + // for (i=0; i<3; i++) { // check how many balls are on the ball ramp + // if (Switch[41+i]) { + // InLock++;}} + ActivateTimer(1000, 0, BC_BallEnd);} + else { + LockedBalls[Player] = 0; + BlinkScore(0); // stop score blinking + BC_BallEnd2(BallsInTrunk); // add bonus count here and start BallEnd2 afterwards + }}}}} void BC_BallEnd2(byte Balls) { if (BallWatchdogTimer) { diff --git a/BlackKnight.ino b/BlackKnight.ino index 0059e63..418e175 100755 --- a/BlackKnight.ino +++ b/BlackKnight.ino @@ -336,6 +336,7 @@ void BK_AttractNumCycle(byte Step) { Timer0 = ActivateTimer(3000, Step, BK_AttractNumCycle);} // come back for the next 'page' void BK_AttractDisplayCycle(byte Step) { + static byte Count = 0; static byte Timer0 = 0; static byte Timer1 = 0; static byte Timer2 = 0; @@ -360,15 +361,33 @@ void BK_AttractDisplayCycle(byte Step) { RemoveBlinkLamp(6); return; case 1: // attract mode title 'page' - WriteUpper2(" THE "); - Timer1 = ActivateTimer(50, 5, BK_AttractDisplayCycle); - Timer3 = ActivateTimer(900, 7, BK_AttractDisplayCycle); - WriteLower2(" BLACK KNIGHT "); - Timer2 = ActivateTimer(1400, 6, BK_AttractDisplayCycle); - if (NoPlayers) { // if there were no games before skip the next step - Step++;} + if (Count == 2) { + if (APC_settings[Volume]) { // system set to digital volume control? + analogWrite(VolumePin,255-APC_settings[Volume]);} // adjust PWM to volume setting + else { + digitalWrite(VolumePin,HIGH);} // turn off the digital volume control + Count++; + Step = 0; + Timer0 = 0; + ShowLampPatterns(0); // stop lamp animations + BK_AttractDisplayCycle(0); // stop display animations + for (byte i=0; i< 8; i++) { + LampColumns[i] = 0;} + LampPattern = LampColumns; + BK_RulesDisplay(1); + Count = 0; + return;} else { - Step = 3;} + Count++; + WriteUpper2(" THE "); + Timer1 = ActivateTimer(50, 5, BK_AttractDisplayCycle); + Timer3 = ActivateTimer(900, 7, BK_AttractDisplayCycle); + WriteLower2(" BLACK KNIGHT "); + Timer2 = ActivateTimer(1400, 6, BK_AttractDisplayCycle); + if (NoPlayers) { // if there were no games before skip the next step + Step++;} + else { + Step = 3;}} break; case 2: // show scores of previous game WriteUpper2(" "); // erase display @@ -448,10 +467,11 @@ void BK_AttractModeSW(byte Event) { // Attract Mode switch beh RemoveBlinkLamp(6); // stop blinking of Highest Score lamp StrobeLights(0); ShowLampPatterns(0); // stop lamp animations - if (APC_settings[0]) { // check display setting - BK_AttractNumCycle(0);} // stop Sys7 standard animation - else { // it's the 4Alpha + Credit display - BK_AttractDisplayCycle(0);} // stop animation + if (APC_settings[0]) { // check display setting + BK_AttractNumCycle(0);} // stop Sys7 standard animation + else { // it's the 4Alpha + Credit display + BK_AttractDisplayCycle(0);} // stop animation + BK_RulesDisplay(0); BK_TestMode_Enter(); break; case 3: // start game @@ -468,6 +488,7 @@ void BK_AttractModeSW(byte Event) { // Attract Mode switch beh BK_AttractNumCycle(0);} // stop Sys7 standard animation else { // it's the 4Alpha + Credit display BK_AttractDisplayCycle(0);} // stop animation + BK_RulesDisplay(0); if (APC_settings[Volume]) { // system set to digital volume control? analogWrite(VolumePin,255-APC_settings[Volume]);} // adjust PWM to volume setting else { @@ -934,12 +955,15 @@ void BK_GameMain(byte Event) { // game switch events else { if (Multiballs > 1) { // multiball running? if (game_settings[BK_MultiballJackpot]) { - BK_Jackpot(2);} - BK_GiveMultiballs(1);} // eject ball with some ado + BK_Jackpot(2); + ActivateTimer(2000, 8, DelaySolenoid); + break;} + else { + BK_GiveMultiballs(1);}} // eject ball with some ado else { StopPlayingMusic();} - PlaySound(50, "0_31.snd"); - ActivateTimer(1000, 8, DelaySolenoid);}} // eject ball + PlaySound(50, "0_31.snd"); + ActivateTimer(1000, 8, DelaySolenoid);}} // eject ball break; case 25: case 26: // lower left drop targets @@ -1769,6 +1793,7 @@ void BK_Jackpot(byte State) { KillTimer(Timer); Timer = 0;} PrevState = 0; + AfterSound = BK_PlayBgMusic; break; case 1: // light lower eject hole if (!PrevState) { @@ -2081,6 +2106,241 @@ void BK_HandleVolumeSetting(bool change) { PlayMusic(50, "BK_M01.snd");} analogWrite(VolumePin,255-APC_settings[Volume]-SettingsPointer[AppByte]);}} // adjust PWM to volume setting +void BK_RulesEffect(byte State) { + const byte Pattern[22] = {4,0,4,8,12,9,12,11,44,11,108,11,124,43,125,59,125,187,127,187,127,191}; + for (byte i=0; i<15; i++) { + if (i == 7) { + continue;} + if (!(*(DisplayUpper+2+2*i)) && !(*(DisplayUpper+3+2*i))) { + continue;} + if (State < 11) { + *(DisplayUpper+2+2*i) = *(DisplayUpper+2+2*i) | Pattern[2*State]; + *(DisplayUpper+3+2*i) = *(DisplayUpper+3+2*i) | Pattern[2*State+1];} + else { + *(DisplayUpper+2+2*i) = *(DisplayUpper+2+2*i) & (255 - Pattern[2*(State-11)]); + *(DisplayUpper+3+2*i) = *(DisplayUpper+3+2*i) & (255 - Pattern[2*(State-11)+1]);}} + if (State < 22) { + ActivateTimer(30, State+1, BK_RulesEffect);}} + +void BK_RulesDisplay(byte State) { + static byte Timer = 0; + switch(State) { + case 0: + if (Timer) { + KillTimer(Timer);} + Timer = 0; + StopAllBlinkLamps(); + ReleaseSolenoid(11); + break; + case 1: + if (Timer) { + return;} + ActivateSolenoid(0, 11); + /* no break */ + case 3: + case 5: + case 7: + WriteUpper(" BLACK KNIGHT "); + WriteLower("RULES-- "); + PlaySound(50, "0_6f.snd"); + Timer = ActivateTimer(300, State+1, BK_RulesDisplay); + break; + case 9: + WriteUpper(" BLACK KNIGHT "); + WriteLower("RULES-- "); + PlaySound(50, "0_6f.snd"); + Timer = ActivateTimer(2000, State+1, BK_RulesDisplay); + break; + case 2: + case 4: + case 6: + case 8: + case 10: + WriteUpper(" "); + WriteLower(" "); + Timer = ActivateTimer(300, State+1, BK_RulesDisplay); + break; + case 11: + digitalWrite(VolumePin,HIGH); // mute sound + WriteUpper(" DROP TARGETS"); + WriteLower(" "); + for (byte i=0; i<4; i++) { + AddBlinkLamp(17+i, 100);} + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 12: + WriteUpper(" LITE "); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 13: + WriteUpper(" ARROWS AND "); + for (byte i=0; i<3; i++) { + AddBlinkLamp(25+i, 100); + AddBlinkLamp(29+i, 100); + AddBlinkLamp(33+i, 100); + AddBlinkLamp(37+i, 100);} + for (byte i=0; i<4; i++) { + RemoveBlinkLamp(17+i);} + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 14: + WriteUpper(" MAGNA SAVE "); + *(DisplayUpper+13*2) = 128 | *(DisplayUpper+13*2); + *(DisplayUpper+13*2+1) = 64 | *(DisplayUpper+13*2+1); // add a dot in column 12 + AddBlinkLamp(9, 100); + AddBlinkLamp(10, 100); + for (byte i=0; i<3; i++) { + RemoveBlinkLamp(25+i); + RemoveBlinkLamp(29+i); + RemoveBlinkLamp(33+i); + RemoveBlinkLamp(37+i);} + Timer = ActivateTimer(2700, State+1, BK_RulesDisplay); + break; + case 15: + Timer = ActivateTimer(800, State+1, BK_RulesDisplay); + BK_RulesEffect(0); + break; + case 16: + WriteUpper(" ALL UPPER "); + for (byte i=0; i<3; i++) { + AddBlinkLamp(33+i, 100); + AddBlinkLamp(37+i, 100);} + RemoveBlinkLamp(9); + RemoveBlinkLamp(10); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 17: + WriteUpper(" OR LOWER "); + WriteLower(" ARROWS "); + for (byte i=0; i<3; i++) { + AddBlinkLamp(25+i, 100); + AddBlinkLamp(29+i, 100); + RemoveBlinkLamp(33+i); + RemoveBlinkLamp(37+i);} + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 18: + WriteUpper(" LITE "); + WriteLower(" "); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 19: + WriteUpper(" EXTRA BALL "); + *(DisplayUpper+14*2) = 128 | *(DisplayUpper+14*2); + *(DisplayUpper+14*2+1) = 64 | *(DisplayUpper+14*2+1); // add a dot in column 13 + AddBlinkLamp(22, 100); + AddBlinkLamp(41, 100); + for (byte i=0; i<3; i++) { + RemoveBlinkLamp(25+i); + RemoveBlinkLamp(29+i);} + Timer = ActivateTimer(2700, State+1, BK_RulesDisplay); + break; + case 20: + Timer = ActivateTimer(800, State+1, BK_RulesDisplay); + BK_RulesEffect(0); + break; + case 21: + WriteUpper(" LOCK 2 BALLS "); + AddBlinkLamp(24, 100); + RemoveBlinkLamp(22); + RemoveBlinkLamp(41); + BK_LockChaseLight(1); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 22: + WriteUpper(" FOR DOUBLE "); + AddBlinkLamp(28, 100); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 23: + WriteUpper(" AND 3 BALLS "); + RemoveBlinkLamp(24); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 24: + WriteUpper("FOR 3X SCORE "); + *(DisplayUpper+14*2) = 128 | *(DisplayUpper+14*2); + *(DisplayUpper+14*2+1) = 64 | *(DisplayUpper+14*2+1); // add a dot in column 13 + AddBlinkLamp(32, 100); + RemoveBlinkLamp(28); + BK_LockChaseLight(0); + TurnOffLamp(21); + TurnOffLamp(40); + TurnOffLamp(42); + Timer = ActivateTimer(2700, State+1, BK_RulesDisplay); + break; + case 25: + if (game_settings[BK_MultiballJackpot]) { + Timer = ActivateTimer(800, State+1, BK_RulesDisplay);} + else { + Timer = ActivateTimer(800, 42, BK_RulesDisplay);} + BK_RulesEffect(0); + break; + case 26: + WriteUpper(" DURING "); + RemoveBlinkLamp(32); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 27: + WriteUpper(" MULTIBALL "); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 28: + WriteUpper(" EJECT HOLE "); + AddBlinkLamp(24, 100); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 29: + WriteUpper(" LITES JACKPOT"); + *(DisplayUpper+15*2) = 128 | *(DisplayUpper+15*2); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + Timer = ActivateTimer(2700, State+1, BK_RulesDisplay); + break; + case 30: + Timer = ActivateTimer(800, State+1, BK_RulesDisplay); + BK_RulesEffect(0); + break; + case 31: + WriteUpper(" SHOOT LOCK "); + RemoveBlinkLamp(24); + BK_LockChaseLight(1); + Timer = ActivateTimer(2500, State+1, BK_RulesDisplay); + break; + case 33: + case 35: + case 37: + case 39: + WriteUpper(" FOR "); + Timer = ActivateTimer(500, State+1, BK_RulesDisplay); + break; + case 32: + case 34: + case 36: + case 38: + case 40: + WriteUpper(" FOR "); + DisplayScore(2, game_settings[BK_MultiballJackpot]*500000); + Timer = ActivateTimer(500, State+1, BK_RulesDisplay); + break; + case 41: + *(DisplayUpper+15*2) = 128 | *(DisplayUpper+15*2); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + Timer = ActivateTimer(2700, State+1, BK_RulesDisplay); + break; + case 42: + Timer = ActivateTimer(800, State+1, BK_RulesDisplay); + BK_LockChaseLight(0); + TurnOffLamp(21); + TurnOffLamp(40); + TurnOffLamp(42); + BK_RulesEffect(0); + break; + case 43: + Timer = 0; + ReleaseSolenoid(11); + BK_AttractMode(); + break;}} + // Test mode void BK_TestMode_Enter() { diff --git a/DOC/BlackKnight.md b/DOC/BlackKnight.md index a4127bb..9791866 100644 --- a/DOC/BlackKnight.md +++ b/DOC/BlackKnight.md @@ -48,7 +48,7 @@ The same needs to be done for the High Score table feature. In this case the nam | 7 | Restore Default | - | - | - | No setting - restores the default settings | | 8 | Exit Settings | - | - | - | No setting - exits the settings mode and writes the new setting to an SD card if present | -Check the [settings page](https://github.com/AmokSolderer/APC/blob/V00.31/DOC/Settings.md#using-the-settings-menu) if you're not sure how to use these game settings. +Check the [settings page](https://github.com/AmokSolderer/APC/blob/master/DOC/Settings.md#using-the-settings-menu) if you're not sure how to use these game settings. ## Things to do * The Jackpot still needs some sound effects. diff --git a/DOC/Changes.md b/DOC/Changes.md index 2d7a8f2..557805a 100644 --- a/DOC/Changes.md +++ b/DOC/Changes.md @@ -1,5 +1,25 @@ # APC News and Changelog +## December 2023 + +### New SW Version V1.00 + +* Exception rules for System 3 Disco Fever have been added. [Sound files for Disco Fever are also available](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) +* Exception rules for System 6 Alien Poker have been added. [Sound files for Alien Poker are also available](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) +* Bugfix -> if a sound/music file was invoked immediately after another, the first one was dropped regardless of the priority. +* Bugfix -> Controlling the original audio boards of System 3 - 6 games has been fixed. +* Support for single ball games added to BaseCode +* Solenoid latches are updated every ms instead of on request which will revoke any accidental state change of solenoids. This can happen in heavily distorted environments when the distortion spikes are strong enough to trigger the latches. Distortion levels like these usually mean that something is wrong in your machine, e.g. a broken free-wheeling diode can cause this. +* System watchdog implemented. If the timer interrupt controlling the HW should not occur for 2s the system will restart. +* System 3 game adjustments implemented. You can now use the settings system of the APC to do the adjustments of System 3 games. +* New command StopAllBlinkLamps() implemented + +### Misc + +* Page for [installation frames](https://github.com/AmokSolderer/APC/blob/master/DOC/Frames.md) started. These make it easier to mount an APC in your backbox. + +* PinMame Sound how-tos added to make it more clear what to do to let the APC play the original sounds + ## March 2023 ### New HW Version V3.1 diff --git a/DOC/Frames.md b/DOC/Frames.md new file mode 100644 index 0000000..ed5f5a3 --- /dev/null +++ b/DOC/Frames.md @@ -0,0 +1,48 @@ +# Installation frames + +The APC board is a lot smaller than the boards it replaces. That means you cannot use the fixtures of the original boards to attach the APC to, which makes affixing an APC board in your backbox a bit cumbersome. + +The installation frames on this page offer a solution to overcome this problem. Depending on the game generation they consist of two to four parts to build a frame and four legs. The legs are mounted below the attachment holes of the APC. They have a double purpose as they are mainly hollow but the hole has a cross like shape and can therefore be used as a thread for a 2mm woodscrew. + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/FrameExample.JPG) + +## System 3 - 7 + +The left side of the System 7 frame is split into two parts to be also suited for smaller printers. +The lower rail which holds the original CPU board must be removed for this to fit. + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/FrameSys7.JPG) + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/FrameSys7_2.jpg) + +The STL files for printing can be found [here](https://github.com/AmokSolderer/APC/blob/master/DOC/Hardware/InstallationFrames/System7_stl.zip) + +## System 9 + +Note that the two parts of the System 9 frame are not identical as the APC board must be shifted a bit to the right to avoid a metal mounting post of the backbox. + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/FrameSys9.JPG) + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/CometLED.jpg) + +The STL files for printing can be found [here](https://github.com/AmokSolderer/APC/blob/master/DOC/Hardware/InstallationFrames/System9_stl.zip) + +## System 11 without interconnect board + +This frame is for the early System11 games without an interconnect board. It consists of four parts as the right side is split into two parts to be also suited for smaller printers. + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/FrameSys11a.JPG) + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/APC_Pinbot.JPG) + +The STL files for printing can be found [here](https://github.com/AmokSolderer/APC/blob/master/DOC/Hardware/InstallationFrames/System11_stl.zip) + +## System 11 with interconnect board + +This frame is for the later System11 games with an interconnect board. It consists of four parts as the lower side is split into two parts to be also suited for smaller printers. + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/FrameSys11c.JPG) + +![SD slot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/APC_Rollergames.JPG) + +The STL files for printing can be found [here](https://github.com/AmokSolderer/APC/blob/master/DOC/Hardware/InstallationFrames/System11c_stl.zip) diff --git a/DOC/Hardware/APC_FabricationFiles_SSOP/APC_cpl_top.csv b/DOC/Hardware/APC_FabricationFiles_SSOP/APC_cpl_top.csv index 84f47a1..c7d12bf 100644 --- a/DOC/Hardware/APC_FabricationFiles_SSOP/APC_cpl_top.csv +++ b/DOC/Hardware/APC_FabricationFiles_SSOP/APC_cpl_top.csv @@ -31,13 +31,9 @@ P13,144.145mm,-154.305mm,top,0.0 P2,179.959mm,-154.305mm,top,0.0 C9,193.548mm,-235.966mm,top,180.0 R21,124.46mm,-173.81mm,top,90.0 -1PIN,213.995mm,-19.05mm,top,0.0 -1PIN,20.32mm,-19.05mm,top,0.0 U14,38.1mm,-92.075mm,top,270.0 U21,210.82mm,-230.505mm,top,270.0 C11,203.835mm,-229.87mm,top,180.0 -1PIN,213.995mm,-247.65mm,top,0.0 -1PIN,20.32mm,-247.65mm,top,0.0 C2,192.659mm,-223.012mm,top,90.0 P7,149.225mm,-177.165mm,top,90.0 Q5,179.705mm,-161.29mm,top,90.0 @@ -48,7 +44,6 @@ U19,124.46mm,-222.25mm,top,0.0 Q7,125.73mm,-178.79822mm,top,270.0 Q8,35.56mm,-191.77mm,top,270.0 P3,29.845mm,-190.5mm,top,0.0 -P10,159.385mm,-144.78mm,top,180.0 P12,172.72mm,-39.37mm,top,270.0 1J8,22.86mm,-166.37mm,top,90.0 P1,161.925mm,-154.305mm,top,0.0 diff --git a/DOC/Hardware/InstallationFrames/System11_stl.zip b/DOC/Hardware/InstallationFrames/System11_stl.zip new file mode 100644 index 0000000..43be571 Binary files /dev/null and b/DOC/Hardware/InstallationFrames/System11_stl.zip differ diff --git a/DOC/Hardware/InstallationFrames/System11c_stl.zip b/DOC/Hardware/InstallationFrames/System11c_stl.zip new file mode 100644 index 0000000..e290efc Binary files /dev/null and b/DOC/Hardware/InstallationFrames/System11c_stl.zip differ diff --git a/DOC/Hardware/InstallationFrames/System7_stl.zip b/DOC/Hardware/InstallationFrames/System7_stl.zip new file mode 100644 index 0000000..64baf7e Binary files /dev/null and b/DOC/Hardware/InstallationFrames/System7_stl.zip differ diff --git a/DOC/Hardware/InstallationFrames/System9_stl.zip b/DOC/Hardware/InstallationFrames/System9_stl.zip new file mode 100644 index 0000000..6262b88 Binary files /dev/null and b/DOC/Hardware/InstallationFrames/System9_stl.zip differ diff --git a/DOC/LisyDebug.md b/DOC/LisyDebug.md index 8c8a548..6b4c59d 100644 --- a/DOC/LisyDebug.md +++ b/DOC/LisyDebug.md @@ -2,6 +2,8 @@ This page covers just the APC relevant basics. In some cases it might be necessary to read the corresponding chapters in the [Lisy manual](https://lisy.dev/documentation.html). +Note that the following debug features are only available while using the normal Lisy version. Lisy Embedded does not include these features. + The basic mode of Lisy is selected by the Lisy_Jumpers P12 which are depicted below. ![Lisy Jumper](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/LisyJumper.png) diff --git a/DOC/PICS/APC_Pinbot.JPG b/DOC/PICS/APC_Pinbot.JPG index 07550e8..c0566ea 100644 Binary files a/DOC/PICS/APC_Pinbot.JPG and b/DOC/PICS/APC_Pinbot.JPG differ diff --git a/DOC/PICS/APC_Rollergames.JPG b/DOC/PICS/APC_Rollergames.JPG index affe3ba..a96187c 100644 Binary files a/DOC/PICS/APC_Rollergames.JPG and b/DOC/PICS/APC_Rollergames.JPG differ diff --git a/DOC/PICS/Audacity.png b/DOC/PICS/Audacity.png new file mode 100644 index 0000000..54d7028 Binary files /dev/null and b/DOC/PICS/Audacity.png differ diff --git a/DOC/PICS/CometLED.jpg b/DOC/PICS/CometLED.jpg index 32bfd98..0550e3f 100644 Binary files a/DOC/PICS/CometLED.jpg and b/DOC/PICS/CometLED.jpg differ diff --git a/DOC/PICS/FrameExample.JPG b/DOC/PICS/FrameExample.JPG new file mode 100644 index 0000000..71e1d3f Binary files /dev/null and b/DOC/PICS/FrameExample.JPG differ diff --git a/DOC/PICS/FrameSys11a.JPG b/DOC/PICS/FrameSys11a.JPG new file mode 100644 index 0000000..f6698af Binary files /dev/null and b/DOC/PICS/FrameSys11a.JPG differ diff --git a/DOC/PICS/FrameSys11c.JPG b/DOC/PICS/FrameSys11c.JPG new file mode 100644 index 0000000..5ea1ac4 Binary files /dev/null and b/DOC/PICS/FrameSys11c.JPG differ diff --git a/DOC/PICS/FrameSys7.JPG b/DOC/PICS/FrameSys7.JPG new file mode 100644 index 0000000..456a43a Binary files /dev/null and b/DOC/PICS/FrameSys7.JPG differ diff --git a/DOC/PICS/FrameSys7_2.jpg b/DOC/PICS/FrameSys7_2.jpg new file mode 100644 index 0000000..9e9d739 Binary files /dev/null and b/DOC/PICS/FrameSys7_2.jpg differ diff --git a/DOC/PICS/FrameSys9.JPG b/DOC/PICS/FrameSys9.JPG new file mode 100644 index 0000000..6c32497 Binary files /dev/null and b/DOC/PICS/FrameSys9.JPG differ diff --git a/DOC/PICS/Pinbot.JPG b/DOC/PICS/Pinbot.JPG new file mode 100644 index 0000000..60c6b16 Binary files /dev/null and b/DOC/PICS/Pinbot.JPG differ diff --git a/DOC/PinMame.md b/DOC/PinMame.md index 4b7e80a..52d56aa 100644 --- a/DOC/PinMame.md +++ b/DOC/PinMame.md @@ -1,70 +1,42 @@ # PinMame Sound -For some of the game generations you could install the original audio board. In this case you cannot do any sound related changes of course. For System 3 - 6, the sound board is controlled by some reserved solenoid drivers, so it will work out of the box. System 7 needs an [adapter](https://github.com/AmokSolderer/APC/blob/master/DOC/Prepare.md#system-7-audio-cable) for connecting the audio board to the HW extension interface of the APC. System 9, 11, 11a and 11b are having some sound related circuitry on the CPU, so their audio boards cannot be used any more. +This page is to guide you through the process of enabling the APC to generate the original sound and music for your pinball machine. +If you're lucky then your machine is listed below which means that someone else has done the work for you. If not then I'm afraid that you have to read the rest of this page also. -Our current setup is not using the Raspberry Pi to generate the PinMame sounds. Instead all sound and music files have to be created once and stored on the SD card of the APC. Of course this requires some work, but we think the benefits are worth it. +# Available Sounds -First of all, the Pi uses the Unix version of PinMame which has severe sound quality issues while the audio quality of the APC is very good. But there're some more advantages in having all sound files on the SD card. One is that you can use the same files for PinMame, MPF or when programming the APC natively. This may not sound like a big deal, but it gives you the opportunity to run your game in PinMame and introduce certain changes by using the [APC API](https://github.com/AmokSolderer/APC/blob/master/DOC/Software/APC_SW_reference.pdf). A description of this can be found on the [Howto page](https://github.com/AmokSolderer/APC/tree/master/DOC/PinMame_howto.md). +At the moment the list of available sound packages is still quite short, but we hope that some of you will do this for their machines also. Of course it would be great if you'd share your sound package with the rest of us. Up to now we haven't found some server space to store the files on, so send me a PM via Pinside or Flippertreff (see the feedback section on the main page) if you want to have one of the sound packages listed below. -A good example is a local fellow who wants his System 3 'Disco Fever' to play songs like 'Saturday Night Fever' additionally to the normal sounds of the machine. We still have to add PinMame support for System 3, but once we have it adding the music should be an easy task. The commands of the APC API can be used to start the music when the game starts and to stop it when it ends. You could even identify certain trigger points to switch to another song and so on. - -To make this possible, it is necessary that PinMame and the APC can play sounds simultaneously which would normally require an additional HW sound mixer which mixes both sound channels together. In order to avoid this we decided to let the APC do the complete sound handling with PinMame just telling him which sound to play when. - -The drawback of this is that you have to extract the music files from PinMame. Furthermore we have to understand how the audio boards work and emulate their behaviour. The first task is easy and if we find some server space to store the files somewhere in the internet then this work must only be done once for each game. For System 3 - 9 games I expect this to be easy anyway, as they have a very limited sound performance. You could use the Audio Debug Mode which is explained later to find out which sounds to extract. - -## Available Sounds - -At the moment the list of available sound packages is still quite short, but we hope that some of you will do this for their machines also. Of course it would be great if you'd share you sound package with the rest of us. Up to now we haven't found some server space to store the files on, so send me a PM via Pinside or Flippertreff (see the feedback section on the main page) if you want to have one of the sound packages listed below. - -|System|Game| Sound File URL|Comments| +|System|Game| Sound Files |Comments| |--|--|--|--| +|3|Disco Fever| available on request| PinMameExceptions might still need some finetuning | +|4|Flash| available on request| | |6|Firepower| Ask Matiou in the Pinside forum | PinMameExceptions also done by Matiou | -|7|Barracora| URL available on request| | -|7|Black Knight| URL available on request| | -|7|Jungle Lord| URL available on request| Works great. Sound samples are also good. | +|6|Alien Poker| available on request | PinMameExceptions might still need some finetuning | +|7|Barracora| available on request | PinMameExceptions might still need some finetuning | +|7|Black Knight| available on request| | +|7|Jungle Lord| available on request| Works great. Sound samples are also good. | |7|Pharaoh| Ask Grangeomatic in the Pinside forum| PinMameExceptions also done by Grangeomatic | -|9|Comet|URL available on request| Works great. Sound samples are also good. | -|11a|Pinbot| URL available on request| Some PinMame sounds are quite bad, e.g. visor opening and closing. May be someone can sample them from the orignal HW.| +|9|Comet|available on request| Works great. Sound samples are also good. | +|11a|Pinbot| available on request| Some PinMame sounds are quite bad, e.g. visor opening and closing. May be someone can sample them from the orignal HW.| |11a|F-14 Tomcat| Ask Snux in the Pinside forum | PinMameExceptions also done by Snux | -|11c|Rollergames| URL available on request| There're problem with this game. For some reason PinMame restarts random music tracks all 5 seconds. The issue doesn't seem to affect the Windows version of PinMame. Please contact me if you have any idea what the root cause might be. | - -## Audio boards - -Understanding the audio boards is the second task at hand. Again I expect this to be easy for games prior to System 11, but it took me roughly a week before I was satified with the sounds of my Pinbot. Of course I started from scratch, so it's going to be easier for the next one doing this, but System 11 features various audio boards and I don't know how much they differ from the Pinbot's. For this reason I'm going to maintain a second list here where all PinMame relevant information about the various systems are listed. -Alas, the sound commands seem to be unique for every machine which requires a slightly different handling of sound commands depending of which machine is selected. The APC SW features a machine specific exception handling which allows you to implement these special audio commands for your machine. A description of this can be found on the [Howto page](https://github.com/AmokSolderer/APC/tree/master/DOC/PinMame_howto.md). - -### System 7 - -Up to now we know standard commands which seem to be identical for System7 audio boards: - -|Command / Hex|Comment| -|--|--| -|2c|Stop Sound| -|7f|Not a real sound command, but used to reset the data bus to the audio board between commands| - -These boards are featuring what I call a sound series. This means that if a certain sound number is called multiple times, a different version of this sound is played (usually with a higher tune). For the APC these sound file names have to be like 0_2a_001, where the leading zero is selecting sound channel 0 (System 7 games use only this channel), 2a being the sound number and 001 the tune of this sound. The latter number is counted up for all tunes available for this sound. -One of these sound series is usually the BG sound. This sound can be interrupted by other sounds but continues afterwards. As the numbers of these sounds are different from game to game they have to be set as an exception for every game. - -### System 11 +|11c|Rollergames| available on request| There're problem with this game. For some reason PinMame restarts random music tracks all 5 seconds. The issue doesn't seem to affect the Windows version of PinMame. Please contact me if you have any idea what the root cause might be. | -The first System11 machines had two independent audio circuits, each containing a processor and sound EPROMS. One of these circuits was located on the main CPU board while the other was on a seperate audio board. -In case of the Pinbot, PinMame distinguishes these boards by using two audio prefixes which can be selected by 00 and 01 in PinMame's 'Sound Command Mode'. Both of these prefixes offer different sound commands. Our current status of what we know about these sound commands is listed below. There're still lots of gaps, but we didn't want to spend too much effort in investigating these boards. Again, we see this more as a community task and there're probably plenty of people out there with a lot more knowledge about this stuff than we have. +# What needs to be done -So if you have additional information to fill these gaps please tell us and we're going to include it. +The fact that you're reading this probably means that your machine is not yet supported. So let's first summarize what you have to do to change that: -|Prefix|Command / Hex|Comment| -|--|--|--| -|00|00|Stop Sound| -|01|00|Stop Music| -|01|01 - 08|Looped music tracks, some of them have an intro before the looping part begins| -|01|40 - 42|Transistions and endings for looped music| -|01|7f|Stop sounds of prefix 01| -|01|6X|Music volume setting 0x60 is full volume 0x64 is almost nothing| +* Get the sounds. This can be done from the internet, PinMame or by self recording. +* Adjust the amplitude, DC offset and sampling rate if necessary. You need a mono WAV file with 44.1KHz sampling rate +* Ensure the filenames are correct to help the APC find the correct audio file +* Convert them to the APC sound file format +* Put them on the SD card of the APC +* Do the necessary PinMameExceptions to incorporate the special functions of the respective audio board -Prefix 01 plays music and sounds simultaneously. It looks like sound numbers below 0x80 are for music and the rest is for sounds. As the APC has only two independent sound channels, only the music of prefix 01 is played on the APC's music channel while the sounds are being forwarded to the sound channel. +Click on the generation of your game to get to the corresponding how-to page: -Somewhere in System11B all audio circuitry was removed from the CPU board and the functionality was added to the audio board. For these games only the prefix 01 is used by PinMame. +[System 3 - 7 sound how-to](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_3_7.md) -## How to make it work +[System 9 sound how-to](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_9.md) -So much for the theory. Let's proceed to the [Howto page](https://github.com/AmokSolderer/APC/tree/master/DOC/PinMame_howto.md). +[System 11 sound how-to](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_11.md) diff --git a/DOC/PinMameExceptions.md b/DOC/PinMameExceptions.md index 65461e3..c95d2b7 100644 --- a/DOC/PinMameExceptions.md +++ b/DOC/PinMameExceptions.md @@ -1,9 +1,50 @@ # PinMameExceptions -PinMameExceptions are a simple but powerful tool to change the behaviour of your game even though it is controlled by PinMame. Some exceptions are inevitable to make the APC generate the sound for your machine correctly. A description for this is on the [PinMame howto](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame_howto.md) page. +PinMameExceptions are a simple but powerful tool to change the behaviour of your game even though it is controlled by PinMame. Some exceptions are inevitable to make the APC generate the sound for your machine correctly. A description for this is on the [PinMame sound howto](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) page. In this section we'll focus on exceptions that change a game. This can be by just fixing flaws in the original game SW or adding new features. +## Fixing the drop targets of System 3 & 4 games + +PinMame has a timing issue with System 3 & 4 games. For some reason the activation times of some solenoids is too short to trigger them correctly. This is especially problematic with drop targets because they won't reset any more. +Even though it's a PinMame issue we decided to fix this in PinMameExceptions which means you have to do it if your machines shows this problem. + +Let's use the System 3 Disco Fever as an example. In this machine solenoid 2 is for resetting the drop targets. +Normally PinMame sends turn-on and off commands for the solenoids and does therefore control the activation time by itself. In order to increase the length of the activation time, we capture the turn-on command from PinMame and activate the solenoid for a time of our choosing, in this case 80ms. + + case SolenoidActCommand: + if (Command == 2) { // ball release + ActivateSolenoid(80, 2); // FEV drop target reset + return(1);} + +Here it's important to end this case with return(1) as the 1 will cause the APC not to process this command any further, which means it will skip the normal turn-on command for this solenoid. +However, this alone won't do the trick. PinMame will still send the turn-off command for solenoid 2 which will turn it off even though the activation of 80ms time is not over yet. To prevent this from happening we have to tell the APC to ignore all solenoid turn-off commands for solenoid 2: + + case SolenoidRelCommand: + if (Command == 2) { // ignore turn-off command + return(1);} + +We just use return(1) again to tell the APC not no process this command any further. + +The complete PinMameExceptions to increase the activation times of both drop target reset coils look like this: + + byte EX_DiscoFever(byte Type, byte Command){ + switch(Type){ + case SolenoidActCommand: + if (Command == 2) { // ball release + ActivateSolenoid(80, 2); // FEV drop target reset + return(1);} + else if (Command == 3) { // ER drop target reset + ActivateSolenoid(60, 3); // Fix to increase the strength of the ball release + return(1);} + return(0); + case SolenoidRelCommand: + if (Command == 2 || Command == 3) { // ignore turn-off commands for drop target solenoids + return(1);} // ignore it + return(0); + default: + return(0);}} + ## Doing exceptions for the magna save of the Jungle Lord Jungle Lord features timed magna saves which means that the magnets are just activated for as long as the magna save buttons are pressed. @@ -36,7 +77,7 @@ For timed magna saves we have to do the same for the magna save buttons being re ## Improving the System7 ball release -The previous examples were just to improve the emulation of a machine, which is necessary to make your game work correct with PinMame. However, you are not limited to this, but you can also use this to apply changes to a game. +The previous example was just to improve the emulation of a machine, which is necessary to make your game work correct with PinMame. However, you are not limited to this, but you can also use this to apply changes to a game. As an example I want to fix one nasty problem that affected both of my System7 machines. It didn't happen often but regularly that a ball ejected into the plunger lane bounced back from the side rail and into the trunk again. If it got back into the trunk completely the SW would recognize this and eject the ball again. But sometimes the ball didn't really get back into the trunk. Instead it got stuck above the shooter lane feeder and there wasn't much I could do about it except of opening the coin door and operating the feeder manually to push the ball into the shooter lane. I really wonder how these games survived in the pubs with a bug like this. diff --git a/DOC/PinMameSound_11.md b/DOC/PinMameSound_11.md new file mode 100644 index 0000000..8cf3334 --- /dev/null +++ b/DOC/PinMameSound_11.md @@ -0,0 +1,337 @@ +# System 11 + +The first System11 machines had two independent audio circuits, each containing a processor and sound EPROMS. One of these circuits was located on the main CPU board while the other was on a seperate audio board. PinMame distinguishes these boards by using two audio prefixes which can be selected by 00 and 01 in PinMame's 'Sound Command Mode'. Both of these prefixes offer different sound commands. +The table below shows the special commands of the Pinbot audio system that we know about. There might still be some gaps, but it's enough to make the Pinbot work. +So if you have additional information to fill these gaps please tell us and we're going to include it. + +For this documentation I expect the other System 11 machines to work in a similar way. + +|Prefix|Command / Hex|Comment| +|--|--|--| +|00|00|Stop Sound| +|01|00|Stop Music| +|01|01 - 08|Looped music tracks, some of them have an intro before the looping part begins| +|01|40 - 42|Transistions and endings for looped music| +|01|7f|Stop sounds of prefix 01| +|01|6X|Music volume setting 0x60 is full volume 0x64 is almost nothing| + +## Sound file preparation + +There're multiple ways to obtain the original sounds for System 11 machines. Some can be found on the internet, directly recorded from your pinball machine or you can extract them from PinMame. What ever you do, the result should be a mono WAV file with 44.1KHz sampling rate and reasonable amplitude. +If you find the files on the internet you can proceed to the [Audio file conversion](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_11.md#audio-file-conversion) section, as you don't have to extract them from PinMame. + +The method decribed here is the the manual way, there're also more automatic solutions which might save some time. I've never tried them myself, but Mokopin (from the Flippertreff forum) has written some [Instructions for extracting sound files](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSounds.md) which explain the automatic extraction of sound files and the use of Audacity in more detail. + +### Which sounds are required? + +System 11 boards can handle 8 bit sound commands which leads to a possible number of 256 sounds per board minus some control commands, but only a part of these numbers are actually used by a game. +With this manual method we have to extract and preprocess every sound by hand, so we try to keep the number of sounds as low as possible by just extracting those which are actually requested by PinMame. + +When you start from scratch you should play your game with Lisy being in Sound Debug Mode. Please read the [Controlling Lisy page](https://github.com/AmokSolderer/APC/blob/master/DOC/LisyDebug.md) to learn how to do this. + +I'd recommend to do the Williams sound and music test while PinMame is running. This will already give you most if not all of the music numbers and some sounds to start with. Later you'll have to repeat this step or use the [Audio Debug Mode](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_11.md#finding-out-which-sounds-are-still-missing) of the APC to find the remaining missing sounds. + +When you're done press the shutdown switch SW1 on your APC board to make it exit the emulation and store the log file on the SD card. The file will be located on the Pi's SD card in the folder lisy/lisy_m/debug. + +In lisy_m_debug.txt playing a sound looks like + + [461.372965][0.000064] LISY_W sound_handler: board:0 0x79 (121) + [461.372985][0.000020] play soundindex 121 on board 0 + [461.373005][0.000020] USB_write(3 bytes): 0x32 0x01 0x79 + +In this case sound 0x79 of prefix (board) 0 shall be played which means you have to record the sound command 0079 in PinMame. + +The Lisy sound debug log only tells you which sound numbers are needed for your game. Now it's time to extract the corresponding sound files. + +### Manual extraction from PinMame + +For this to work you have to install PinMame32 for Windows as the Unix version has severe sound issues. + +After PinMame is running, press F4 to enter the 'Sound Command Mode' then press DEL for the Manual / Command Mode. + +The following steps have to be repeated for every sound you want to record. That means you have to do it for every sound number you've found in the Lisy sound debug log. +Enter the prefix and the hexadecimal sound number. Then press F5 to start the recording, SPACE to play the sound and F5 again to stop recording. The sound can be found in PinMame's 'wave' folder. +The sound command 00 (with the corresponding prefix) stops the currently playing sound / music. +Not all sound numbers play a sound in PinMame, as some are just controlling the audio board. These commands have to be covered by PinMameExceptions and can be ignored for now. +Remember to rename the files immediately to the correct names to avoid confusion. + +### Preprocessing the WAV files with Audacity + +For some reason the PinMame sounds have a sampling rate of 48KHz and must therefore be converted to the normal sampling rate of 44.1KHz. You can use a free audio editor like Audacity (https://www.audacityteam.org) to adjust the start and end time of your sound sample as well as the sampling rate. +Furthermore, the volume of the sound and music files generated by PinMame differs quite a lot, which means you should also use Audacity to adjust those levels accordingly. + +![Audacity](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/Audacity.png) + +This is the manual way to preprocess a file recorded from PinMame. + +* Open the file with Audacity +* Press the button for the Selection Tool (1) and select the part of the sound you want to use +* Delete the rest (2) +* Click inside the cyan box next to the waveform (3) to select the whole track +* Select the 'Tracks -> Sampling rate' menu and set the sampling rate to 44100Hz +* If you have a stereo track, use the 'Tracks -> Mix -> Mix Stereo Down to Mono' menu to get mono +* Select the 'Effect -> Normalize' menu to open the window with the normalizing options +* Select 'Remove DC offset' and 'Normalize peak amplitude to' 0dB. +* Select the 'File -> Export -> Export Selected Audio...' and save it with the 'Signed 16-bit PCM' encoding + +Most of this process can be automatized as explained by Mokopin in his [Instructions for extracting sound files](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSounds.md). + +## Audio file conversion + +Put all preprocessed WAV files in one folder, check that the names are correct and execute the [AudioSaveFolder.pl](https://github.com/AmokSolderer/APC/blob/master/DOC/UsefulSWtools.md) tool. +The resulting .snd files must be copied to the APC's SD card. Please format your card before with a special SD card formatting tool from the SD Association (sdcard.org) to improve the card's access times. + +If you start your game now you should already hear most of the sounds, but everything out of the ordinary like correct music handling and so on won't work correctly. We need to tell the APC about these special sound sommands and how to deal with them. + +## PinMameExceptions for audio + +The APC features a machine specific exception handling, which means that you can manipulate your game even though it is running in PinMame. The exceptions are written in C and are using the commands of the APC SW framework. More information about the framework can be found in [the tutorial](https://github.com/AmokSolderer/APC/blob/master/DOC/GameCodeTutorial.md) and the [APC software reference](https://github.com/AmokSolderer/APC/blob/master/DOC/Software/APC_SW_reference.pdf). + +To enable PinMameExceptions for your machine you have to add a game specific section to the PinMameExceptions.ino file and recompile the SW. +You can manipulate sound, lamp, switch, display and solenoid commands. Some of these expections are necessary to make your machine work correctly, but you can also do improvements or moderate rule changes. + +In the case at hand we're using these exceptions to tell the APC what to do when a certain audio command is issued by PinMame. I'm using the Pinbot as an example here. + +### Setting up PinMameExceptions for the Pinbot + +The following steps have been done by me to make my Pinbot work correctly. That means you can find the code which is described here in PinMameExceptions.ino. + +There's a template named + + byte EX_Blank(byte Type, byte Command) + +in PinMameExceptions.ino to be used as a starting point. So let's create a copy of this and rename it to + + byte EX_Pinbot(byte Type, byte Command) + +In order for the system to use this code section, we have to add it to EX_Init which is at the end of PinMameExceptions.ino and determines which code is used for which machine. The games are being distinguished by their [PinMame game number](https://github.com/AmokSolderer/APC/blob/master/DOC/lisyminigames.csv). Hence we have to add a case 43 for the Pinbot. + + void EX_Init(byte GameNumber) { + switch(GameNumber) { + case 20: // Jungle Lord + EX_EjectSolenoid = 2; // specify eject coil for improved ball release + PinMameException = EX_JungleLord; // use exception rules for Jungle Lord + break; + break; + case 39: // Comet + PinMameException = EX_Comet; // use exception rules for Comet + break; + case 43: // Pinbot + PinMameException = EX_Pinbot; // use exception rules for Pinbot + break; + case 44: // F-14 Tomcat + PinMameException = EX_F14Tomcat; // use exception rules for Tomcat + break; + default: + PinMameException = EX_DummyProcess;}} + +All games not listet here do not have exceptions defined yet, so the exception pointer just points to a dummy process which only plays the standard sounds, but knows no exceptions. + +### Handling ordinary sounds + +As Pinbot uses two audio boards, we need to define a SoundCommandCh1 and a SoundCommandCh2 case in our EX_Pinbot program. Hence you can delete all cases except of these two and the default case. The result should look like this: + + byte EX_Pinbot(byte Type, byte Command){ // use this as a template and an example of how to add your own exceptions + switch(Type){ // usually just a few exception cases are needed, just delete the rest + case SoundCommandCh1: // sound commands for channel 1 + if (Command == 38){ // sound command 0x26 + // enter your special sound command 0x26 here + } + else { + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + case SoundCommandCh2: // sound commands for channel 2 + if (Command == 38){ // sound command 0x26 + // enter your special sound command 0x26 here + } + else { + char FileName[9] = "1_00.snd"; // handle standard music + if (USB_GenerateFilename(2, Command, FileName)) { // create filename and check whether file is present + PlayMusic(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: // use default treatment for undefined types + return(0);}} + +For System11c games with only one audio board you can remove the SoundCommandCh1 case also. +What we have now is a default handler for all the sound numbers that have no exceptions defined. The filenames of these sounds just consist of the channel number (prefix), an underscore and the sound number in hex followed by ".snd". There is a routine called + + byte USB_GenerateFilename(byte Channel, byte Sound, char* FileName) + +which adds the hex code to a given filename and handles the display messages if the audio debug mode is active. It returns a 1 in case the soundfile does exist and a 0 if it doesn't. Is is therefore only necessary to play the sound when the return value has been 1. + +In EX_Blank the 'if' for sound command 38 is just meant as an example to make it clear where exceptions have to be added, but for the Pinbot we can change this to incorporate the Sound Stop command (00) for each channel: + + byte EX_Pinbot(byte Type, byte Command){ // use this as a template and an example of how to add your own exceptions + switch(Type){ // usually just a few exception cases are needed, just delete the rest + if (!Command){ // sound command 0x00 - stop sound + AfterSound = 0; + StopPlayingSound();} + else { + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + case SoundCommandCh2: // sound commands for channel 2 + if (!Command) { // sound command 0x00 - stop music + AfterMusic = 0; + StopPlayingMusic();} + else { + char FileName[9] = "1_00.snd"; // handle standard music + if (USB_GenerateFilename(2, Command, FileName)) { // create filename and check whether file is present + PlayMusic(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: // use default treatment for undefined types + return(0);}} + +Note, that the APC refers to it's two channels as the Sound and Music channel and the commands are named accordingly. Check the [APC software reference](https://github.com/AmokSolderer/APC/blob/master/DOC/Software/APC_SW_reference.pdf) for more information. + +### Handling music + +Most of the music scores of System11 machines consist of an intro and a looping part which is repeated all over. If you want it easy, you could just put the intro and some repetitions of the looping part in one file. This will work in most cases, but if you wait long enough the music will just run out. +In order to prevent this from happening, the intro and the looping part must be in separate files and an exception rule must handle the sequencing. + +Let's use music track 4 from Pinbot as an example. In the SoundCommandCh2 case the following code handles track4: + + else if (Command == 4) { // music track 4 + PlayMusic(50, "1_04.snd"); // play non looping part of music track + QueueNextMusic("1_04L.snd");} // queue looping part as next music to be played + +When audio channel 2 receives command 4 then the intro 1_04.snd is played first and the QueueNextMusic command is used to queue the looping part 1_04L.snd to be played after the intro has run out. As long as the AfterMusic variable is not set to zero, the looping part will be repeated automatically. + +Such a rule needs to be present for each music score your machine features. + +Note that if you're machine is using vocals as part of the music then these have to be mixed into the music. The reason for this is explained [here](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_11.md#special-cases). + +### Implementing music volume + +System11 machines are reducing the volume of their music track when certain sounds are played. This is done by sending the 0x6X command with the prefix of the music board. 0x60 is the code for full volume and 0x61 and so on for less volume. The granularity might differ for the various System11 generations, so you might want to adapt this for your game. However, for Pinbot 0x64 is already almost inaudible. +The APC handles the volume of the music by the MusicVolume variable. A value of zero means full volume and every count up cuts the amplitude by half. Hence, the corresponding code looks like this: + + else if (Command > 95 && Command < 100) { // music volume command 0x6X + MusicVolume = Command - 96;} + +### Dealing with priorities + +Normally a sound is stopped by another sound being requested for the same channel. This is fine for the majority of the sounds, but sounds like speech are more important than others and we don't want the APC to cut them. Hence, we have to define priorities for those sounds which are more (or less) important than others. +In Pinbot's case all sound numbers higher than 128 requested with prefix 0 are callouts and are therefore called with a priority of 51 instead of the 50 which is used for the rest: + + else { // proceed with standard sound handling + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + if (Command < 128) { // play speech with a higher priority + PlaySound(50, (char*) FileName);} + else { + PlaySound(51, (char*) FileName);}}} + +Check the [APC software reference](https://github.com/AmokSolderer/APC/blob/master/DOC/Software/APC_SW_reference.pdf) for more information about priorities. + +### Dealing with unknown commands + +There're still several commands we don't know the meaning of. If you find out what they do please drop us a note, but for the time being we can just ignore them (which worked quite well so far). So for every command that doesn't produce a sound in PinMame, just add a line like + + else if (Command > 29 && Command < 48) { } // ignore unknown sound commands 0x1d to 0x30 + +to prevent them from causing 'File not found' messages on your display. + +### The result + +The final result is shown below. It contains the PinMameExceptions needed to make the audio for Pinbot work. + + byte EX_Pinbot(byte Type, byte Command){ + switch(Type){ + case SoundCommandCh1: // sound commands for channel 1 + if (!Command){ // sound command 0x00 - stop sound + AfterSound = 0; + StopPlayingSound();} + else if (Command == 85) { } // ignore sound command 0x55 + else if (Command == 105) { } // ignore strange misplaced sound during multiball + else if (Command == 170) { } // ignore sound command 0xaa + else if (Command == 255) { } // ignore sound command 0xff + else { // proceed with standard sound handling + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + if (Command < 128) { // play speech with a higher priority + PlaySound(50, (char*) FileName);} + else { + PlaySound(51, (char*) FileName);}}} + return(0); // return number not relevant for sounds + case SoundCommandCh2: // sound commands for channel 2 + if (!Command) { // sound command 0x00 - stop music + AfterMusic = 0; + StopPlayingMusic();} + else if (Command == 127) { // sound command 0x7f - stop sound + AfterSound = 0; + StopPlayingSound();} + else if (Command > 29 && Command < 48) { } // ignore unknown sound commands 0x1d to 0x30 + else if (Command > 79 && Command < 89) { } // ignore unknown sound commands 0x4f to 0x59 + else if (Command > 95 && Command < 100) { // music volume command 0x6X + MusicVolume = Command - 96;} + else if (Command == 170) { } // ignore unknown sound command 0xaa + else if (Command == 255) { } // ignore unknown sound command 0xff + else if (Command == 1) { // music track 1 + PlayMusic(50, "1_01L.snd"); // play music track + QueueNextMusic("1_01L.snd");} // track is looping so queue it also + else if (Command == 2) { // music track 2 + PlayMusic(50, "1_02.snd"); // play non looping part of music track + QueueNextMusic("1_02L.snd");} // queue looping part as next music to be played + else if (Command == 3) { // music track 3 + PlayMusic(50, "1_03L.snd"); // play music track + QueueNextMusic("1_03L.snd");} // track is looping so queue it also + else if (Command == 4) { // music track 4 + PlayMusic(50, "1_04.snd"); // play non looping part of music track + QueueNextMusic("1_04L.snd");} // queue looping part as next music to be played + else if (Command == 5) { // music track 5 + PlayMusic(50, "1_05.snd"); // play non looping part of music track + QueueNextMusic("1_04L.snd");} // queue looping part as next music to be played + else if (Command == 6) { // music track 6 + PlayMusic(50, "1_06.snd"); // play non looping part of music track + QueueNextMusic("1_06L.snd");} // queue looping part as next music to be played + else if (Command == 8) { // music track 8 + PlayMusic(50, "1_08.snd"); // play non looping part of music track + QueueNextMusic("1_01L.snd");} // queue looping part as next music to be played + else if (Command == 10) { // music track 0xa + PlayMusic(50, "1_0a.snd"); // play non looping part of music track + QueueNextMusic("1_02L.snd");} // queue looping part as next music to be played + else if (Command == 65) { // music track 0x41 + PlayMusic(50, "1_01L.snd"); // play non looping part of music track + QueueNextMusic("1_01L.snd");} // queue looping part as next music to be played + else if (Command == 66) { // music track 0x42 + PlayMusic(50, "1_0a.snd"); // play non looping part of music track + QueueNextMusic("1_02L.snd");} // queue looping part as next music to be played + else if (Command == 148) { // music track 0x94 + PlayMusic(50, "1_94.snd"); // play non looping part of music track + QueueNextMusic("1_94L.snd");} // queue looping part as next music to be played + else { + char FileName[9] = "1_00.snd"; // handle standard sound + if (USB_GenerateFilename(2, Command, FileName)) { // create filename and check whether file is present + if (Command < 128) { // play only music on the music channel + AfterMusic = 0; // stop looping music + PlayMusic(50, (char*) FileName);} // play on the music channel + else { + PlaySound(50, (char*) FileName);}}} // play on the sound channel + return(0); // return number not relevant for sounds + default: // use default treatment for undefined types + return(0);}} // no exception rule found for this type so proceed as normal + +### Finding out which sounds are still missing + +After you have extracted most of the files, there'll be the point when only a few files are still missing and you need to find out their numbers. Of course you could still scan the Lisy log for unknown sound numbers, but in this stage it might be easier to use the 'Audio Debug Mode' of the APC. +You can activate this mode in the [game settings](https://github.com/AmokSolderer/APC/blob/master/DOC/Settings.md#game-settings-in-remote-control-mode). + +In Audio Debug mode the lower display(s) are used for audio information. The Player 3 display (or the left part of the lower display for BK2K type displays) shows information for sound prefix 00 and the Player 4 display (right part of lower display for BK2K) does the same for prefix 01. If the requested sound is found on the SD card, it's hex number is shown in the left side of the corresponding display and the sound is played normally. If the sound file is missing it's hex number is shown on the right side of the corresponding display which makes it easy to find missing sound files. +As the pre System11 displays cannot show letters, the corresponding sound numbers are shown in decimal values when this kind of display is selected. + +By that you can just play your game and only if the number of a missing sound file pops up on the right side of your display you note it down and add it later. + +## Sound problems + +Most sound problems like lags and stuttering are caused by the performance of the SD card. Take a look at the [If things don't work](https://github.com/AmokSolderer/APC/blob/master/DOC/Problems.md) section for more. + +## Special cases + +On the old audio boards each specific kind of sound was generated by special circuitry. For example music was done with synthesizer ICs, speech with special DACs and so on. In total this leads to up to five different, but specialized audio channels which were mixed together eventually. +Like all modern audio systems, the APC works differently. It has two channels, but doesn't need special audio ICs as every sound can be produced by each channel. In most cases this works quite well, but in some cases we have to adjust the sound files to prevent some sounds from getting lost. + +A good example is the music of later System11 games which also include vocals. As the old sound board would produce the music and the voices with different circuits, PinMame will send one command for the music and own commands for each part of the vocals. This will lead to a conflict when additional sounds have to be played simultaneously as the APC has just one channel for sounds. On the other hand, the APC doesn't need a seperation between music and vocals, so we can just use Audacity to mix the vocals into the music file. diff --git a/DOC/PinMameSound_3_7.md b/DOC/PinMameSound_3_7.md new file mode 100644 index 0000000..dfeb2c4 --- /dev/null +++ b/DOC/PinMameSound_3_7.md @@ -0,0 +1,238 @@ +# System 3 - 6 + +These machines only have one sound channel which means they can only play one sound at a time. This is also valid for later games featuring some kind of permanent background sound - due to the limitation to one channel this sound needs to be suspended for as long as a foreground sound is played. + +The communication between CPU and audio board is 5 bit wide, which leads to 32 possible sound commands (0x00 - 0x1f). +Not all of these commands have to be sounds, up to now we know of two control commands which seem to be common for System 3 - 6 audio boards: + +|Command / Hex|Comment| +|--|--| +|0c|Stop Sound| +|1f|Not a real sound command, but used to reset the data bus to the audio board between commands| + +Some of these games are featuring what I call a sound series. This means that if a certain sound number is called multiple times, a different version of this sound is played (usually with a higher pitch). For the APC these sound file names have to be named like 0_0a_001, where the leading zero is selecting sound channel 0 (System 3 - 6 games use only this channel), 0a being the sound number in hex format and 001 the pitch of this sound. The latter number is counted up (in decimal format) for all pitches available for this sound. + +# System 7 + +The audio boards haven't changed much between System 3 - 6 and System 7. This means everything written above is also valid for System 7 with two exceptions: +For some reason PinMame adds 32 (hex 20) to each audio command when it's a System 7 game. Hence, the audio commands are in the range from 0x20 to 0x3f and not from 0x00 to 0x1f as for the previous generations. +This changes the 'Stop Sound' Command to 0x2c. +The second exception is that 0x7f is sent for the bus initialization, but we don't care that much about this as the APC needs no initialization and we therefore ignore this command anyway. +Find the special command table for System 7 below: + +|Command / Hex|Comment| +|--|--| +|2c|Stop Sound| +|7f|Not a real sound command, but used to reset the data bus to the audio board between commands| + +## Sound file preparation + +If your game features one or more of the above mentioned sound series then extracting the sounds from PinMame will be quite cumbersome as every pitch of a series has to be recorded and preprocessed. +Luckily all sounds for these machines are available on the internet and only the naming has to be adapted. + +In order to find out the correct sound names you have to install PinMame32 for Windows. +After PinMame is running press F4 to enter the 'Sound Command Mode'. To play a sound you have to enter 7fXX with XX being the sound number in hex format and press the space bar. Try to find a match for each previously downloaded WAV file. If you found one, rename the WAV file to 0_XX.wav with XX being the sound number you just found out. If you found a sound series rename their pitches to 0_XX_001 and increase the 001 for each pitch. +This could be performed with an automatic renaming tool ( eg. Bulk Rename Utility https://www.bulkrenameutility.co.uk). + +The big advantage of the downloaded files is that they usually don't need any preprocessing as their amplitude and sampling rate are already fine (unlike those derived from PinMame), so you can directly convert them. + +## Audio file conversion + +Put all renamed WAV files in one folder and execute the [AudioSaveFolder.pl](https://github.com/AmokSolderer/APC/blob/master/DOC/UsefulSWtools.md) tool. +The resulting .snd files must be copied to the APC SD card. Please format your card before with a special SD card formatting tool from the SD Association (sdcard.org) to improve the card's access times. + +If you start your game now you should already hear most of the sounds, but everything out of the ordinary like sound series and so on won't work correctly. We need to tell the APC about these special sound sommands and how to deal with them. + +## PinMameExceptions for audio + +The APC features a machine specific exception handling, which means that you can manipulate your game even though it is running in PinMame. The exceptions are written in C and are using the commands of the APC SW framework. More information about the framework can be found in [the tutorial](https://github.com/AmokSolderer/APC/blob/master/DOC/GameCodeTutorial.md) and the [APC software reference](https://github.com/AmokSolderer/APC/blob/master/DOC/Software/APC_SW_reference.pdf). + +To enable PinMameExceptions for your machine you have to add a game specific section to the PinMameExceptions.ino file and recompile the SW. +You can manipulate sound, lamp, switch, display and solenoid commands. Some of these expections are necessary to make your machine work correctly, but you can also do improvements or moderate rule changes. + +In the case at hand we're using these exceptions to tell the APC what to do when a certain audio command is issued by PinMame. I'm using the Jungle Lord as an example here because it incorporates everything these audio boards can do AFAIK. +Note that the only difference between System 3 - 6 and System 7 is that PinMame adds 32 (hex 20) to each audio command when it's a System 7 game. Hence, the audio commands are in the range from 0x20 to 0x3f and not from 0x00 to 0x1f as for the previous generations. +However, the basic setup is the same for every System 3 - 7 game. + +### Setting up PinMameExceptions for the Jungle Lord + +The following steps have been done by me to make my Jungle Lord work correctly. That means you can find the code which is described here in PinMameExceptions.ino. + +There's a template named + + byte EX_Blank(byte Type, byte Command) + +in PinMameExceptions.ino to be used as a starting point. So let's create a copy of this and rename it to + + byte EX_JungleLord(byte Type, byte Command) + +In order for the system to use this code section, we have to add it to EX_Init which is at the end of PinMameExceptions.ino and determines which code is used for which machine. The games are being distinguished by their [PinMame game number](https://github.com/AmokSolderer/APC/blob/master/DOC/lisyminigames.csv). Hence we have to add a case 20 for the Jungle Lord. + + void EX_Init(byte GameNumber) { + switch(GameNumber) { + case 20: // Jungle Lord + EX_EjectSolenoid = 2; // specify eject coil for improved ball release + PinMameException = EX_JungleLord; // use exception rules for Jungle Lord + break; + break; + case 39: // Comet + PinMameException = EX_Comet; // use exception rules for Comet + break; + case 43: // Pinbot + PinMameException = EX_Pinbot; // use exception rules for Pinbot + break; + case 44: // F-14 Tomcat + PinMameException = EX_F14Tomcat; // use exception rules for Tomcat + break; + default: + PinMameException = EX_DummyProcess;}} + +All games not listet here do not have exceptions defined yet, so the exception pointer just points to a dummy process which only plays the standard sounds, but knows no exceptions. +The definition of EX_EjectSolenoid is only there because I also want to improve the ball release of System 7 machines. The way it works is handled on the general [PinMameExceptions page](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameExceptions.md), but let's skip it for now as it has nothing to do with the sound. + +### Handling of ordinary sounds + +As System7 just uses one sound channel, all sound exceptions have to be put into the SoundCommandCh1 case of our EX_JungleLord program. Hence you can delete all cases except of SoundCommandCh1 and the default case. The result should look like this: + + byte EX_JungleLord(byte Type, byte Command){ // use this as a template and an example of how to add your own exceptions + switch(Type){ // usually just a few exception cases are needed, just delete the rest + case SoundCommandCh1: // sound commands for channel 1 + if (Command == 38){ // sound command 0x26 + // enter your special sound command 0x26 here + } + else { + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: // use default treatment for undefined types + return(0);}} + +What we have now is a default handler for all the sound numbers that have no exceptions defined. +The filenames of these sounds just consist of the channel number, an underscore and the sound number in hex followed by ".snd". There is a routine called + + byte USB_GenerateFilename(byte Channel, byte Sound, char* FileName) + +which adds the hex code to a given filename and handles the display messages if the audio debug mode is active. It returns a 1 in case the soundfile does exist and a 0 if it doesn't. Is is therefore only necessary to play the sound when the return value has been 1. + +In EX_Blank the 'if' for sound command 38 is just meant as an example to make it clear where exceptions have to be added, but for the Jungle Lord we can keep this as our first special audio command is indeed 38 (0x26). + +### Random sounds + +The first exception is the 0x26 sound command which triggers one of four random spoken phrases. In the exception handler this looks like this: + + case SoundCommandCh1: // sound commands for channel 1 + if (Command == 38){ // sound command 0x26 - start game + char FileName[13] = "0_26_000.snd"; // generate base filename + FileName[7] = 48 + random(4) + 1; // change the counter according to random number + PlaySound(52, (char*) FileName);} // play the corresponding sound file + +The APC expects the corresponding sound files to be named 0_26_00X.snd with the X being one for the first file, two for the second and so on. +First we generate the base filename "0_26_000.snd", then we change the 8th character of this string to a random number between 1 and 4 and play the sound file with a priority of 52. + +### Looping sound series + +The next special sound command of the Jungle Lord is 0x2d. This is a looping sound series, which means it starts again with the first pitch after the last has been played. The corresponding code is: + + else if (Command == 45){ // sound command 0x2d - multiball start - sound series + if (SoundSeries[1] < 31) // this sound has 31 pitches + SoundSeries[1]++; // every call of this sound proceeds with next pitch + else + SoundSeries[1] = 1; // start all over again + char FileName[13] = "0_2d_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[1] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[1] % 100) / 10; // the same with the 6th character + PlaySound(51, (char*) FileName);} // play the sound + +For this we need an additional variable 'SoundSeries' which stores the number of the pitch currently being played. As Jungle Lord features more than one sound series we need an array here. This variable has to be defined as static byte at the beginning of our EX_JungleLord. +At first it is checked whether the last pitch of this series is currently being played. If yes then the pitch number is set back to one otherwise it is increased by one. After that the base filename is generated, the new pitch number is written into it and the sound is played. + +### The background sound + +Sound command 0x2a is also a sound series, so it's treated very similarly. + + else if (Command == 42){ // sound command 0x2a - background sound - sound series + SoundSeries[1] = 0; // reset the multiball start sound + if (SoundSeries[0] < 29) // BG sound has 29 pitches + SoundSeries[0]++; // every call of this sound proceeds with the next pitch + char FileName[13] = "0_2a_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[0] % 100) / 10; // the same with the 6th character + for (byte i=0; i<12; i++) { // store the name of this sound + USB_RepeatSound[i] = FileName[i];} + QueueNextSound(USB_RepeatSound); // select this sound to be repeated + PlaySound(51, (char*) FileName);} // play the sound + +One difference is that this sound series is not a looping one which means the pitch counter is not reset to one, but stays at the highest value until it is reset by the stop sound command 0x2c. Furthermore this command resets the pitch of the 0x2d sound series (SoundSeries[1] = 0;). +However, the major difference is that 0x2a is the background sound which can be interrupted by other sounds, but will continue afterwards. +In the APC SW the Aftersound pointer can be used for this. This pointer can be set to a routine which is called automatically when a sound has run out. That's why the name of the current pitch is always stored in the USB_RepeatSound variable. Then QueueNextSound is called which takes the filename USB_RepeatSound points to as an argument. It sets the AfterSound pointer to a routine which will play the filename stored in USB_RepeatSound when the current sound file is running out. This ensures that whenever the background sound is being interrupted by another sound, it is immediately being restarted as soon as this sound has run out. +Setting AfterSound = 0 will disable this mechanism. + +### Defining the basic special commands + +At last we have to implement the two basic special commands every system 3 - 7 game has. These are the sound stop command (0x0c for Sys 3 - 6 and 0x2c for Sys 7) and the bus initialization (0x1f for Sys 3 - 6 and 0x7f for Sys 7). +Let's do the stop command first: + + else if (Command == 44) { // sound command 0x2c - stop sound + AfterSound = 0; + SoundSeries[0] = 0; // Reset BG sound + SoundSeries[1] = 0; // reset the multiball start sound + StopPlayingSound();} + +This command sets AfterSound = 0 which will prevent the BG sound from being restarted. Then it resets both sound series counters and stops the current playback. + +Another basic command every System 3 - 7 game has is the bus initialization (0x1f for Sys 3 - 6 and 0x7f for Sys7). This command is only needed when an old audio board is used, so we can just ignore it for the APC. + + if (Command == 127) { } // ignore sound command 0x7f - audio bus init - not relevant for APC sound + +### The result + +The final result is shown below. It contains the PinMameExceptions needed to make the audio for Jungle Lord work. + + byte EX_JungleLord(byte Type, byte Command){ + static byte SoundSeries[2]; // buffer to handle pre system11 sound series + switch(Type){ + case SoundCommandCh1: // sound commands for channel 1 + if (Command == 127) { } // ignore sound command 0x7f - audio bus init - not relevant for APC sound + else if (Command == 38){ // sound command 0x26 - start game + char FileName[13] = "0_26_000.snd"; // generate base filename + FileName[7] = 48 + random(4) + 1; // change the counter according to random number + PlaySound(52, (char*) FileName);} // play the corresponding sound file + else if (Command == 42){ // sound command 0x2a - background sound - sound series + SoundSeries[1] = 0; // reset the multiball start sound + if (SoundSeries[0] < 29) // BG sound has 29 pitches + SoundSeries[0]++; // every call of this sound proceeds with the next pitch + char FileName[13] = "0_2a_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[0] % 100) / 10; // the same with the 6th character + for (byte i=0; i<12; i++) { // store the name of this sound + USB_RepeatSound[i] = FileName[i];} + QueueNextSound(USB_RepeatSound); // select this sound to be repeated + PlaySound(51, (char*) FileName);} // play the sound + else if (Command == 44) { // sound command 0x2c - stop sound + AfterSound = 0; + SoundSeries[0] = 0; // Reset BG sound + SoundSeries[1] = 0; // reset the multiball start sound + StopPlayingSound();} + else if (Command == 45){ // sound command 0x2d - multiball start - sound series + if (SoundSeries[1] < 31) // this sound has 31 pitches + SoundSeries[1]++; // every call of this sound proceeds with next pitch + else + SoundSeries[1] = 1; // start all over again + char FileName[13] = "0_2d_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[1] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[1] % 100) / 10; // the same with the 6th character + PlaySound(51, (char*) FileName);} // play the sound + else { // standard sound + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: + return(0);}} // no exception rule found for this type so proceed as normal + +The actual EX_JungleLord in PinMameExceptions.ino looks slightly different as it features some additional improvements, but these have nothing to do with audio. However, if you're interested you can find more infos about that on the general [PinMameExceptions page](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameExceptions.md). + +## Sound problems + +Most sound problems like lags and stuttering are caused by the performance of the SD card. Take a look at the [If things don't work](https://github.com/AmokSolderer/APC/blob/master/DOC/Problems.md) section for more. diff --git a/DOC/PinMameSound_9.md b/DOC/PinMameSound_9.md new file mode 100644 index 0000000..8ad4bf5 --- /dev/null +++ b/DOC/PinMameSound_9.md @@ -0,0 +1,239 @@ +# System 9 + +The audio generation circuits of System 9 machines are basically the same as of System 7. The main difference seems to be that System 9 boards use larger EPROMs and can therefore store more sound data. Hence, the data bus is not limited to 5 bit any more but uses the whole 8 bit which makes 256 different sound commands possible. +Furthermore these machine feature some kind of crude background music. As these board still have just one audio channel, this 'music' must be interrupted as long as another sound is played. + +This documentation uses Comet as an example as APC sound files and PinMameExceptions are available for this machine. I expect the other System 9 machines to work in a similar way. + +Comet uses the following audio commands which are not sounds: + +|Command / Hex|Comment| +|--|--| +|00|Stop Sound/Music| +|0b, fe|Unknown command -> ignored| +|2f|Background music| +|ff|Stop Sound/Music| + +## Sound file preparation + +There're multiple ways to obtain the original sounds for System 9 machines. Some can be found on the internet, directly recorded from your pinball machine or you can extract them from PinMame. What ever you do, the result should be a mono WAV file with 44.1KHz sampling rate and reasonable amplitude. +If you find the files on the internet you can proceed to the [Audio file conversion](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_9.md#audio-file-conversion) section, as you don't have to extract them from PinMame. + +The method decribed here is the the manual way, there're also more automatic solutions which might save some time. I've never tried them myself, but Mokopin (from the Flippertreff forum) has written some [Instructions for extracting sound files](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSounds.md) which explain the automatic extraction of sound files and the use of Audacity in more detail. + +### Which sounds are required? + +System 9 boards can handle 8 bit sound commands which leads to a possible number of 256 sounds minus some control commands, but only a part of these numbers are actually used by a game. +With this manual method we have to extract and preprocess every sound by hand, so we try to keep the number of sounds as low as possible by just extracting those which are actually requested by PinMame. + +When you start from scratch you should play your game with Lisy being in Sound Debug Mode. Please read the [Controlling Lisy page](https://github.com/AmokSolderer/APC/blob/master/DOC/LisyDebug.md) to learn how to do this. + +Now start a game and let Lisy log the sound commands. This will already give you the most common sound numbers to start with. Later you'll have to repeat this step or use the [Audio Debug Mode](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_9.md#finding-out-which-sounds-are-still-missing) of the APC to find the remaining missing sounds. + +When you're done press the shutdown switch SW1 on your APC board to make it exit the emulation and store the log file on the SD card. The file will be located on the Pi's SD card in the folder lisy/lisy_m/debug. + +In lisy_m_debug.txt playing a sound looks like + + [461.372965][0.000064] LISY_W sound_handler: board:0 0x79 (121) + [461.372985][0.000020] play soundindex 121 on board 0 + [461.373005][0.000020] USB_write(3 bytes): 0x32 0x01 0x79 + +In this case sound 0x79 shall be played which means you have to record the sound command 79 in PinMame. + +The Lisy sound debug log only tells you which sound numbers are needed for your game. Now it's time to extract the corresponding sound files. + +### Manual extraction from PinMame + +For this to work you have to install PinMame32 for Windows as the Unix version has severe sound issues. + +After PinMame is running, press F4 to enter the 'Sound Command Mode' then press DEL for the Manual / Command Mode. + +The following steps have to be repeated for every sound you want to record. That means you have to do it for every sound number you've found in the Lisy sound debug log. +Enter the hexadecimal sound number. Then press F5 to start the recording, SPACE to play the sound and F5 again to stop recording. The sound can be found in PinMame's 'wave' folder. +The sound command 00 stops the currently playing sound / music. +Not all sound numbers play a sound in PinMame, as some are just controlling the audio board. These commands have to be covered by PinMameExceptions and can be ignored for now. +Remember to rename the files immediately to the correct names to avoid confusion. + +### Preprocessing the WAV files with Audacity + +For some reason the PinMame sounds have a sampling rate of 48KHz and must therefore be converted to the normal sampling rate of 44.1KHz. You can use a free audio editor like Audacity (https://www.audacityteam.org) to adjust the start and end time of your sound sample as well as the sampling rate. +Furthermore, the volume of the sound and music files generated by PinMame differs quite a lot, which means you should also use Audacity to adjust those levels accordingly. + +![Audacity](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/Audacity.png) + +This is the manual way to preprocess a file recorded from PinMame. + +* Open the file with Audacity +* Press the button for the Selection Tool (1) and select the part of the sound you want to use +* Delete the rest (2) +* Click inside the cyan box next to the waveform (3) to select the whole track +* Select the 'Tracks -> Sampling rate' menu and set the sampling rate to 44100Hz +* If you have a stereo track, use the 'Tracks -> Mix -> Mix Stereo Down to Mono' menu to get mono +* Select the 'Effect -> Normalize' menu to open the window with the normalizing options +* Select 'Remove DC offset' and 'Normalize peak amplitude to' 0dB. +* Select the 'File -> Export -> Export Selected Audio...' and save it with the 'Signed 16-bit PCM' encoding + +Most of this process can be automatized as explained by Mokopin in his [Instructions for extracting sound files](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSounds.md). + +## Audio file conversion + +Put all preprocessed WAV files in one folder, check that the names are correct and execute the [AudioSaveFolder.pl](https://github.com/AmokSolderer/APC/blob/master/DOC/UsefulSWtools.md) tool. +The resulting .snd files must be copied to the APC's SD card. Please format your card before with a special SD card formatting tool from the SD Association (sdcard.org) to improve the card's access times. + +If you start your game now you should already hear most of the sounds, but everything out of the ordinary like correct music handling and so on won't work correctly. We need to tell the APC about these special sound sommands and how to deal with them. + +## PinMameExceptions for audio + +The APC features a machine specific exception handling, which means that you can manipulate your game even though it is running in PinMame. The exceptions are written in C and are using the commands of the APC SW framework. More information about the framework can be found in [the tutorial](https://github.com/AmokSolderer/APC/blob/master/DOC/GameCodeTutorial.md) and the [APC software reference](https://github.com/AmokSolderer/APC/blob/master/DOC/Software/APC_SW_reference.pdf). + +To enable PinMameExceptions for your machine you have to add a game specific section to the PinMameExceptions.ino file and recompile the SW. +You can manipulate sound, lamp, switch, display and solenoid commands. Some of these expections are necessary to make your machine work correctly, but you can also do improvements or moderate rule changes. + +In the case at hand we're using these exceptions to tell the APC what to do when a certain audio command is issued by PinMame. I'm using the Comet as an example here. + +### Setting up PinMameExceptions for the Comet + +The following steps have been done by me to make my Comet work correctly. That means you can find the code which is described here in PinMameExceptions.ino. + +There's a template named + + byte EX_Blank(byte Type, byte Command) + +in PinMameExceptions.ino to be used as a starting point. So let's create a copy of this and rename it to + + byte EX_Comet(byte Type, byte Command) + +In order for the system to use this code section, we have to add it to EX_Init which is at the end of PinMameExceptions.ino and determines which code is used for which machine. The games are being distinguished by their [PinMame game number](https://github.com/AmokSolderer/APC/blob/master/DOC/lisyminigames.csv). Hence we have to add a case 39 for the Comet. + + void EX_Init(byte GameNumber) { + switch(GameNumber) { + case 20: // Jungle Lord + EX_EjectSolenoid = 2; // specify eject coil for improved ball release + PinMameException = EX_JungleLord; // use exception rules for Jungle Lord + break; + break; + case 39: // Comet + PinMameException = EX_Comet; // use exception rules for Comet + break; + case 43: // Pinbot + PinMameException = EX_Pinbot; // use exception rules for Pinbot + break; + case 44: // F-14 Tomcat + PinMameException = EX_F14Tomcat; // use exception rules for Tomcat + break; + default: + PinMameException = EX_DummyProcess;}} + +All games not listet here do not have exceptions defined yet, so the exception pointer just points to a dummy process which only plays the standard sounds, but knows no exceptions. + +### Handling ordinary sounds + +As System9 just uses one sound channel, all sound exceptions have to be put into the SoundCommandCh1 case of our EX_Comet program. Hence you can delete all cases except of SoundCommandCh1 and the default case. The result should look like this: + + byte EX_Comet(byte Type, byte Command){ // use this as a template and an example of how to add your own exceptions + switch(Type){ // usually just a few exception cases are needed, just delete the rest + case SoundCommandCh1: // sound commands for channel 1 + if (Command == 38){ // sound command 0x26 + // enter your special sound command 0x26 here + } + else { + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: // use default treatment for undefined types + return(0);}} + +What we have now is a default handler for all the sound numbers that have no exceptions defined. +The filenames of these sounds just consist of the channel number, an underscore and the sound number in hex followed by ".snd". There is a routine called + + byte USB_GenerateFilename(byte Channel, byte Sound, char* FileName) + +which adds the hex code to a given filename and handles the display messages if the audio debug mode is active. It returns a 1 in case the soundfile does exist and a 0 if it doesn't. Is is therefore only necessary to play the sound when the return value has been 1. + +In EX_Blank the 'if' for sound command 38 is just meant as an example to make it clear where exceptions have to be added, but for the Comet we can change this to incorporate the Sound Stop commands (00 and 0xff): + + byte EX_Comet(byte Type, byte Command) { + switch(Type){ + case SoundCommandCh1: // sound commands for channel 1 + if (!Command || Command > 254) { // sound command 0x00 and 0xff -> stop sound + AfterMusic = 0; + StopPlayingMusic(); + StopPlayingSound();} + else { // handle standard sound + char FileName[9] = "0_00.snd"; + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: + return(0);}} + +### Background music + +Comet features a simple background music which is looping all over again. If you want it easy, you could just put some repetitions of the music in one file. This will work in most cases, but if you wait long enough the music will just run out. +In order to prevent this from happening, an exception rule must handle the looping. + + else if (Command == 47) { // play BG music + PlayMusic(50, "0_2f.snd"); + QueueNextMusic("0_2f.snd");} // track is looping so queue it also + +When audio command 47 is issued by PinMame then the music is played and the QueueNextMusic command is used to queue the file also for looping. As long as the AfterMusic variable is not set to zero, the looping part will be repeated automatically. + +Due to the restriction to one audio channel of the old audio boards, this music had to be suspended every time another sound was played. +Note that the code snippet above is using the PlayMusic and QueueNextMusic commands to play this file on the music channel of the APC which means that the music is running independently from the sounds. +We can therefore choose whether we want to mute the music for as long as another sound is played or whether we want to play the music continuously in the background like System11 machines are doing. +In this case I decided for the latter and only muted the music for one special sound sequence which starts with sound number 9 and ends with number 0xf1 (dec 241). The APC handles the volume of the music by the MusicVolume variable. A value of zero means full volume and every count up cuts the amplitude by half. Hence, the corresponding code looks like this: + + if (Command == 9) { + MusicVolume = 4;} // reduce music volume + if (Command == 241) { // sound 0xf1 + RestoreMusicVolumeAfterSound(25);} // and restore it + +### Dealing with unknown commands + +There're still several commands we don't know the meaning of. If you find out what they do please drop us a note, but for the time being we can just ignore them (which worked quite well so far). So for every command that doesn't produce a sound in PinMame, just add a line like + + else if (Command == 11 || Command == 254) { } // ignore sound commands 0x0b and 0xfe + +to prevent them from causing 'File not found' messages on your display. + +### The result + +The final result is shown below. It contains the PinMameExceptions needed to make the audio for Comet work. + + byte EX_Comet(byte Type, byte Command) { + switch(Type){ + case SoundCommandCh1: // sound commands for channel 1 + if (!Command || Command > 254) { // sound command 0x00 and 0xff -> stop sound + AfterMusic = 0; + StopPlayingMusic(); + StopPlayingSound();} + else if (Command == 11 || Command == 254) { } // ignore sound commands 0x0b and 0xfe + else if (Command == 47) { // play BG music + PlayMusic(50, "0_2f.snd"); + QueueNextMusic("0_2f.snd");} // track is looping so queue it also + else { // handle standard sound + if (Command == 9) { + MusicVolume = 4;} // reduce music volume + if (Command == 241) { // sound 0xf1 + RestoreMusicVolumeAfterSound(25);} // and restore it + char FileName[9] = "0_00.snd"; + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: + return(0);}} // no exception rule found for this type so proceed as normal + +### Finding out which sounds are still missing + +After you have extracted most of the files, there'll be the point when only a few files are still missing and you need to find out their numbers. Of course you could still scan the Lisy log for unknown sound numbers, but in this stage it might be easier to use the 'Audio Debug Mode' of the APC. +You can activate this mode in the [game settings](https://github.com/AmokSolderer/APC/blob/master/DOC/Settings.md#game-settings-in-remote-control-mode). + +In Audio Debug mode the lower display(s) are used for audio information. The Player 3 display (or the left part of the lower display for BK2K type displays) shows information for sound prefix 00 and the Player 4 display (right part of lower display for BK2K) does the same for prefix 01. If the requested sound is found on the SD card, it's hex number is shown in the left side of the corresponding display and the sound is played normally. If the sound file is missing it's hex number is shown on the right side of the corresponding display which makes it easy to find missing sound files. +As the pre System11 displays cannot show letters, the corresponding sound numbers are shown in decimal values when this kind of display is selected. + +By that you can just play your game and only if the number of a missing sound file pops up on the right side of your display you note it down and add it later. + +## Sound problems + +Most sound problems like lags and stuttering are caused by the performance of the SD card. Take a look at the [If things don't work](https://github.com/AmokSolderer/APC/blob/master/DOC/Problems.md) section for more. diff --git a/DOC/Pinbot.md b/DOC/Pinbot.md new file mode 100644 index 0000000..ea09b56 --- /dev/null +++ b/DOC/Pinbot.md @@ -0,0 +1,56 @@ +# The APC Pinbot + +This page is about the Pinbot.ino game SW which is part of the APC SW. Note that you have to have the Pinbot sound files for this game to work. + +![APC Pinbot](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/Pinbot.JPG) + +This is a view inside the backbox. + +![APC open PB](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/APC_Pinbot.JPG) + +I'm using an APC 2 prototype board for this. As I'm running the code natively on the Arduino I don't need Lisy/PinMame and therefore no Raspberry Pi. + +## Features + +* Ball Saver -> Pinbot is an outlane monster and when a ball drains in an outlane right after it was launched then I'm really getting furious. Hence there's an optional Ball Saver which saves any ball draining in an outlane before any flipper buttons had been pressed. +* 3 ball Multiball -> Pinbot's multiball is kind of lame IMHO. That's why there's a 3 ball multiball option in my SW. Activate the option, add a third ball and enjoy a different ruleset introducing an unlimited million Jackpot. + +## 3 ball Multiball + +Switching the 'Multiball' game setting to '3 ball' (and adding a third ball) switches to a different ruleset. +Like the original Pinbot, this one will also explain it's rules if you keep it waiting a while in Attract Mode. Depending on the 'Multiball' settings it will explain the normal Pinbot rules (2 ball) or the new 3 ball rules. + +The rules for the 3 ball mode are shown [here](https://youtu.be/IGrUnbhkijU). + +## How to set up this game + +You need to have the audio files for this game. [Contact me](https://github.com/AmokSolderer/APC/tree/master#feedback) to get the corresponding sound file package. + +If you want to use the 3 ball Multiball you have to add a third ball apparently. Don't forget to remove it when you switch back to 3 ball mode. + +## BK Game Settings + +| Number | Text | Item Nr | Item Text | Default | Comment | +|--|--|--|--|--|--| +| 0 | Drop TG Time | - | - | 15 | Down time of the drop targets | +| 1 | Reach Planet | 0 | Pluto | - | Planet to reach for Extra Ball | +| 1 | | 1 | Neptune | - | | +| 1 | | 2 | Uranus | - | | +| 1 | | 3 | Saturn | - | | +| 1 | | 4 | Jupiter | - | | +| 1 | | 5 | Mars | - | | +| 1 | | 6 | Earth | X | | +| 1 | | 7 | Venus | - | | +| 1 | | 8 | Mercury | - | | +| 2 | Energy Timer | - | - | 15 | Time to collect energy | +| 3 | Multiball | 0 | 2 Ball | - | 2 ball multiball (Normal Pinbot) gameplay - see features description| +| 3 | | 1 | 3 Ball | - | 3 ball multiball gameplay | +| 4 | Ball Saver | - | - | No | Bool setting - see features description | +| 5 | Eject Strength | - | - | 30 | Strength of the outhole kicker (debug option)| +| 6 | Hold Time | - | - | 10 | Time to hold balls in locks during 3 ball multiball | +| 7 | Reset High | - | - | - | No setting - resets the high scores for this game | +| 8 | Restore Default | - | - | - | No setting - restores the default settings | +| 9 | Exit Settings | - | - | - | No setting - exits the settings mode and writes the new setting to an SD card if present | + +Check the [settings page](https://github.com/AmokSolderer/APC/blob/master/DOC/Settings.md#using-the-settings-menu) if you're not sure how to use these game settings. + diff --git a/DOC/Prepare.md b/DOC/Prepare.md index d6a79a3..a513985 100644 --- a/DOC/Prepare.md +++ b/DOC/Prepare.md @@ -20,6 +20,10 @@ This adapter can be build very easily by just using a micro SD adapter and solde Of course you cannot use the SD-Card slot and the self made adapter simultaneously, as the APC can only use one SD-Card at a time. +### Installation frames + +There're [installation frames](https://github.com/AmokSolderer/APC/blob/master/DOC/Frames.md) available which can be 3D printed and help you install the APC board in your backbox. + # Machine specific preparation This page is divided in different sections, depending on which generation your pinball machine is from. diff --git a/DOC/RunGame.md b/DOC/RunGame.md index e738e7d..b0416c2 100644 --- a/DOC/RunGame.md +++ b/DOC/RunGame.md @@ -35,11 +35,27 @@ If you feel that your game is not running at the correct speed, you can change P boot/lisy/lisy_m/cfg/lisyminigames.csv There's a throttle value specified for each game. Changing this value to a lower value will make the game run faster and vice versa. +## PinMame sound -### PinMame sound +### Using old audio boards -When you're not using an old audio board but you want the APC to do the audio instead, then you need to have the necessary sound files. Check the table on the [PinMame Sound](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) page to see whether your game is already supported and the audio files are available. If your game is listet here, you can simply request the files and put them on the SD card on the APC board (not the one of the Pi). That's it, have fun. -If your game is not yet supported or you want to change the sounds (or even rules) then read the [PinMame howto](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame_howto.md). +For some game generations you could install the original audio board. In this case you cannot do any sound related changes of course. +For System 3 - 6, the sound board is controlled by some reserved solenoid drivers, so it will work out of the box. System 7 needs an [adapter](https://github.com/AmokSolderer/APC/blob/master/DOC/Prepare.md#system-7-audio-cable) for connecting the audio board to the HW extension interface of the APC. +For the APC to control the external audio board you have to enter the [game settings](https://github.com/AmokSolderer/APC/blob/master/DOC/Settings.md#game-settings-in-remote-control-mode) while in Remote Control Mode and set setting 2 (PinMame Sound) from 0 (APC) to 1 (Board). + +System 9 and 11 audio boards are not supported, you have to let the APC generate the sound instead. + +### Using the APC to generate audio + +Letting the APC generate the sounds has several benefits. + +The audio quality of the APC is quite good. It is also better in noise suppression than the old audio boards, so if you have a hum in your sound due to worn out capacitors, the chances are that it is much weaker with the APC. +You can also change your sounds as you wish. This can be as simple as replacing certain sound files, but you could also add a music track to your old System 3 - 7 game. Let your Flash play Queen as background music if you like. + +The drawback of this is that someone has to extract the music files from PinMame and if your game is not already supported then this someone is probably you. + +Check the table on the [PinMame Sound](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) page to see whether your game is already supported and the audio files are available. If your game is listet there, you can simply request the files and put them on the SD card on the APC board (not the one of the Pi). That's it, have fun. +If your game is not yet supported then you should also read this page to find out how to proceed. ## MPF diff --git a/DOC/SetUpBC.md b/DOC/SetUpBC.md index 8b67d16..4b232e5 100644 --- a/DOC/SetUpBC.md +++ b/DOC/SetUpBC.md @@ -22,6 +22,8 @@ To get there press the Advance button while running the Base Code, then enter th You have to get the required values from the manual of your pinball machine. Alas, the names of the switches and solenoids have changed over the years. The default values are valid for a System11c Rollergames and I have added the corresponding manual pages below to make it easier for you to identify the right parts. +For single ball games set the 'Installed Balls' setting to 1. The shooter lane feeder should be set to your Outhole Kicker solenoid. + ![RollergamesSwitches](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/RG_Sw.JPG) ![RollergamesSolenoids](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/RG_Sol.JPG) diff --git a/DOC/Settings.md b/DOC/Settings.md index aa1cbbc..67cfdce 100644 --- a/DOC/Settings.md +++ b/DOC/Settings.md @@ -33,11 +33,19 @@ Games with a credit display show 00 for System Settings and 01 for Game Settings For pre Sys11 displays all text settings are represented by their item number as shown in the tables. For example, if a pre Sys11 display is selected and the credit display shows 00 01 you have selected the System Settings and the 'Active Game' setting is currently being shown. An early System11 display (upper display row alphanumerical and lower row numerical) will still try to show the name of the currently selected game as a text which is not perfect, but good enough. A pre System11 display will show the item number instead and if there is a 3 stated in the player 4 display then you're in Remote Control mode. -### Using PinMame settings +## Using PinMame settings -While you're running PinMame you can adjust the original Williams settings as usual with one exception: +While you're running PinMame you can adjust the original Williams System 4 - 11 adjustments as usual with one exception: You must not keep Advance pressed for more than 1 second with Up/Down in up position as this will trigger the APC settings. If you want to browse the Williams Settings quickly, just do it backwards with Up/Down in down position. +### System 3 game adjustments + +Doing game adjustments on a System 3 game is done with DIP switches. The procedure is quite cumbersome and error prone which is why we're using the APC's settings menu instead. +The following description works only for System 3 games being controlled by Lisy/PinMame, for all other generations the adjustment process hasn't changed from the original. + +Keep the Advance switch pressed for more than one second with the Up/Down switch in up position. This will end PinMame and enter the settings menu of the APC. The content of the 18 System 3 adjustments is now stored in the 'Game Settings' 46 to 63 as shown in the table below (Game Settings in Remote Control mode). +Enter the game settings and do your changes. When you're done, just select menu entry 65 (Exit Settings) and press the start button. After a few seconds PinMame will restart with the new settings applied. + ## System Settings | Number | Text | Item Nr | Item Text | Default | Comment | diff --git a/DOC/Software/APC_SW_reference.pdf b/DOC/Software/APC_SW_reference.pdf index 687db7d..d49c7bb 100644 Binary files a/DOC/Software/APC_SW_reference.pdf and b/DOC/Software/APC_SW_reference.pdf differ diff --git a/DOC/UsefulSWtools.md b/DOC/UsefulSWtools.md index a7c3cdd..edd0c1f 100644 --- a/DOC/UsefulSWtools.md +++ b/DOC/UsefulSWtools.md @@ -1,4 +1,7 @@ -I have written some perl scripts to do certain APC related work. +# Usefull SW tools + +I have written some perl scripts to do certain APC related work. +All thess scripts can be executed with a perl interpreter, e.g. for windows available from http://strawberryperl.com ## Audio data converter diff --git a/DOC/lisyminigames.csv b/DOC/lisyminigames.csv index 1524611..62c1d0b 100644 --- a/DOC/lisyminigames.csv +++ b/DOC/lisyminigames.csv @@ -1,84 +1,84 @@ -No;Mame Name;Long Name;Type;throttle;comment -0;httip_l1;Hot Tip ;SYS3;120; -1;lucky_l1;Lucky Seven ;SYS3;120; -2;wldcp_l1;World Cup ;SYS3;120; -3;cntct_l1;Contact ;SYS3;120; -4;disco_l1;Disco Fever ;SYS3;120; -5;phnix_l1;Phoenix ;SYS4;120; -6;flash_l1;Flash ;SYS4;120; -7;flash_l2;Flash ;SYS4;120; -8;trizn_l1;Tri Zone ;SYS4;120; -9;pkrno_l1;Pokerino ;SYS4;120; -10;tmwrp_l2;Time Warp ;SYS4;120; -11;stlwr_l2;Stellar Wars ;SYS4;120; -12;lzbal_l2;Laser Ball ;SYS6;120; -13;scrpn_l1;Scorpion ;SYS6;120; -14;blkou_l1;Blackout ;SYS6;120; -15;grgar_l1;Gorgar ;SYS6;120; -16;frpwr_l6;Firepower ;SYS6;120; -17;algar_l1;Algar ;SYS6A;120; -18;alpok_l6.zip;Alien Poker ;SYS6A;120; -19;csmic_l1;Cosmic Gunfight ;SYS7;120; -20;jngld_l2;Jungle Lord ;SYS7;120; -21;pharo_l2;Pharaoh ;SYS7;120; -22;solar_l2;Solar Fire ;SYS7;120; -23;prototy;Thunderball;SYS7;120; -24;hypbl_l6;Hyperball ;SYS7;120; -25;barra_l1;Barracora ;SYS7;120; -26;vrkon_l1;Varkon ;SYS7;120; -27;wrlok_l3;Warlok ;SYS7;120; -28;dfndr_l4;Defender ;SYS7;120; -29;jst_l2;Joust ;SYS7;120; -30;lsrcu_l2;Laser Cue ;SYS7;120; -31;fpwr2_l2;Firepower II ;SYS7;120; -32;ratrc_l1;Rat Race;SYS7;120; -33;strlt_l1;Star Light ;SYS7;120; -34;bk_l4;Black Knight ;SYS7;120; -35;tmfnt_l5;Time Fantasy ;SYS7;120; -36;pfevr_l2;Pennant Fever ;SYS8;120; -37;sshtl_l7.zip;Space Shuttle ;SYS9;120; -38;sorcr_l2;Sorcerer ;SYS9;120; -39;comet_l5;Comet ;SYS9;120; -40;hs_l4;High Speed ;SYS11;120; -41;grand_l4;Grand Lizard ;SYS11;120; -42;rdkng_l4;Road Kings ;SYS11RK;120; -43;pb_l5;Pinbot ;SYS11A_;120; -44;f14_l1;F-14 Tomcat ;SYS11A_;120; -45;fire_l3;Fire! ;SYS11A;120; -46;milln_l3;Millionaire ;SYS11A;120; -47;bguns_l8;Big Guns ;SYS11B;120; -48;spstn_l5;Space Station ;SYS11B;120; -49;gmine_l2;Gold Mine ;SYS11B;120; -50;cycln_l5;Cyclone ;SYS11B;120; -51;bnzai_l3;Banzai Run ;SYS11B;120; -52;swrds_l2;Swords of Fury ;SYS11B;120; -53;taxi_l4;Taxi ;SYS11B;120; -54;tdawg_l1;Top Dawg ;SYS11B;120; -55;jokrz_l6;Jokerz! ;SYS11B;120; -56;esha_la3;Earthshaker ;SYS11B;120; -57;bk2k_l4;Black Knight 2000 ;SYS11B;120; -58;tsptr_l3;Transporter the Rescue ;SYS11B;120; -59;polic_l4;Police Force ;SYS11B;120; -60;shfin_l1;Shuffle Inn ;SYS11B;120; -61;afm_11b;Elvira and the Party Monsters ;SYS11B;120; -62;bcats_l5;Bad Cats ;SYS11B;120; -63;mousn_l1;Mousin' Around! ;SYS11B;120; -64;whirl_l3;Whirlwind;SYS11B;120; -65;gs_l4;The Bally Game Show ;SYS11C;120; -66;pool_l7;Pool Sharks ;SYS11C;120; -67;rollr_l3;Rollergames ;SYS11C;120; -68;diner_l4;Diner ;SYS11C;120; -69;radcl_l1;Radical! ;SYS11C;120; -70;dd_l2;Dr. Dude ;SYS11C;120; -71;rvrbt_l3;Riverboat Gambler ;SYS11RG;120; -72;bbnny_l2;Bugs Bunny's Birthday Ball ;SYS11C;120; -73;lwar_a83;Laser War;DE_A1;120; -74;ssvc_a26;Secret Service;DE_A2;120; -75;torp_e21;Torpedo Alley;DE_A2;120; -76;tmac_a24;Time Machine;DE_A2;120; -77;play_a24;Playboy 35th Anniversary ;DE_A2;120; -78;mnfb_c27;Monday Night Football;DE_A2;120; -79;robo_a34;Robocop;DE_A2;120; -80;poto_a32;Phantom of the Opera;DE_A2;120; -81;bttf_a28;Back to the Future;DE_A2;120; -82;simp_a27;The Simpsons;DE_A3;120; +No,Mame Name,Long Name,Type,throttle,comment +0,httip_l1,Hot Tip ,SYS3,120, +1,lucky_l1,Lucky Seven ,SYS3,120, +2,wldcp_l1,World Cup ,SYS3,120, +3,cntct_l1,Contact ,SYS3,120, +4,disco_l1,Disco Fever ,SYS3,120, +5,phnix_l1,Phoenix ,SYS4,120, +6,flash_l1,Flash ,SYS4,120, +7,flash_l2,Flash ,SYS4,120, +8,trizn_l1,Tri Zone ,SYS4,120, +9,pkrno_l1,Pokerino ,SYS4,120, +10,tmwrp_l2,Time Warp ,SYS4,120, +11,stlwr_l2,Stellar Wars ,SYS4,120, +12,lzbal_l2,Laser Ball ,SYS6,120, +13,scrpn_l1,Scorpion ,SYS6,120, +14,blkou_l1,Blackout ,SYS6,120, +15,grgar_l1,Gorgar ,SYS6,120, +16,frpwr_l6,Firepower ,SYS6,120, +17,algar_l1,Algar ,SYS6A,120, +18,alpok_l6.zip,Alien Poker ,SYS6A,120, +19,csmic_l1,Cosmic Gunfight ,SYS7,120, +20,jngld_l2,Jungle Lord ,SYS7,120, +21,pharo_l2,Pharaoh ,SYS7,120, +22,solar_l2,Solar Fire ,SYS7,120, +23,prototy,Thunderball,SYS7,120, +24,hypbl_l6,Hyperball ,SYS7,120, +25,barra_l1,Barracora ,SYS7,120, +26,vrkon_l1,Varkon ,SYS7,120, +27,wrlok_l3,Warlok ,SYS7,120, +28,dfndr_l4,Defender ,SYS7,120, +29,jst_l2,Joust ,SYS7,120, +30,lsrcu_l2,Laser Cue ,SYS7,120, +31,fpwr2_l2,Firepower II ,SYS7,120, +32,ratrc_l1,Rat Race,SYS7,120, +33,strlt_l1,Star Light ,SYS7,120, +34,bk_l4,Black Knight ,SYS7,120, +35,tmfnt_l5,Time Fantasy ,SYS7,120, +36,pfevr_l2,Pennant Fever ,SYS8,120, +37,sshtl_l7.zip,Space Shuttle ,SYS9,120, +38,sorcr_l2,Sorcerer ,SYS9,120, +39,comet_l5,Comet ,SYS9,120, +40,hs_l4,High Speed ,SYS11,120, +41,grand_l4,Grand Lizard ,SYS11,120, +42,rdkng_l4,Road Kings ,SYS11RK,120, +43,pb_l5,Pinbot ,SYS11A_,120, +44,f14_l1,F-14 Tomcat ,SYS11A_,120, +45,fire_l3,Fire! ,SYS11A,120, +46,milln_l3,Millionaire ,SYS11A,120, +47,bguns_l8,Big Guns ,SYS11B,120, +48,spstn_l5,Space Station ,SYS11B,120, +49,gmine_l2,Gold Mine ,SYS11B,120, +50,cycln_l5,Cyclone ,SYS11B,120, +51,bnzai_l3,Banzai Run ,SYS11B,120, +52,swrds_l2,Swords of Fury ,SYS11B,120, +53,taxi_l4,Taxi ,SYS11B,120, +54,tdawg_l1,Top Dawg ,SYS11B,120, +55,jokrz_l6,Jokerz! ,SYS11B,120, +56,esha_la3,Earthshaker ,SYS11B,120, +57,bk2k_l4,Black Knight 2000 ,SYS11B,120, +58,tsptr_l3,Transporter the Rescue ,SYS11B,120, +59,polic_l4,Police Force ,SYS11B,120, +60,shfin_l1,Shuffle Inn ,SYS11B,120, +61,afm_11b,Elvira and the Party Monsters ,SYS11B,120, +62,bcats_l5,Bad Cats ,SYS11B,120, +63,mousn_l1,Mousin' Around! ,SYS11B,120, +64,whirl_l3,Whirlwind,SYS11B,120, +65,gs_l4,The Bally Game Show ,SYS11C,120, +66,pool_l7,Pool Sharks ,SYS11C,120, +67,rollr_l3,Rollergames ,SYS11C,120, +68,diner_l4,Diner ,SYS11C,120, +69,radcl_l1,Radical! ,SYS11C,120, +70,dd_l2,Dr. Dude ,SYS11C,120, +71,rvrbt_l3,Riverboat Gambler ,SYS11RG,120, +72,bbnny_l2,Bugs Bunny's Birthday Ball ,SYS11C,120, +73,lwar_a83,Laser War,DE_A1,120, +74,ssvc_a26,Secret Service,DE_A2,120, +75,torp_e21,Torpedo Alley,DE_A2,120, +76,tmac_a24,Time Machine,DE_A2,120, +77,play_a24,Playboy 35th Anniversary ,DE_A2,120, +78,mnfb_c27,Monday Night Football,DE_A2,120, +79,robo_a34,Robocop,DE_A2,120, +80,poto_a32,Phantom of the Opera,DE_A2,120, +81,bttf_a28,Back to the Future,DE_A2,120, +82,simp_a27,The Simpsons,DE_A3,120, diff --git a/PinMameExceptions.ino b/PinMameExceptions.ino index a69ea1f..e394bcf 100644 --- a/PinMameExceptions.ino +++ b/PinMameExceptions.ino @@ -111,6 +111,91 @@ byte EX_DummyProcess(byte Type, byte Command) { // plays just the standard PlayMusic(51, (char*) FileName);}} return(0);} // no exception rule found for this type so proceed as normal +byte EX_DiscoFever(byte Type, byte Command){ + switch(Type){ + case SolenoidActCommand: + if (Command == 2) { // ball release + ActivateSolenoid(80, 2); // FEV drop target reset + return(1);} + else if (Command == 3) { // ER drop target reset + ActivateSolenoid(60, 3); // Fix to increase the strength of the ball release + return(1);} + return(0); + case SolenoidRelCommand: + if (Command == 2 || Command == 3) { // ignore turn-off commands for drop target solenoids + return(1);} // ignore it + return(0); + default: + return(0);}} + +byte EX_Flash(byte Type, byte Command){ + static byte SoundSeries[3]; // buffer to handle pre system11 sound series + switch(Type){ + case SolenoidActCommand: + if (Command == 1) { // ball release + ActivateSolenoid(40, 1); // Fix to increase the strength of the ball release + return(1);} + else if (Command == 3) { // reset of the lower two of the five drop targets + ActivateSolenoid(40, 3); // Fix to increase the strength of the ball release + return(1);} + return(0); + case SolenoidRelCommand: + if (Command == 1 || Command == 3) { // ball release or the reset of the lower two of the five drop targets + return(1);} // ignore it + return(0); + case SoundCommandCh1: // sound commands for channel 1 + if (Command == 31) { } // ignore sound command 0x1f - audio bus init - not relevant for APC sound / also ignore 0xff whatever it is + else if (Command == 12) { // sound command 0x0c - stop sound + AfterSound = 0; + SoundSeries[0] = 0; + SoundSeries[1] = 0; // reset the multiball start sound + SoundSeries[2] = 0; // Reset BG sound + StopPlayingSound();} + else if (Command == 10){ // sound command 0x0a - sound series + if (SoundSeries[0] < 80) { // this sound has 80 pitches + SoundSeries[0]++;} // every call of this sound proceeds with next pitch + char FileName[13] = "0_0a_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[0] % 100) / 10; // the same with the 6th character + PlaySound(51, (char*) FileName);} // play the sound + else if (Command == 11) { // sound command 0x0b - sound series + if (SoundSeries[1] < 200) { // this sound has 200 pitches + SoundSeries[1]++;} + char FileName[13] = "0_0b_000.snd"; + FileName[7] = 48 + (SoundSeries[1] % 10); + FileName[6] = 48 + (SoundSeries[1] % 100) / 10; + FileName[5] = 48 + (SoundSeries[1] / 100); // the same with the 5th character + PlaySound(51, (char*) FileName);} + else if (Command == 13) { // sound command 0x0d - background sound - sound series + SoundSeries[0] = 0; + if (SoundSeries[2] < 25) { // this sound has 25 pitches + SoundSeries[2]++;} + char FileName[13] = "0_0e_000.snd"; + FileName[7] = 48 + (SoundSeries[2] % 10); + FileName[6] = 48 + (SoundSeries[2] % 100) / 10; + for (byte i=0; i<12; i++) { // prepare the filename + USB_RepeatSound[i] = FileName[i];} + QueueNextSound(USB_RepeatSound); // select this sound to be repeated + PlaySound(51, (char*) FileName);} + else if (Command == 14) { // sound command 0x0e - background sound - sound series + SoundSeries[2] = 0; + char FileName[13] = "0_0e_001.snd"; + for (byte i=0; i<12; i++) { // prepare the filename + USB_RepeatSound[i] = FileName[i];} + QueueNextSound(USB_RepeatSound); // select this sound to be repeated + PlaySound(51, (char*) FileName);} + else if (Command == 15) { // sound command 0x0f - start game + char FileName[13] = "0_0f_000.snd"; // generate base filename + FileName[7] = 48 + random(8) + 1; // change the counter according to random number + PlaySound(51, (char*) FileName);} // play the corresponding sound file + else { // standard sound + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: + return(0);}} + byte EX_Firepower(byte Type, byte Command){ // thanks to Matiou for sending me this code static byte SoundSeries[5] = {0, 0, 0, 0, 0}; // buffer to handle pre system11 sound series static byte PlayingMultiballSound = 0; @@ -118,83 +203,127 @@ byte EX_Firepower(byte Type, byte Command){ // thanks to Matiou for se switch(Type){ case SoundCommandCh1: // sound commands for channel 1 - if (Command == 33 || // ignore sound command 0x21, 0x27, 0x7f,0xff - Command == 39 || - Command == 127 || - Command == 255) { } - else if (Command == 108) { // sound command 0x6c - stop all sounds and reset series - AfterSound = 0; - SoundSeries[0] = 0; - SoundSeries[1] = 0; - SoundSeries[2] = 0; - SoundSeries[3] = 0; - SoundSeries[4] = 0; - StopPlayingSound();} - else { - if (Command > 127) { - Command &= 127;} - if (Command == 100){ // 0x64 End game - random speech - char FileName[13] = "0_64_000.snd"; // generate base filename - FileName[7] = 48 + random(9) + 1; // change the counter according to random number - PlaySound(52, (char*) FileName);} // play the corresponding sound file - else if (Command == 103){ // 0x67 Fire one/two/three series (multiball!) - // code for individual sounds - if (PlayCombinedSoundForMultiball == 0) { - PlayingMultiballSound = 1; // remember we're in a multiball start session - if (SoundSeries[0] < 3) // this sound has 3 tunes - SoundSeries[0]++; // every call of this sound proceeds with next tune - else // - SoundSeries[0] = 1; // start all over again - char FileName[13] = "0_67_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current tune - PlaySound(51, (char*) FileName);} // play the sound - else { // code for combined sounds (not standard but works better) - if (PlayingMultiballSound == 0) { - PlayingMultiballSound = 1; - char FileName[13] = "0_67_004.snd"; // this wav is combined version from 67_001 to 67_003 - PlaySound(51, (char*) FileName);}}} - else if (Command == 105){ // 0x69 Bonus - if (SoundSeries[1] < 146) // this sound has 146 tunes - SoundSeries[1]++; // every call of this sound proceeds with next tune - char FileName[13] = "0_69_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries[1] % 10); // change the 7th character of filename according to current tune - FileName[6] = 48 + (SoundSeries[1] % 100) / 10; // the same with the 6th character - FileName[5] = 48 + (SoundSeries[1] / 100); // the same with the 5th character - PlaySound(51, (char*) FileName);} // play the sound - else if (Command == 106) { // 0x6a Whirlling background - if (SoundSeries[2] < 29 ) // this sound has 29 tunes - SoundSeries[2]++; // every call of this sound proceeds with next tune - char FileName[13] = "0_6a_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries[2] % 10); // change the 7th character of filename according to current tune - FileName[6] = 48 + (SoundSeries[2] % 100) / 10; // the same with the 6th character - PlaySound(51, (char*) FileName);} // play the sound - else if (Command == 109) { // 0x6d Spinner - if (SoundSeries[3] < 31 ) // this sound has 31 tunes - SoundSeries[3]++; // every call of this sound proceeds with next tune - char FileName[13] = "0_6d_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries[3] % 10); // change the 7th character of filename according to current tune - FileName[6] = 48 + (SoundSeries[3] % 100) / 10; // the same with the 6th character - PlaySound(51, (char*) FileName);} // play the sound - else if (Command == 110) { // 0x6e Background // repeated - PlayingMultiballSound = 0; // if the background plays, we're not in a multiball start session - if (SoundSeries[4] < 31 ) // this sound has 31 tunes - SoundSeries[4]++; // every call of this sound proceeds with next tune - char FileName[13] = "0_6e_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries[4] % 10); // change the 7th character of filename according to current tune - FileName[6] = 48 + (SoundSeries[4] % 100) / 10; // the same with the 6th character - for (byte i=0; i<12; i++) { // store the name of this sound - USB_RepeatSound[i] = FileName[i];} - QueueNextSound(USB_RepeatSound); // select this sound to be repeated - PlaySound(51, (char*) FileName);} // play the sound - else if ((Command == 104 || Command == 107 || Command == 60 || Command == 63) // ignore these sounds at beginning of multiball - && PlayingMultiballSound == 1) { } - else { // standard sound - char FileName[9] = "0_00.snd"; // handle standard sound - if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present - PlaySound(51, (char*) FileName);}}} - return(0); // return number not relevant for sounds + if (Command == 31) { } // ignore sound command 0x1f + else if (Command == 12) { // sound command 0x0c - stop all sounds and reset series + AfterSound = 0; + SoundSeries[0] = 0; + SoundSeries[1] = 0; + SoundSeries[2] = 0; + SoundSeries[3] = 0; + SoundSeries[4] = 0; + StopPlayingSound();} + else { + if (Command == 4){ // 0x04 End game - random speech + char FileName[13] = "0_04_000.snd"; // generate base filename + FileName[7] = 48 + random(9) + 1; // change the counter according to random number + PlaySound(52, (char*) FileName);} // play the corresponding sound file + else if (Command == 7){ // 0x07 Fire one/two/three series (multiball!) + // code for individual sounds + if (PlayCombinedSoundForMultiball == 0) { + PlayingMultiballSound = 1; // remember we're in a multiball start session + if (SoundSeries[0] < 3) // this sound has 3 pitches + SoundSeries[0]++; // every call of this sound proceeds with next pitch + else // + SoundSeries[0] = 1; // start all over again + char FileName[13] = "0_07_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current pitch + PlaySound(51, (char*) FileName);} // play the sound + else { // code for combined sounds (not standard but works better) + if (PlayingMultiballSound == 0) { + PlayingMultiballSound = 1; + char FileName[13] = "0_07_004.snd"; // this wav is combined version from 67_001 to 67_003 + PlaySound(51, (char*) FileName);}}} + else if (Command == 9){ // 0x09 Bonus + if (SoundSeries[1] < 146) // this sound has 146 pitches + SoundSeries[1]++; // every call of this sound proceeds with next pitch + char FileName[13] = "0_09_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[1] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[1] % 100) / 10; // the same with the 6th character + FileName[5] = 48 + (SoundSeries[1] / 100); // the same with the 5th character + PlaySound(51, (char*) FileName);} // play the sound + else if (Command == 10) { // 0x0a Whirling background + if (SoundSeries[2] < 29 ) // this sound has 29 pitches + SoundSeries[2]++; // every call of this sound proceeds with next pitch + char FileName[13] = "0_0a_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[2] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[2] % 100) / 10; // the same with the 6th character + PlaySound(51, (char*) FileName);} // play the sound + else if (Command == 13) { // 0x0d Spinner + if (SoundSeries[3] < 31 ) // this sound has 31 pitches + SoundSeries[3]++; // every call of this sound proceeds with next pitch + char FileName[13] = "0_0d_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[3] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[3] % 100) / 10; // the same with the 6th character + PlaySound(51, (char*) FileName);} // play the sound + else if (Command == 14) { // 0x0e Background // repeated + PlayingMultiballSound = 0; // if the background plays, we're not in a multiball start session + if (SoundSeries[4] < 31 ) // this sound has 31 pitches + SoundSeries[4]++; // every call of this sound proceeds with next pitch + char FileName[13] = "0_0e_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[4] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[4] % 100) / 10; // the same with the 6th character + for (byte i=0; i<12; i++) { // store the name of this sound + USB_RepeatSound[i] = FileName[i];} + QueueNextSound(USB_RepeatSound); // select this sound to be repeated + PlaySound(51, (char*) FileName);} // play the sound + else if ((Command == 8 || Command == 11) // ignore these sounds at beginning of multiball + && PlayingMultiballSound == 1) { } + else { // standard sound + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}}} + return(0); // return number not relevant for sounds + default: + return(0);}} // no exception rule found for this type so proceed as normal + +byte EX_AlienPoker(byte Type, byte Command){ + static byte SoundSeries[3] = {0, 0, 0}; // buffer to handle pre system11 sound series + switch(Type){ + case SoundCommandCh1: // sound commands for channel 1 + if (Command == 31) { } // ignore sound command 0x1f + else if (Command == 12) { // sound command 0x0c - stop all sounds and reset series + AfterSound = 0; + SoundSeries[0] = 0; + SoundSeries[1] = 0; + SoundSeries[2] = 0; + StopPlayingSound();} + else if (Command == 10) { // sound series + if (SoundSeries[0] < 29 ) // this sound has 29 pitches + SoundSeries[0]++; // every call of this sound proceeds with next pitch + char FileName[13] = "0_0a_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[0] % 100) / 10; // the same with the 6th character + PlaySound(51, (char*) FileName);} // play the sound + else if (Command == 13) { // sound series + if (SoundSeries[1] < 31 ) // this sound has 31 pitches + SoundSeries[1]++; // every call of this sound proceeds with next pitch + char FileName[13] = "0_0d_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[1] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[1] % 100) / 10; // the same with the 6th character + PlaySound(51, (char*) FileName);} // play the sound + else if (Command == 14) { // 0x0e Background sound series - repeated + SoundSeries[0] = 0; + SoundSeries[1] = 0; + if (SoundSeries[2] < 36 ) // this sound has 36 pitches + SoundSeries[2]++; // every call of this sound proceeds with next pitch + char FileName[13] = "0_0e_000.snd"; // generate base filename + FileName[7] = 48 + (SoundSeries[2] % 10); // change the 7th character of filename according to current pitch + FileName[6] = 48 + (SoundSeries[2] % 100) / 10; // the same with the 6th character + for (byte i=0; i<12; i++) { // store the name of this sound + USB_RepeatSound[i] = FileName[i];} + QueueNextSound(USB_RepeatSound); // select this sound to be repeated + PlaySound(51, (char*) FileName);} // play the sound + else if (Command == 23){ // sound command 0x17 - game over random phrase + char FileName[13] = "0_17_000.snd"; // generate base filename + FileName[7] = 48 + random(6) + 1; // change the counter according to random number + PlaySound(52, (char*) FileName);} // play the corresponding sound file + else { // standard sound + char FileName[9] = "0_00.snd"; // handle standard sound + if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds default: - return(0);}} // no exception rule found for this type so proceed as normal + return(0);}} // no exception rule found for this type so proceed as normal byte EX_JungleLord(byte Type, byte Command){ static byte SoundSeries[2]; // buffer to handle pre system11 sound series @@ -207,10 +336,10 @@ byte EX_JungleLord(byte Type, byte Command){ PlaySound(52, (char*) FileName);} // play the corresponding sound file else if (Command == 42){ // sound command 0x2a - background sound - sound series SoundSeries[1] = 0; // reset the multiball start sound - if (SoundSeries[0] < 29) // BG sound has 29 tunes - SoundSeries[0]++; // every call of this sound proceeds with the next tune + if (SoundSeries[0] < 29) // BG sound has 29 pitches + SoundSeries[0]++; // every call of this sound proceeds with the next pitch char FileName[13] = "0_2a_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current tune + FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current pitch FileName[6] = 48 + (SoundSeries[0] % 100) / 10; // the same with the 6th character for (byte i=0; i<12; i++) { // store the name of this sound USB_RepeatSound[i] = FileName[i];} @@ -222,12 +351,12 @@ byte EX_JungleLord(byte Type, byte Command){ SoundSeries[1] = 0; // reset the multiball start sound StopPlayingSound();} else if (Command == 45){ // sound command 0x2d - multiball start - sound series - if (SoundSeries[1] < 31) // this sound has 31 tunes - SoundSeries[1]++; // every call of this sound proceeds with next tune + if (SoundSeries[1] < 31) // this sound has 31 pitches + SoundSeries[1]++; // every call of this sound proceeds with next pitch else SoundSeries[1] = 1; // start all over again char FileName[13] = "0_2d_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries[1] % 10); // change the 7th character of filename according to current tune + FileName[7] = 48 + (SoundSeries[1] % 10); // change the 7th character of filename according to current pitch FileName[6] = 48 + (SoundSeries[1] % 100) / 10; // the same with the 6th character PlaySound(51, (char*) FileName);} // play the sound else { // standard sound @@ -277,10 +406,10 @@ byte EX_Pharaoh(byte Type, byte Command){ // thanks to Grangeomatic SoundSeries = 0; // Reset BG sound StopPlayingSound();} else if (Command == 45) { // sound command 0x2d - background sound - sound series - if (SoundSeries < 31) // sound series has 31 different tunes - SoundSeries++; // switch to the next tune when sound command is called again + if (SoundSeries < 31) // sound series has 31 different pitches + SoundSeries++; // switch to the next pitch when sound command is called again char FileName[13] = "0_2d_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries % 10); // change the 7th character of filename according to current tune + FileName[7] = 48 + (SoundSeries % 10); // change the 7th character of filename according to current pitch FileName[6] = 48 + (SoundSeries % 100) / 10; // the same with the 6th character for (byte i=0; i<12; i++) { // store filename to be repeated USB_RepeatSound[i] = FileName[i];} @@ -321,10 +450,10 @@ byte EX_Barracora(byte Type, byte Command){ SoundSeries[1] = 0; // reset the multiball start sound StopPlayingSound();} else if (Command == 45){ // sound command 0x2d - sound series - if (SoundSeries[0] < 31) { // this sound has 31 tunes - SoundSeries[0]++;} // every call of this sound proceeds with next tune + if (SoundSeries[0] < 32) { // this sound has 32 pitches + SoundSeries[0]++;} // every call of this sound proceeds with next pitch char FileName[13] = "0_2d_000.snd"; // generate base filename - FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current tune + FileName[7] = 48 + (SoundSeries[0] % 10); // change the 7th character of filename according to current pitch FileName[6] = 48 + (SoundSeries[0] % 100) / 10; // the same with the 6th character for (byte i=0; i<12; i++) { // prepare the filename USB_RepeatSound[i] = FileName[i];} @@ -332,7 +461,7 @@ byte EX_Barracora(byte Type, byte Command){ PlaySound(51, (char*) FileName);} // play the sound else if (Command == 46) { // sound command 0x2e - background sound - sound series SoundSeries[0] = 0; - if (SoundSeries[1] < 29) { // this sound has 29 tunes + if (SoundSeries[1] < 30) { // this sound has 30 pitches SoundSeries[1]++;} char FileName[13] = "0_2e_000.snd"; FileName[7] = 48 + (SoundSeries[1] % 10); @@ -442,7 +571,8 @@ byte EX_BlackKnight(byte Type, byte Command){ return(0);}} byte EX_Comet(byte Type, byte Command) { - if (Type == SoundCommandCh1) { // sound commands for channel 1 + switch(Type){ + case SoundCommandCh1: // sound commands for channel 1 if (!Command || Command > 254) { // sound command 0x00 and 0xff -> stop sound AfterMusic = 0; StopPlayingMusic(); @@ -454,12 +584,14 @@ byte EX_Comet(byte Type, byte Command) { else { // handle standard sound if (Command == 9) { MusicVolume = 4;} // reduce music volume - if (Command == 241) { + if (Command == 241) { // sound 0xf1 RestoreMusicVolumeAfterSound(25);} // and restore it char FileName[9] = "0_00.snd"; if (USB_GenerateFilename(1, Command, FileName)) { // create filename and check whether file is present - PlaySound(51, (char*) FileName);}}} - return(0);} // no exception rule found for this type so proceed as normal + PlaySound(51, (char*) FileName);}} + return(0); // return number not relevant for sounds + default: + return(0);}} // no exception rule found for this type so proceed as normal byte EX_Pinbot(byte Type, byte Command){ switch(Type){ @@ -776,9 +908,20 @@ byte EX_Blank(byte Type, byte Command){ // use this as a template void EX_Init(byte GameNumber) { switch(GameNumber) { + case 4: // Disco Fever + //SolRecycleTime[5-1] = 250; // set recycle time for eject hole to prevent double kicking + PinMameException = EX_DiscoFever; // use exception rules for Flash + break; + case 6: // Flash + SolRecycleTime[5-1] = 250; // set recycle time for eject hole to prevent double kicking + PinMameException = EX_Flash; // use exception rules for Flash + break; case 16: // Firepower PinMameException = EX_Firepower; // use exception rules for Firepower break; + case 18: // Alien Poker + PinMameException = EX_AlienPoker; // use exception rules for Alien Poker + break; case 20: // Jungle Lord EX_EjectSolenoid = 2; // specify eject coil for improved ball release PinMameException = EX_JungleLord; // use exception rules for Jungle Lord diff --git a/Pinbot.ino b/Pinbot.ino index 90486c5..9d1cfbc 100644 --- a/Pinbot.ino +++ b/Pinbot.ino @@ -6,7 +6,6 @@ bool PB_DropWait = false; // ignore drop target swit bool PB_DropRamp = false; // ramp needs to be dropped when possible bool PB_EnergyActive = false; // score energy active? bool PB_SkillShot = false; // is the skill shot active? -bool PB_EjectIgnore = false; // ignore the hole switch while the ball is in the hole bool PB_IgnoreLock = false; // ignore the lock switches to cope with switch bouncing bool PB_SpecialLit = false; // is the special lit? byte PB_BallSave = 0; // prevent immediate outlane drains 0=inactive 1=active 2=triggered @@ -26,6 +25,7 @@ byte PB_EjectMode[5]; // current mode of the eje byte PB_EnergyValue[5]; // energy value for current player (value = byte*2000) byte PB_LampsToLight = 2; // number of lamps to light when chest is hit byte *PB_ChestPatterns; // pointer to the current chest lamp pattern +byte PB_MballState = 1; // status variable for the 3 ball multiball feature const unsigned int PB_SolTimes[32] = {50,30,30,70,50,200,30,30,0,0,0,0,0,0,150,150,50,0,50,50,50,50,0,0,50,150,150,150,150,150,150,100}; // Activation times for solenoids (last 8 are C bank) const byte PB_BallSearchCoils[10] = {3,4,5,17,19,22,6,20,21,0}; // coils to fire when the ball watchdog timer runs out @@ -34,7 +34,7 @@ const byte PB_MultiballSeq[69] = {16,5,15,5,26,5,29,10,26,5,15,5,16,10,15,5,26,5 const byte PB_ScoreEnergySeq[7] = {31,10,31,10,31,10,0}; const byte PB_LeftBBinserts[5] = {28, 20, 28, 20, 0}; const byte PB_RightBBinserts[5] = {27, 20, 27, 20, 0}; -const byte PB_BB_FlasherCycle[17] = {32, 6, 30, 2, 31, 8, 29, 8, 26, 8, 15, 8, 16, 3, 9, 1, 0}; +const byte PB_BB_FlasherCycle[15] = {32, 6, 30, 2, 31, 8, 29, 8, 26, 8, 15, 8, 16, 3, 0}; const byte PB_Ball_Locked[5] = {26,30,26,30,0}; const byte PB_SkillShotFail[25] = {26,10,15,40,26,10,15,40,26,10,15,40,26,10,15,40,26,10,15,40,26,10,15,40,0}; const byte PB_MultiplierSeq[83] = {27,2,29,6,26,7,27,6,15,5,16,8,27,5,29,8,30,5,26,1,31,11,28,1,15,6,31,3,16,3,28,8,27,1,29,9,26,5,27,4,15,4,31,7,16,3,31,7,26,1,29,10,32,1,15,11,30,1,16,4,31,3,29,8,28,6,26,5,31,7,15,1,16,11,29,10,31,2,15,6,16,4,0}; @@ -44,9 +44,10 @@ const byte PB_ExBallLamps[4] = {49, 50, 58, 57}; const byte PB_ACselectRelay = 14; // solenoid number of the A/C select relay const char PB_TestSounds[10][15] = {{"1_01L.snd"},{"1_02.snd"},{"1_02L.snd"},{"1_03L.snd"},{"1_04.snd"},{"1_04L.snd"},{"1_05.snd"},{"1_06.snd"},{"1_06L.snd"},0}; const char PB_TxTMballs[2][17] = {{" 2 BALL "},{" 3 BALL "}}; +const char PB_PlanetTxt[9][17] = {{" PLUTO "},{" NEPTUNE "},{" URANUS "},{" SATURN "},{" JUPITER "},{" MARS "},{" EARTH "},{" VENUS "},{" MERCURY "}}; const struct SettingTopic PB_setList[11] = {{"DROP TG TIME ",HandleNumSetting,0,3,30}, - {" REACH PLANET ",HandleNumSetting,0,1,9}, + {" REACH PLANET ",HandleTextSetting,&PB_PlanetTxt[0][0],0,8}, {" ENERGY TIMER ",HandleNumSetting,0,1,90}, {" BALL SAVER ",HandleBoolSetting,0,0,0}, {" MULTIBALL ",HandleTextSetting,&PB_TxTMballs[0][0],0,1}, @@ -344,6 +345,11 @@ const byte PB_WalkingLines[199] = {15,0b01000,0b01000,0b01000,0b01000,0b01000, 15,0b10000,0b01000,0b01000,0b00100,0b00100, 15,0b10000,0b10000,0b10000,0b10000,0b10000,0}; +const byte PB_Characters[185] = {0,0,0,0,0,12,18,18,18,12,4,12,4,4,14,12,18,4,8,30,12,2,12,2,12,10,18,30,2,2,28,16,28,2,12,12,16,28,18,12,30,2,4,8,8,12,18,12,18,12,12,18,14,2,12, // Blank,0,1,2,3,4,5,6,7,8,9 + 12,18,30,18,18,28,18,28,18,28,14,16,16,16,14,28,18,18,18,28,30,16,28,16,30,30,16,28,16,16,14,16,22,18,14,18,18,30,18,18,4,4,4,4,4,14,2,2,10,4,18,20,24,20,18, // a,b,c,d,e,f,g,h,i,j,k + 8,8,8,8,14,17,27,21,17,17,17,25,21,19,17,12,18,18,18,12,28,18,28,16,16,12,18,18,22,12,28,18,28,20,18,14,16,12,2,28,14,4,4,4,4,18,18,18,18,12,17,17,10,10,4, // l,m,n,o,p,q,r,s,t,u,v + 17,17,21,21,10,17,10,4,10,17,17,10,4,4,4,31,2,4,8,31}; // w,x,y,z + struct GameDef PB_GameDefinition = { PB_setList, // GameSettingsList (byte*)PB_defaults, // GameDefaultsPointer @@ -357,12 +363,24 @@ void PB_init() { Serial.begin(115200);} SolRecycleTime[20-1] = 200; // set recycle time for both slingshots SolRecycleTime[21-1] = 200; - digitalWrite(VolumePin,HIGH); // set volume to zero ACselectRelay = PB_ACselectRelay; // assign the number of the A/C select relay GameDefinition = PB_GameDefinition;} // read the game specific settings and highscores void PB_AttractMode() { + InLock = 0; + if (QuerySwitch(25)) { // count locked balls + InLock++;} + if (QuerySwitch(26)) { + InLock++;} + if (QuerySwitch(38)) { // and check the eject hole + InLock++;} + if (QuerySwitch(16)) { // ball in the outhole? + ActA_BankSol(1);} AfterMusic = 0; + if (APC_settings[Volume]) { // system set to digital volume control? + analogWrite(VolumePin,255-APC_settings[Volume]);} // adjust PWM to volume setting + else { + digitalWrite(VolumePin,HIGH);} // turn off the digital volume control DispRow1 = DisplayUpper; DispRow2 = DisplayLower; Switch_Pressed = PB_AttractModeSW; @@ -373,6 +391,7 @@ void PB_AttractMode() { PB_AttractDisplayCycle(1);} void PB_AttractDisplayCycle(byte Step) { + static byte Count = 0; static byte Timer0 = 0; static byte Timer1 = 0; static byte Timer2 = 0; @@ -391,20 +410,43 @@ void PB_AttractDisplayCycle(byte Step) { if (Timer3) { KillTimer(Timer3); Timer3 = 0;} + Count = 0; ScrollUpper(100); // stop scrolling ScrollLower(100); AddScrollUpper(100); return; case 1: // attract mode title 'page' - WriteUpper2("THE APC "); - Timer1 = ActivateTimer(50, 5, PB_AttractDisplayCycle); - Timer3 = ActivateTimer(2000, 7, PB_AttractDisplayCycle); - WriteLower2(" "); - Timer2 = ActivateTimer(1400, 6, PB_AttractDisplayCycle); - if (NoPlayers) { // if there were no games before skip the next step - Step++;} + if (Count == 20) { + Count++; + Step = 1; + WriteUpper(" NOW I SEE YOU "); // erase display + WriteLower(" "); + PlaySound(55, "0_b0.snd");} // 'now I see you' + //Timer3 = ActivateTimer(3000, 10, PB_AttractDisplayCycle);} + else if (Count == 40) { + Timer0 = 0; + Count++; + Step = 1; + LampReturn = PB_RestoreLamps; + ShowLampPatterns(0); // stop lamp animations + PB_AttractDisplayCycle(0); // stop display animations + for (byte i=0; i< 8; i++) { + LampColumns[i] = 0;} + LampPattern = LampColumns; + PB_RulesDisplay(1); + Count = 0; + return;} else { - Step = 3;} + Count++; + WriteUpper2("THE APC "); + Timer1 = ActivateTimer(50, 5, PB_AttractDisplayCycle); + Timer3 = ActivateTimer(2000, 7, PB_AttractDisplayCycle); + WriteLower2(" "); + Timer2 = ActivateTimer(1400, 6, PB_AttractDisplayCycle); + if (NoPlayers) { // if there were no games before skip the next step + Step++;} + else { + Step = 3;}} break; case 2: // show scores of previous game WriteUpper2(" "); // erase display @@ -455,7 +497,13 @@ void PB_AttractDisplayCycle(byte Step) { Timer3 = 0; WriteUpper2("PINBOT "); AddScrollUpper(0); - return;} + return; + case 10: + AddBlinkLamp(1, 150); // blink Game Over lamp + LampReturn = PB_AttractLampCycle; + PB_AttractLampCycle(1); + PB_AttractDisplayCycle(1); + break;} PB_CheckForLockedBalls(0); // check for a ball in the outhole Timer0 = ActivateTimer(4000, Step, PB_AttractDisplayCycle);} // come back for the next 'page' @@ -497,64 +545,70 @@ void PB_ShowMessage(byte Seconds) { // switch to the second di void PB_AttractModeSW(byte Select) { switch(Select) { case 3: // credit button - RemoveBlinkLamp(1); // stop the blinking of the game over lamp - PB_PlayAfterGameSequence(0); // stop end of game animation - LampReturn = PB_RestoreLamps; - ShowLampPatterns(0); // stop lamp animations - PB_AttractDisplayCycle(0); // stop display animations - if (APC_settings[Volume]) { // system set to digital volume control? - analogWrite(VolumePin,255-APC_settings[Volume]);} // adjust PWM to volume setting - else { - digitalWrite(VolumePin,HIGH);} // turn off the digital volume control - Switch_Pressed = AddPlayerSW; - for (byte i=0; i< 8; i++) { - LampColumns[i] = 0;} - LampPattern = LampColumns; - TurnOnLamp(3); // turn on Ball in Play lamp - NoPlayers = 0; - WriteUpper(" "); - WriteLower(" "); - Ball = 1; - PB_AddPlayer(); - for (byte i=1;i<5;i++) { // for all players - PB_Chest_Status[i] = 0; // reset the number of number of visor openings - PB_ResetPlayersChestLamps(i); // reset the chest lamps - PB_EnergyValue[i] = 25; // reset the energy value to 50K - PB_ExBallsLit[i] = 0; // reset the lit extra balls - PB_EjectMode[i] = 0; // reset the mode of the eject hole - PB_LitChestLamps[Player-1] = 0; // reset the number of lit chest lamps - PB_Planet[i] = 0;} // reset reached planets - InLock = 0; - Player = 1; - ExBalls = 0; - Multiballs = 1; - Bonus = 1; - BonusMultiplier = 1; - if (QuerySwitch(49) || QuerySwitch(50) || QuerySwitch(51)) { // any drop target down? - ActA_BankSol(4);} // reset it - if (!QuerySwitch(44)) { // ramp in up state? - ActA_BankSol(6);} // put it down - ActivateSolenoid(0, 12); // turn off playfield GI - PB_ChestMode = 20; // just play a chest pattern - PB_ChestPatterns = (byte*)PB_GameStartPat; // set chest lamps pattern - PB_ChestLightHandler(100); // start player - ActivateTimer(2400, 0, PB_GameStart) ; // release a new ball (2 expected balls in the trunk) - PlaySound(55, "0_ad.snd"); // 'Pinbot circuits activated' - ActivateSolenoid(0, 23); // enable flipper fingers - ActivateSolenoid(0, 24); + if (!InLock) { // only if all balls have been cleared out + RemoveBlinkLamp(1); // stop the blinking of the game over lamp + PB_PlayAfterGameSequence(0); // stop end of game animation + LampReturn = PB_RestoreLamps; + ShowLampPatterns(0); // stop lamp animations + PB_AttractDisplayCycle(0); // stop display animations + PB_RulesDisplay(0); + PB_RuleLampEffects(0); + MusicVolume = 0; + Switch_Pressed = AddPlayerSW; + for (byte i=0; i< 8; i++) { + LampColumns[i] = 0;} + LampPattern = LampColumns; + TurnOnLamp(3); // turn on Ball in Play lamp + NoPlayers = 0; + WriteUpper(" "); + WriteLower(" "); + Ball = 1; + PB_AddPlayer(); + for (byte i=1;i<5;i++) { // for all players + PB_Chest_Status[i] = 0; // reset the number of number of visor openings + PB_ResetPlayersChestLamps(i); // reset the chest lamps + PB_EnergyValue[i] = 25; // reset the energy value to 50K + PB_ExBallsLit[i] = 0; // reset the lit extra balls + PB_EjectMode[i] = 0; // reset the mode of the eject hole + PB_LitChestLamps[Player-1] = 0; // reset the number of lit chest lamps + PB_Planet[i] = 0;} // reset reached planets + InLock = 0; + Player = 1; + ExBalls = 0; + Multiballs = 1; + PB_MballState = 1; + Bonus = 1; + BonusMultiplier = 1; + if (QuerySwitch(49) || QuerySwitch(50) || QuerySwitch(51)) { // any drop target down? + ActA_BankSol(4);} // reset it + if (!QuerySwitch(44)) { // ramp in up state? + ActA_BankSol(6);} // put it down + ActivateSolenoid(0, 12); // turn off playfield GI + PB_ChestMode = 20; // just play a chest pattern + PB_ChestPatterns = (byte*)PB_GameStartPat; // set chest lamps pattern + PB_ChestLightHandler(100); // start player + ActivateTimer(2400, 0, PB_GameStart) ; // release a new ball (2 expected balls in the trunk) + PlaySound(55, "0_ad.snd"); // 'Pinbot circuits activated' + ActivateSolenoid(0, 23); // enable flipper fingers + ActivateSolenoid(0, 24);} break; case 8: // high score reset digitalWrite(Blanking, LOW); // invoke the blanking break; + case 16: // outhole + if (!BlockOuthole) { + BlockOuthole = true; // block outhole until this ball has been processed + ActivateTimer(200, 100, PB_AttractModeSW);} // check again in 200ms + break; case 46: if (PB_CloseVisorFlag) { - PlaySound(52, "0_f3.snd"); + //PlaySound(52, "0_f3.snd"); PB_CloseVisorFlag = false; ReleaseSolenoid(13);} break; case 47: if (PB_OpenVisorFlag) { - PlaySound(52, "0_f3.snd"); + //PlaySound(52, "0_f3.snd"); PB_OpenVisorFlag = false; ReleaseSolenoid(13);} break; @@ -575,7 +629,17 @@ void PB_AttractModeSW(byte Select) { else { Settings_Enter();} break; - }} + case 100: // push ball in trunk + if (QuerySwitch(16)) { // outhole switch still active? + if (QuerySwitch(17) && QuerySwitch(18)) { // all balls found? - only possible in 3Mball mode + InLock = 0;} + ActA_BankSol(1); // use outhole kicker + ActivateTimer(1000, 100, PB_AttractModeSW);} // check again in 500ms + else { + if (!game_settings[PB_Multiballs] && QuerySwitch(17) && QuerySwitch(18)) { // all balls found in 2Mball mode + InLock = 0;} // stop blocking new games + BlockOuthole = false;} + break;}} void PB_GameStart(byte Dummy) { UNUSED(Dummy); @@ -588,8 +652,6 @@ void AddPlayerSW(byte Switch) { void PB_CheckForLockedBalls(byte Dummy) { // check if balls are locked and release them UNUSED(Dummy); - if (QuerySwitch(16)) { // for the outhole - ActA_BankSol(1);} if (QuerySwitch(38)) { // for the single eject hole ActA_BankSol(3);} if (QuerySwitch(25) || QuerySwitch(26)) { // for the eyes @@ -615,20 +677,24 @@ void PB_AddPlayer() { void PB_NewBall(byte Balls) { // release ball (Balls = expected balls on ramp) ShowAllPoints(0); + if (game_settings[PB_Multiballs]) { // 3 ball multiball selected? + PB_SolarValue = 0; // reset jackpot + PB_SkillMultiplier = 1;} + else { // 2 ball multiball selected + if (Balls < 10) { // is it an extra ball? + PB_SkillMultiplier = 0;} // no extra ball -> reset skill shot multiplier + else { // it's an extra ball + Balls -= 10;}} // restore balls value PlayMusic(50, "1_94.snd"); // play non looping part of music track QueueNextMusic("1_94L.snd"); // queue looping part as next music to be played} Bonus = 1; BonusMultiplier = 1; // reset bonus multiplier for (byte i=0; i<4; i++) { // turn off the corresponding lamps TurnOffLamp(9+i);} - if (Balls < 10) { // is it an extra ball? - PB_SkillMultiplier = 0;} // no extra ball -> reset skill shot multiplier - else { // it's an extra ball - Balls -= 10;} // restore balls value *(DisplayUpper+16) = LeftCredit[32 + 2 * Ball]; // show current ball in left credit BlinkScore(1); // turn on score blinking PB_ClearChest(); // turn off chest lamps - if ((PB_Chest_Status[Player] > 100)) { // > 100 means visor has to be open + if (PB_Chest_Status[Player] > 100 && PB_MballState != 3) { // > 100 means visor has to be open PB_Chest_Status[Player] = PB_Chest_Status[Player] - 100; // use it as a counter for opened visors PB_LampsToLight = 1; PB_ChestMode = 0; // indicate an open visor @@ -662,17 +728,11 @@ void PB_NewBall(byte Balls) { // release ball (Balls = e TurnOnLamp(PB_ExBallLamps[i]);} else { TurnOffLamp(PB_ExBallLamps[i]);}} - if (PB_EjectMode[Player] < 5) { - for (byte i=0; i 10) { // sun already reached? Planets = Planets - 10;} @@ -721,6 +781,10 @@ void PB_CheckShooterLaneSwitch(byte Switch) { if (Switch == 20) { // shooter lane switch released? Switch_Released = DummyProcess; PlaySound(53, "1_95.snd"); + if (PB_MballState == 4) { // 3 ball multiball running? + Multiballs = 3; // resume multiball + AddBlinkLamp(35, 100); // start blinking of solar energy ramp + PB_ShooterLaneWarning(0);} // turn off shooter lane warning if (!BallWatchdogTimer) { BallWatchdogTimer = ActivateTimer(30000, 0, PB_SearchBall);}}} @@ -812,7 +876,7 @@ void PB_ResetBallWatchdog(byte Switch) { // handle switches during case 22: c = 20; PlayFlashSequence((byte*) PB_SkillShotFail); - PlaySound(53, "1_91.snd"); + PlaySound(54, "1_91.snd"); break; case 23: // 100K hole hit c = 100; @@ -826,7 +890,7 @@ void PB_ResetBallWatchdog(byte Switch) { // handle switches during case 24: c = 5; PlayFlashSequence((byte*) PB_SkillShotFail); - PlaySound(51, "1_91.snd"); + PlaySound(53, "1_91.snd"); break;} if (!PB_ChestMode) { // visor is open if (InLock) { @@ -849,6 +913,35 @@ void PB_ResetBallWatchdog(byte Switch) { // handle switches during BallWatchdogTimer = ActivateTimer(30000, 0, PB_SearchBall);} PB_GameMain(Switch);} // process current switch +void PB_ShooterLaneWarning(byte State) { + static byte Timer = 0; + switch (State) { + case 0: // stop shooter lane warning + if (Timer) { + KillTimer(Timer); + Timer = 0;} + break; + case 1: // activate shooter lane warning + if (QuerySwitch(20)) { // ball still in shooter lane? + Multiballs = 2; // stop jackpot as long as ball is in the shooter lane + WriteUpper2(" LAUNCH BALL "); + WriteLower2(" "); + ShowMessage(1); + PlaySound(55, "0_6b.snd"); // warning sound + Switch_Released = PB_CheckShooterLaneSwitch; // set mode to register when ball is shot + Timer = ActivateTimer(1500, 2, PB_ShooterLaneWarning);} + else { + PB_SkillShot = false;} + break; + case 2: // every second + Timer = 0; // this case is called by timer + if (QuerySwitch(20)) { // ball still in shooter lane? + WriteUpper2(" LAUNCH BALL "); + WriteLower2(" "); + ShowMessage(1); + PlaySound(55, "0_6b.snd"); // warning sound + Timer = ActivateTimer(1500, 2, PB_ShooterLaneWarning);}}} + void PB_BallReleaseCheck(byte Switch) { // handle switches during ball release if ((Switch > 11)&&(Switch != 17)&&(Switch != 18)&&(Switch != 19)&&(Switch != 44)&&(Switch != 46)&&(Switch != 47)) { // playfield switch activated? if (CheckReleaseTimer) { @@ -900,45 +993,83 @@ byte PB_CountBallsInTrunk() { Balls++;}} return Balls;} +byte PB_SearchBallCycle(byte Counter) { + WriteUpper(" BALL SEARCH"); + ActivateSolenoid(0, PB_BallSearchCoils[Counter]); // fire coil to get ball free + if (PB_BallSearchCoils[Counter] == 5) { // ramp raise? + PB_DropRamp = true;} // set flag to drop ramp + if (PB_BallSearchCoils[Counter] == 6) { // ramp down? + PB_DropRamp = false;} // clear flag to drop ramp + Counter++; + if (Counter == 9) { // all coils fired? + Counter = 0;} // start again + if (QuerySwitch(46) && !QuerySolenoid(13)) { // visor closed and motor not active? + ActivateSolenoid(0, 13); // open it enough to deactivate switch 46 + ActivateTimer(2000, 0, PB_CloseVisor);} // and prepare to close it again + return(Counter);} + void PB_SearchBall(byte Counter) { // ball watchdog timer has run out + Serial.print("SearchBall Mballs = "); + Serial.println(PB_MballState); BallWatchdogTimer = 0; if (!QuerySwitch(10) && !QuerySwitch(11) && !QuerySwitch(20)) { // if ball is waiting to be launched or any flipper finger up if (QuerySwitch(16)) { // ball in outhole? BlockOuthole = false; ActivateTimer(1000, 0, PB_ClearOuthole);} + else if (QuerySwitch(38)) { // ball in eject hole? + PB_HandleEjectHole(3);} // release it else { byte c = PB_CountBallsInTrunk(); // recount all balls if (c == 5) { // balls have not settled yet WriteUpper(" BALL STUCK "); BallWatchdogTimer = ActivateTimer(1000, 0, PB_SearchBall);} // and try again in 1s else { - if (c == 2) { // found 2 balls in trunk? - if (BlockOuthole) { // is the outhole blocked - PB_BallEnd(0);} // then it was probably a ball loss gone wrong - else { - ActivateTimer(1000, 2, PB_NewBall);}} // otherwise try it with a new ball - else { - byte c2 = 0; - for (byte i=0; i<2; i++) { // count balls in lock - if (QuerySwitch(25+i)) { - c2++;}} - if (c2 > InLock) { // more locked balls found than expected? - PB_HandleLock(0); // lock them - BallWatchdogTimer = ActivateTimer(30000, 0, PB_SearchBall);} + if (game_settings[PB_Multiballs]) { // 3 ball multiball selected? + switch (PB_MballState) { + case 1: // one ball in play + Counter = PB_SearchBallCycle(Counter); // fire coils to search ball + BallWatchdogTimer = ActivateTimer(1000, Counter, PB_SearchBall); + break; + case 2: // one ball in lock + case 6: + PB_CountBallsInLock(); + if (InLock != 1) { // number of locked balls not as expected + PB_HandleLock(0);} // and call it + else { + Counter = PB_SearchBallCycle(Counter); // fire coils to search ball + BallWatchdogTimer = ActivateTimer(1000, Counter, PB_SearchBall);} + break; + case 3: // two balls in lock + PB_CountBallsInLock(); + if (InLock != 2) { // number of locked balls not as expected + PB_HandleLock(0);} // and call it + else if (c > 0) { // unexpected ball in trunk + PB_BallEnd(c);} + else { + Counter = PB_SearchBallCycle(Counter); // fire coils to search ball + BallWatchdogTimer = ActivateTimer(1000, Counter, PB_SearchBall);} + break; + default: + Counter = PB_SearchBallCycle(Counter); // fire coils to search ball + BallWatchdogTimer = ActivateTimer(1000, Counter, PB_SearchBall); + break;}} + else { // 2 ball multiball selected + if (c == 2) { // found 2 balls in trunk? + if (BlockOuthole) { // is the outhole blocked + PB_BallEnd(0);} // then it was probably a ball loss gone wrong + else { + ActivateTimer(1000, 2, PB_NewBall);}} // otherwise try it with a new ball else { - WriteUpper(" BALL SEARCH"); - ActivateSolenoid(0, PB_BallSearchCoils[Counter]); // fire coil to get ball free - if (PB_BallSearchCoils[Counter] == 5) { // ramp raise? - PB_DropRamp = true;} // set flag to drop ramp - if (PB_BallSearchCoils[Counter] == 6) { // ramp down? - PB_DropRamp = false;} // clear flag to drop ramp - Counter++; - if (Counter == 9) { // all coils fired? - Counter = 0;} // start again - if (QuerySwitch(46) && !QuerySolenoid(13)) { // visor closed and motor not active? - ActivateSolenoid(0, 13); // open it enough to deactivate switch 46 - ActivateTimer(2000, 0, PB_CloseVisor);} // and prepare to close it again - BallWatchdogTimer = ActivateTimer(1000, Counter, PB_SearchBall);}}}}} // come again in 1s if no switch is activated + byte c2 = 0; + for (byte i=0; i<2; i++) { // count balls in lock + if (QuerySwitch(25+i)) { + c2++;}} + if (c2 > InLock) { // more locked balls found than expected? + PB_HandleLock(0); // lock them + BallWatchdogTimer = ActivateTimer(30000, 0, PB_SearchBall);} + else { + Counter = PB_SearchBallCycle(Counter); // fire coils to search ball + BallWatchdogTimer = ActivateTimer(1000, Counter, PB_SearchBall);}}}}}} // come again in 1s if no switch is activated else { BallWatchdogTimer = ActivateTimer(30000, 0, PB_SearchBall);}} @@ -954,58 +1085,132 @@ void PB_CloseVisor(byte State) { else { PB_CloseVisorFlag = true;}} // set flag to stop visor motor when closed +void PB_CountBallsInLock() { + InLock = 0; + for (byte i=0; i<2; i++) { // check how many balls are in the eyes + if (QuerySwitch(25+i)) { + InLock++;}}} + void PB_ClearOuthole(byte State) { static byte Trunk; - switch (State) { - case 0: // inital call - if (QuerySwitch(16)) { - if (!BlockOuthole) { // outhole switch still active? - BlockOuthole = true; // block outhole until this ball has been processed + if (game_settings[PB_Multiballs]) { // 3 ball multiball selected? + switch (State) { + case 0: // inital call + if (QuerySwitch(16)) { // outhole switch still active? Trunk = PB_CountBallsInTrunk(); - if ((!game_settings[PB_Multiballs] && ((Multiballs == 1 && Trunk == 1 - InLock) || (Multiballs == 2 && Trunk == 0))) || (game_settings[PB_Multiballs] && ((Multiballs == 1 && Trunk == 2 - InLock) || (Multiballs == 2 && Trunk == 0)))) { // ball count OK? - ActivateTimer(10, 5, PB_ClearOuthole);} - else { // ball count not OK - InLock = 0; + switch (PB_MballState) { + case 1: // one ball in play + if (Trunk == 2) { // 2 balls expected to be in trunk + ActivateTimer(10, 5, PB_ClearOuthole);} // proceed to next state + else { // wrong ball count + ActivateTimer(1000, 1, PB_ClearOuthole);} + break; + case 2: // one ball in lock + case 5: // two balls in game but none in lock + case 6: // one ball in lock after multiball + if (Trunk == 1) { // 1 ball expected to be in trunk + ActivateTimer(10, 5, PB_ClearOuthole);} // proceed to next state + else { // wrong ball count + ActivateTimer(1000, 1, PB_ClearOuthole);} + break; + case 3: // two balls in lock + case 4: // 3 ball multiball + if (!Trunk) { // 0 balls expected to be in trunk + ActivateTimer(10, 5, PB_ClearOuthole);} // proceed to next state + else { // wrong ball count + ActivateTimer(1000, 1, PB_ClearOuthole);} + break;}} + else { // outhole free + BlockOuthole = false;} + break; + case 1: // trunk count doesn't match + Trunk = PB_CountBallsInTrunk(); + PB_CountBallsInLock(); + switch (PB_MballState) { + case 1: // one ball in play + if (Trunk == 2) { // 2 balls expected to be in trunk + ActivateTimer(10, 5, PB_ClearOuthole);} // proceed to next state + else { // wrong ball count + if (!InLock) { + PB_MballState = 5; + ActivateTimer(10, 0, PB_ClearOuthole);} + else if (InLock == 1) { + PB_MballState = 6; + ActivateTimer(10, 0, PB_ClearOuthole);} + else { + PB_MballState = 3; + ActivateTimer(10, 0, PB_ClearOuthole);}} + break; + case 2: // one ball in lock + case 5: // 2 balls still in game + case 6: // one ball re-locked + if (Trunk == 1) { // 1 ball expected to be in trunk + ActivateTimer(10, 5, PB_ClearOuthole);} // proceed to next state + else if (Trunk == 2){ // ball may have jumped through outhole + if (!QuerySwitch(16)) { // no ball in outhole? + ActivateTimer(1000, 10, PB_ClearOuthole);} // then it's confirmed + else { + PB_MballState = 1; + ActivateTimer(10, 0, PB_ClearOuthole);}} + break; + case 3: // 2 balls in lock + case 4: // 3 ball multiball + if (!Trunk) { // trunk is supposed to be empty + ActivateTimer(10, 5, PB_ClearOuthole);} // proceed to next state + else if (Trunk == 1){ // ball may have jumped through outhole + if (!QuerySwitch(16)) { // no ball in outhole? + ActivateTimer(1000, 10, PB_ClearOuthole);}}} // then it's confirmed + break; + case 5: // ball count OK, put ball in trunk + if (!C_BankActive) { + ActivateSolenoid(game_settings[PB_BallEjectStrength], 1); // put ball in trunk + ActivateTimer(1000, 10, PB_ClearOuthole);} + else { + ActivateTimer(1000, 5, PB_ClearOuthole);} // try again + break; + case 10: // ball was kicked in outhole + if (QuerySwitch(16)) { // ball still in outhole? + if (PB_MballState == 4 || PB_MballState == 5) { // probably a double drain + PB_BallEnd(1); // call ball end twice + BlockOuthole = true;} // block outhole again + ActA_BankSol(1); // make the coil a bit stronger + ActivateTimer(2000, 10, PB_ClearOuthole);} // and come back in 2s + else { + PB_BallEnd(Trunk+1);}}} + else { // 2 ball multiball selected + switch (State) { + case 0: + if (QuerySwitch(16)) { // outhole switch still active? + if (!C_BankActive) { // correct solenoid bank active? + ActivateSolenoid(30, 1); // put ball in trunk + ActivateTimer(2000, 1, PB_ClearOuthole);} // come back to check + else { + ActivateTimer(500, 0, PB_ClearOuthole);}} // wait for A-bank to be active + else { // outhole free + BlockOuthole = false;} + break; + case 1: // check if ball is in trunk + case 2: + case 3: + Trunk = PB_CountBallsInTrunk(); + if ((Trunk == 5)||(Trunk < 3-Multiballs-InLock)) { // something's wrong in the trunk + InLock = 0; + if (Multiballs == 1) { for (byte i=0; i<2; i++) { // check how many balls are in the eyes if (QuerySwitch(25+i)) { - InLock++;}} - ActivateTimer(1000, 1, PB_ClearOuthole);}} // if not try again in 1s - else { // outhole still blocked - ActivateTimer(2000, 0, PB_ClearOuthole);}} // try again - else { // outhole free - BlockOuthole = false;} - break; - case 1: // trunk count doesn't match - Trunk = PB_CountBallsInTrunk(); - InLock = 0; - if ((!game_settings[PB_Multiballs] && ((Multiballs == 1 && Trunk == 1 - InLock) || (Multiballs == 2 && Trunk == 0))) || (game_settings[PB_Multiballs] && ((Multiballs == 1 && Trunk == 2 - InLock) || (Multiballs == 2 && Trunk == 0)))) { // ball count OK? - ActivateTimer(10, 5, PB_ClearOuthole);} // proceed - else { // ball count not OK - for (byte i=0; i<2; i++) { // check how many balls are in the eyes - if (QuerySwitch(25+i)) { - InLock++;}}} - ActivateTimer(2000, 5, PB_ClearOuthole); // and try again - break; - case 5: - if (!C_BankActive) { - ActivateSolenoid(game_settings[PB_BallEjectStrength], 1); // put ball in trunk - ActivateTimer(1000, 10, PB_ClearOuthole);} - else { - ActivateTimer(1000, 5, PB_ClearOuthole);} // try again - break; - case 10: // ball was kicked in outhole - if (QuerySwitch(16)) { // ball still in outhole? - if ((!game_settings[PB_Multiballs] && Trunk == 0) || (game_settings[PB_Multiballs] && Trunk < 2)) { // assume that 2 balls have been in the outhole - Trunk++;} - ActA_BankSol(1); // make the coil a bit stronger - ActivateTimer(2000, 10, PB_ClearOuthole);} // and come back in 2s - else { - PB_BallEnd(Trunk+1);}}} - -void PB_MultiballThunder2(byte Dummy) { - UNUSED(Dummy); - PlayMusic(50, "1_04.snd"); // play non looping part of music track - QueueNextMusic("1_04L.snd");} // queue looping part as next music to be played + InLock++;}}} + WriteLower(" BALL ERROR "); + if (QuerySwitch(16)) { // ball still in outhole? + ActA_BankSol(1); // make the coil a bit stronger + ActivateTimer(2000, 1, PB_ClearOuthole);} // and come back in 2s + else { + State++; + ActivateTimer(1000, State, PB_ClearOuthole);}} // if not try again in 1s + else { + ActivateTimer(100, Trunk, PB_BallEnd);} + break; + case 4: // ball count still wrong but proceeding anyway + ActivateTimer(100, Trunk, PB_BallEnd);}}} void PB_MultiballThunder(byte State) { if (State < 8) { @@ -1015,10 +1220,16 @@ void PB_MultiballThunder(byte State) { else if (State < 9) { MusicVolume = 0; PlaySound(54, "0_fb.snd"); - ActivateTimer(3300, 0, PB_MultiballThunder2); - ActivateTimer(6000, 9, PB_MultiballThunder);} + ActivateTimer(3300, 9, PB_MultiballThunder); + ActivateTimer(6000, 10, PB_MultiballThunder);} + else if (State < 10) { + PlayMusic(50, "1_04.snd"); // play non looping part of music track + QueueNextMusic("1_04L.snd");} // queue looping part as next music to be played else { - StopPlayingSound();}} + if (Multiballs == 3 && PB_SolarValue == 1000) { // 1M jackpot in 3 ball multiball mode? + PlaySound(58, "0_af.snd");} // 'million activated' + else { + StopPlayingSound();}}} void PB_RampThunder(byte State) { // State = 0 -> Stop static byte Timer = 0; @@ -1200,12 +1411,7 @@ void PB_GameMain(byte Switch) { TurnOffLamp(58); PB_ExBallsLit[Player]--; PB_GiveExBall();} - if (PB_EjectMode[Player] < 5) { - if (PB_EjectMode[Player] == 4) { - AddBlinkLamp(15, 100);} - else { - AddBlinkLamp(PB_EjectMode[Player]+13, 100);} - PB_EjectMode[Player] = PB_EjectMode[Player] + 5;} + PB_HandleEjectHole(11); // proceed to next eject hole state break; case 15: // right outlane PlaySound(51, "1_a5.snd"); @@ -1222,7 +1428,9 @@ void PB_GameMain(byte Switch) { PB_GiveExBall();}} break; case 16: // outhole - ActivateTimer(200, 0, PB_ClearOuthole); // check again in 200ms + if (!BlockOuthole) { + BlockOuthole = true; // block outhole until this ball has been processed + ActivateTimer(200, 0, PB_ClearOuthole);} // check again in 200ms break; case 19: // advance planet if (PB_SpecialLit) { // special lit? @@ -1252,7 +1460,10 @@ void PB_GameMain(byte Switch) { PlaySound(55, "0_af.snd"); // "Million activated" PlayFlashSequence((byte*) PB_OpenVisorSeq);}} else { - PB_SkillMultiplier = 1;} + PB_SkillMultiplier = 1; + if (PB_MballState == 4) { // 3 ball multiball running? + RemoveBlinkLamp(35); // solar energy lamp + ActivateTimer(200, 1, PB_ShooterLaneWarning);}} // turn on shooter lane warning PB_SkillShot = true;} // the first shot is a skill shot break; case 25: // left eye @@ -1300,67 +1511,31 @@ void PB_GameMain(byte Switch) { PB_SetChestLamps(Switch-28);} // add the lamps for the hit row / column in PB_ChestLamp break; case 38: // eject hole - if (!PB_EjectIgnore) { - PB_EjectIgnore = true; - PB_AddBonus(1); - Serial.print("EjLock = "); - Serial.println(InLock); - Serial.print("EjBalls = "); - Serial.println(Multiballs); - if (Multiballs == 3) { // 3 ball multiball running? - ActivateTimer(game_settings[PB_MballHoldTime]*1000, 3, PB_ClearEjectHole);} - else { - if (game_settings[PB_Multiballs] && Multiballs == 1) { // 3 ball multiball mode? - PlaySound(55, "0_ae.snd"); // 'shoot for solar value' - Multiballs = 3; - ActivateTimer(1000, 3, PB_ClearEjectHole); - ActivateTimer(2400, 0, PB_Multiball); // call after sound - break;} - if (PB_EjectMode[Player] < 5) { // eject hole not lit - PlaySound(51, "1_a3.snd"); - if (LampPattern == LampColumns) { // only if no other lamp effect is running - PatPointer = PB_EjectHole; // set the pointer to the lamp pattern - FlowRepeat = 1; // set the repetitions - ActivateTimer(1700, 0, PB_EnergyRestoreLamps) ; // call this when the lamp pattern has run out - ShowLampPatterns(1);} // play the lamp pattern - Points[Player] += 10000; - ShowPoints(Player); - ActivateTimer(1000, 3, PB_ClearEjectHole);} - else { - ActivateTimer(400, 7, PB_PlayEjectHoleSounds); - if (PB_EjectMode[Player] == 9) { - RemoveBlinkLamp(15); - TurnOnLamp(15); - PB_EjectMode[Player] = 4; - Points[Player] += Multiballs * 75000;} - else { - RemoveBlinkLamp(PB_EjectMode[Player] + 8); - TurnOnLamp(PB_EjectMode[Player] + 8); - PlayFlashSequence((byte*) PB_OpenVisorSeq); // play flasher sequence - Points[Player] += Multiballs * (PB_EjectMode[Player] - 4) * 25000; - ShowPoints(Player); - PB_EjectMode[Player] = PB_EjectMode[Player] - 4; - if (PB_EjectMode[Player] == 4) { - PB_AddExBall();}} - ActivateTimer(1000, 3, PB_ClearEjectHole);}}} + PB_HandleEjectHole(1); break; case 39: // solar ramp exit uint16_t Buffer; PB_AddBonus(1); RampSound = false; MusicVolume = 3; // reduce music volume - if (PB_SolarValueTimer) { // solar ramp lit + if (Multiballs == 3 || PB_SolarValueTimer) { // solar ramp lit + if (game_settings[PB_Multiballs]) { // 3 ball multiball mode? + if (PB_SolarValue < 1000) { // jackpot < 1M? + PB_SolarValue += 200;} // add 200K + Buffer = PB_SolarValue;} + else { // 2 ball multiball mode? + if (PB_SolarValueTimer) { + KillTimer(PB_SolarValueTimer); + PB_SolarValueTimer = 0; + RemoveBlinkLamp(35);} // solar energy lamp + ActivateTimer(2000,1,PB_EyeBlink); + Buffer = PB_SolarValue; + PB_SolarValue = 100; + PB_ClearOutLock(0);} + PlayFlashSequence((byte*) PB_OpenVisorSeq); PB_MultiballThunder(0); // play sound effects - KillTimer(PB_SolarValueTimer); - PB_SolarValueTimer = 0; - RemoveBlinkLamp(35); // solar energy lamp Points[Player] += PB_SolarValue * 1000; - ShowPoints(Player); - ActivateTimer(2000,1,PB_EyeBlink); - Buffer = PB_SolarValue; - PB_SolarValue = 100; - PlayFlashSequence((byte*) PB_OpenVisorSeq); - PB_ClearOutLock(0);} + ShowPoints(Player);} else { // solar ramp not lit PlaySound(51, "1_a9.snd"); Points[Player] += 1000; @@ -1548,10 +1723,6 @@ void PB_AddBonus(byte BonusToAdd) { DispRow1 = DisplayUpper2;} ShowMessage(2);}} -void PB_ClearEjectHole(byte Solenoid) { // activate solenoid after delay time - PB_EjectIgnore = false; - ActA_BankSol(Solenoid);} - void PB_StartChestPattern(byte Dummy) { UNUSED(Dummy); LampPattern = LampColumns; @@ -1775,39 +1946,47 @@ void PB_HandleLock(byte State) { ActivateTimer(200, 1, PB_HandleLock);} // and come back to recheck else { // number of locked balls as expected if (InLock) { // locked ball found? - Serial.print("InLock = "); - Serial.println(InLock); - Serial.print("Mballs = "); - Serial.println(Multiballs); - if (PB_ChestMode) { // visor is supposed to be closed - PB_ClearOutLock(1);} // remove balls from lock - else { - if (game_settings[PB_Multiballs]) { // 3 ball multiball mode? - if (InLock == 1) { // 1 ball in lock - if (Multiballs == 1) { - if (QuerySwitch(17) && QuerySwitch(18)) { // still two balls in the trunk? - ActivateSolenoid(0, 12); // turn off playfield GI - PB_EyeBlink(0); // stop eye blinking - PlayFlashSequence((byte*) PB_Ball_Locked); - PlayMusic(52, "1_80.snd"); - ActivateTimer(1000, 1, PB_2ndLock); - PB_GiveBall(2);}} - else if (Multiballs == 3) { - ActivateTimer(game_settings[PB_MballHoldTime]*1000, 0, PB_ClearOutLock);}} - else { // 2 balls in lock - if (Multiballs < 3) { - if (QuerySwitch(17)) { // still one ball in the trunk? - MusicVolume = 3; // reduce music volume - ActivateSolenoid(0, 12); // turn off playfield GI - PB_EyeBlink(0); // stop eye blinking - // TODO close visor - PlayFlashSequence((byte*) PB_Ball_Locked); - ActivateTimer(1000, 2, PB_2ndLock); - PlaySound(55, "0_b0.snd"); // 'now I see you' - PB_GiveBall(1);}} - else { - ActivateTimer(game_settings[PB_MballHoldTime]*1000, 0, PB_ClearOutLock);}}} - else { // 2ball multiball mode + if (game_settings[PB_Multiballs]) { // 3 ball multiball mode? + switch (PB_MballState) { + case 1: // one ball locked + if (InLock == 1) { + ActivateSolenoid(0, 12); // turn off playfield GI + PB_EyeBlink(0); // stop eye blinking + PlayFlashSequence((byte*) PB_Ball_Locked); + PlayMusic(52, "1_80.snd"); + ActivateTimer(1000, 1, PB_2ndLock); // 'partial link up' + PB_GiveBall(2); + PB_MballState = 2;} + else { // not the correct amount of balls in lock + if (InLock) { // two balls in lock + PB_MballState = 2; + ActivateTimer(200, 1, PB_HandleLock);}} + break; + case 2: // second ball locked + case 6: // second ball re-locked + //PB_EyeFlash(1); + if (InLock == 2) { // correct number of balls in lock? + MusicVolume = 3; + PB_HandleEjectHole(15); // start eject hole animation + PlaySound(55, "0_b0.snd"); // 'now I see you' + ActivateTimer(2400, 25, RestoreMusicVolume); // restore music volume after sound has been played + ActivateTimer(2000, 0, PB_CloseVisor); // close visor + ActivateSolenoid(0, 13); // start visor motor + PB_GiveBall(1); + PB_MballState = 3;} + break; + case 4: // 3 ball multiball + ActivateTimer(game_settings[PB_MballHoldTime]*1000, 0, PB_ClearOutLock); + break; + case 5: // still two balls in game + PB_MballState = 6; + if (InLock == 2) { // second ball also locked? + ActivateTimer(200, 1, PB_HandleLock);} + break;}} + else { // 2ball multiball mode + if (PB_ChestMode) { // visor is supposed to be closed + PB_ClearOutLock(1);} // remove balls from lock + else { if (InLock == 1) { if (Multiballs > 1) { // multiball already running? MusicVolume = 4; // reduce music volume @@ -1850,6 +2029,128 @@ void PB_HandleLock(byte State) { ActivateTimer(200, 1, PB_HandleLock);} // come back to recheck after ball eject PB_ClearOutLock(1);}}}}}}} // eject 1 ball and close visor +void PB_HandleEjectHole(byte State) { + static bool EjectIgnore = false; + static byte MBallAnimation = 0; + static byte Timer = 0; + const byte AniPattern[7] = {0b1000, 0b1001, 0b1011, 0b0111, 0b0110, 0b0100,0}; + switch (State) { + case 1: // initial call + if (!EjectIgnore) { // hole not locked? + EjectIgnore = true; // lock it + ActivateTimer(200, 2, PB_HandleEjectHole);} + break; + case 2: // ball has settled + if (QuerySwitch(38)) { // is it still in the hole? + PB_AddBonus(1); + if (PB_MballState == 4) { // 3 ball multiball running? + ActivateTimer(game_settings[PB_MballHoldTime]*1000, 3, PB_HandleEjectHole);} + else { + if (PB_MballState == 3) { // 3 ball multiball to start? + PlaySound(55, "0_ae.snd"); // 'shoot for solar value' + Multiballs = 3; // set score multiplier + PB_MballState = 4; + AddBlinkLamp(35, 100); // start blinking of solar energy ramp + PB_HandleEnergy(0); // turn off energy and lower ramp + ActivateTimer(1000, 3, PB_HandleEjectHole); + PB_ClearOutLock(0); + ActivateTimer(2400, 0, PB_Multiball);} // call after sound + else { // no 3 ball multiball to start + if (PB_EjectMode[Player] < 5) { // eject hole not lit + PlaySound(51, "1_a3.snd"); + if (LampPattern == LampColumns) { // only if no other lamp effect is running + PatPointer = PB_EjectHole; // set the pointer to the lamp pattern + FlowRepeat = 1; // set the repetitions + ActivateTimer(1700, 0, PB_EnergyRestoreLamps) ; // call this when the lamp pattern has run out + ShowLampPatterns(1);} // play the lamp pattern + Points[Player] += 10000; + ShowPoints(Player); + ActivateTimer(1000, 3, PB_HandleEjectHole);} + else { // eject hole lit + ActivateTimer(400, 7, PB_PlayEjectHoleSounds); + if (PB_EjectMode[Player] == 9) { + RemoveBlinkLamp(15); + TurnOnLamp(15); + PB_EjectMode[Player] = 4; + Points[Player] += Multiballs * 75000;} + else { + RemoveBlinkLamp(PB_EjectMode[Player] + 8); + TurnOnLamp(PB_EjectMode[Player] + 8); + PlayFlashSequence((byte*) PB_OpenVisorSeq); // play flasher sequence + Points[Player] += Multiballs * (PB_EjectMode[Player] - 4) * 25000; + ShowPoints(Player); + PB_EjectMode[Player] = PB_EjectMode[Player] - 4; + if (PB_EjectMode[Player] == 4) { + PB_AddExBall();}} + ActivateTimer(1000, 3, PB_HandleEjectHole);}}}} + else { + EjectIgnore = false;} + break; + case 3: + ActA_BankSol(3); // eject ball + ActivateTimer(200, 4, PB_HandleEjectHole); + break; + case 4: // check whether ball is gone + if (QuerySwitch(38)) { + ActivateTimer(100, 3, PB_HandleEjectHole);} + else { + EjectIgnore = false;} + break; + case 10: // restore eject hole lamps + if (PB_EjectMode[Player] < 5) { + for (byte i=0; i 4) { // any blinking eject mode lamps? + if (PB_EjectMode[Player] == 9) { // turn them off + RemoveBlinkLamp(15);} + else { + RemoveBlinkLamp(PB_EjectMode[Player] + 8);}} + MBallAnimation = 1; + ActivateTimer(10, 20, PB_HandleEjectHole);} // start animation + break; + case 16: // end animation + if (Timer) { + KillTimer(Timer); + Timer = 0;} + MBallAnimation = 0; + TurnOffLamp(13); + TurnOffLamp(14); + TurnOffLamp(15); + TurnOffLamp(16); + ActivateTimer(10, 10, PB_HandleEjectHole); // restore eject hole lamps + break; + case 20: // play animation + byte Buff = AniPattern[MBallAnimation-1]; + for (byte i=0; i<4; i++) { + if (Buff & 1) { + TurnOnLamp(13+i);} + else { + TurnOffLamp(13+i);} + Buff = Buff>>1;} + if (MBallAnimation < 7) { + MBallAnimation++; + Timer = ActivateTimer(120, 20, PB_HandleEjectHole);} + else { + MBallAnimation = 1; + Timer = ActivateTimer(900, 20, PB_HandleEjectHole);} + break;}} + void PB_Multiball_RestoreLamps(byte Dummy) { UNUSED(Dummy); PB_EyeBlink(1); @@ -1857,9 +2158,19 @@ void PB_Multiball_RestoreLamps(byte Dummy) { LampPattern = LampColumns;} void PB_MballDisplay(byte Step) { - const byte PB_2MballDispUpper[78] = {17,0,81,4,85,4,93,4,92,4,76,4,12,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,17,0,81,4,85,4,93,4,92,4,76,4,12,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; // display pattern during multiball + const byte PB_2MballDispUpper[78] = {17,0,81,4,85,4,93,4,92,4,76,4,12,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,17,0,81,4,85,4,93,4,92,4,76,4,12,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; // display pattern during 2 ball multiball const byte PB_2MballDispLower[78] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,96,0,98,0,106,0,122,0,90,0,26,0,24,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + const byte PB_3MballDispUpper[78] = {3,0,67,4,71,4,79,4,78,4,76,4,12,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,3,0,67,4,71,4,79,4,78,4,76,4,12,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; // display pattern during 3 ball multiball + const byte PB_3MballDispLower[78] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,160,0,162,0,170,0,186,0,154,0,26,0,24,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; static byte Timer = 0; + byte *PB_MballDispUpper; + byte *PB_MballDispLower; + if (Multiballs == 3) { + PB_MballDispUpper = (byte* ) PB_3MballDispUpper; + PB_MballDispLower = (byte* ) PB_3MballDispLower;} + else { + PB_MballDispUpper = (byte* ) PB_2MballDispUpper; + PB_MballDispLower = (byte* ) PB_2MballDispLower;} if (!Step) { if (Timer) { KillTimer(Timer); @@ -1868,16 +2179,16 @@ void PB_MballDisplay(byte Step) { Step--; if (Player != 1) { for (byte y=0;y<14;y++) { - *(DisplayUpper+y+2) = PB_2MballDispUpper[2*Step+y];}} + *(DisplayUpper+y+2) = PB_MballDispUpper[2*Step+y];}} if (Player != 2) { for (byte y=0;y<14;y++) { - *(DisplayUpper+y+18) = PB_2MballDispUpper[2*Step+y+18];}} + *(DisplayUpper+y+18) = PB_MballDispUpper[2*Step+y+18];}} if (Player != 3) { for (byte y=0;y<14;y++) { - *(DisplayLower+y+2) = PB_2MballDispLower[2*Step+y];}} + *(DisplayLower+y+2) = PB_MballDispLower[2*Step+y];}} if (Player != 4) { for (byte y=0;y<14;y++) { - *(DisplayLower+y+18) = PB_2MballDispLower[2*Step+y+18];}} + *(DisplayLower+y+18) = PB_MballDispLower[2*Step+y+18];}} if (!Step) { Step = 24;} Timer = ActivateTimer(50, Step, PB_MballDisplay);}} @@ -1940,26 +2251,35 @@ void PB_ReopenVisor(byte Dummy) { // reopen visor if solar v if (Multiballs == 2) { PB_EyeBlink(1);} PB_SolarValueTimer = 0; - RemoveBlinkLamp(35); + RemoveBlinkLamp(35); // solar energy lamp PB_ClearOutLock(0);} -void PB_ClearOutLock(byte CloseVisor) { +void PB_ClearOutLock(byte State) { // CloseVisor = 0 -> eject 1 ball / CloseVisor = 1 -> Visor will be closed / = 2 -> clear out both balls but don't close visor + static byte Timer = 0; + if (State < 3) { // inital call? + if (Timer) { + KillTimer(Timer);} + State += 3;} if (QuerySolenoid(13)) { // visor motor on? - ActivateTimer(1100, CloseVisor, PB_ClearOutLock);} // come back later + ActivateTimer(1100, State, PB_ClearOutLock);} // come back later else { if (QuerySwitch(47)) { // visor is open if (QuerySwitch(25)) { // left eye? - ActA_BankSol(7);} // eject it + ActA_BankSol(7); // eject it + if (QuerySwitch(26) && State == 5) { // 2nd ball shall be ejected also + ActivateTimer(1000, 3, PB_ClearOutLock);} + else if (State == 4) { // closed visor requested? + ActivateTimer(1000, 1, PB_CloseVisor);}} else { if (QuerySwitch(26)) { // right eye - ActA_BankSol(8);}} // eject it - if (CloseVisor) { // closed visor requested? - ActivateTimer(1000, 1, PB_CloseVisor);}} // set close flag - else { + ActA_BankSol(8);} // eject it + if (State == 4) { // closed visor requested? + ActivateTimer(1000, 1, PB_CloseVisor);}}} // set close flag + else if (QuerySwitch(25) || QuerySwitch(26)) { // open visor if there's still a ball in lock ActivateTimer(1000, 0, PB_OpenVisor); PlaySound(52, "0_f1.snd"); // moving visor sound ActivateSolenoid(0, 13); // activate visor motor - ActivateTimer(2000, CloseVisor, PB_ClearOutLock);}}} + ActivateTimer(2000, State, PB_ClearOutLock);}}} void PB_DropTargetReset(byte Counter) { PlaySound(52, "0_9b.snd"); @@ -1976,8 +2296,8 @@ void PB_EnergyReset(byte Counter) { if (Counter) { ActivateTimer(110, Counter, PB_EnergyReset);} else { - PB_EnergyActive = false; // energy value off - PB_DropRamp = true;}} // ramp needs to be dropped + PB_EnergyActive = false; // energy value off + PB_DropRamp = true;}} // ramp needs to be dropped void PB_TurnOffLamp(byte Lamp) { TurnOffLamp(Lamp);} @@ -2030,7 +2350,7 @@ void PB_HandleDropTargets(byte Target) { PB_AdvancePlanet(1);} else { if (!PB_DropTimer) { // first target hit - if (Target-8 == PB_DropBlinkLamp) { // blinking target hit? + if (Target-8 == PB_DropBlinkLamp && PB_MballState != 4) { // blinking target hit? //MusicVolume = 4; // reduce music volume PlaySound(51, "0_71.snd"); ActivateTimer(1000, 0, PB_RaiseRamp); // raise ramp in 1s @@ -2088,10 +2408,10 @@ void PB_AdvancePlanet(byte State) { byte Planets = PB_Planet[Player]; switch (State) { case 0: // to be called by AfterSound - RemoveBlinkLamp(18+game_settings[PB_ReachPlanet]); + RemoveBlinkLamp(19+game_settings[PB_ReachPlanet]); RemoveBlinkLamp(51); // stop blinking of special lamp if (PB_Planet[Player] && PB_Planet[Player] != 10 && PB_Planet[Player] != 20) { // planets lit? - TurnOnLamp(18+game_settings[PB_ReachPlanet]);} + TurnOnLamp(19+game_settings[PB_ReachPlanet]);} PB_GiveExBall(); RestoreMusicVolume(25); break; @@ -2100,7 +2420,7 @@ void PB_AdvancePlanet(byte State) { if (PB_Planet[Player] > 20) { // sun already reached for the 2nd time? PB_Planet[Player] = 11;} // set it back to the sun Planets = PB_Planet[Player]; - if (PB_Planet[Player] == game_settings[PB_ReachPlanet]) { // blinking planet reached? + if (PB_Planet[Player] == game_settings[PB_ReachPlanet]+1) { // blinking planet reached? MusicVolume = 4; // reduce music volume PlayFlashSequence((byte*) PB_OpenVisorSeq); // play flasher sequence ActC_BankSol(1); // use knocker @@ -2126,7 +2446,7 @@ void PB_AdvancePlanet(byte State) { QueueNextSound("0_e1_000.snd"); ActivateTimer(500, 2, PB_AdvancePlanet); ActivateTimer(4050, 21, PB_AdvancePlanet); // reset AfterSound - RemoveBlinkLamp(18+game_settings[PB_ReachPlanet]);} // stop blinking + RemoveBlinkLamp(19+game_settings[PB_ReachPlanet]);} // stop blinking break; case 2: // first step of advance planet if (Planets > 10) { // sun already reached? @@ -2140,8 +2460,8 @@ void PB_AdvancePlanet(byte State) { ActivateTimer(1000, 20, PB_AdvancePlanet);} break; case 20: // animation reached planet - if (PB_Planet[Player] < game_settings[PB_ReachPlanet]) { - AddBlinkLamp(18+game_settings[PB_ReachPlanet], 100);} + if (PB_Planet[Player] < game_settings[PB_ReachPlanet]+1) { + AddBlinkLamp(19+game_settings[PB_ReachPlanet], 100);} if (Planets > 10) { // sun already reached? Planets = Planets - 10;} RemoveBlinkLamp(18 + Planets); @@ -2306,8 +2626,7 @@ void PB_PlayAfterGameSequence(byte State) { PlayMusic(50, "1_80.snd"); Timer = ActivateTimer(6000, State+1, PB_PlayAfterGameSequence);} else { - Timer = 0; - digitalWrite(VolumePin,HIGH);}} // set volume to zero + Timer = 0;}} else { if (!State) { if (Timer) { @@ -2317,77 +2636,114 @@ void PB_PlayAfterGameSequence(byte State) { ReleaseAllSolenoids();}}} void PB_BallEnd(byte Balls) { // ball has been kicked into trunk - PB_EyeBlink(0); - if (Multiballs == 2) { // multiball running? - if (PB_SolarValueTimer) { // solar value jackpot active? - KillTimer(PB_SolarValueTimer); - PB_SolarValueTimer = 0; - RemoveBlinkLamp(35);} // solar energy lamp - Multiballs = 1; // turn it off - PB_MballDisplay(0); // stop display animation - PB_ShowMessage(255); // release message block - WriteUpper(" "); - WriteLower(" "); - ShowPoints(Player); - PB_LampSweepActive = 0; // turn off backbox lamp sweep - ReleaseSolenoid(11); // turn backbox GI back on - if (APC_settings[Volume]) { - analogWrite(VolumePin,255-APC_settings[Volume]);} // reduce volume back to normal - PlayMusic(50, "1_0a.snd"); // play multiball end theme - QueueNextMusic("1_02L.snd"); // track is looping so queue it also - if (Balls == 2) { // 2 balls detected in the trunk - Balls = PB_CountBallsInTrunk(); // count again - ActivateTimer(1000, Balls, PB_BallEnd);} // come back and check again - else { - PB_ClearOutLock(1); // clear out lock and close visor + if (game_settings[PB_Multiballs]) { // 3 ball multiball selected? + switch (PB_MballState) { + case 4: // 3 ball multiball running + RemoveBlinkLamp(35); // solar energy lamp + Multiballs = 1; // reset score multiplier + PB_MballDisplay(0); // stop display animation + PB_SolarValue = 0; // reset jackpot + PB_MballState = 5; // indicate a ball loss + PB_MballDisplay(0); // stop display animation + PB_ShooterLaneWarning(0); // turn off shooter lane warning + PB_ShowMessage(255); // release message block + WriteUpper(" "); + WriteLower(" "); + ShowPoints(Player); + PB_LampSweepActive = 0; // turn off backbox lamp sweep + PB_HandleEjectHole(16); // stop eject hole animation + ReleaseSolenoid(11); // turn backbox GI back on + if (QuerySwitch(38)) { // ball in eject hole? + ActivateTimer(1000, 3, PB_HandleEjectHole);} // clear eject hole + BlockOuthole = false; // remove outhole block + PB_ClearOutLock(2); // clear out locks but don't close visor InLock = 0; - PB_ChestLightHandler(0); // stop chest animation + return; + case 5: // 2 balls in game after multiball + case 6: // 1 ball in lock after multiball + PB_ClearOutLock(1); // clear out lock and close visor + InLock = 0; + PB_ChestLightHandler(0); // stop chest animation PB_ChestMode = 1; - PB_ClearChest(); // turn off chest lamps - PB_ChestLightHandler(100); // restart chest animation - ActivateTimer(3000, 10, PB_Multiball); // return to main music theme - BlockOuthole = false;}} // remove outhole block - else { // no multiball running - LockedBalls[Player] = 0; - PB_HandleDropTargets(100); // turn off drop target blinking - PB_HandleEnergy(0); // turn off energy lamp and sounds - if (!QuerySwitch(44)) { // ramp in up state? - ActA_BankSol(6);} // drop ramp - BlinkScore(0); // stop score blinking - PB_CycleDropLights(0); // stop the blinking drop target lights - PB_ChestLightHandler(0); // stop chest animation - for (byte i=0; i<5; i++) { // turn off blinking row / column - RemoveBlinkLamp(PB_ChestRows[PB_ChestMode][i]);} - PB_ClearChest(); // turn off chest lamps - if (!PB_ChestMode) { - PB_Chest_Status[Player] = PB_Chest_Status[Player] + 100;} // indicate that the visor has been open - else { // visor is closed - if (PB_ChestMode < 11 && PB_LitChestLamps[Player-1]) { // player already has lit chest lamps - PB_LitChestLamps[Player-1] += 100;}} // indicate that the chest lamps have been lit, but the visor is still closed - RemoveBlinkLamp(18+game_settings[PB_ReachPlanet]); - if (BallWatchdogTimer) { - KillTimer(BallWatchdogTimer); - BallWatchdogTimer = 0;} - if (PB_EjectMode[Player] > 4) { // any blinking eject mode lamps? - if (PB_EjectMode[Player] == 9) { // turn them off - RemoveBlinkLamp(15);} - else { - RemoveBlinkLamp(PB_EjectMode[Player] + 8);}} - for (byte i=0; i<4; i++) { // turn off all eject mode lamps - TurnOffLamp(13+i);} - if (PB_BallSave == 2) { // ball saver has been triggered - BlockOuthole = false; // remove outhole block - ActivateTimer(2000, 0, PB_AfterExBallRelease); - ActivateTimer(1000, Balls, PB_NewBall);} - else { // no ball saver + PB_ClearChest(); // turn off chest lamps + PB_ChestLightHandler(100); // restart chest animation + ActivateTimer(3000, 10, PB_Multiball); + PB_MballState = 1; + BlockOuthole = false; // remove outhole block + return;}} + else { // not in 3 ball multiball mode + PB_EyeBlink(0); + if (Multiballs == 2) { // multiball running? + if (PB_SolarValueTimer) { // solar value jackpot active? + KillTimer(PB_SolarValueTimer); + PB_SolarValueTimer = 0; + RemoveBlinkLamp(35);} // solar energy lamp + Multiballs = 1; // turn it off + PB_MballDisplay(0); // stop display animation + PB_ShowMessage(255); // release message block WriteUpper(" "); WriteLower(" "); - WriteUpper2(" BONUS "); - ShowNumber(15, Bonus*1000); - StopPlayingMusic(); - PlaySound(53, "0_2c.snd"); - AppByte = Balls; - ActivateTimer(200, 0, PB_CountBonus);}}} + ShowPoints(Player); + PB_LampSweepActive = 0; // turn off backbox lamp sweep + ReleaseSolenoid(11); // turn backbox GI back on + if (APC_settings[Volume]) { + analogWrite(VolumePin,255-APC_settings[Volume]);} // reduce volume back to normal + PlayMusic(50, "1_0a.snd"); // play multiball end theme + QueueNextMusic("1_02L.snd"); // track is looping so queue it also + if (Balls == 2) { // all balls detected in the trunk + Balls = PB_CountBallsInTrunk(); // count again + ActivateTimer(1000, Balls, PB_BallEnd);} // come back and check again + else { + PB_ClearOutLock(1); // clear out lock and close visor + InLock = 0; + PB_ChestLightHandler(0); // stop chest animation + PB_ChestMode = 1; + PB_ClearChest(); // turn off chest lamps + PB_ChestLightHandler(100); // restart chest animation + ActivateTimer(3000, 10, PB_Multiball); // return to main music theme + BlockOuthole = false;} // remove outhole block + return;}} + LockedBalls[Player] = 0; + PB_HandleDropTargets(100); // turn off drop target blinking + PB_HandleEnergy(0); // turn off energy lamp and sounds + if (!QuerySwitch(44)) { // ramp in up state? + ActA_BankSol(6);} // drop ramp + BlinkScore(0); // stop score blinking + PB_CycleDropLights(0); // stop the blinking drop target lights + PB_ChestLightHandler(0); // stop chest animation + for (byte i=0; i<5; i++) { // turn off blinking row / column + RemoveBlinkLamp(PB_ChestRows[PB_ChestMode][i]);} + PB_ClearChest(); // turn off chest lamps + if (!PB_ChestMode) { + PB_Chest_Status[Player] = PB_Chest_Status[Player] + 100;} // indicate that the visor has been open + else { // visor is closed + if (PB_ChestMode < 11 && PB_LitChestLamps[Player-1]) { // player already has lit chest lamps + PB_LitChestLamps[Player-1] += 100;}} // indicate that the chest lamps have been lit, but the visor is still closed + RemoveBlinkLamp(19+game_settings[PB_ReachPlanet]); + if (BallWatchdogTimer) { + KillTimer(BallWatchdogTimer); + BallWatchdogTimer = 0;} + if (PB_EjectMode[Player] > 4) { // any blinking eject mode lamps? + if (PB_EjectMode[Player] == 9) { // turn them off + RemoveBlinkLamp(15);} + else { + RemoveBlinkLamp(PB_EjectMode[Player] + 8);}} + for (byte i=0; i<4; i++) { // turn off all eject mode lamps + TurnOffLamp(13+i);} + if (PB_BallSave == 2) { // ball saver has been triggered + BlockOuthole = false; // remove outhole block + ActivateTimer(2000, 0, PB_AfterExBallRelease); + ActivateTimer(1000, Balls, PB_NewBall);} + else { // no ball saver + WriteUpper(" "); + WriteLower(" "); + WriteUpper2(" BONUS "); + ShowNumber(15, Bonus*1000); + StopPlayingMusic(); + PlaySound(53, "0_2c.snd"); + AppByte = Balls; + StopAllBlinkLamps(); + ActivateTimer(200, 0, PB_CountBonus);}} void PB_BlinkPlanet(byte State) { // blink planets during bonus count static byte Counter = 0; @@ -2412,7 +2768,6 @@ void PB_BlinkPlanet(byte State) { // blink planets during bo void PB_CountBonus(byte State) { static uint32_t TotalBonus; const byte Pattern[11] = {5,3,13,14,4,2,12,15,6,10,11}; - const char PlanetTxt[9][15] = {{" PLUTO "},{" NEPTUNE"},{" URANUS "},{" SATURN "},{" JUPITER"},{" MARS "},{" EARTH "},{" VENUS "},{" MERCURY"}}; if (State < 11) { // show bonus *(DisplayUpper+2*Pattern[State]) = *(DisplayUpper2+2*Pattern[State]); *(DisplayUpper+2*Pattern[State]+1) = *(DisplayUpper2+2*Pattern[State]+1); @@ -2486,7 +2841,7 @@ void PB_CountBonus(byte State) { PlaySound(53, "0_65.snd"); PB_BlinkPlanet(State-11); Points[Player] += 20000; // add points for each planet - WriteUpper((char*)PlanetTxt[State - 30]); // show planet names + WriteUpper((char*)PB_PlanetTxt[State - 30]); // show planet names ShowPoints(Player); byte Planets = PB_Planet[Player]; if (Planets > 10) { // sun already reached? @@ -2512,7 +2867,7 @@ void PB_BallEnd2() { TurnOffLamp(51); if ((Points[Player] > HallOfFame.Scores[3]) && (Ball == APC_settings[NofBalls])) { // last ball & high score? Switch_Pressed = DummyProcess; // Switches do nothing - PB_Congrats(0);} + PB_Congrats(1);} else { if ((PB_EjectMode[Player] == 4) || (PB_EjectMode[Player] == 9)) { // eject hole mode maxed out? PB_EjectMode[Player] = 0;} // reset it for the next ball @@ -2529,33 +2884,50 @@ void PB_BallEnd3(byte Balls) { Ball++; ActivateTimer(100, Balls, PB_NewBall);} else { // game end + PB_EyeBlink(0); ReleaseSolenoid(23); // disable flipper fingers ReleaseSolenoid(24); LampPattern = NoLamps; // Turn off all lamps TurnOffLamp(3); // turn off Ball in Play lamp + PB_HandleEjectHole(16); // stop eject hole animation PB_PlayAfterGameSequence(1); // start end of game animation GameDefinition.AttractMode();}}} -void PB_Congrats(byte Dummy) { // show congratulations - UNUSED(Dummy); - LampPattern = NoLamps; - ActC_BankSol(1); - //AfterMusic = PB_EnterInitials2; // TODO fix congrats - PlayMusic(50, "1_06.snd"); - QueueNextMusic("1_06L.snd"); // queue looping part as next music to be played} - ActivateSolenoid(0, 11); - ActivateSolenoid(0, 12); - PB_LampSweepActive = 2; - PB_LampSweep(4); - WriteUpper(" "); - WriteLower(" "); - DisplayScore(3, Points[Player]); - DisplayScore(4, Points[Player]); - WriteUpper2(" GREAT "); - ActivateTimer(50, 0, ScrollUpper); - ActivateTimer(1400, 1, PB_ScrollCongrats); - //ActivateTimer(3000, 1, PB_ScrollCongrats2); - ActivateTimer(5000, Player, PB_Congrats2);} +void PB_Congrats(byte State) { // show congratulations + static byte Timer = 0; + switch(State) { + case 0: + if (Timer) { + KillTimer(Timer); + Timer = 0;} + break; + case 1: + for (byte i=0; i< 8; i++) { + LampColumns[i] = 0;} + LampPattern = LampColumns; + ActC_BankSol(1); + //AfterMusic = PB_EnterInitials2; // TODO fix congrats + PlayMusic(50, "1_06.snd"); + QueueNextMusic("1_06L.snd"); // queue looping part as next music to be played} + ActivateSolenoid(0, 11); + ActivateSolenoid(0, 12); + PB_LampSweepActive = 2; + PB_LampSweep(4); + WriteUpper(" "); + WriteLower(" "); + DisplayScore(3, Points[Player]); + DisplayScore(4, Points[Player]); + WriteUpper2(" GREAT "); + ActivateTimer(50, 0, ScrollUpper); + ActivateTimer(1400, 1, PB_ScrollCongrats); + //ActivateTimer(3000, 1, PB_ScrollCongrats2); + ActivateTimer(5000, Player, PB_Congrats2); + Timer = ActivateTimer(3000, 2, PB_Congrats); + break; + case 2: + PlayFlashSequence((byte*) PB_BB_FlasherCycle); + Timer = ActivateTimer(3000, 2, PB_Congrats); + break;}} void PB_ScrollCongrats(byte Dummy) { UNUSED(Dummy); @@ -2596,12 +2968,8 @@ void PB_EnterInitials(byte Switch) { KillTimer(ByteBuffer2); ByteBuffer++; if (ByteBuffer > 2) { - if (APC_settings[Volume]) { - ByteBuffer3 = APC_settings[Volume]; - FadeOutMusic(100); - ActivateTimer(1000, 0, PB_EnterInitials2);} - else { - StopPlayingMusic();}} + FadeOutMusic(100); + ActivateTimer(1000, 0, PB_EnterInitials2);} else { PB_BlinkInitial(1);} break; @@ -2634,18 +3002,33 @@ void PB_BlinkInitial(byte State) { // blink actual character if (State) { *(DisplayUpper+20+4*ByteBuffer) = 0; // show a blank *(DisplayUpper+21+4*ByteBuffer) = 0; + PB_ClearChest(); State = 0;} else { + byte Buffer = 0; for (byte i=0; i<3; i++) { *(DisplayUpper+20+4*i) = DispPattern1[(EnterIni[i]-32)*2]; *(DisplayUpper+21+4*i) = DispPattern1[(EnterIni[i]-32)*2+1];}// restore the characters + if (EnterIni[ByteBuffer] == 32) { // It's a blank + Buffer = 0;} + else if (EnterIni[ByteBuffer] < 65) { // It's a number + Buffer = EnterIni[ByteBuffer] - 47;} + else { // It's a letter + Buffer = EnterIni[ByteBuffer] - 54;} + for (byte x=0; x<5; x++) { // for all chest rows + byte Mask = 16; // mask to access the stored lamps for this player + for (byte y=0; y<5; y++) { // for all columns + if (PB_Characters[Buffer*5+x] & Mask) { + TurnOnLamp(28+x+8*y);} + else { + TurnOffLamp(28+x+8*y);} + Mask = Mask>>1;}} State = 1;} ByteBuffer2 = ActivateTimer(100+State*2000, State, PB_BlinkInitial);} // and come back void PB_EnterInitials2(byte Dummy) { UNUSED(Dummy); - if (APC_settings[Volume]) { - analogWrite(VolumePin, 255);} + PB_Congrats(0); if (ByteBuffer > 2) { ByteBuffer2 = HandleHighScores(Points[Player]); WriteUpper2(" HIGH SCORE "); @@ -2677,6 +3060,648 @@ void PB_ResetHighScores(bool change) { // delete the high scores else { WriteLower(" SCORES ");}} +void PB_RulesEffect(byte State) { + const byte Pattern[22] = {4,0,4,8,12,9,12,11,44,11,108,11,124,43,125,59,125,187,127,187,127,191}; + for (byte i=0; i<15; i++) { + if (i == 7) { + continue;} + if (!(*(DisplayUpper+2+2*i)) && !(*(DisplayUpper+3+2*i))) { + continue;} + if (State < 11) { + *(DisplayUpper+2+2*i) = *(DisplayUpper+2+2*i) | Pattern[2*State]; + *(DisplayUpper+3+2*i) = *(DisplayUpper+3+2*i) | Pattern[2*State+1];} + else { + *(DisplayUpper+2+2*i) = *(DisplayUpper+2+2*i) & (255 - Pattern[2*(State-11)]); + *(DisplayUpper+3+2*i) = *(DisplayUpper+3+2*i) & (255 - Pattern[2*(State-11)+1]);}} + if (State < 22) { + ActivateTimer(30, State+1, PB_RulesEffect);}} + +void PB_RuleLampEffects(byte State) { + static byte Timer = 0; + switch(State) { + case 0: + if (Timer) { + KillTimer(Timer);} + Timer = 0; + PB_ClearChest(); + ReleaseSolenoid(7); + ReleaseSolenoid(8); + break; + case 1: + if (Timer) { + return;} + TurnOnLamp(60); + /* no break */ + case 2: + for (byte i=0; i<4;i++) { + TurnOnLamp(28+8*i); + TurnOffLamp(61+i);} + Timer = ActivateTimer(200, 3, PB_RuleLampEffects); + break; + case 3: + for (byte i=0; i<4;i++) { + TurnOffLamp(28+8*i); + TurnOnLamp(61+i);} + Timer = ActivateTimer(200, 2, PB_RuleLampEffects); + break; + case 5: + if (Timer) { + return;} + /* no break */ + case 6: + ActivateSolenoid(0, 7); + ActivateSolenoid(0, 8); + Timer = ActivateTimer(300, 7, PB_RuleLampEffects); + break; + case 7: + ReleaseSolenoid(7); + ReleaseSolenoid(8); + Timer = ActivateTimer(300, 6, PB_RuleLampEffects); + break; + case 10: + if (Timer) { + return;} + TurnOnLamp(28); + Timer = ActivateTimer(100, 11, PB_RuleLampEffects); + break; + case 36: + Timer = 0; + break; + default: + byte Lamp = State - 10; + byte Lamp2 = Lamp % 5; + Lamp = Lamp / 5; + TurnOnLamp(28 + Lamp2 + Lamp * 8); + Timer = ActivateTimer(100, State+1, PB_RuleLampEffects); + break;}} + +void PB_RulesDisplay(byte State) { + static byte Timer = 0; + switch(State) { + case 0: + PB_RuleLampEffects(0); + if (Timer) { + KillTimer(Timer);} + Timer = 0; + StopAllBlinkLamps(); + ReleaseSolenoid(12); + ReleaseSolenoid(14); + break; + case 1: + if (Timer) { + return;} + ActivateSolenoid(0, 14); + ActivateSolenoid(0, 12); + /* no break */ + case 3: + case 5: + case 7: + WriteUpper("PINBOT RULES--"); + WriteLower(" "); + PlaySound(50, "0_6f.snd"); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 9: + WriteUpper("PINBOT RULES--"); + WriteLower(" "); + PlaySound(50, "0_6f.snd"); + Timer = ActivateTimer(2000, State+1, PB_RulesDisplay); + break; + case 2: + case 4: + case 6: + case 8: + case 10: + WriteUpper(" "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 11: + WriteUpper("PLUNGER MAKES "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 12: + WriteUpper(" "); + if (game_settings[PB_Multiballs]) { + Timer = ActivateTimer(300, 150, PB_RulesDisplay);} + else { + Timer = ActivateTimer(300, State+1, PB_RulesDisplay);} + break; + case 13: + WriteUpper("VORTEX 1X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 14: + WriteUpper("VORTEX 2X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 15: + WriteUpper("VORTEX 3X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 16: + WriteUpper("VORTEX 4X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 17: + WriteUpper("VORTEX 5X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 18: + WriteUpper("VORTEX 6X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 19: + WriteUpper("VORTEX 7X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 20: + WriteUpper("VORTEX 8X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 21: + WriteUpper("VORTEX 9X "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 22: + WriteUpper("VORTEX 10X "); + *(DisplayUpper+13*2+1) = 64 | *(DisplayUpper+13*2+1); // add a dot in column 12 + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 23: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 24: + case 26: + case 28: + case 30: + case 32: + WriteUpper(" WHEN "); + DisplayScore(1, 1000000); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 25: + case 27: + case 29: + case 31: + WriteUpper(" WHEN "); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 33: + WriteUpper("VORTEX AT 10X "); + *(DisplayUpper+14*2+1) = 64 | *(DisplayUpper+14*2+1); // add a dot in column 13 + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 34: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 35: + WriteUpper("ADVANCEPLANETS"); + for (byte i=19; i<28; i++) { + AddBlinkLamp(i, 100);} + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 36: + WriteUpper(" BY 3 BANK "); + for (byte i=41; i<44; i++) { + AddBlinkLamp(i, 100);} + for (byte i=19; i<28; i++) { + RemoveBlinkLamp(i); + TurnOnLamp(i);} + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 37: + WriteUpper(" OR BY TARGET "); + *(DisplayUpper+14*2+1) = 64 | *(DisplayUpper+14*2+1); // add a dot in column 13 + AddBlinkLamp(18, 100); + for (byte i=41; i<44; i++) { + RemoveBlinkLamp(i); + TurnOnLamp(i);} + Timer = ActivateTimer(2200, State+1, PB_RulesDisplay); + break; + case 38: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 39: + {char Planet[15] = " REACH "; + for (byte i=7; i<15; i++) { + Planet[i] = PB_PlanetTxt[game_settings[PB_ReachPlanet]][i];} + WriteUpper((char*) Planet);} + AddBlinkLamp(19+game_settings[PB_ReachPlanet], 100); + RemoveBlinkLamp(18); + TurnOnLamp(18); + Timer = ActivateTimer(2200, State+1, PB_RulesDisplay); + break; + case 40: + WriteUpper(" FOR SPECIAL"); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + Timer = ActivateTimer(2200, State+1, PB_RulesDisplay); + break; + case 41: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + RemoveBlinkLamp(19+game_settings[PB_ReachPlanet]); + for (byte i=19; i<28; i++) { + TurnOffLamp(i);} + PB_RulesEffect(0); + break; + case 42: + WriteUpper(" REACH THE SUN"); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 43: + case 44: + case 45: + case 46: + case 47: + case 48: + case 49: + case 50: + case 51: + TurnOnLamp(State-24); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 52: + ActivateSolenoid(150, 8); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 53: + WriteUpper("TO LITESPECIAL"); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + AddBlinkLamp(51, 100); + Timer = ActivateTimer(2200, State+1, PB_RulesDisplay); + break; + case 54: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 55: + case 56: + case 57: + case 58: + case 59: + case 60: + case 61: + case 62: + WriteUpper(" JET BUMPERS"); + RemoveBlinkLamp(51); + for (byte i=19;i<28;i++) { + TurnOffLamp(i);} + for (byte i=41; i<44; i++) { + TurnOffLamp(i);} + TurnOffLamp(18); + ActivateSolenoid(150, 6); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 63: + case 64: + case 65: + case 66: + case 67: + case 68: + case 69: + case 70: + WriteUpper(" ADD ENERGY"); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + ActivateSolenoid(150, 6); + Timer = ActivateTimer(300, State+1, PB_RulesDisplay); + break; + case 71: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 72: + WriteUpper(" RAISE RAMP "); + TurnOffLamp(18); + for (byte i=41; i<44; i++) { + AddBlinkLamp(i, 100);} + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 73: + WriteUpper(" TO LITE "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 74: + WriteUpper(" SCORE ENERGY"); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + AddBlinkLamp(34, 100); + for (byte i=41; i<44; i++) { + RemoveBlinkLamp(i);} + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 75: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 76: + WriteUpper(" RAMP GIVES "); + AddBlinkLamp(35, 100); + RemoveBlinkLamp(34); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 77: + WriteUpper(" BONUS MULT "); + *(DisplayUpper+13*2+1) = 64 | *(DisplayUpper+13*2+1); // add a dot in column 12 + for (byte i=9; i<13; i++) { + AddBlinkLamp(i, 100);} + RemoveBlinkLamp(35); + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 78: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 79: + WriteUpper(" HIT 5 BANKS"); + for (byte i=9; i<13; i++) { + RemoveBlinkLamp(i);} + PB_RuleLampEffects(1); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 80: + WriteUpper(" TO "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 81: + WriteUpper(" OPEN VISOR "); + *(DisplayUpper+14*2+1) = 64 | *(DisplayUpper+14*2+1); // add a dot in column 13 + PB_RuleLampEffects(0); + PB_RuleLampEffects(10); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 82: + ActivateSolenoid(0, 10); + ActivateSolenoid(0, 18); + Timer = ActivateTimer(2000, 0, PB_OpenVisor); + ActivateSolenoid(0, 13); + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 83: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 84: + WriteUpper("FILLING CHEST"); + PB_ClearChest(); + PB_RuleLampEffects(10); + Timer = ActivateTimer(3000, State+1, PB_RulesDisplay); + break; + case 85: + WriteUpper(" AGAIN "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 86: + WriteUpper(" LITES EX BALL"); + AddBlinkLamp(49, 100); + AddBlinkLamp(50, 100); + AddBlinkLamp(57, 100); + AddBlinkLamp(58, 100); + *(DisplayUpper+10*2+1) = 64 | *(DisplayUpper+10*2+1); // add a dot in column 9 + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + Timer = ActivateTimer(2200, State+1, PB_RulesDisplay); + break; + case 87: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 88: + WriteUpper(" LOCK BALLS "); + RemoveBlinkLamp(49); + RemoveBlinkLamp(50); + RemoveBlinkLamp(57); + RemoveBlinkLamp(58); + PB_EyeBlink(1); + if (game_settings[PB_Multiballs]) { + Timer = ActivateTimer(2500, 160, PB_RulesDisplay);} + else { + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay);} + break; + case 89: + WriteUpper(" FOR "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 90: + WriteUpper("DOUBLE SCORE "); + *(DisplayUpper+14*2+1) = 64 | *(DisplayUpper+14*2+1); // add a dot in column 13 + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 91: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 92: + WriteUpper(" DURING "); + PB_EyeBlink(0); + ActivateSolenoid(0, 10); + ActivateSolenoid(0, 18); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 93: + WriteUpper(" MULTI BALL "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 94: + WriteUpper(" LOCK 1 BALL "); + PB_EyeFlash(1); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 95: + WriteUpper(" TO START "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 96: + WriteUpper(" SOLAR ECLIPSE"); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + PB_EyeFlash(0); + PB_EyeFlash(1); + AddBlinkLamp(35, 100); + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 97: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 98: + WriteUpper(" GO UP RAMP "); + PB_EyeFlash(0); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 99: + WriteUpper(" TO COLLECT"); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 100: + WriteUpper(" SOLAR SCORE "); + WriteLower(" OF "); + DisplayScore(4, 100000); + RemoveBlinkLamp(35); + Timer = ActivateTimer(2000, 0, PB_CloseVisor); + ActivateSolenoid(0, 13); + Timer = ActivateTimer(5000, State+1, PB_RulesDisplay); + break; + case 101: + Timer = 0; + ReleaseSolenoid(12); + ReleaseSolenoid(14); + PB_AttractMode(); + break; + case 150: + DisplayScore(2, 100000); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 151: + Timer = ActivateTimer(800, 35, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 160: + WriteUpper(" TO LITE "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 161: + WriteUpper(" THIRD LOCK "); + *(DisplayUpper+13*2+1) = 64 | *(DisplayUpper+13*2+1); // add a dot in column 12 + PB_EyeBlink(0); + PB_HandleEjectHole(15); + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 162: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 163: + WriteUpper(" LOCK BALL "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 164: + WriteUpper(" TO START "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 165: + WriteUpper(" MULTI BALL "); + *(DisplayUpper+13*2+1) = 64 | *(DisplayUpper+13*2+1); // add a dot in column 12 + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 166: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 167: + WriteUpper(" DURING "); + PB_ClearChest(); + PB_HandleEjectHole(16); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 168: + WriteUpper(" MULTI BALL "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 169: + WriteUpper(" SHOOT RAMP "); + AddBlinkLamp(35, 100); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 170: + WriteUpper(" FOR "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 171: + case 172: + case 173: + case 174: + case 175: + WriteUpper("JACKPOT "); + DisplayScore(2, 200000 * (State - 170)); + Timer = ActivateTimer(500, State+1, PB_RulesDisplay); + break; + case 176: + case 178: + case 180: + case 182: + WriteUpper("JACKPOT "); + Timer = ActivateTimer(500, State+1, PB_RulesDisplay); + break; + case 177: + case 179: + case 181: + WriteUpper("JACKPOT "); + DisplayScore(2, 1000000); + Timer = ActivateTimer(500, State+1, PB_RulesDisplay); + break; + case 183: + WriteUpper("JACKPOT "); + RemoveBlinkLamp(35); + DisplayScore(2, 1000000); + *(DisplayUpper+15*2+1) = 64 | *(DisplayUpper+15*2+1); // add a dot in column 14 + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 184: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_RulesEffect(0); + break; + case 185: + WriteUpper(" LOCKS "); + PB_EyeFlash(1); + PB_HandleEjectHole(15); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 186: + WriteUpper(" HOLD BALLS "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 187: + {char Time[15] = " FOR S "; + Time[11] = 48 + game_settings[PB_MballHoldTime] % 10; + if (game_settings[PB_MballHoldTime] > 9) { + Time[10] = 48 + game_settings[PB_MballHoldTime] / 10;} + WriteUpper((char*) Time); + *(DisplayUpper+14*2+1) = 64 | *(DisplayUpper+14*2+1);} // add a dot in column 13 + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 188: + Timer = ActivateTimer(800, State+1, PB_RulesDisplay); + PB_EyeFlash(0); + PB_HandleEjectHole(16); + PB_RulesEffect(0); + break; + case 189: + WriteUpper(" IF ONE BALL "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 190: + WriteUpper(" DRAINS "); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 191: + WriteUpper("RE-LOCK2 BALLS"); + PB_EyeBlink(1); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 192: + WriteUpper(" TO RE-LITE"); + Timer = ActivateTimer(2500, State+1, PB_RulesDisplay); + break; + case 193: + WriteUpper(" THIRD LOCK "); + *(DisplayUpper+13*2+1) = 64 | *(DisplayUpper+13*2+1); // add a dot in column 12 + PB_EyeBlink(0); + PB_HandleEjectHole(15); + Timer = ActivateTimer(2700, State+1, PB_RulesDisplay); + break; + case 194: + PB_RulesEffect(0); + Timer = ActivateTimer(2000, State+1, PB_RulesDisplay); + break; + case 195: + Timer = 0; + PB_HandleEjectHole(16); + ReleaseSolenoid(12); + ReleaseSolenoid(14); + PB_AttractMode(); + break;}} + + void PB_Testmode(byte Select) { Switch_Pressed = PB_Testmode; switch(AppByte) { // which testmode? @@ -2804,10 +3829,6 @@ void PB_Testmode(byte Select) { case 3: if (!AppByte2) { // first sound? WriteUpper("PLAYING MUSIC "); - if (APC_settings[Volume]) { // system set to digital volume control? - analogWrite(VolumePin,255-APC_settings[Volume]);} // adjust PWM to volume setting - else { - digitalWrite(VolumePin,HIGH);} // turn off the digital volume control *(DisplayLower+30) = DispPattern2[32 + 2 * ((AppByte2+1) % 10)]; // show the actual sound number *(DisplayLower+31) = DispPattern2[33 + 2 * ((AppByte2+1) % 10)]; *(DisplayLower+28) = DispPattern2[32 + 2 * ((AppByte2+1) - ((AppByte2+1) % 10)) / 10]; @@ -2820,7 +3841,6 @@ void PB_Testmode(byte Select) { break; case 72: AfterMusic = 0; - digitalWrite(VolumePin,HIGH); // turn off the digital volume control StopPlayingMusic(); if (AppByte2) { AppByte2 = 0;} diff --git a/README.md b/README.md index 1d300d4..9a47e04 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ To summarize the above it can be said that the APC does replace all CPU, power d ![APC 2.0](https://github.com/AmokSolderer/APC/blob/master/DOC/PICS/APC.JPG) The picture shows an APC 3.1 board with the new on-board SD-Card slot below the Arduino. -See my [APC 3 video](https://www.youtube.com/watch?v=4EgOTJyxMXo) to get an impression what the boards can do. +See my [APC 3 video](https://www.youtube.com/watch?v=4EgOTJyxMXo) to get an impression what this board can do. There's a table of contents of the available documentation at the end of this page. @@ -51,7 +51,7 @@ To see the APC in an early stage you might want take a look at my [Black Knight You can use [Lisy](https://lisy.dev/apc.html) to run PinMame on an APC board. This spares you the effort to do any game Software as you can run the old ROM code. For System 3 - 7 machines the APC can be used with the old original soundboards. That means in this case it's a plug & play solution. -You might want the APC to generate the audio anyway, either because you don't have an audio board, you want to do your own sounds or you have a System 9 or 11 game. In this case then it's going to require some work to set up your game with PinMame. Take a look at the [PinMame Sound page](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) to see what I mean. +You might want the APC to generate the audio anyway, either because you don't have an audio board, you want to do your own sounds or you have a System 9 or 11 game. In this case it's going to require some work to set up your game with PinMame. Take a look at the [PinMame Sound page](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) to see what I mean. With PinMame running for your game you can use the [PinMameExceptions](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameExceptions.md) functionality of the APC to change the rules of your game even though it's running under PinMame control. Watch my [Jungle Lord video](https://www.youtube.com/watch?v=bbfhH_-gMfE) so see an example. The corresponding code can be found in PinMameExceptions.ino in the AmokPrivate branch on Github. @@ -94,7 +94,7 @@ The second board is a [driver for 8 additional solenoids](https://github.com/Amo I use [special alphanumerical displays](https://github.com/AmokSolderer/APC/blob/master/DOC/Sys7Alpha.md) in my Black Knight which can also be found in the HW section as well as an LED replacement for the original System7 numerical displays. -## Current Status (March 2023) +## Current Status (December 2023) The following table gives an overview about the various system generations the APC can be used with and if at least one machine of each generation has been confirmed to work with it. Additionally you can see whether PinMame or [MPF](http://missionpinball.org/) have been tested with at least one machine of this generation and whether some special preparation like additional cables are required. Details about these cables can be found [here](https://github.com/AmokSolderer/APC/blob/master/DOC/HowToStart.md#cable-extensions) @@ -102,14 +102,14 @@ The PinMame support is still under development and even if a generation is basic | Williams System | Tested | PinMame support | MPF support | Comment | |--|--|--|--|--| -|3| Not yet | Not yet | Yes| | -|4| Yes | Not yet | Yes| | +|3| Yes | Yes | Yes|When PinMame is used, some solenoid activation times need to be increase by a [PinMameException](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameExceptions.md#fixing-the-drop-targets-of-system-3-&-4-games)| +|4| Yes | Yes | Yes|When PinMame is used, some solenoid activation times need to be increase by a [PinMameException](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameExceptions.md#fixing-the-drop-targets-of-system-3-&-4-games)| |6| Yes | Yes | Yes | | |7| Yes | Yes | Yes | Needs two additional wires | |9| Yes | Yes | Yes | | |11| Yes | Yes | Yes | | |11a| Yes | Yes | Yes | Some cable ties have to be cut and the wiring harness opened a bit | -|11b| Not yet | Not yet | Yes | | +|11b| Not yet | Yes | Yes | | |11c| Yes | Yes | Yes | The wires of three connectors must be extended | The following Data East MPUs are almost identical to their counterparts from Williams but DE used 2.1 audio boards. Check the [known issues](https://github.com/AmokSolderer/APC#known-issues) for details @@ -151,7 +151,7 @@ Feedback is very important for me, because if there is none I must assume that n ## How to get startet? -I'm sorry, but I'm not going to sell any boards. Check whether anyone in the above mentioned forums has a spare board for sale. If this fails you can still order populated board in China. +I'm sorry, but I'm not going to sell any boards. Check whether anyone in the above mentioned forums has a spare board for sale. If this fails you can still order populated boards from China. If you're interested in using an APC, then be sure to follow the instructions given in the first part of the documentation listet below. They will guide you through the process of getting and setting up an APC board. ## Documentation contents @@ -176,22 +176,24 @@ If you're interested in using an APC, then be sure to follow the instructions gi 4. Running PinMame 4.1. [Lisy Homepage](https://lisy.dev/apc.html) - Location of the Lisy SW download and more -4.2. [PinMame Sound](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) - shows the current status of the APC and Lisy running PinMame -4.3. [PinMame Sound howto](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame_howto.md) - If your game is not yet supported, you can learn here how to change that -4.4. [PinMameExceptions](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameExceptions.md) - change your game, but let PinMame do the main work -4.5. [PinMame game numbers](https://github.com/AmokSolderer/APC/blob/master/DOC/lisyminigames.csv) - list of the PinMame game numbers -4.6. [Controlling Lisy](https://github.com/AmokSolderer/APC/blob/master/DOC/LisyDebug.md) - updating Lisy and using the debug mode -4.7. [Instructions for extracting sound files](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSounds.md) - automatic extraction of sound files and the use of Audacity in more detail (by Mokopin) +4.2. [PinMame Sound](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMame.md) - shows whether your game is supported and what to do if not +4.3. [PinMame System 3 - 7 Sound howto](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_3_7.md) - How to enable the sound of System 3 - 7 games +4.4. [PinMame System 9 Sound howto](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_9.md) - How to enable the sound of System 9 games +4.5. [PinMame System 11 Sound howto](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSound_11.md) - How to enable the sound of System 11 games +4.6. [PinMameExceptions](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameExceptions.md) - change your game, but let PinMame do the main work +4.7. [PinMame game numbers](https://github.com/AmokSolderer/APC/blob/master/DOC/lisyminigames.csv) - list of the PinMame game numbers +4.8. [Controlling Lisy](https://github.com/AmokSolderer/APC/blob/master/DOC/LisyDebug.md) - updating Lisy and using the debug mode +4.9. [Instructions for extracting sound files](https://github.com/AmokSolderer/APC/blob/master/DOC/PinMameSounds.md) - automatic extraction of sound files and the use of Audacity in more detail (by Mokopin) 5. Using MPF 5.1. [MPF runs APC](https://www.youtube.com/watch?v=w4Po8OE5Zkw) - see my first humble MPF steps in this video 5.2. [MPF setup](https://github.com/AmokSolderer/APC/tree/master/DOC/Software/MPF) - my MPF test config files -5.3. Lisy runs MPF -6. Additional APC hardware - boards that might come in handy with the APC +6. Additional APC hardware - boards and other stuff that might come in handy with the APC 6.1. [APC LED expansion board](https://github.com/AmokSolderer/APC/blob/master/DOC/LEDexpBoard.md) - a board to control WS2812 based LED strips with the APC 6.2. [APC solenoid expansion board](https://github.com/AmokSolderer/APC/blob/master/DOC/SolExpBoard.md) - to control additional features 6.3. [System 7 alphanumeric display](https://github.com/AmokSolderer/APC/blob/master/DOC/Sys7Alpha.md) - to have alphanumerical displays in a pre System 11 machine +6.4. [Installation frames](https://github.com/AmokSolderer/APC/blob/master/DOC/Frames.md) - to mount the APC in your backbox 7. Additional non APC hardware - just some stuff I designed over the years. Can also be used without the APC 7.1. [System 7 LED display](https://github.com/AmokSolderer/APC/tree/master/DOC/Hardware/Sys7_Display) - an LED replacement display for System 7, purely numerical @@ -199,7 +201,7 @@ If you're interested in using an APC, then be sure to follow the instructions gi 8. APC games - Complete games running natively on the APC or games that run in PinMame but have been changed 8.1. [Black Knight](https://github.com/AmokSolderer/APC/blob/master/DOC/BlackKnight.md) - Complete game code with some additional features -8.2. [Pin Bot](https://github.com/AmokSolderer/APC/blob/master/DOC/Pinbot.md) - Complete game code with some additional features +8.2. [Pinbot](https://github.com/AmokSolderer/APC/blob/master/DOC/Pinbot.md) - Complete game code with some additional features 8.3. [Comet](https://github.com/AmokSolderer/APC/blob/master/DOC/Comet.md) - Some extensions to the original game code running in PinMame 9. Videos - For the generation Youtube diff --git a/USBcontrol.ino b/USBcontrol.ino index d19f8f7..2a8b2bb 100644 --- a/USBcontrol.ino +++ b/USBcontrol.ino @@ -440,42 +440,41 @@ void USB_SerialCommand() { USB_WriteByte((byte) QuerySolenoid(USB_SerialBuffer[0]));} break; case 21: // set solenoid # to on - if (!PinMameException(SolenoidActCommand, USB_SerialBuffer[0])){ // check for machine specific exceptions - if (USB_SerialBuffer[0] < 23) { // max 24 solenoids - if (!SolRecycleTimers[USB_SerialBuffer[0]-1]) { // recycling time over for this coil? - SolChange = false; // block IRQ solenoid handling - if (USB_SerialBuffer[0] > 8) { // does the solenoid not belong to the first latch? - if (USB_SerialBuffer[0] < 17) { // does it belong to the second latch? - SolBuffer[1] |= 1<<(USB_SerialBuffer[0]-9); // latch counts from 0 - SolLatch |= 2;} // select second latch + if (game_settings[USB_PinMameGame] > 18 || USB_SerialBuffer[0] < 9 || USB_SerialBuffer[0] > 13) { // block solenoid commands for Sys 3 - 6 sound control lines + if (!PinMameException(SolenoidActCommand, USB_SerialBuffer[0])){ // check for machine specific exceptions + if (USB_SerialBuffer[0] < 23) { // max 24 solenoids + if (!SolRecycleTimers[USB_SerialBuffer[0]-1]) { // recycling time over for this coil? + SolChange = false; // block IRQ solenoid handling + if (USB_SerialBuffer[0] > 8) { // does the solenoid not belong to the first latch? + if (USB_SerialBuffer[0] < 17) { // does it belong to the second latch? + SolBuffer[1] |= 1<<(USB_SerialBuffer[0]-9);} // latch counts from 0 + else { + SolBuffer[2] |= 1<<(USB_SerialBuffer[0]-17);}} else { - SolBuffer[2] |= 1<<(USB_SerialBuffer[0]-17); - SolLatch |= 4;}} // select third latch - else { - SolBuffer[0] |= 1<<(USB_SerialBuffer[0]-1); - SolLatch |= 1;} // select first latch - SolChange = true;}} - else if (USB_SerialBuffer[0] == 23) { // right flipper - ActivateSolenoid(0, 23);} - else if (USB_SerialBuffer[0] == 24) { // left flipper - ActivateSolenoid(0, 24);} - else if (USB_SerialBuffer[0] == 25) { // 25 is a shortcut for both flipper fingers - ActivateSolenoid(0, 23); // enable both flipper fingers - ActivateSolenoid(0, 24);} - else if ((USB_SerialBuffer[0] <= SolMax) && APC_settings[SolenoidExp]) { // sol exp board selected - WriteToHwExt(SolBuffer[3] |= 1<<(USB_SerialBuffer[0]-26), 128+4); - WriteToHwExt(SolBuffer[3] |= 1<<(USB_SerialBuffer[0]-26), 4);}} + SolBuffer[0] |= 1<<(USB_SerialBuffer[0]-1);} + SolChange = true;}} + else if (USB_SerialBuffer[0] == 23) { // right flipper + ActivateSolenoid(0, 23);} + else if (USB_SerialBuffer[0] == 24) { // left flipper + ActivateSolenoid(0, 24);} + else if (USB_SerialBuffer[0] == 25) { // 25 is a shortcut for both flipper fingers + ActivateSolenoid(0, 23); // enable both flipper fingers + ActivateSolenoid(0, 24);} + else if ((USB_SerialBuffer[0] <= SolMax) && APC_settings[SolenoidExp]) { // sol exp board selected + WriteToHwExt(SolBuffer[3] |= 1<<(USB_SerialBuffer[0]-26), 128+4); + WriteToHwExt(SolBuffer[3] |= 1<<(USB_SerialBuffer[0]-26), 4);}}} break; case 22: // set solenoid # to off - if (!PinMameException(SolenoidRelCommand, USB_SerialBuffer[0])){ // check for machine specific exceptions - if (USB_SerialBuffer[0] < 25) { // max 24 solenoids - ReleaseSolenoid(USB_SerialBuffer[0]);} - else if (USB_SerialBuffer[0] == 25) { // 25 is a shortcut for both flipper fingers - ReleaseSolenoid(23); // disable both flipper fingers - ReleaseSolenoid(24);} - else if ((USB_SerialBuffer[0] <= SolMax) && APC_settings[SolenoidExp]) { // sol exp board selected - WriteToHwExt(SolBuffer[3] &= 255-(1<<(USB_SerialBuffer[0]-26)), 128+4); - WriteToHwExt(SolBuffer[3] &= 255-(1<<(USB_SerialBuffer[0]-26)), 4);}} + if (game_settings[USB_PinMameGame] > 18 || USB_SerialBuffer[0] < 9 || USB_SerialBuffer[0] > 13) { // block solenoid commands for Sys 3 - 6 sound control lines + if (!PinMameException(SolenoidRelCommand, USB_SerialBuffer[0])){ // check for machine specific exceptions + if (USB_SerialBuffer[0] < 25) { // max 24 solenoids + ReleaseSolenoid(USB_SerialBuffer[0]);} + else if (USB_SerialBuffer[0] == 25) { // 25 is a shortcut for both flipper fingers + ReleaseSolenoid(23); // disable both flipper fingers + ReleaseSolenoid(24);} + else if ((USB_SerialBuffer[0] <= SolMax) && APC_settings[SolenoidExp]) { // sol exp board selected + WriteToHwExt(SolBuffer[3] &= 255-(1<<(USB_SerialBuffer[0]-26)), 128+4); + WriteToHwExt(SolBuffer[3] &= 255-(1<<(USB_SerialBuffer[0]-26)), 4);}}} break; case 23: // pulse solenoid if (USB_SolTimes[USB_SerialBuffer[0]-1]) { // pulse length set? @@ -922,8 +921,7 @@ void USB_SerialCommand() { if (game_settings[USB_PinMameSound]) { // use old audio board if (game_settings[USB_PinMameGame] < 19) { // Sys 3 - 6 game SolBuffer[1] = SolBuffer[1] & 224; // turn off sound related solenoids - SolBuffer[1] = SolBuffer[1] | (USB_SerialBuffer[1] & 31); // write sound number to solenoids 9 - 13 - SolLatch |= 2;} // trigger update of 2nd solenoid latch + SolBuffer[1] = SolBuffer[1] | (~USB_SerialBuffer[1] & 31);} // write sound number to solenoids 9 - 13 else if (game_settings[USB_PinMameGame] < 40) { // Sys 7 - 9 game WriteToHwExt(USB_SerialBuffer[1], 128+16); // turn on Sel14 WriteToHwExt(USB_SerialBuffer[1], 16);} // turn off Sel14 @@ -1103,6 +1101,8 @@ void USB_SerialCommand() { game_settings[USB_SerialBuffer[1]] = USB_SerialBuffer[2];} else { // APC settings selected APC_settings[USB_SerialBuffer[1]] = USB_SerialBuffer[2];} + break; + case 66: // init system to apply changed settings Init_System2(1); break; case 80: // send command to HW_ext interface @@ -1129,7 +1129,6 @@ void USB_SerialCommand() { SolBuffer[1] = 0; SolBuffer[2] = 192; // keep the flipper fingers alive SolBuffer[3] = 0; - SolLatch = 7; // signal all latches to be processed SolChange = true; if (APC_settings[SolenoidExp]) { // sol exp board selected WriteToHwExt(0, 128+4);