Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx
Binary file not shown.
4 changes: 4 additions & 0 deletions VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using FluentAssertions;
using NUnit.Framework;
using VisualPinball.Engine.Test.Test;
using VisualPinball.Engine.VPT;
using VisualPinball.Engine.VPT.Trough;

namespace VisualPinball.Engine.Test.VPT.Trough
Expand All @@ -42,10 +43,13 @@ public void ShouldWriteTroughData()

private static void ValidateTroughData(TroughData data)
{
data.Type.Should().Be(TroughType.ModernOpto);
data.BallCount.Should().Be(3);
data.SwitchCount.Should().Be(4);
data.KickTime.Should().Be(112);
data.RollTime.Should().Be(113);
data.TransitionTime.Should().Be(114);
data.JamSwitch.Should().Be(true);
data.PlayfieldEntrySwitch.Should().Be("BallDrain");
data.PlayfieldExitKicker.Should().Be("BallRelease");
}
Expand Down
49 changes: 44 additions & 5 deletions VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,65 @@ namespace VisualPinball.Engine.Test.VPT.Trough
public class TroughTests
{
[Test]
public void ShouldReturnCorrectSwitchesForModern()
public void ShouldReturnCorrectSwitchesForModernOpto()
{
var data = new TroughData("Trough") {
Type = TroughType.Modern,
TransitionTime = 100,
RollTime = 1000,
Type = TroughType.ModernOpto,
SwitchCount = 3
};
var trough = new Engine.VPT.Trough.Trough(data);
var switches = trough.AvailableSwitches.ToArray();

trough.RollTimeDisabled.Should().Be(900);
trough.RollTimeEnabled.Should().Be(100);

switches.Should().HaveCount(3);
switches[0].Id.Should().Be("1");
switches[1].Id.Should().Be("2");
switches[2].Id.Should().Be("3");
}

[Test]
public void ShouldReturnCorrectCoilsForModern()
public void ShouldReturnCorrectCoilsForModernOpto()
{
var data = new TroughData("Trough") {
Type = TroughType.Modern,
Type = TroughType.ModernOpto,
};
var trough = new Engine.VPT.Trough.Trough(data);
var coils = trough.AvailableCoils.ToArray();

coils.Should().HaveCount(1);
coils[0].Id.Should().Be(Engine.VPT.Trough.Trough.EjectCoilId);
}

[Test]
public void ShouldReturnCorrectSwitchesForModernMechanical()
{
var data = new TroughData("Trough") {
TransitionTime = 100,
RollTime = 1000,
Type = TroughType.ModernMech,
SwitchCount = 3
};
var trough = new Engine.VPT.Trough.Trough(data);
var switches = trough.AvailableSwitches.ToArray();

trough.RollTimeDisabled.Should().Be(500);
trough.RollTimeEnabled.Should().Be(500);

switches.Should().HaveCount(3);
switches[0].Id.Should().Be("1");
switches[1].Id.Should().Be("2");
switches[2].Id.Should().Be("3");
}

[Test]
public void ShouldReturnCorrectCoilsForModernMechanical()
{
var data = new TroughData("Trough") {
Type = TroughType.ModernMech,
};
var trough = new Engine.VPT.Trough.Trough(data);
var coils = trough.AvailableCoils.ToArray();
Expand Down Expand Up @@ -136,7 +175,7 @@ public void ShouldReturnCorrectCoilsForClassicSingleBall()
var coils = trough.AvailableCoils.ToArray();

coils.Should().HaveCount(1);
coils[0].Id.Should().Be(Engine.VPT.Trough.Trough.EntryCoilId);
coils[0].Id.Should().Be(Engine.VPT.Trough.Trough.EjectCoilId);
}
}
}
9 changes: 5 additions & 4 deletions VisualPinball.Engine/VPT/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,11 @@ public static class SwitchType

public static class TroughType
{
public const int Modern = 0;
public const int TwoCoilsNSwitches = 1;
public const int TwoCoilsOneSwitch = 2;
public const int ClassicSingleBall = 3;
public const int ModernOpto = 0;
public const int ModernMech = 1;
public const int TwoCoilsNSwitches = 2;
public const int TwoCoilsOneSwitch = 3;
public const int ClassicSingleBall = 4;
}

public static class CoilDestination
Expand Down
69 changes: 63 additions & 6 deletions VisualPinball.Engine/VPT/Trough/Trough.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,43 @@ public class Trough : Item<TroughData>, ISwitchableDevice, ICoilableDevice

public const string EntrySwitchId = "drain_switch";
public const string TroughSwitchId = "trough_switch";
public const string JamSwitchId = "jam_switch";
public const string EjectCoilId = "eject_coil";
public const string EntryCoilId = "entry_coil";

public IEnumerable<GamelogicEngineSwitch> AvailableSwitches {
get {

switch (Data.Type) {
case TroughType.Modern:
case TroughType.ModernOpto:
case TroughType.ModernMech:
return Enumerable.Repeat(0, Data.SwitchCount)
.Select((_, i) => new GamelogicEngineSwitch
{ Description = SwitchDescription(i), Id = $"{i + 1}" });
{ Description = SwitchDescription(i), Id = $"{i + 1}" })
.Concat(Data.JamSwitch
? new [] { new GamelogicEngineSwitch {Description = "Jam Switch", Id = JamSwitchId }}
: new GamelogicEngineSwitch[0]
);

case TroughType.TwoCoilsNSwitches:
return new[] {
new GamelogicEngineSwitch {Description = "Entry Switch", Id = EntrySwitchId}
}.Concat(Enumerable.Repeat(0, Data.SwitchCount)
.Select((_, i) => new GamelogicEngineSwitch
{ Description = SwitchDescription(i), Id = $"{i + 1}"} )
).Concat(Data.JamSwitch
? new [] { new GamelogicEngineSwitch {Description = "Jam Switch", Id = JamSwitchId }}
: new GamelogicEngineSwitch[0]
);

case TroughType.TwoCoilsOneSwitch:
return new[] {
new GamelogicEngineSwitch {Description = "Entry Switch", Id = EntrySwitchId},
new GamelogicEngineSwitch {Description = "Trough Switch", Id = TroughSwitchId},
};
}.Concat(Data.JamSwitch
? new [] { new GamelogicEngineSwitch {Description = "Jam Switch", Id = JamSwitchId }}
: new GamelogicEngineSwitch[0]
);

case TroughType.ClassicSingleBall:
return new[] {
Expand All @@ -62,15 +75,15 @@ public IEnumerable<GamelogicEngineSwitch> AvailableSwitches {

default:
throw new ArgumentException("Invalid trough type " + Data.Type);

}
}
}

public IEnumerable<GamelogicEngineCoil> AvailableCoils {
get {
switch (Data.Type) {
case TroughType.Modern:
case TroughType.ModernOpto:
case TroughType.ModernMech:
return new[] {
new GamelogicEngineCoil {Description = "Eject", Id = EjectCoilId}
};
Expand All @@ -82,14 +95,58 @@ public IEnumerable<GamelogicEngineCoil> AvailableCoils {
};
case TroughType.ClassicSingleBall:
return new[] {
new GamelogicEngineCoil {Description = "Entry", Id = EntryCoilId}
new GamelogicEngineCoil {Description = "Eject", Id = EjectCoilId}
};
default:
throw new ArgumentException("Invalid trough type " + Data.Type);
}
}
}

/// <summary>
/// Time in milliseconds it takes the switch to enable when the ball enters.
/// </summary>
/// <exception cref="ArgumentException"></exception>
public int RollTimeEnabled {
get {
switch (Data.Type) {
case TroughType.ModernOpto:
return Data.TransitionTime;

case TroughType.ModernMech:
case TroughType.TwoCoilsNSwitches:
case TroughType.TwoCoilsOneSwitch:
case TroughType.ClassicSingleBall:
return Data.RollTime / 2;

default:
throw new ArgumentException("Invalid trough type " + Data.Type);
}
}
}

/// <summary>
/// Time in milliseconds it takes the switch to disable after ball starts rolling.
/// </summary>
/// <exception cref="ArgumentException"></exception>
public int RollTimeDisabled {
get {
switch (Data.Type) {
case TroughType.ModernOpto:
return Data.RollTime - Data.TransitionTime;

case TroughType.ModernMech:
case TroughType.TwoCoilsNSwitches:
case TroughType.TwoCoilsOneSwitch:
case TroughType.ClassicSingleBall:
return Data.RollTime / 2;

default:
throw new ArgumentException("Invalid trough type " + Data.Type);
}
}
}

public Trough(TroughData data) : base(data)
{
}
Expand Down
16 changes: 11 additions & 5 deletions VisualPinball.Engine/VPT/Trough/TroughData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class TroughData : ItemData
public string Name;

[BiffInt("TYPE", Pos = 2)]
public int Type = TroughType.Modern;
public int Type = TroughType.ModernOpto;

[BiffString("ENTS", Pos = 3)]
public string PlayfieldEntrySwitch = string.Empty;
Expand All @@ -53,11 +53,17 @@ public class TroughData : ItemData
[BiffInt("SCNT", Pos = 6)]
public int SwitchCount = 6;

[BiffInt("RTIM", Pos = 7)]
public int RollTime = 100;
[BiffBool("HJSW", Pos = 7)]
public bool JamSwitch;

[BiffInt("KTIM", Pos = 8)]
public int KickTime = 200;
[BiffInt("RTIM", Pos = 8)]
public int RollTime = 300;

[BiffInt("TTIM", Pos = 9)]
public int TransitionTime = 50;

[BiffInt("KTIM", Pos = 10)]
public int KickTime = 100;

public TroughData(string name) : base(StoragePrefix.GameItem)
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

If you are unfamiliar with ball troughs, have a quick look at [MPF's documentation](https://mpf-docs.readthedocs.io/en/latest/mechs/troughs/), which does an excellent job explaining them.

VPE comes with a trough mechanism that simulates the behaviour of a real-world ball trough. This is especially important when emulating existing games, since the [gamelogic engine](../gamelogic-engine.md) expects the trough's switches to be in a plausible state, or else it may have errors.
VPE comes with a trough mechanism that simulates the behavior of a real-world ball trough. This is especially important when emulating existing games, since the [gamelogic engine](../gamelogic-engine.md) expects the trough's switches to be in a plausible state, or else it may have errors.

## Creating a Trough

Expand All @@ -12,68 +12,100 @@ When importing a `.vpx` file that doesn't have any troughs (which is likely, bec

<img src="trough-inspector.png" width="343" class="img-responsive pull-right" style="margin-left: 15px">

To interact with the game, you'll need to setup an **entry switch** to drain the ball into the trough, and an **exit kicker** to release a new ball from the trough. This terminology may seem weird, since the ball *exits* the playfield when draining, but the links are labelled in relation to the trough.
To interact with the game, you must set up an **input switch** to drain the ball into the trough, and an **exit kicker** to release a new ball from the trough. This terminology may seem weird, since the ball *exits* the playfield when draining, but from the trough's perspective, that's where the ball *enters*.

You can setup these links under *Playfield Links* by selecting the trough in the hierarchy panel and linking them to the desired items using the inspector.

> [!NOTE]
> Both the input switch and the exit kicker are not related to the gamelogic engine. Their goal is purely to link the physics simulation to the trough logic, whose behavior is not physically simulated.
>
> Many games *do* have an input switch (which we call *drain switch*) and an exit kicker (we that one *eject coil*). They are simulated by the trough itself and show up in the switch- and coil manager under the trough device.

The inspector also lets you configure other options:

- **Ball Count** defines how many balls the trough can hold.
- **Switch Count** sets how many ball switches are available. This is usually the same number as the ball count.
- **Ball Count** defines how many balls the trough holds when the game starts.
- **Switch Count** sets how many ball switches are available. This is usually the same number as the ball count. The drain switch and the jam switch are excluded from this count.
- **Has Jam Switch** defines if the trough has a jam switch. This switch is often called *eject switch* as well.
- **Roll Time** sets how long it takes the ball to roll from one switch to the next.
- **Kick Time** defines how long it takes the ball to get kicked from the drain into the trough.
- **Transition Time** is only relevant for opto switches and defines how long the switch closes between balls.

## Trough Types

VPE supports several variants of troughs found on real machines. You can configure the behavior of the trough by changing the *Type* in the inspector when the trough is selected in the hierarchy.

In this section we'll again link to the excellent MPF documentation explaining each of the different types.
In this section we'll again link to the excellent MPF documentation explaining each of the different types. We'll also provide an animation of the trough inspector during gameplay showing how the switches and coils behave in real time.

### Modern Mechanical

<img src="trough-mechanical.gif" width="348" class="img-responsive pull-right" style="margin-left: 15px">

[Modern troughs with mechanical switches](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-2-modern-trough-with-mechanical-switches) are covered by this type.

The ball drains from the playfield directly into the ball stack, and every ball slot has an associated switch. When a ball gets ejected, the remaining balls move down simultaneously to the next position. During that movement, their switches get first opened and then closed again when they reach the next position. The time of this movement is defined by *Roll Time*.

*The animation on the right shows a 6-ball trough filled with three balls. It starts by ejecting a ball, followed by draining that ball, rolling back onto the stack.*

### Modern (opto or mechanical)
### Modern Opto

<img src="trough-modern.png" width="343" class="img-responsive pull-right" style="margin-left: 15px">
<img src="trough-opto.gif" width="348" class="img-responsive pull-right" style="margin-left: 15px">

Modern troughs with both [optical](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-1-modern-trough-with-opto-sensors) and [mechanical](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-2-modern-trough-with-mechanical-switches) switches are covered by this type.
[Modern troughs with optical switches](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-1-modern-trough-with-opto-sensors) work similar similar to their mechanical counterparts. However there are two differences:

The ball drains from the playfield directly into the ball stack, and every ball slot has an associated switch.
1. Opto switches have the inverse value of mechanical switches. That means per default, an opto switch is *closed*, and when a ball rolls through, it opens. It's kind of logical, because the ball *blocks* the beam of light thus *opening* the circuit, while a mechanical switch gets *closed* by the ball's weight.
2. Timings are different. When a ball approaches an opto switch, the switch gets triggered as soon as the ball's *front* hits the beam, while a mechanical switch gets triggered when the ball's *center* is over it. This results in very short closing times when the ball stack moves to the next position after a ball eject.

During gameplay, if you select the trough in the hierarchy, it displays the status of every switch in real time for debug purposes.
We call this closing time the *transition time* - it's the time during stack transition when all switches briefly close.

*Like before, the animation shows a 6-ball trough filled with three balls. It starts by ejecting a ball, followed by draining that ball, rolling back onto the stack.*

> [!NOTE]
> When a transition time is set to `0`, only the first and the last switch of the stack change value (as opposed to each position opening and closing immediately).

### Two coils and multiple switches

<img src="trough-2cns.png" width="343" class="img-responsive pull-right" style="margin-left: 15px">
<img src="trough-2cns.gif" width="348" class="img-responsive pull-right" style="margin-left: 15px">

[Troughs of this type](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-3-older-style-with-two-coils-and-switches-for-each-ball) can be found in older machines from the 80s and early 90s. They consist of two parts:

1. A drain, the ball rolls into when leaving the playfield
2. A ball stack, where the out of play balls are kept.
2. A ball stack, where the out of play balls are held.

In terms of switches, they still include a switch per ball in the stack, but also an additional drain switch to trigger kicking the ball from the drain into the stack.

*The animation shows a 6-ball trough filled with three balls. It starts by ejecting a ball, followed by draining that ball. The ball stays in the drain until the entry coil activates, which makes the ball roll over to the ball stack.*

### Two coils and one switch

<img src="trough-2c1s.png" width="343" class="img-responsive pull-right" style="margin-left: 15px">
<img src="trough-2c1s.gif" width="348" class="img-responsive pull-right" style="margin-left: 15px">

A trough can also have [only one switch](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-4-older-style-with-two-coils-and-only-one-ball-switch) in the ball stack.

Instead of a *Switch Count* like the previous types, you select a *Switch Position*, which is the position in the ball stack at which the ball farthest away from the eject coil sits.

*The animation shows a 6-ball trough filled with six balls. It starts by ejecting a ball, followed by draining that ball. The ball stays in the drain until the entry coil activates, which makes the ball roll over to the ball stack.*

### Classic single ball

<img src="trough-single-ball.png" width="343" class="img-responsive pull-right" style="margin-left: 15px">
<img src="trough-single-ball.gif" width="348" class="img-responsive pull-right" style="margin-left: 15px">

A single ball trough may work [with](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-5-classic-single-ball-single-coil) or [without](https://docs.missionpinball.org/en/latest/mechs/troughs/#option-6-classic-single-ball-single-coil-no-shooter-lane) a shooter lane. The principle is simple: After draining, the ball is kept on the drain coil, which ejects the ball either directly into the plunger lane or back onto the playfield.

*The animation shows single ball trough that ejects a ball and drains it a few seconds later.*

## Switch Setup

The number of simulated switches in the trough depends on the type of the trough and the *Switch Count* property in the inspector panel. For recreations, you can quickly determine the number of trough switches by looking at the switch matrix in the operation manual, it usually matches the number of balls installed in the game.
The number of simulated switches in the trough depends on the type of trough and the *Switch Count* property in the inspector panel. For recreations, you can quickly determine the number of trough switches by looking at the switch matrix in the operation manual, it usually matches the number of balls installed in the game.

To configure the switches, open the [switch manager](../../editor/switch-manager.md) and add the trough switches if they're not already there. For *Destination* select "Device", under *Element*, select the trough you've created and which switch to connect. For a modern five-ball trough, it will look something like this:

![Switch Manager](trough-switches.png)

## Coil Setup

VPE's trough supports two coils, an entry coil which drains the ball from the outhole into the trough, and an eject coil which pushes a new ball into the plunger lane. To configure the coils, open the [coil manager](../../editor/coil-manager.md), find or add the coils, and link them to the trough like you did with the switches:
VPE's trough supports up to two coils, an entry coil which drains the ball from the outhole into the trough, and an eject coil which pushes a new ball into the plunger lane. To configure the coils, open the [coil manager](../../editor/coil-manager.md), find or add the coils, and link them to the trough like you did with the switches:

![Coil Manager](trough-coils.png)

![Coil Manager](trough-coils.png)
> [!NOTE]
> Depending on which trough type is set, different coils and switches show up under the trough device.
Loading