diff --git a/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx b/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx index 136b02bc0..96b4c3f59 100644 Binary files a/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx and b/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx differ diff --git a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs index 4ea8b0f00..cb2615f44 100644 --- a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs +++ b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs @@ -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 @@ -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"); } diff --git a/VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs b/VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs index 706a04ad6..49700eb24 100644 --- a/VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs +++ b/VisualPinball.Engine.Test/VPT/Trough/TroughTests.cs @@ -25,15 +25,20 @@ 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"); @@ -41,10 +46,44 @@ public void ShouldReturnCorrectSwitchesForModern() } [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(); @@ -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); } } } diff --git a/VisualPinball.Engine/VPT/Enums.cs b/VisualPinball.Engine/VPT/Enums.cs index 4a6897ef5..5c9de0a02 100644 --- a/VisualPinball.Engine/VPT/Enums.cs +++ b/VisualPinball.Engine/VPT/Enums.cs @@ -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 diff --git a/VisualPinball.Engine/VPT/Trough/Trough.cs b/VisualPinball.Engine/VPT/Trough/Trough.cs index aea248c51..5e9955440 100644 --- a/VisualPinball.Engine/VPT/Trough/Trough.cs +++ b/VisualPinball.Engine/VPT/Trough/Trough.cs @@ -30,16 +30,23 @@ public class Trough : Item, 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 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[] { @@ -47,13 +54,19 @@ public IEnumerable AvailableSwitches { }.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[] { @@ -62,7 +75,6 @@ public IEnumerable AvailableSwitches { default: throw new ArgumentException("Invalid trough type " + Data.Type); - } } } @@ -70,7 +82,8 @@ public IEnumerable AvailableSwitches { public IEnumerable AvailableCoils { get { switch (Data.Type) { - case TroughType.Modern: + case TroughType.ModernOpto: + case TroughType.ModernMech: return new[] { new GamelogicEngineCoil {Description = "Eject", Id = EjectCoilId} }; @@ -82,7 +95,7 @@ public IEnumerable 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); @@ -90,6 +103,50 @@ public IEnumerable AvailableCoils { } } + /// + /// Time in milliseconds it takes the switch to enable when the ball enters. + /// + /// + 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); + } + } + } + + /// + /// Time in milliseconds it takes the switch to disable after ball starts rolling. + /// + /// + 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) { } diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs b/VisualPinball.Engine/VPT/Trough/TroughData.cs index 745c7c880..5b3adc23d 100644 --- a/VisualPinball.Engine/VPT/Trough/TroughData.cs +++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs @@ -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; @@ -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) { diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.gif b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.gif new file mode 100644 index 000000000..8342d57f3 Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.gif differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.png deleted file mode 100644 index 2e228a27d..000000000 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2c1s.png and /dev/null differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.gif b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.gif new file mode 100644 index 000000000..43f77c098 Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.gif differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.png deleted file mode 100644 index 768c32185..000000000 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-2cns.png and /dev/null differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png index c0b69e343..2b2d16e14 100644 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-mechanical.gif b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-mechanical.gif new file mode 100644 index 000000000..fa5e5d923 Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-mechanical.gif differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-modern.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-modern.png deleted file mode 100644 index c34e0dd67..000000000 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-modern.png and /dev/null differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-opto.gif b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-opto.gif new file mode 100644 index 000000000..dd601b3d4 Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-opto.gif differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-single-ball.gif b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-single-ball.gif new file mode 100644 index 000000000..ecbf323e7 Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-single-ball.gif differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-single-ball.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-single-ball.png deleted file mode 100644 index 6a4cae59e..000000000 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-single-ball.png and /dev/null differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md index d05d67396..ac78f9ea6 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md @@ -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 @@ -12,61 +12,90 @@ When importing a `.vpx` file that doesn't have any troughs (which is likely, bec -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 + + + +[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 - + -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 - + [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 - + 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 - + 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: @@ -74,6 +103,9 @@ To configure the switches, open the [switch manager](../../editor/switch-manager ## 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) \ No newline at end of file +> [!NOTE] +> Depending on which trough type is set, different coils and switches show up under the trough device. diff --git a/VisualPinball.Unity/Documentation~/docfx.json b/VisualPinball.Unity/Documentation~/docfx.json index 349209ee1..44cf4a76b 100644 --- a/VisualPinball.Unity/Documentation~/docfx.json +++ b/VisualPinball.Unity/Documentation~/docfx.json @@ -36,6 +36,7 @@ { "files": [ "**/*.png", + "**/*.gif", "**/*.jpg", "**/*.svg", "**/*.ico", diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs index 16dc59fe3..f86984b3c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs @@ -27,14 +27,16 @@ namespace VisualPinball.Unity.Editor public class TroughInspector : ItemMainInspector { private static readonly string[] TypeLabels = { - "Modern (opto or mechanical)", + "Modern Opto", + "Modern Mechanical", "Two coils multiple switches", "Two coils one switch", "Classic single ball", }; private static readonly int[] TypeValues = { - TroughType.Modern, + TroughType.ModernOpto, + TroughType.ModernMech, TroughType.TwoCoilsNSwitches, TroughType.TwoCoilsOneSwitch, TroughType.ClassicSingleBall @@ -51,20 +53,26 @@ public override void OnInspectorGUI() } switch (Data.Type) { - case TroughType.Modern: + case TroughType.ModernOpto: + case TroughType.ModernMech: case TroughType.TwoCoilsNSwitches: ItemDataSlider("Switch Count", ref Data.SwitchCount, 1, 10, false); + ItemDataField("Has Jam Switch", ref Data.JamSwitch, false); break; case TroughType.TwoCoilsOneSwitch: ItemDataSlider("Switch Position", ref Data.SwitchCount, 1, 10, false); + ItemDataField("Has Jam Switch", ref Data.JamSwitch, false); break; } - if (Data.Type != TroughType.Modern && Data.Type != TroughType.TwoCoilsNSwitches) { + if (Data.JamSwitch || Data.Type != TroughType.ModernOpto && Data.Type != TroughType.ModernMech && Data.Type != TroughType.TwoCoilsNSwitches) { ItemDataField("Kick Time (ms)", ref Data.KickTime, false); } ItemDataField("Roll Time (ms)", ref Data.RollTime, false); + if (Data.Type == TroughType.ModernOpto) { + ItemDataField("Transition Time (ms)", ref Data.TransitionTime, false); + } if (!Application.isPlaying) { if (_togglePlayfield = EditorGUILayout.BeginFoldoutHeaderGroup(_togglePlayfield, "Playfield Links")) { @@ -78,10 +86,15 @@ public override void OnInspectorGUI() if (Application.isPlaying) { EditorGUILayout.Separator(); + + + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + EditorGUILayout.LabelField("Switch status:", new GUIStyle(GUI.skin.label) { fontStyle = FontStyle.Bold }); var troughApi = _table.GetComponent().TableApi.Trough(Item.Name); - if (Data.Type != TroughType.Modern) { + if (Data.Type != TroughType.ModernOpto && Data.Type != TroughType.ModernMech) { DrawSwitch("Drain Switch", troughApi.EntrySwitch); } @@ -94,12 +107,30 @@ public override void OnInspectorGUI() } } + if (Data.JamSwitch) { + DrawSwitch("Jam Switch", troughApi.JamSwitch); + } + if (troughApi.UncountedDrainBalls > 0) { EditorGUILayout.LabelField("Undrained balls:", troughApi.UncountedDrainBalls.ToString()); } if (troughApi.UncountedStackBalls > 0) { EditorGUILayout.LabelField("Unswitched balls:", troughApi.UncountedStackBalls.ToString()); } + + GUILayout.EndVertical(); + GUILayout.BeginVertical(); + + EditorGUILayout.LabelField("Coil status:", new GUIStyle(GUI.skin.label) { fontStyle = FontStyle.Bold }); + + if (Data.Type != TroughType.ModernOpto && Data.Type != TroughType.ModernMech && Data.Type != TroughType.ClassicSingleBall) { + DrawCoil("Entry Coil", troughApi.EntryCoil); + } + + DrawCoil("Eject Coil", troughApi.ExitCoil); + + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); } } @@ -112,6 +143,16 @@ private static void DrawSwitch(string label, DeviceSwitch sw) GUI.DrawTexture(switchPos, Icons.Switch(sw.IsClosed, IconSize.Small, sw.IsClosed ? IconColor.Orange : IconColor.Gray)); } + + private static void DrawCoil(string label, DeviceCoil coil) + { + var labelPos = EditorGUILayout.GetControlRect(); + labelPos.height = 18; + var switchPos = new Rect((float) (labelPos.x + (double) EditorGUIUtility.labelWidth - 20.0), labelPos.y, labelPos.height, labelPos.height); + GUI.Label(labelPos, label); + GUI.DrawTexture(switchPos, Icons.Bolt(IconSize.Small, coil.IsEnabled ? IconColor.Orange : IconColor.Gray)); + } + protected override void FinishEdit(string label, bool dirtyMesh = true) { base.FinishEdit(label, dirtyMesh); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/bolt.png b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/bolt.png new file mode 100644 index 000000000..2e56f70e1 Binary files /dev/null and b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/bolt.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/bolt.png.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/bolt.png.meta new file mode 100644 index 000000000..a8f238fc8 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/bolt.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 14f6d8f44d59c444a8091645d30885b7 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/bolt.png b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/bolt.png new file mode 100644 index 000000000..b573d229d Binary files /dev/null and b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/bolt.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/bolt.png.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/bolt.png.meta new file mode 100644 index 000000000..2fcc4c7bf --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/bolt.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 8a3beba79b64b7c47bc5cd5112de73e3 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/bolt.png b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/bolt.png new file mode 100644 index 000000000..c9925e44b Binary files /dev/null and b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/bolt.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/bolt.png.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/bolt.png.meta new file mode 100644 index 000000000..450d43974 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/bolt.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 9197062b633c35544b21db1ce53413f2 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/bolt.png b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/bolt.png new file mode 100644 index 000000000..f86946d49 Binary files /dev/null and b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/bolt.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/bolt.png.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/bolt.png.meta new file mode 100644 index 000000000..8e4df2655 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/bolt.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: bacc4102d19783f4cac00af99e746639 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs index fb7506069..a47654575 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs @@ -39,6 +39,7 @@ public IconVariant(string name, IconSize size, IconColor color) } private const string BumperName = "bumper"; + private const string BoltName = "bolt"; private const string CoilName = "coil"; private const string FlipperName = "flipper"; private const string GateName = "gate"; @@ -61,9 +62,9 @@ public IconVariant(string name, IconSize size, IconColor color) private const string SwitchNoName = "switch_no"; private static readonly string[] Names = { - BumperName, CoilName, FlipperName, GateName, KeyName, KickerName, LightName, PlayfieldName, PlungerName, - PlugName, PrimitiveName, RampName, RubberName, SpinnerName, SurfaceName, HitTargetName, TableName, - TriggerName, TroughName, SwitchNcName, SwitchNoName + BumperName, BoltName, CoilName, FlipperName, GateName, KeyName, KickerName, LightName, PlayfieldName, + PlungerName, PlugName, PrimitiveName, RampName, RubberName, SpinnerName, SurfaceName, HitTargetName, + TableName, TriggerName, TroughName, SwitchNcName, SwitchNoName }; private readonly Dictionary _icons = new Dictionary(); @@ -115,6 +116,7 @@ private Icons() public static Texture2D Switch(bool isClosed, IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(isClosed ? SwitchNcName : SwitchNoName, size, color); public static Texture2D Coil(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(CoilName, size, color); public static Texture2D Key(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(KeyName, size, color); + public static Texture2D Bolt(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(BoltName, size, color); public static Texture2D ByComponent(T mb, IconSize size = IconSize.Large, IconColor color = IconColor.Gray) where T : class diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs index 2a4d6143b..875bb1d27 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceCoil.cs @@ -20,6 +20,8 @@ namespace VisualPinball.Unity { public class DeviceCoil: IApiCoil { + public bool IsEnabled; + private readonly Action _onEnable; private readonly Action _onDisable; @@ -31,11 +33,15 @@ public DeviceCoil(Action onEnable = null, Action onDisable = null) public void OnCoil(bool enabled, bool isHoldCoil) { + IsEnabled = enabled; if (enabled) { _onEnable?.Invoke(); } else { _onDisable?.Invoke(); } +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif } public void OnChange(bool enabled) => OnCoil(enabled, false); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index c1f578733..fc51200e7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -35,30 +35,61 @@ public class DeviceSwitch : IApiSwitch /// public bool IsClosed => _switchHandler.IsClosed; + /// + /// Indicates whether the switch is currently enabled. + /// + /// + /// + /// We sometimes need to check the status of a switch and don't care whether it's an opto switch (which returns + /// the inverted value) or not. + /// + public bool IsEnabled => _invertValue ? !IsClosed : IsClosed; + + /// + /// If true, *setting* the switch will inverse the given value. + /// + /// + /// + /// This is important for opto switches since the work the other way around. + /// + private readonly bool _invertValue; + private readonly bool _isPulseSwitch; private readonly SwitchHandler _switchHandler; - public DeviceSwitch(string name, bool isPulseSwitch, Player player) + public DeviceSwitch(string name, bool isPulseSwitch, bool isOptoSwitch, Player player) { _isPulseSwitch = isPulseSwitch; - _switchHandler = new SwitchHandler(name, player); + _invertValue = isOptoSwitch; + _switchHandler = new SwitchHandler(name, player, isOptoSwitch); } public void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig.WithPulse(_isPulseSwitch)); public void AddWireDest(WireDestConfig wireConfig) => _switchHandler.AddWireDest(wireConfig); public void DestroyBall(Entity ballEntity) { } // device switches can't destroy balls - public void SetSwitch(bool closed) + /// + /// Enables or disables the switch. + /// + /// If true, closes mechanical switch or opens opto switch. If false, opens mechanical switch or closes opto switch. + public void SetSwitch(bool enabled) { + var closed = _invertValue ? !enabled : enabled; _switchHandler.OnSwitch(closed); Switch?.Invoke(this, new SwitchEventArgs(closed, Entity.Null)); } - public void ScheduleSwitch(bool closed, int delay) + /// + /// Schedules the switch to be enabled or disabled. + /// + /// If true, closes mechanical switch or opens opto switch. If false, opens mechanical switch or closes opto switch. + /// Delay in milliseconds + public void ScheduleSwitch(bool enabled, int delay) { if (delay == 0) { - SetSwitch(closed); + SetSwitch(enabled); } else { + var closed = _invertValue ? !enabled : enabled; _switchHandler.ScheduleSwitch(closed, delay, c => { Switch?.Invoke(this, new SwitchEventArgs(c, Entity.Null)); }); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index ad21d8c4c..f9c9a816a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -105,7 +105,7 @@ public void OnInit(TableApi tableApi, BallManager ballManager) _switchStatus[SwCreateBall] = false; // eject ball onto playfield - //OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); } public void OnUpdate() @@ -172,10 +172,9 @@ public void Switch(string id, bool isClosed) OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilAutoPlunger, isClosed)); break; - case SwTroughDrain: + case SwTrough4: if (isClosed) { - //OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); - //OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEntry, true)); + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); } break; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs index cae551391..5b3b1fba7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs @@ -35,10 +35,11 @@ public class SwitchHandler private static VisualPinballSimulationSystemGroup SimulationSystemGroup => World.DefaultGameObjectInjectionWorld.GetOrCreateSystem(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - public SwitchHandler(string name, Player player) + public SwitchHandler(string name, Player player, bool isClosed = false) { _name = name; _player = player; + IsClosed = isClosed; } /// diff --git a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs index 1abdb9777..993b8b51b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs @@ -292,7 +292,7 @@ private void CreateTrough() var troughData = new TroughData("Trough") { BallCount = 4, SwitchCount = 4, - Type = TroughType.TwoCoilsNSwitches + Type = TroughType.ModernMech }; if (_table.Has("BallRelease")) { troughData.PlayfieldExitKicker = "BallRelease"; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs index 4ec347cfe..df2f2e28b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs @@ -80,7 +80,7 @@ void IApi.OnDestroy() #region IApiSwitchable - private protected DeviceSwitch CreateSwitch(string name, bool isPulseSwitch) => new DeviceSwitch(name, isPulseSwitch, _player); + private protected DeviceSwitch CreateSwitch(string name, bool isPulseSwitch, bool isOptoSwitch) => new DeviceSwitch(name, isPulseSwitch, isOptoSwitch, _player); private protected void AddSwitchId(SwitchConfig switchConfig) => _switchHandler.AddSwitchId(switchConfig); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs index 8b01dafea..4acf3fff1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -54,7 +54,7 @@ public class TroughApi : ItemApi, IApi, IApiInitializable, I /// /// /// - /// Is null for , all of modern's switches are in . + /// Is null for , all of modern's switches are in . /// public DeviceSwitch EntrySwitch { get; private set; } @@ -67,14 +67,34 @@ public class TroughApi : ItemApi, IApi, IApiInitializable, I public DeviceSwitch StackSwitch() => _stackSwitches[0]; /// - /// Returns the stack switch at a given position for and - /// troughs. + /// Returns the stack switch at a given position for , + /// and troughs. /// /// /// Position, where 0 is the switch of the ball being ejected next. /// Switch in the ball stack + public DeviceSwitch StackSwitch(int pos) => _stackSwitches[pos]; + /// + /// The jam switch sits right above ball 1 and shortly enables and disables after eject. + /// + public DeviceSwitch JamSwitch; + + /// + /// Entry coil shoots the ball from the drain into the trough. + /// + /// + /// + /// Is null for and + /// + public DeviceCoil EntryCoil; + + /// + /// Triggers the kicker that ejects the ball. + /// + public DeviceCoil ExitCoil; + /// /// The stack of a trough can hold an unlimited number of balls. This counts the number of balls in the stack /// *additionally* to those counted by the stack's switches. @@ -118,20 +138,6 @@ public class TroughApi : ItemApi, IApi, IApiInitializable, I /// private DeviceSwitch[] _stackSwitches; - /// - /// Entry coil shoots the ball from the drain into the trough. - /// - /// - /// - /// Is null for - /// - private DeviceCoil _entryCoil; - - /// - /// Triggers the kicker that ejects the ball. - /// - private DeviceCoil _exitCoil; - /// /// Number of virtual balls on switches in the ball stack. /// @@ -176,14 +182,14 @@ void IApiInitializable.OnInit(BallManager ballManager) } // setup switches - if (Data.Type != TroughType.Modern) { - EntrySwitch = CreateSwitch(Trough.EntrySwitchId, false); + if (Data.Type != TroughType.ModernOpto && Data.Type != TroughType.ModernMech) { + EntrySwitch = CreateSwitch(Trough.EntrySwitchId, false, false); _switchLookup[Trough.EntrySwitchId] = EntrySwitch; } if (Data.Type == TroughType.TwoCoilsOneSwitch) { _stackSwitches = new[] { - CreateSwitch(Trough.TroughSwitchId, false) + CreateSwitch(Trough.TroughSwitchId, false, false) }; _switchLookup[Trough.TroughSwitchId] = StackSwitch(); @@ -191,7 +197,7 @@ void IApiInitializable.OnInit(BallManager ballManager) _stackSwitches = new DeviceSwitch[Data.SwitchCount]; foreach (var sw in Item.AvailableSwitches) { if (int.TryParse(sw.Id, out var id)) { - _stackSwitches[id - 1] = CreateSwitch(sw.Id, false); + _stackSwitches[id - 1] = CreateSwitch(sw.Id, false, Data.Type == TroughType.ModernOpto); _switchLookup[sw.Id] = _stackSwitches[id - 1]; } else { @@ -200,17 +206,23 @@ void IApiInitializable.OnInit(BallManager ballManager) } // pull next ball on modern - if (Data.Type == TroughType.Modern) { + if (Data.Type == TroughType.ModernOpto || Data.Type == TroughType.ModernMech) { _stackSwitches[Data.SwitchCount - 1].Switch += OnLastStackSwitch; } } + if (Data.JamSwitch) { + JamSwitch = CreateSwitch(Trough.JamSwitchId, false, Data.Type == TroughType.ModernOpto); + _switchLookup[Trough.JamSwitchId] = JamSwitch; + } + // setup coils - _entryCoil = new DeviceCoil(OnEntryCoilEnabled); - _exitCoil = new DeviceCoil(() => EjectBall()); + EntryCoil = new DeviceCoil(OnEntryCoilEnabled); + ExitCoil = new DeviceCoil(() => EjectBall()); // fill up the ball stack - for (var i = 0; i < Data.BallCount; i++) { + var ballCount = Data.Type == TroughType.ClassicSingleBall ? 1 : Data.BallCount; + for (var i = 0; i < ballCount; i++) { AddBall(); } @@ -224,7 +236,8 @@ void IApiInitializable.OnInit(BallManager ballManager) private void AddBall() { switch (Data.Type) { - case TroughType.Modern: + case TroughType.ModernOpto: + case TroughType.ModernMech: case TroughType.TwoCoilsNSwitches: if (_countedStackBalls < Data.BallCount) { _stackSwitches[_countedStackBalls].SetSwitch(true); @@ -248,8 +261,12 @@ private void AddBall() break; case TroughType.ClassicSingleBall: - if (!EntrySwitch.IsClosed) { + if (!EntrySwitch.IsEnabled) { EntrySwitch.SetSwitch(true); + _countedStackBalls++; // entry and stack is the same here + + } else { + UncountedStackBalls++; } break; @@ -263,9 +280,11 @@ private void AddBall() /// private void OnEntry(object sender, SwitchEventArgs args) { - Logger.Info("Draining ball into trough."); - _drainSwitch.DestroyBall(args.BallEntity); - DrainBall(); + if (args.IsClosed) { + Logger.Info("Draining ball into trough."); + _drainSwitch.DestroyBall(args.BallEntity); + DrainBall(); + } } /// @@ -275,7 +294,8 @@ private void OnEntry(object sender, SwitchEventArgs args) private void DrainBall() { switch (Data.Type) { - case TroughType.Modern: + case TroughType.ModernOpto: + case TroughType.ModernMech: // ball rolls directly into the trough RollOverEntryBall(0); break; @@ -284,7 +304,7 @@ private void DrainBall() case TroughType.TwoCoilsOneSwitch: case TroughType.ClassicSingleBall: - if (EntrySwitch.IsClosed) { // if the drain slot is already occupied, queue it. + if (EntrySwitch.IsEnabled) { // if the drain slot is already occupied, queue it. UncountedDrainBalls++; } else { // otherwise just close the entry switch @@ -310,14 +330,15 @@ private void DrainBall() private void OnEntryCoilEnabled() { switch (Data.Type) { - case TroughType.Modern: + case TroughType.ModernOpto: + case TroughType.ModernMech: // modern troughs don't have an entry coil break; case TroughType.TwoCoilsNSwitches: case TroughType.TwoCoilsOneSwitch: // push the ball from the drain to the trough - if (EntrySwitch.IsClosed) { + if (EntrySwitch.IsEnabled) { EntrySwitch.SetSwitch(false); RollOverEntryBall(0); DrainNextUncountedBall(); @@ -326,7 +347,7 @@ private void OnEntryCoilEnabled() case TroughType.ClassicSingleBall: // balls get ejected immediately - if (EntrySwitch.IsClosed) { + if (EntrySwitch.IsEnabled) { EntrySwitch.SetSwitch(false); EjectBall(); DrainNextUncountedBall(); @@ -360,44 +381,43 @@ private void RollOverEntryBall(int t) // pos 0 is the eject position, ball enters at the opposite end var pos = Data.SwitchCount - 1; - var openSwitches = Data.SwitchCount - _countedStackBalls; switch (Data.Type) { - case TroughType.Modern: + case TroughType.ModernOpto: + case TroughType.ModernMech: case TroughType.TwoCoilsNSwitches: // if entry position is occupied by another ball that just went in, queue. - if (_stackSwitches[pos].IsClosed) { + if (_stackSwitches[pos].IsEnabled) { UncountedStackBalls++; return; } + _countedStackBalls++; + // these are switches where the balls rolls over, so close and re-open them. - for (var i = 0; i < openSwitches - 1; i++) { + for (var i = 0; i < Data.SwitchCount - _countedStackBalls; i++) { _stackSwitches[pos].ScheduleSwitch(true, t); - t += Data.RollTime / 2; + t += Item.RollTimeDisabled; _stackSwitches[pos].ScheduleSwitch(false, t); - t += Data.RollTime / 2; + t += Item.RollTimeEnabled; pos--; } // switch nearest to the eject comes last, but doesn't re-open. _stackSwitches[pos].ScheduleSwitch(true, t); - _countedStackBalls++; + break; case TroughType.TwoCoilsOneSwitch: - if (_countedStackBalls < Data.SwitchCount - 1) { + _countedStackBalls++; + if (_countedStackBalls < Data.SwitchCount) { StackSwitch().ScheduleSwitch(true, t); - t += Data.RollTime / 2; + t += Item.RollTimeDisabled; StackSwitch().ScheduleSwitch(false, t); - } else if (_countedStackBalls == Data.SwitchCount - 1) { + } else if (_countedStackBalls == Data.SwitchCount) { StackSwitch().SetSwitch(true); } - _countedStackBalls++; - break; - case TroughType.ClassicSingleBall: - // nothing going on here on stack side break; default: @@ -421,25 +441,54 @@ public bool EjectBall() return false; } if (_countedStackBalls > 0) { - Logger.Info("Spawning new ball."); + + // open the switch of the ejected ball immediately + switch (Data.Type) { + case TroughType.ModernOpto: + case TroughType.ModernMech: + case TroughType.TwoCoilsNSwitches: + if (!_stackSwitches[0].IsEnabled) { + Logger.Warn("Ball not in eject position yet, ignoring."); + return false; + } + break; + + case TroughType.TwoCoilsOneSwitch: + // no switches at position 0 here. + break; + + case TroughType.ClassicSingleBall: + if (!EntrySwitch.IsEnabled) { + Logger.Warn("No ball, ignoring."); + return false; + } + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + Logger.Info("Trough: Spawning new ball."); _ejectKicker.CreateBall(); _ejectKicker.Kick(); // open the switch of the ejected ball immediately switch (Data.Type) { - case TroughType.Modern: + case TroughType.ModernOpto: + case TroughType.ModernMech: case TroughType.TwoCoilsNSwitches: _stackSwitches[0].SetSwitch(false); break; - case TroughType.TwoCoilsOneSwitch: + case TroughType.ClassicSingleBall: - // no switches at position 0 here. + EntrySwitch.SetSwitch(false); break; - default: - throw new ArgumentOutOfRangeException(); + // no switches at position 0 for other types. } + + TriggerJamSwitch(); RollOverStackBalls(); RollOverNextUncountedStackBall(); RefreshUI(); @@ -449,6 +498,14 @@ public bool EjectBall() return false; } + private void TriggerJamSwitch() + { + if (Data.JamSwitch) { + JamSwitch.ScheduleSwitch(true, Data.KickTime / 2); + JamSwitch.ScheduleSwitch(false, Data.KickTime); + } + } + /// /// Simulates all balls in the ball stack moving at once to the next position, /// due to an ejected ball. @@ -457,35 +514,39 @@ private void RollOverStackBalls() { var pos = _countedStackBalls - 1; switch (Data.Type) { - case TroughType.Modern: + + case TroughType.ModernOpto: + case TroughType.ModernMech: case TroughType.TwoCoilsNSwitches: // don't re-close the switch nearest to the entry - _stackSwitches[pos].ScheduleSwitch(false, Data.RollTime / 2); + _stackSwitches[pos].ScheduleSwitch(false, Item.RollTimeDisabled); // move remaining but last ball (which has been ejected) one position further, // all at the same time for (var i = 0; i < _countedStackBalls - 2; i++) { pos--; - _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime / 2); - _stackSwitches[pos].ScheduleSwitch(false, Data.RollTime); + if (Item.RollTimeEnabled != 0) { + _stackSwitches[pos].ScheduleSwitch(false, Item.RollTimeDisabled); + _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime); + } } // just close the switch for the last ball, since it has already been opened. if (pos-- > 0) { - _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime / 2); + _stackSwitches[pos].ScheduleSwitch(true, Data.RollTime); } break; case TroughType.TwoCoilsOneSwitch: // there is only one switch in the trough, so if it's closed, open it. - if (StackSwitch().IsClosed) { - StackSwitch().ScheduleSwitch(false, Data.RollTime / 2); + if (StackSwitch().IsEnabled) { + StackSwitch().ScheduleSwitch(false, Item.RollTimeDisabled); } break; case TroughType.ClassicSingleBall: - // no switches in this trough at all. + // no switches in this trough. break; default: @@ -496,7 +557,8 @@ private void RollOverStackBalls() private void OnLastStackSwitch(object sender, SwitchEventArgs switchEventArgs) { - if (!switchEventArgs.IsClosed && UncountedStackBalls > 0) { + var enabled = Data.Type == TroughType.ModernOpto ? !switchEventArgs.IsClosed : switchEventArgs.IsClosed; + if (!enabled && UncountedStackBalls > 0) { RefreshUI(); UncountedStackBalls--; RollOverEntryBall(Data.RollTime / 2); @@ -511,7 +573,8 @@ private void RollOverNextUncountedStackBall() } _countedStackBalls++; switch (Data.Type) { - case TroughType.Modern: + case TroughType.ModernOpto: + case TroughType.ModernMech: case TroughType.TwoCoilsNSwitches: _stackSwitches[_countedStackBalls - 1].ScheduleSwitch(true, Data.RollTime); break; @@ -558,10 +621,10 @@ IApiCoil IApiCoilDevice.Coil(string coilId) { switch (coilId) { case Trough.EntryCoilId: - return _entryCoil; + return EntryCoil; case Trough.EjectCoilId: - return _exitCoil; + return ExitCoil; default: return null; @@ -577,7 +640,7 @@ void IApi.OnDestroy() if (_drainSwitch != null) { _drainSwitch.Switch -= OnEntry; } - if (Data.Type == TroughType.Modern) { + if (Data.Type == TroughType.ModernOpto || Data.Type == TroughType.ModernMech) { _stackSwitches[Data.SwitchCount - 1].Switch -= OnLastStackSwitch; } }