diff --git a/README.md b/README.md
index a63e8e57c..f33d08e7f 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,8 @@ other "current gen" engines, while keeping backwards-compatibility.
VPE also aims to significantly improve the editor experience by extending the
editor of the game engine.
+For a more detailed overview, header over to the [website](https://docs.visualpinball.org/creators-guide/introduction/overview.html)!
+
## How?
The "core" of VPE (i.e. the `VisualPinball.Engine` project) is a pure C# port
@@ -32,7 +34,7 @@ VPE is still work in progress. You can check the current features list [here](ht
and the open issues [here](https://github.com/freezy/VisualPinball.Engine/issues).
There are a few videos in the [VPF thread](https://www.vpforums.org/index.php?showtopic=43651),
-where you can discuss.
+where you can discuss. Screenshots are [here](https://github.com/freezy/VisualPinball.Engine/wiki/Unity-Screenshots! :)
## Credits
diff --git a/VisualPinball.Engine.Test/Fixtures~/MappingsTest.vpx b/VisualPinball.Engine.Test/Fixtures~/MappingsTest.vpx
index d9aa82095..d7e8aaa7d 100644
Binary files a/VisualPinball.Engine.Test/Fixtures~/MappingsTest.vpx and b/VisualPinball.Engine.Test/Fixtures~/MappingsTest.vpx differ
diff --git a/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx b/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx
new file mode 100644
index 000000000..24e3db48a
Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx differ
diff --git a/VisualPinball.Engine.Test/Test/Fixtures.cs b/VisualPinball.Engine.Test/Test/Fixtures.cs
index 8fad2c9b2..03e75338c 100644
--- a/VisualPinball.Engine.Test/Test/Fixtures.cs
+++ b/VisualPinball.Engine.Test/Test/Fixtures.cs
@@ -51,6 +51,7 @@ public static class VpxPath
public static readonly string Texture = PathHelper.GetFixturePath("TextureTest.vpx");
public static readonly string Timer = PathHelper.GetFixturePath("TimerTest.vpx");
public static readonly string Trigger = PathHelper.GetFixturePath("TriggerTest.vpx");
+ public static readonly string Trough = PathHelper.GetFixturePath("TroughTest.vpx");
}
public static class ObjPath
diff --git a/VisualPinball.Engine.Test/VPT/Mappings/MappingsDataTests.cs b/VisualPinball.Engine.Test/VPT/Mappings/MappingsDataTests.cs
index 06f6e0d56..3515b7090 100644
--- a/VisualPinball.Engine.Test/VPT/Mappings/MappingsDataTests.cs
+++ b/VisualPinball.Engine.Test/VPT/Mappings/MappingsDataTests.cs
@@ -44,7 +44,7 @@ public void ShouldWriteMappingsData()
private static void ValidateTableData(MappingsData data)
{
- data.Switches.Length.Should().Be(4);
+ data.Switches.Length.Should().Be(13);
data.Switches[0].Id.Should().Be("s_create_ball");
data.Switches[0].Description.Should().Be("Create Ball");
@@ -72,7 +72,20 @@ private static void ValidateTableData(MappingsData data)
data.Switches[3].Source.Should().Be(SwitchSource.Constant);
data.Switches[3].Constant.Should().Be(SwitchConstant.NormallyClosed);
- data.Coils.Length.Should().Be(2);
+ data.Switches[3].Id.Should().Be("s_right_flipper");
+ data.Switches[3].Description.Should().Be("Right Flipper");
+ data.Switches[3].Source.Should().Be(SwitchSource.Constant);
+ data.Switches[3].Constant.Should().Be(SwitchConstant.NormallyClosed);
+
+ data.Switches[7].Id.Should().Be("s_trough1");
+ data.Switches[7].Description.Should().Be("Trough 1 (eject)");
+ data.Switches[7].Source.Should().Be(SwitchSource.Device);
+ data.Switches[7].Device.Should().Be("Trough");
+ data.Switches[7].DeviceItem.Should().Be("1");
+ data.Switches[7].Type.Should().Be(SwitchType.OnOff);
+ data.Switches[7].PulseDelay.Should().Be(250);
+
+ data.Coils.Length.Should().Be(6);
data.Coils[0].Id.Should().Be("c_auto_plunger");
data.Coils[0].Description.Should().Be("Auto Plunger");
@@ -86,6 +99,14 @@ private static void ValidateTableData(MappingsData data)
data.Coils[1].PlayfieldItem.Should().Be("Flipper1");
data.Coils[1].Type.Should().Be(CoilType.DualWound);
data.Coils[1].HoldCoilId.Should().Be("c_left_flipper_hold");
+
+ data.Coils[5].Id.Should().Be("c_trough_eject");
+ data.Coils[5].Description.Should().Be("Trough Eject");
+ data.Coils[5].Destination.Should().Be(CoilDestination.Device);
+ data.Coils[5].Device.Should().Be("Trough");
+ data.Coils[5].DeviceItem.Should().Be("eject");
+ data.Coils[5].Type.Should().Be(CoilType.SingleWound);
+ data.Coils[5].HoldCoilId.Should().Be("");
}
}
}
diff --git a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs
new file mode 100644
index 000000000..c69fefdb0
--- /dev/null
+++ b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs
@@ -0,0 +1,53 @@
+// Visual Pinball Engine
+// Copyright (C) 2020 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using FluentAssertions;
+using NUnit.Framework;
+using VisualPinball.Engine.Test.Test;
+using VisualPinball.Engine.VPT.Trough;
+
+namespace VisualPinball.Engine.Test.VPT.Trough
+{
+ public class TroughDataTests
+ {
+ [Test]
+ public void ShouldReadTroughData()
+ {
+ var table = Engine.VPT.Table.Table.Load(VpxPath.Trough);
+ ValidateTroughData(table.Trough("Trough1").Data);
+ }
+
+ [Test]
+ public void ShouldWriteTroughData()
+ {
+ const string tmpFileName = "ShouldWriteTroughData.vpx";
+ var table = Engine.VPT.Table.Table.Load(VpxPath.Trough);
+ table.Save(tmpFileName);
+ var writtenTable = Engine.VPT.Table.Table.Load(tmpFileName);
+ ValidateTroughData(writtenTable.Trough("Trough1").Data);
+ }
+
+ private static void ValidateTroughData(TroughData data)
+ {
+ data.BallCount.Should().Be(3);
+ data.SwitchCount.Should().Be(4);
+ data.SettleTime.Should().Be(112);
+ data.EntryKicker.Should().Be("BallDrain");
+ data.ExitKicker.Should().Be("BallRelease");
+ data.JamSwitch.Should().Be("TroughJam");
+ }
+ }
+}
diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs
index 84141bf0a..ee1c4b768 100644
--- a/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs
+++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs
@@ -22,5 +22,7 @@ public struct GamelogicEngineCoil
public string Description;
public string PlayfieldItemHint;
public string MainCoilIdOfHoldCoil;
+ public string DeviceHint;
+ public string DeviceItemHint;
}
}
diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs
index 3a293e752..5153a197e 100644
--- a/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs
+++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs
@@ -23,5 +23,7 @@ public struct GamelogicEngineSwitch
public string InputActionHint;
public string InputMapHint;
public string PlayfieldItemHint;
+ public string DeviceHint;
+ public string DeviceItemHint;
}
}
diff --git a/VisualPinball.Engine/Game/ICoilable.cs b/VisualPinball.Engine/Game/ICoilable.cs
index 33962c2a2..b4fd19383 100644
--- a/VisualPinball.Engine/Game/ICoilable.cs
+++ b/VisualPinball.Engine/Game/ICoilable.cs
@@ -19,6 +19,5 @@ namespace VisualPinball.Engine.Game
public interface ICoilable
{
string Name { get; }
- bool IsDualWound { get; set; }
}
}
diff --git a/VisualPinball.Engine/Game/ICoilableDevice.cs b/VisualPinball.Engine/Game/ICoilableDevice.cs
new file mode 100644
index 000000000..1443bf351
--- /dev/null
+++ b/VisualPinball.Engine/Game/ICoilableDevice.cs
@@ -0,0 +1,28 @@
+// Visual Pinball Engine
+// Copyright (C) 2020 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System.Collections.Generic;
+using VisualPinball.Engine.Game.Engines;
+
+namespace VisualPinball.Engine.Game
+{
+ public interface ICoilableDevice
+ {
+ string Name { get; }
+
+ IEnumerable AvailableCoils { get; }
+ }
+}
diff --git a/VisualPinball.Engine/Game/ICoilableDevice.cs.meta b/VisualPinball.Engine/Game/ICoilableDevice.cs.meta
new file mode 100644
index 000000000..5dffe9fff
--- /dev/null
+++ b/VisualPinball.Engine/Game/ICoilableDevice.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 29418066007efd546bf0919c88d02a88
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Engine/Game/ISwitchableDevice.cs b/VisualPinball.Engine/Game/ISwitchableDevice.cs
new file mode 100644
index 000000000..d159aef6f
--- /dev/null
+++ b/VisualPinball.Engine/Game/ISwitchableDevice.cs
@@ -0,0 +1,28 @@
+// Visual Pinball Engine
+// Copyright (C) 2020 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System.Collections.Generic;
+using VisualPinball.Engine.Game.Engines;
+
+namespace VisualPinball.Engine.Game
+{
+ public interface ISwitchableDevice
+ {
+ string Name { get; }
+
+ IEnumerable AvailableSwitches { get; }
+ }
+}
diff --git a/VisualPinball.Engine/Game/ISwitchableDevice.cs.meta b/VisualPinball.Engine/Game/ISwitchableDevice.cs.meta
new file mode 100644
index 000000000..050f95f1e
--- /dev/null
+++ b/VisualPinball.Engine/Game/ISwitchableDevice.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f8a55aed6ac821c44ac1699352ef97ee
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Engine/VPT/Item.cs b/VisualPinball.Engine/VPT/Item.cs
index 34de7558e..d2f0ee8b5 100644
--- a/VisualPinball.Engine/VPT/Item.cs
+++ b/VisualPinball.Engine/VPT/Item.cs
@@ -15,7 +15,6 @@
// along with this program. If not, see .
using System;
-using VisualPinball.Engine.Math;
namespace VisualPinball.Engine.VPT
{
diff --git a/VisualPinball.Engine/VPT/ItemType.cs b/VisualPinball.Engine/VPT/ItemType.cs
index 6546da8aa..849d328b6 100644
--- a/VisualPinball.Engine/VPT/ItemType.cs
+++ b/VisualPinball.Engine/VPT/ItemType.cs
@@ -54,5 +54,6 @@ public enum ItemType
// VPE internal
Ball = 100,
+ Trough = 101,
}
}
diff --git a/VisualPinball.Engine/VPT/Mappings/Mappings.cs b/VisualPinball.Engine/VPT/Mappings/Mappings.cs
index e62c2950e..fece24b9d 100644
--- a/VisualPinball.Engine/VPT/Mappings/Mappings.cs
+++ b/VisualPinball.Engine/VPT/Mappings/Mappings.cs
@@ -18,6 +18,7 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using NLog;
using VisualPinball.Engine.Common;
using VisualPinball.Engine.Game;
using VisualPinball.Engine.Game.Engines;
@@ -29,6 +30,8 @@ public class Mappings : Item
public override string ItemName { get; } = "Mapping";
public override string ItemGroupName { get; } = "Mappings";
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
public Mappings() : this(new MappingsData("Mappings"))
{
}
@@ -49,12 +52,16 @@ public bool IsEmpty()
#region Switch Population
- public void PopulateSwitches(GamelogicEngineSwitch[] engineSwitches, IEnumerable tableSwitches)
+ public void PopulateSwitches(GamelogicEngineSwitch[] engineSwitches, IEnumerable tableSwitches, IEnumerable tableSwitchDevices)
{
var switches = tableSwitches
.GroupBy(x => x.Name.ToLower())
.ToDictionary(x => x.Key, x => x.First());
+ var switchDevices = tableSwitchDevices
+ .GroupBy(x => x.Name.ToLower())
+ .ToDictionary(x => x.Key, x => x.First());
+
foreach (var engineSwitch in GetSwitchIds(engineSwitches))
{
var switchMapping = Data.Switches.FirstOrDefault(mappingsSwitchData => mappingsSwitchData.Id == engineSwitch.Id);
@@ -64,6 +71,8 @@ public void PopulateSwitches(GamelogicEngineSwitch[] engineSwitches, IEnumerable
var description = engineSwitch.Description ?? string.Empty;
var source = GuessSwitchSource(engineSwitch);
var playfieldItem = source == SwitchSource.Playfield ? GuessPlayfieldSwitch(switches, engineSwitch) : null;
+ var device = source == SwitchSource.Device ? GuessDevice(switchDevices, engineSwitch) : null;
+ var deviceItem = source == SwitchSource.Device && device != null ? GuessDeviceSwitch(engineSwitch, device) : default;
var inputActionMap = source == SwitchSource.InputSystem
? string.IsNullOrEmpty(engineSwitch.InputMapHint) ? InputConstants.MapCabinetSwitches : engineSwitch.InputMapHint
: string.Empty;
@@ -77,7 +86,9 @@ public void PopulateSwitches(GamelogicEngineSwitch[] engineSwitches, IEnumerable
Source = source,
PlayfieldItem = playfieldItem != null ? playfieldItem.Name : string.Empty,
InputActionMap = inputActionMap,
- InputAction = inputAction
+ InputAction = inputAction,
+ Device = device != null ? device.Name : string.Empty,
+ DeviceItem = deviceItem.Id
});
}
}
@@ -105,7 +116,6 @@ public IEnumerable GetSwitchIds(GamelogicEngineSwitch[] e
Id = mappingsSwitchData.Id
});
}
-
}
ids.Sort((s1, s2) => s1.Id.CompareTo(s2.Id));
@@ -114,6 +124,10 @@ public IEnumerable GetSwitchIds(GamelogicEngineSwitch[] e
private static int GuessSwitchSource(GamelogicEngineSwitch engineSwitch)
{
+ if (!string.IsNullOrEmpty(engineSwitch.DeviceHint)) {
+ return SwitchSource.Device;
+ }
+
return !string.IsNullOrEmpty(engineSwitch.InputActionHint) ? SwitchSource.InputSystem : SwitchSource.Playfield;
}
@@ -137,6 +151,33 @@ private static ISwitchable GuessPlayfieldSwitch(Dictionary
return switches.ContainsKey(matchKey) ? switches[matchKey] : null;
}
+ private static ISwitchableDevice GuessDevice(Dictionary switchDevices, GamelogicEngineSwitch engineSwitch)
+ {
+ // match by regex if hint provided
+ if (!string.IsNullOrEmpty(engineSwitch.DeviceHint)) {
+ foreach (var deviceName in switchDevices.Keys) {
+ var regex = new Regex(engineSwitch.DeviceHint.ToLower());
+ if (regex.Match(deviceName).Success) {
+ return switchDevices[deviceName];
+ }
+ }
+ }
+ return null;
+ }
+
+ private static GamelogicEngineSwitch GuessDeviceSwitch(GamelogicEngineSwitch engineSwitch, ISwitchableDevice device)
+ {
+ if (!string.IsNullOrEmpty(engineSwitch.DeviceItemHint)) {
+ foreach (var deviceSwitch in device.AvailableSwitches) {
+ var regex = new Regex(engineSwitch.DeviceItemHint.ToLower());
+ if (regex.Match(deviceSwitch.Id).Success) {
+ return deviceSwitch;
+ }
+ }
+ }
+ return default;
+ }
+
#endregion
#region Coil Population
@@ -147,12 +188,16 @@ private static ISwitchable GuessPlayfieldSwitch(Dictionary
///
/// List of coils provided by the gamelogic engine
/// List of coils on the playfield
- public void PopulateCoils(GamelogicEngineCoil[] engineCoils, IEnumerable tableCoils)
+ public void PopulateCoils(GamelogicEngineCoil[] engineCoils, IEnumerable tableCoils, IEnumerable tableCoilDevices)
{
var coils = tableCoils
.GroupBy(x => x.Name.ToLower())
.ToDictionary(x => x.Key, x => x.First());
+ var coilDevices = tableCoilDevices
+ .GroupBy(x => x.Name.ToLower())
+ .ToDictionary(x => x.Key, x => x.First());
+
var holdCoils = new List();
foreach (var engineCoil in GetCoils(engineCoils)) {
@@ -165,14 +210,19 @@ public void PopulateCoils(GamelogicEngineCoil[] engineCoils, IEnumerable coilDevices, GamelogicEngineCoil engineCoil)
+ {
+ // match by regex if hint provided
+ if (!string.IsNullOrEmpty(engineCoil.DeviceHint)) {
+ foreach (var deviceName in coilDevices.Keys) {
+ var regex = new Regex(engineCoil.DeviceHint.ToLower());
+ if (regex.Match(deviceName).Success) {
+ return coilDevices[deviceName];
+ }
+ }
+ }
+ return null;
+ }
+
+ private static GamelogicEngineCoil GuessDeviceCoil(GamelogicEngineCoil engineCoil, ICoilableDevice device)
+ {
+ if (!string.IsNullOrEmpty(engineCoil.DeviceItemHint)) {
+ foreach (var deviceCoil in device.AvailableCoils) {
+ var regex = new Regex(engineCoil.DeviceItemHint.ToLower());
+ if (regex.Match(deviceCoil.Id).Success) {
+ return deviceCoil;
+ }
+ }
+ }
+ return default;
+ }
+
///
/// Returns a sorted list of coil names from the gamelogic engine,
/// appended with the additional names in the coil mapping. In short,
diff --git a/VisualPinball.Engine/VPT/Rubber/Rubber.cs b/VisualPinball.Engine/VPT/Rubber/Rubber.cs
index 6ae41f93f..85aa94ca9 100644
--- a/VisualPinball.Engine/VPT/Rubber/Rubber.cs
+++ b/VisualPinball.Engine/VPT/Rubber/Rubber.cs
@@ -66,7 +66,7 @@ public static Rubber GetDefault(Table.Table table)
#region IRenderable
- Matrix3D IRenderable.TransformationMatrix(Table.Table table, Origin origin) => Matrix3D.Identity;
+ Matrix3D IRenderable.TransformationMatrix(Table.Table table, Origin origin) => _meshGenerator.GetPostMatrix(table, origin);
public RenderObject GetRenderObject(Table.Table table, string id = null, Origin origin = Origin.Global, bool asRightHanded = true)
{
diff --git a/VisualPinball.Engine/VPT/Table/Table.cs b/VisualPinball.Engine/VPT/Table/Table.cs
index 81f8735e8..bbef877ed 100644
--- a/VisualPinball.Engine/VPT/Table/Table.cs
+++ b/VisualPinball.Engine/VPT/Table/Table.cs
@@ -111,6 +111,7 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable)
private readonly Dictionary _textBoxes = new Dictionary();
private readonly Dictionary _timers = new Dictionary();
private readonly Dictionary _triggers = new Dictionary();
+ private readonly Dictionary _troughs = new Dictionary();
public Bumper.Bumper Bumper(string name) => _bumpers[name];
public Decal.Decal Decal(int i) => _decals[i];
@@ -131,6 +132,7 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable)
public TextBox.TextBox TextBox(string name) => _textBoxes[name];
public Timer.Timer Timer(string name) => _timers[name];
public Trigger.Trigger Trigger(string name) => _triggers[name];
+ public Trough.Trough Trough(string name) => _troughs[name];
public IEnumerable Renderables => new IRenderable[] { this }
.Concat(_bumpers.Values)
@@ -166,9 +168,10 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable)
.Concat(_surfaces.Values)
.Concat(_textBoxes.Values)
.Concat(_timers.Values)
- .Concat(_triggers.Values);
+ .Concat(_triggers.Values)
+ .Concat(_troughs.Values);
- public IEnumerable ItemDatas => new ItemData[] {}
+ public IEnumerable ItemDatas => new ItemData[] { }
.Concat(_bumpers.Values.Select(i => i.Data))
.Concat(_decals.Select(i => i.Data))
.Concat(_dispReels.Values.Select(i => i.Data))
@@ -187,7 +190,8 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable)
.Concat(_surfaces.Values.Select(i => i.Data))
.Concat(_textBoxes.Values.Select(i => i.Data))
.Concat(_timers.Values.Select(i => i.Data))
- .Concat(_triggers.Values.Select(i => i.Data));
+ .Concat(_triggers.Values.Select(i => i.Data))
+ .Concat(_troughs.Values.Select(i => i.Data));
public IEnumerable Hittables => new IHittable[] {this}
.Concat(_bumpers.Values)
@@ -204,7 +208,6 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable)
.Concat(_triggers.Values)
.SelectMany(ApplyColliderOverrides);
-
public IEnumerable Playables => new IPlayable[0]
.Concat(_bumpers.Values)
.Concat(_flippers.Values)
@@ -228,12 +231,18 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable)
.Concat(_spinners.Values)
.Concat(_triggers.Values);
+ public IEnumerable SwitchableDevices => new ISwitchableDevice[0]
+ .Concat(_troughs.Values);
+
public IEnumerable Coilables => new ICoilable[0]
.Concat(_bumpers.Values)
.Concat(_flippers.Values)
.Concat(_kickers.Values)
.Concat(_plungers.Values);
+ public IEnumerable CoilableDevices => new ICoilableDevice[0]
+ .Concat(_troughs.Values);
+
private void AddItem(string name, TItem item, IDictionary d, bool updateStorageIndices) where TItem : IItem
{
if (updateStorageIndices) {
@@ -324,6 +333,10 @@ private Dictionary GetItemDictionary() where T : IItem
return _triggers as Dictionary;
}
+ if (typeof(T) == typeof(VPT.Trough.Trough)) {
+ return _troughs as Dictionary;
+ }
+
return null;
}
@@ -394,7 +407,6 @@ public void ReplaceAll(IEnumerable items) where T : IItem
/// True if the game item exists, false otherwise
public bool Has(string name) where T : IItem => GetItemDictionary().ContainsKey(name);
-
///
/// Returns all game items of a given type.
///
@@ -521,6 +533,7 @@ public RenderObjectGroup GetRenderObjects(Table table, Origin origin = Origin.Gl
public HitObject[] GetHitShapes() => _hitGenerator.GenerateHitObjects(this).ToArray();
public bool IsCollidable => true;
+ public bool HasTrough => _troughs.Count > 0;
public HitPlane GeneratePlayfieldHit() => _hitGenerator.GeneratePlayfieldHit(this);
public HitPlane GenerateGlassHit() => _hitGenerator.GenerateGlassHit(this);
diff --git a/VisualPinball.Engine/VPT/Table/TableLoader.cs b/VisualPinball.Engine/VPT/Table/TableLoader.cs
index 6b2370980..3f65d59df 100644
--- a/VisualPinball.Engine/VPT/Table/TableLoader.cs
+++ b/VisualPinball.Engine/VPT/Table/TableLoader.cs
@@ -112,6 +112,7 @@ public static void LoadGameItem(byte[] itemData, int storageIndex, out ItemType
case ItemType.TextBox: item = new TextBox.TextBox(reader, itemName); break;
case ItemType.Timer: item = new Timer.Timer(reader, itemName); break;
case ItemType.Trigger: item = new Trigger.Trigger(reader, itemName); break;
+ case ItemType.Trough: item = new Trough.Trough(reader, itemName); break;
default:
Logger.Info("Unhandled item type " + itemType);
itemType = ItemType.Invalid; break;
@@ -238,6 +239,11 @@ private static void LoadGameItems(Table table, CFStorage storage)
table.Add(item);
break;
}
+ case ItemType.Trough: {
+ var item = new Trough.Trough(reader, itemName);
+ table.Add(item);
+ break;
+ }
}
}
}
diff --git a/VisualPinball.Engine/VPT/Trough.meta b/VisualPinball.Engine/VPT/Trough.meta
new file mode 100644
index 000000000..d0ffa86e7
--- /dev/null
+++ b/VisualPinball.Engine/VPT/Trough.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3fd1432f7bc694b779c7e8e0d9018022
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Engine/VPT/Trough/Trough.cs b/VisualPinball.Engine/VPT/Trough/Trough.cs
new file mode 100644
index 000000000..035dea9d6
--- /dev/null
+++ b/VisualPinball.Engine/VPT/Trough/Trough.cs
@@ -0,0 +1,68 @@
+// Visual Pinball Engine
+// Copyright (C) 2020 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using VisualPinball.Engine.Game;
+using VisualPinball.Engine.Game.Engines;
+
+namespace VisualPinball.Engine.VPT.Trough
+{
+ public class Trough : Item, ISwitchableDevice, ICoilableDevice
+ {
+ public override string ItemName { get; } = "Trough";
+ public override string ItemGroupName { get; } = null;
+
+ public const string JamSwitchId = "jam";
+ public const string EjectCoilId = "eject";
+ public const string EntryCoilId = "entry";
+
+ public IEnumerable AvailableSwitches => Enumerable.Repeat(0, Data.SwitchCount)
+ .Select((_, i) => new GamelogicEngineSwitch {Description = SwitchDescription(i), Id = $"{i + 1}"})
+ .Concat( new[]{ new GamelogicEngineSwitch{Description = "Jam Switch", Id = JamSwitchId} });
+
+ public IEnumerable AvailableCoils => new[] {
+ new GamelogicEngineCoil {Description = "Entry", Id = EntryCoilId},
+ new GamelogicEngineCoil {Description = "Eject", Id = EjectCoilId}
+ };
+
+ public Trough(TroughData data) : base(data)
+ {
+ }
+
+ public Trough(BinaryReader reader, string itemName) : this(new TroughData(reader, itemName))
+ {
+ }
+
+ private string SwitchDescription(int i)
+ {
+ if (i == 0) {
+ return "Ball 1 (eject)";
+ }
+
+ return i == Data.SwitchCount - 1
+ ? $"Ball {i + 1} (entry)"
+ : $"Ball {i + 1}";
+ }
+
+ public static Trough GetDefault(Table.Table table)
+ {
+ var primitiveData = new TroughData(table.GetNewName("Trough"));
+ return new Trough(primitiveData);
+ }
+ }
+}
diff --git a/VisualPinball.Engine/VPT/Trough/Trough.cs.meta b/VisualPinball.Engine/VPT/Trough/Trough.cs.meta
new file mode 100644
index 000000000..c25340d05
--- /dev/null
+++ b/VisualPinball.Engine/VPT/Trough/Trough.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 359e5a55a56254a68931446a9b6d6e9a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs b/VisualPinball.Engine/VPT/Trough/TroughData.cs
new file mode 100644
index 000000000..74e79b289
--- /dev/null
+++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs
@@ -0,0 +1,87 @@
+// Visual Pinball Engine
+// Copyright (C) 2020 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#region ReSharper
+// ReSharper disable UnassignedField.Global
+// ReSharper disable StringLiteralTypo
+// ReSharper disable FieldCanBeMadeReadOnly.Global
+// ReSharper disable ConvertToConstant.Global
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using VisualPinball.Engine.IO;
+using VisualPinball.Engine.VPT.Table;
+
+namespace VisualPinball.Engine.VPT.Trough
+{
+ [Serializable]
+ public class TroughData : ItemData
+ {
+ public override string GetName() => Name;
+ public override void SetName(string name) { Name = name; }
+
+ [BiffString("NAME", IsWideString = true, Pos = 1)]
+ public string Name;
+
+ [BiffString("ENTK", Pos = 2)]
+ public string EntryKicker = string.Empty;
+
+ [BiffString("EXIT", Pos = 3)]
+ public string ExitKicker = string.Empty;
+
+ [BiffString("JAMS", Pos = 4)]
+ public string JamSwitch = string.Empty;
+
+ [BiffInt("BCNT", Pos = 5)]
+ public int BallCount = 6;
+
+ [BiffInt("SCNT", Pos = 6)]
+ public int SwitchCount = 6;
+
+ [BiffInt("TIME", Pos = 7)]
+ public int SettleTime = 100;
+
+ public TroughData(string name) : base(StoragePrefix.GameItem)
+ {
+ Name = name;
+ }
+
+ #region BIFF
+
+ static TroughData()
+ {
+ Init(typeof(TroughData), Attributes);
+ }
+
+ public TroughData(BinaryReader reader, string storageName) : base(storageName)
+ {
+ Load(this, reader, Attributes);
+ }
+
+ public override void Write(BinaryWriter writer, HashWriter hashWriter)
+ {
+ writer.Write((int)ItemType.Trough);
+ WriteRecord(writer, Attributes, hashWriter);
+ WriteEnd(writer, hashWriter);
+ }
+
+ private static readonly Dictionary> Attributes = new Dictionary>();
+
+ #endregion
+ }
+}
diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs.meta b/VisualPinball.Engine/VPT/Trough/TroughData.cs.meta
new file mode 100644
index 000000000..6de4b2d57
--- /dev/null
+++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 23d7577ef803444a88c18204c09da04b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md
index 01d407a33..1309e9816 100644
--- a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md
+++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md
@@ -1,55 +1,58 @@
# Coil Manager
-On a real pinball table, most moving parts, including the flippers, are triggered by [coils](https://en.wikipedia.org/wiki/Inductor) (also called [solenoids](https://en.wikipedia.org/wiki/Solenoid)). It's the job of the [gamelogic engine](~/creators-guide/manual/gamelogic-engine.md) to trigger them when needed.
+On a real pinball table most moving parts, including the flippers, are triggered by [coils](https://en.wikipedia.org/wiki/Inductor) (also called [solenoids](https://en.wikipedia.org/wiki/Solenoid)). In VPE it's the job of the [gamelogic engine](~/creators-guide/manual/gamelogic-engine.md) to trigger them when needed.
-On a typical table there are usually several coils that need to be wired up to the controller board. In VPE, you can do that with the coil manager under *Visual Pinball -> Coil Manager*.
+Just as the coils are physically wired to the power driver board on a regular machine they can be virtually connected in VPE using the coil manager under *Visual Pinball -> Coil Manager*.

## Setup
-Every row in the coil manager corresponds to a wire going from the gamelogic engine output to the coil. Similar to switches, a coil can be linked to multiple outputs, and an output can be linked to multiple coils.
+Every row in the coil manager corresponds to a logical wire going from a gamelogic engine output to the coil. As with switches, a single coil can be linked to multiple outputs, and an output can be linked to multiple coils.
### IDs
-The first column **ID** shows the coil names that the gamelogic engine expects to be wired up.
+The first column, **ID** shows the name that the gamelogic engine exports for each coil.
> [!note]
-> As we cannot be 100% sure that the gamelogic engine has accurate data about the coil names, you can also add coil IDs yourself, but that should be the exception.
+> As we cannot be 100% sure that the gamelogic engine has accurate data about the coil names, you can also add coil IDs manually, but that should be the exception.
### Description
-The **Description** column is an optional free text field. If you're setting up a re-creation, that's where you typically put what is in the game manual. It's purely for your own benefit, and you can keep this empty if you want.
+The **Description** column is optional. If you're setting up a re-creation, you would typically use this for the coil name from the game manual. It's purely for your own benefit, and you can keep this empty if you want.
### Destination
-The **Destination** column defines where the element in the next column is located. Currently, VPE only supports playfield items with one coil. In the future, VPE will support devices with multiple coils, which will also be listed here.
+The **Destination** column defines where the element in the following column is located. There are two options:
+
+- *Playfield* lets you select a game element on the playfield that features the coil
+- *Device* lets you choose a *coil device*, a mechanism which may include multiple coils, such as a [trough](../manual/mechanisms/troughs.md).
### Element
-The **Element** column is where you choose the playfield element with the coil. VPE can receive coil events for bumpers, flippers, kickers and plungers.
+The **Element** column is where you choose which specifc element in the destination column should be activated. VPE can receive coil events for bumpers, flippers, kickers and plungers and coil devices.
> [!note]
-> Bumpers are currently hard-wired, i.e. their switch will directly trigger the coil without going through the gamelogic engine. That means they don't need to be configured in the switch- or coil manager. VPE will make this configurable in the future.
+> Bumpers are currently hard-wired, i.e. their switch will directly trigger the coil without going through the gamelogic engine. That means they don't need to be configured in the switch or coil manager. VPE will make this configurable in the future.
### Type
In the **Type** column you can define whether the coil is single-wound or dual-wound. There's an excellent page about the differences in [MPF's documentation](https://docs.missionpinball.org/en/latest/mechs/coils/dual_vs_single_wound.html). In short, dual-wound coils have two circuits, one for powering the coil, and one for holding it, while single-wound coils only have one.
-This changes in how the coils power off:
+This changes how the coil powers off:
-- For **single-wound** coils, VPE uses the same coil event for powering on and off.
+- For **single-wound** coils, VPE uses the same coil's events for powering on and off.
- For **dual-wound** coils, it uses the *on* event from the main coil and the *off* event from the hold coil.
### Hold Coil
-If the coil type is set to *Dual-Wound*, this column defines the hold coil event, i.e. on which event the coil powers off.
+When the coil type is set to *Dual-Wound*, this column defines the hold coil event, i.e. the event on which the coil powers off.
-These coils are pretty common. For example, *Medieval Madness* has the following dual-wound coils:
+Dual-wound coils are fairly common. For example, *Medieval Madness* has the following dual-wound coils:

*From the Medieval Madness manual*
-In VPE, this would map to the following configuration:
+In VPE, the two flippers would map to the following configuration:

diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.png b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.png
index 9e3c15f6a..7d45211ab 100644
Binary files a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.png and b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.md
index 98cba60ac..65c0f932f 100644
--- a/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.md
+++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.md
@@ -1,8 +1,8 @@
# Switch Manager
-During gameplay, the [gamelogic engine](~/creators-guide/manual/gamelogic-engine.md) needs to know what is happening on the playfield. For that reason, real pinball tables have switches all over the playfield that signal when a ball rolls over a certain position. These switches are also built into targets, bumpers, kickers, and some other items (see *[Supported Game Mechanisms](#supported-game-mechanisms)* below).
+During gameplay, the [gamelogic engine](~/creators-guide/manual/gamelogic-engine.md) needs to know what is happening on the playfield. For that, real pinball tables have switches on the playfield that signal when a ball rolls over or settles in a certain position. These switches are also built into targets, bumpers, kickers, and other mechanisms (see *[Supported Game Mechanisms](#supported-game-mechanisms)* below).
-Wiring these switches up to the gamelogic engine per code can be a tedious process. That's why VPE provides a graphical interface where you can do it easily. It even can guess which switch maps to which game item, if you've named them accordingly.
+Wiring these switches up to the gamelogic engine with code can be a tedious process, so VPE provides a graphical interface where you can do it easily. If you've named them appropriately it can even guess which switch maps to which game item.
You can open the switch manager under *Visual Pinball -> Switch Manager*.
@@ -10,68 +10,64 @@ You can open the switch manager under *Visual Pinball -> Switch Manager*.
## Setup
-Imagine every row as a wire connecting the physical switch to the gamelogic engine. The relation between the two is *0..n -> 0..n*, meaning you can link multiple switches an input and a switch to multiple inputs.
-
-> [!note]
-> We use the terms "open" and "close" for switches, since that's what they do in real life. In electrical terms these are called "NC" (normally closed) and "NO" (normally open). In summary, these terms are synonyms:
->
-> | Technical | Status | Common Term | In code |
-> |-----------|--------|-------------|---------|
-> | NO | Open | Off | `false` |
-> | NC | Closed | On | `true` |
+Imagine every row as a wire connecting the physical switch to the gamelogic engine. The relation between the two is *0..n -> 0..n*, meaning you can link multiple switches to one input or a single switch to multiple inputs.
### IDs
-The first column **ID** shows the switch names that the gamelogic engine expects to be wired up.
+The first column **ID** shows the names of each switch that the gamelogic engine is aware of.
> [!note]
-> As we cannot be 100% sure that the gamelogic engine has accurate data about the switch names, you can also add switch IDs yourself, but that should be the exception.
+> As we cannot be 100% sure that the gamelogic engine has accurate data about the switch names, you can also add switch IDs yourself, but those should be the exception.
### Description
-The **Description** column is an optional free text field. If you're setting up a re-creation, that's where you typically put what's in the game manual. It's purely for your own benefit and you can keep this empty if you want.
+The **Description** column is optional. If you're setting up a re-creation, you would typically use this for the switch name from the game manual. It's purely for your own benefit, and you can keep this empty if you want.
### Source
-The **Source** column defines where the element in the next column is located. There are three options:
+The **Source** column defines where the element in the following column originates. There are four options:
+
+- *Playfield* lets you choose a game item from the playfield
+- *Input System* lets you choose an input action from a pre-defined list, e.g. cabinet switches
+- *Constant* sets the switch once at the beginning of the game to the given value.
+- *Device* lets you choose a switch device containing the switch. Switch devices are mechanisms that include multiple switches, for example [troughs](../manual/mechanisms/troughs.md).
-- *Playfield* lets you choose a game item in the playfield
-- *Input System* lets you choose an input action from a pre-defined list, i.e. cabinet switches
-- *Constant* sets the switch at the beginning of the game to the given value.
### Element
The **Element** column is where you choose which element triggers the switch.
-For **Playfield**, you can choose a game item that triggers switch events. Currently, VPE only emits switch events for items that would do the same in real life, i.e. bumpers, flippers, gates, targets, kickers, spinners and triggers.
+For **Playfield** sources, you can choose a game item that triggers switch events. Currently, VPE only emits switch events for items that would do so in real life, i.e. bumpers, flippers, gates, targets, kickers, spinners and triggers.
> [!note]
> We realize that you might want to use other game items like ramps and walls to emit switch events as well, and we will address this at some point, but for now we're keeping it simple.
-If **Input System** is selected, you choose which input action to use. We call it "input action", because it's not an actual key binding. While actions have default key bindings, the final bindings will be defined in the host application (the VPE player). So what VPE is dealing with in terms of keyboard input is what we call *input actions*.
+If **Input System** is selected, you choose which input action to use (it's an "action", because it's not an permanent key binding). Actions may have default key bindings, but the final bindings to a key or other input will be defined in the host application (the VPE player).
+
+If the source is a **Device**, then there are two values to select. The actual switch device, and which switch of that device should be connected to the gamelogic engine.
Finally, if **Constant** is selected, you choose the value that will be permanently set at the beginning of the game.
### Pulse Delay
-Internally, VPX connects switches to events. For example, a trigger on the playfield has a `Hit` event, which occurs when the ball rolls into the trigger's zone, and an `UnHit` event when the ball leaves that zone. These two events close and open the trigger's switch.
+Internally, VPX connects switches to events. For example, a trigger on the playfield has a `Hit` event, which occurs when the ball rolls into the trigger's collision zone, and an `UnHit` event when the ball leaves that zone. These two events close and open the trigger's switch.
-However, not all mechanisms behave like that. For example a spinner just emits one `Spin` event. So in order to not let the switch closed indefinitely, VPE automatically re-opens it after a given delay.
+However, not all mechanisms behave like that. For example a spinner emits a single `Spin` event. So to prevent the switch from being closed indefinitely VPE automatically re-opens it after a given delay.
-We call that the **Pulse Delay**. "Pulse", because it gets triggered by one event and opens immediately right after.
+We call that the **Pulse Delay**. "Pulse", because it gets triggered by one event and reopens after a brief delay.
-In most cases, you can leave the default delay of 250ms. What's important is that gamelogic engine gets notified not too long after the switch was closed. Note that when setting it to 0, the switch will stay closed.
+In most cases, you can leave the default delay of 250ms. What's important is that the gamelogic engine gets notified not too long after the switch was closed. Note that if pulse delay is set to 0, the switch will stay closed.
## Supported Game Mechanisms
Below a list of game mechanisms that contain built-in switches.
-| | Close | Open |
+| | Closes | Opens |
|-------------|--------------------------------------------------------|------------------------------------------------------------------------------|
-| **Bumper** | On ball collision | *Opens after pulse delay* |
+| **Bumper** | On ball collision | *After pulse delay* |
| **Flipper** | On EOS, i.e. when the flipper reaches its end position | When the flipper switch is opened, i.e. the flipper starts moving down again |
| **Gate** | When ball is passing through the gate | When ball has passed through |
-| **Target** | On collision | *Opens after pulse delay* |
-| **Kicker** | When ball goes into the kicker | When ball's outside the kicker |
-| **Spinner** | On each spin | *Opens after pulse delay* |
+| **Target** | On collision | *After pulse delay* |
+| **Kicker** | When ball enters the kicker | When ball's outside the kicker |
+| **Spinner** | On each spin | *After pulse delay* |
| **Trigger** | When the ball rolls over the trigger | When the ball is outside of the trigger |
\ No newline at end of file
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.png b/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.png
index 76ecc4149..4e405292b 100644
Binary files a/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.png and b/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/flipper.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/flipper.md
deleted file mode 100644
index df242232a..000000000
--- a/VisualPinball.Unity/Documentation~/creators-guide/manual/flipper.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Flipper
-
-Flippers. They flip.
-
-[You can program them too](xref:VisualPinball.Unity.FlipperApi)!
\ No newline at end of file
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/items/flippers.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/flippers.md
similarity index 100%
rename from VisualPinball.Unity/Documentation~/creators-guide/manual/items/flippers.md
rename to VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/flippers.md
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-coils.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-coils.png
new file mode 100644
index 000000000..61f32fba5
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-coils.png 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
new file mode 100644
index 000000000..b158e77b1
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-switches.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-switches.png
new file mode 100644
index 000000000..877485438
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-switches.png differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md
new file mode 100644
index 000000000..79fefbf78
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md
@@ -0,0 +1,31 @@
+# Troughs / Ball Drains
+
+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.
+
+## Creating a Trough
+
+When importing a `.vpx` file that doesn't have any troughs (which is likely, because Visual Pinball doesn't currently handle them in the same way as VPE), VPE will automatically add a main trough to the root of the table. If you're creating a trough for a new game, click on the *Trough* button in the toolbox.
+
+## Linking to the Playfield
+
+
+
+To interact with the game, you'll need to setup an entry kicker 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 the trough's perspective, that's where the ball *enters*.
+
+You can setup the kickers by selecting the trough in the hierarchy panel and linking them to the desired kickers using the inspector.
+
+## Switch Setup
+
+The number of simulated switches in the trough depends on 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.
+
+Open the [switch manager](../../editor/switch-manager.md) and add the trough switches if they're not already there. As *Destination* select "Device", under *Element*, select the trough you've created and which switch to connect. For a five-ball trough, it will look something like this:
+
+
+
+## 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. 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:
+
+
\ No newline at end of file
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
index 88d3be812..e9a36bd3b 100644
--- a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
+++ b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
@@ -30,7 +30,9 @@
items:
- name: Gamelogic Engine
href: manual/gamelogic-engine.md
- - name: Game Items
+ - name: Pinball Mechanisms
items:
+ - name: Troughs / Ball Drains
+ href: manual/mechanisms/troughs.md
- name: Flippers
- href: manual/items/flippers.md
\ No newline at end of file
+ href: manual/mechanisms/flippers.md
\ No newline at end of file
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs
index 81f4d85d1..121716117 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs
@@ -31,7 +31,7 @@ public class DragPointsHandler
///
/// Authoring item
///
- public IItemMainAuthoring Editable { get; private set; }
+ public IItemMainRenderableAuthoring Editable { get; private set; }
///
/// Authoring item as IDragPointsEditable
@@ -87,7 +87,7 @@ public class DragPointsHandler
///
public DragPointsHandler(Object target)
{
- Editable = target as IItemMainAuthoring
+ Editable = target as IItemMainRenderableAuthoring
?? throw new ArgumentException("Target must extend `IEditableItemAuthoring`.");
DragPointEditable = target as IDragPointsEditable
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsItemInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsItemInspector.cs
index bb1e939a4..ce71c8a9d 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsItemInspector.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsItemInspector.cs
@@ -27,7 +27,7 @@ namespace VisualPinball.Unity.Editor
public abstract class DragPointsItemInspector : ItemMainInspector, IDragPointsItemInspector
where TData : ItemData
where TItem : Item, IHittable, IRenderable
- where TMainAuthoring : ItemMainAuthoring
+ where TMainAuthoring : ItemMainRenderableAuthoring
{
///
/// Catmull Curve Handler
@@ -103,7 +103,7 @@ public void PasteDragPoint(int controlId)
/// True if game item is locked, false otherwise.
public bool IsItemLocked()
{
- return !(target is IItemMainAuthoring editable) || editable.IsLocked;
+ return !(target is IItemMainRenderableAuthoring editable) || editable.IsLocked;
}
///
@@ -143,7 +143,7 @@ public void FlipDragPoints(FlipAxis flipAxis)
public void RemapControlPoints()
{
var rebuilt = DragPointsHandler.RemapControlPoints();
- if (rebuilt && target is IItemMainAuthoring meshAuthoring) {
+ if (rebuilt && target is IItemMainRenderableAuthoring meshAuthoring) {
meshAuthoring.SetMeshDirty();
}
}
@@ -179,7 +179,7 @@ public void PrepareUndo(string message)
// Set MeshDirty to true there so it'll trigger again after Undo
var recordObjs = new List