diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84d9f2cd7..ebdd5eb34 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
Built with [Unity 2020.2](https://github.com/freezy/VisualPinball.Engine/pull/255).
### Added
+- Plugin: Mission Pinball Framework ([Documentation](https://docs.visualpinball.org/plugins/mpf/index.html))
+- Gamelogic Engine: Support for hardware rules ([#293](https://github.com/freezy/VisualPinball.Engine/pull/293)).
- Support for Extended ASCII strings ([#291](https://github.com/freezy/VisualPinball.Engine/pull/291)).
- Support for Elasticity Falloff in walls (added in VP 10.7) ([#291](https://github.com/freezy/VisualPinball.Engine/pull/291)).
- Support for table notes (added in VP 10.7) ([#291](https://github.com/freezy/VisualPinball.Engine/pull/291)).
diff --git a/README.md b/README.md
index d86a6ee05..6aa5a0944 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
*A library that implements world's favorite pinball simulator.*
-[](https://github.com/freezy/VisualPinball.Engine/actions?query=workflow%3ABuild) [](https://codecov.io/gh/freezy/VisualPinball.Engine)
+[](https://github.com/freezy/VisualPinball.Engine/actions?query=workflow%3ABuild) [](https://codecov.io/gh/freezy/VisualPinball.Engine) [](https://registry.visualpinball.org/-/web/detail/org.visualpinball.engine.unity)
## Why?
diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs
index 47471903b..686187257 100644
--- a/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs
+++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs
@@ -14,8 +14,13 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+// ReSharper disable InconsistentNaming
+
+using System;
+
namespace VisualPinball.Engine.Game.Engines
{
+ [Serializable]
public class GamelogicEngineCoil
{
public string Id;
@@ -26,6 +31,7 @@ public class GamelogicEngineCoil
public string DeviceHint;
public string DeviceItemHint;
public bool IsLamp;
+ public bool IsUnused;
public GamelogicEngineCoil(string id)
{
diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs
index bc17ef26c..546d0cf30 100644
--- a/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs
+++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs
@@ -14,8 +14,13 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+// ReSharper disable InconsistentNaming
+
+using System;
+
namespace VisualPinball.Engine.Game.Engines
{
+ [Serializable]
public class GamelogicEngineLamp
{
public string Id;
diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs
index b3fb657b9..8091ca732 100644
--- a/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs
+++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs
@@ -15,6 +15,9 @@
// along with this program. If not, see .
// ReSharper disable BuiltInTypeReferenceStyle
+// ReSharper disable InconsistentNaming
+
+using System;
namespace VisualPinball.Engine.Game.Engines
{
@@ -29,18 +32,19 @@ namespace VisualPinball.Engine.Game.Engines
/// This class isn't used during gameplay, but serves to declare the properties
/// that will then used in the mapping.
///
+ [Serializable]
public class GamelogicEngineSwitch
{
///
/// A unique identifier. This is what VPE uses to identify a switch.
///
- public readonly string Id;
+ public string Id;
///
/// A numerical identifier that can be used in gamelogic engines that
/// are tied to numerical identifiers.
///
- public readonly int InternalId;
+ public int InternalId;
///
/// If true, inverts the signal, i.e. disabled switches return "closed" (true),
diff --git a/VisualPinball.Engine/VPT/Mappings/Mappings.cs b/VisualPinball.Engine/VPT/Mappings/Mappings.cs
index 17b6ce581..fb82d5aae 100644
--- a/VisualPinball.Engine/VPT/Mappings/Mappings.cs
+++ b/VisualPinball.Engine/VPT/Mappings/Mappings.cs
@@ -210,6 +210,10 @@ public void PopulateCoils(GamelogicEngineCoil[] engineCoils, IEnumerable mappingsCoilData.Id == engineCoil.Id);
if (coilMapping == null) {
+ if (engineCoil.IsUnused) {
+ continue;
+ }
+
// we'll handle those in a second loop when all the main coils are added
if (!string.IsNullOrEmpty(engineCoil.MainCoilIdOfHoldCoil)) {
holdCoils.Add(engineCoil);
diff --git a/VisualPinball.Engine/VPT/Mappings/MappingsWireData.cs b/VisualPinball.Engine/VPT/Mappings/MappingsWireData.cs
index 27dd90c8b..78f0a6633 100644
--- a/VisualPinball.Engine/VPT/Mappings/MappingsWireData.cs
+++ b/VisualPinball.Engine/VPT/Mappings/MappingsWireData.cs
@@ -77,6 +77,24 @@ public class MappingsWireData : BiffData
[BiffInt("PLSE", Pos = 13)]
public int PulseDelay = 250;
+ public string DestinationId => Destination == WireDestination.Device ? DestinationDeviceItem : DestinationPlayfieldItem;
+
+ [ExcludeFromCodeCoverage]
+ public MappingsWireData(string description, MappingsSwitchData switchMapping, MappingsCoilData coilMapping) : this()
+ {
+ Description = description;
+ Source = switchMapping.Source;
+ SourceDevice = switchMapping.Device;
+ SourceDeviceItem = switchMapping.DeviceItem;
+ SourceInputAction = switchMapping.InputAction;
+ SourceInputActionMap = switchMapping.InputActionMap;
+ SourcePlayfieldItem = switchMapping.PlayfieldItem;
+ Destination = coilMapping.Destination == CoilDestination.Device ? WireDestination.Device : WireDestination.Playfield;
+ DestinationDevice = coilMapping.Device;
+ DestinationDeviceItem = coilMapping.DeviceItem;
+ DestinationPlayfieldItem = coilMapping.PlayfieldItem;
+ }
+
[ExcludeFromCodeCoverage]
public string Src { get {
switch (Source) {
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/gamelogic-engine.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/gamelogic-engine.md
index f03a003c7..50e75f30f 100644
--- a/VisualPinball.Unity/Documentation~/creators-guide/manual/gamelogic-engine.md
+++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/gamelogic-engine.md
@@ -7,7 +7,7 @@ When playing a pinball game, some part of the table is driving the gameplay, i.e
The gamelogic engine is purely gameplay driven. It gets input from switches, computes what will happen next, and updates the hardware components of the table. It does *not* handle game mechanics, which are about simulating the hardware *behavior* of the table - it just toggles it.
-Classic examples of gamelogic engines are [MPF](https://missionpinball.org/) and [PinMAME](https://sourceforge.net/projects/pinmame/).
+Classic examples of gamelogic engines are [MPF](../../plugins/mpf/index.html) and [PinMAME](https://github.com/vpinball/pinmame).
> [!note]
> Let's take a spinning wheel on the playfield as an example. The game*logic* engine's job is to know when to turn it on and off. The game *mechanics* component of the spinning wheel is about rotating the actual playfield element with the right speed, acceleration, and handle ball collisions with a given friction.
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/setup/updating-vpe.md b/VisualPinball.Unity/Documentation~/creators-guide/setup/updating-vpe.md
deleted file mode 100644
index 7ca849848..000000000
--- a/VisualPinball.Unity/Documentation~/creators-guide/setup/updating-vpe.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-description: How to update VPE
----
-# Updating VPE
-
-VPE is under heavy development, so it's frequently updated, usually multiple times per week. In order to not have to delete your existing `VisualPinball.Engine` folder and download and extract the code each time, we recommend using git.
-
-Git is a distributed version control system. It's very sophisticated but can also be a bit overwhelming to use. However, with the cheat sheet below you should be able to handle it.
-
-First you need to [download git](https://git-scm.com/downloads). Make sure it's in your `PATH` environment variable. There are free GUIs for git such as [Fork](https://git-fork.com/), [GitKraken](https://www.gitkraken.com/) or [Source Tree](https://www.sourcetreeapp.com/), but we'll focus on the command line version on Windows here. Linux and macOS are similar but use a command shell or terminal window.
-
-Open a command prompt by pressing the Windows key and typing `cmd`, followed by enter. Make sure that git is installed by typing `git --version`. This should return something like `git version 2.18.0.windows.1`.
-
-Next, go to the folder where you want to have VPE installed. If there is already a folder where you've extracted VPE from before, delete it.
-
-Following the recommended file structure, you would type:
-
-```cmd
-cd %userprofile%\VPE
-git clone https://github.com/freezy/VisualPinball.Engine.git
-git clone https://github.com/freezy/VisualPinball.Unity.Hdrp.git
-```
-
-This downloads the latest version of VPE into `VisualPinball.Engine` and `VisualPinball.Unity.Hdrp` respectively and keeps a link to GitHub. In the future, if you want to update VPE, it's simply a matter of going into the folder and "pull" the changes:
-
-```cmd
-cd %userprofile%\VPE\VisualPinball.Engine
-git pull
-cd ..\VisualPinball.Unity.Hdrp
-git pull
-```
-
-However, you might have experimented in the VPE folder to test out stuff, and git complains it can't update. Here is a way to discard all local changes and pull in what's on GitHub:
-
-```cmd
-git fetch --prune
-git checkout -- **
-git reset --hard origin/master
-```
-
-> [!WARNING]
-> Should you have *committed* changes (as in, you've developed something, and added and commited it to git), this will also discard those changes. But if you have done that you're probably a seasoned developer and know what you're doing, right? :)
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
index 3e5d68a4d..c39aa7cd0 100644
--- a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
+++ b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
@@ -11,8 +11,6 @@
href: setup/installing-vpe.md
- name: Running VPE
href: setup/running-vpe.md
- - name: Updating VPE
- href: setup/updating-vpe.md
- name: Editor
items:
diff --git a/VisualPinball.Unity/Documentation~/docfx.json b/VisualPinball.Unity/Documentation~/docfx.json
index f37b836a8..fa2e8fe2e 100644
--- a/VisualPinball.Unity/Documentation~/docfx.json
+++ b/VisualPinball.Unity/Documentation~/docfx.json
@@ -25,8 +25,10 @@
},
{
"files": [
- "creators-guide/**.md",
"creators-guide/**/toc.yml",
+ "creators-guide/**.md",
+ "plugins/**/toc.yml",
+ "plugins/**.md",
"toc.yml",
"*.md"
]
@@ -80,7 +82,7 @@
"postProcessors": [ "ExtractSearchIndex" ],
"globalMetadata": {
"_appTitle": "VPE Documentation",
- "_appFooter": "Copyright © 2020 VPE Team",
+ "_appFooter": "Copyright © 2021 VPE Team",
"_gitContribute": {
"branch": "master"
}
diff --git a/VisualPinball.Unity/Documentation~/plugins/index.md b/VisualPinball.Unity/Documentation~/plugins/index.md
new file mode 100644
index 000000000..2b6e1f355
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/plugins/index.md
@@ -0,0 +1,13 @@
+---
+title: Plugins
+description: Visual Pinball for Unity - Plugins
+---
+
+# Plugins
+
+VPE has a plug-in system that allows other software to integrate with it. Plugins are typically required on a per-table basis. VPE ships with a number of default plugins which are documented here.
+
+
+## [Mission Pinball Framework](mpf/index.html)
+
+The [Mission Pinball Framework](https://missionpinball.org/) is software written in Python that is used to drive real pinball machines. It integrates with VPE as a gamelogic engine.
diff --git a/VisualPinball.Unity/Documentation~/plugins/mpf/index.md b/VisualPinball.Unity/Documentation~/plugins/mpf/index.md
new file mode 100644
index 000000000..c3d005e60
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/plugins/mpf/index.md
@@ -0,0 +1,17 @@
+---
+title: Mission Pinball Framework
+description: Visual Pinball Engine integration with the Mission Pinball Framework.
+---
+
+
+
+# Mission Pinball Framework
+
+VPE connects to MPF using [gRPC](https://grpc.io/), which is a high-performance, low-latency RPC framework. It works by VPE launching MPF as a Python process. MPF will then spawn a gRPC server, to which VPE connects to.
+
+There are two situations when this is done:
+
+- In edit mode to retrieve available switches, coils and lamps
+- During runtime to drive the game
+
+VPE supports MPF's *hardware rules*, which are dynamic connections between coils and switches handled by the controller boards in order to reduce latency. The media controller is not yet supported.
diff --git a/VisualPinball.Unity/Documentation~/plugins/mpf/setup.md b/VisualPinball.Unity/Documentation~/plugins/mpf/setup.md
new file mode 100644
index 000000000..cdbe954f7
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/plugins/mpf/setup.md
@@ -0,0 +1,36 @@
+---
+title: MPF Setup
+description: How to set up the Mission Pinball Framework with VPE.
+---
+
+# Setup
+
+## Prerequisites
+
+Future plans include shipping MPF entirely with Unity, but currently, you need to have MPF installed on your machine. You can install MPF by:
+
+1. [Installing Python 3.7](https://www.python.org/downloads/)
+2. `pip install --pre mpf mpf-mc`
+
+You can *upgrade* MPF if you already have installed it by running:
+
+```bash
+pip install mpf mpf-mc --pre --upgrade
+```
+
+Note: On MacOS, you may have to substitue `pip` with `pip3`.
+
+You will need at least MPF v0.55.0-dev.12.
+
+## Unity Setup
+
+Mission Pinball Framework integration comes as an UPM package. In Unity, add it by choosing *Window -> Package Manager -> Add package from git URL*:
+
+
+
+Then, input `org.visualpinball.engine.missionpinball` and click *Add* or press `Enter`. This will download and add MPF to the project.
+
+> [!NOTE]
+> You will need to have our scoped registry added in order for Unity to find the MPF package. How to do this is documented in the [general setup section](/creators-guide/setup/installing-vpe.html#vpe-package).
+
+So let's [test it](usage.md).
diff --git a/VisualPinball.Unity/Documentation~/plugins/mpf/unity-add-component.png b/VisualPinball.Unity/Documentation~/plugins/mpf/unity-add-component.png
new file mode 100644
index 000000000..3ccd11675
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/plugins/mpf/unity-add-component.png differ
diff --git a/VisualPinball.Unity/Documentation~/plugins/mpf/usage.md b/VisualPinball.Unity/Documentation~/plugins/mpf/usage.md
new file mode 100644
index 000000000..a5a023983
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/plugins/mpf/usage.md
@@ -0,0 +1,32 @@
+---
+title: MPF Usage
+description: How to use the Mission Pinball Framework with VPE.
+---
+
+# Usage
+
+MPF support is implemented as a [Gamelogic Engine](../../creators-guide/manual/gamelogic-engine.md). It's a [Unity Component](https://docs.unity3d.com/Manual/Components.html), so all you have to do is add it to the root node of your table.
+
+You can do this by selecting the table in the hierarchy, then click *Add Component* in the inspector and select *Visual Pinball -> Game Logic Engine -> Mission Pinball Framework*.
+
+
+
+
+## Retrieve Machine Description
+
+Since the gamelogic engine is the part of VPE that provides switch, coil, and lamp definitions so VPE can link them to the table during gameplay, you'll need to retrieve them from MPF.
+
+You can do this by clicking *Get Machine Description* in the MPF component's inspector. This will save it to the component. You will only need to do this once unless you update the MPF machine config.
+
+> [!NOTE]
+> While VPE could read the MPF machine config itself, we let MPF handle it. That means we run MPF with the given machine config and then query its hardware.
+>
+> While this is a bit slower, it has the advantage of coherent behavior between edit time and runtime, and doesn't add an additional maintenance burden.
+
+## Wire It Up
+
+Now that VPE knows which switches, coils, and lamps your machine expects, you'll need to connect them using the [switch](../../editor/switch-manager.md), [coil](../../editor/coil-manager.md), and [lamp manager](../../editor/lamp-manager.md).
+
+You can watch the entire process in a quick video here:
+
+> [!Video https://www.youtube.com/embed/cdzvMUpdDgs]
diff --git a/VisualPinball.Unity/Documentation~/plugins/toc.yml b/VisualPinball.Unity/Documentation~/plugins/toc.yml
new file mode 100644
index 000000000..a3199da20
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/plugins/toc.yml
@@ -0,0 +1,10 @@
+- name: Overview
+ href: index.md
+- name: Mission Pinball Framework
+ items:
+ - name: Overview
+ href: mpf/index.md
+ - name: Setup
+ href: mpf/setup.md
+ - name: Usage
+ href: mpf/usage.md
diff --git a/VisualPinball.Unity/Documentation~/toc.yml b/VisualPinball.Unity/Documentation~/toc.yml
index 6940f666e..bdab2402d 100644
--- a/VisualPinball.Unity/Documentation~/toc.yml
+++ b/VisualPinball.Unity/Documentation~/toc.yml
@@ -1,5 +1,7 @@
- name: Creator's Guide
href: creators-guide/
+- name: Plugins
+ href: plugins/
- name: API Documentation
href: api/
homepage: api/index.md
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ManagerWindow.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ManagerWindow.cs
index 7f9144e53..f6f2bbbe6 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ManagerWindow.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ManagerWindow.cs
@@ -81,7 +81,7 @@ protected float RowHeight {
public void Reload()
{
- if (_tableAuthoring != null) {
+ if (_tableAuthoring) {
_data = CollectData();
_listView.SetData(_data);
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Common/TableSelector.cs b/VisualPinball.Unity/VisualPinball.Unity/Common/TableSelector.cs
index 4a7b1c32c..d38d22e3e 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Common/TableSelector.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Common/TableSelector.cs
@@ -28,6 +28,11 @@ public TableAuthoring SelectedTable {
set => SetSelectedTable(value);
}
+ public void TableUpdated()
+ {
+ OnTableSelected?.Invoke(this, EventArgs.Empty);
+ }
+
///
/// Returns true if there is an active table component.
///
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs
index 8f0119939..3c52c0c46 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs
@@ -59,8 +59,8 @@ public DeviceSwitch(string name, bool isPulseSwitch, SwitchDefault switchDefault
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig) =>
_switchHandler.AddSwitchDest(switchConfig.WithPulse(_isPulseSwitch).WithDefault(_switchDefault));
- public void AddWireDest(WireDestConfig wireConfig) =>
- _switchHandler.AddWireDest(wireConfig);
+ public void AddWireDest(WireDestConfig wireConfig) => _switchHandler.AddWireDest(wireConfig);
+ void IApiSwitch.RemoveWireDest(string destId) => _switchHandler.RemoveWireDest(destId);
public void DestroyBall(Entity ballEntity) { } // device switches can't destroy balls
///
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
index 5eebe9bd3..746207398 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
@@ -16,6 +16,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NLog;
using Unity.Entities;
using Unity.Mathematics;
@@ -23,11 +24,13 @@
using UnityEngine.InputSystem;
using VisualPinball.Engine.Common;
using VisualPinball.Engine.Game;
+using VisualPinball.Engine.VPT;
using VisualPinball.Engine.VPT.Bumper;
using VisualPinball.Engine.VPT.Flipper;
using VisualPinball.Engine.VPT.Gate;
using VisualPinball.Engine.VPT.HitTarget;
using VisualPinball.Engine.VPT.Kicker;
+using VisualPinball.Engine.VPT.Mappings;
using VisualPinball.Engine.VPT.Plunger;
using VisualPinball.Engine.VPT.Primitive;
using VisualPinball.Engine.VPT.Ramp;
@@ -346,7 +349,9 @@ public void RegisterTrough(Trough trough, GameObject go)
#region Events
+ public void Queue(Action action) => _simulationSystemGroup.QueueBeforeBallCreation(action);
public void ScheduleAction(int timeMs, Action action) => _simulationSystemGroup.ScheduleAction(timeMs, action);
+ public void ScheduleAction(uint timeMs, Action action) => _simulationSystemGroup.ScheduleAction(timeMs, action);
public void OnEvent(in EventData eventData)
{
@@ -389,6 +394,62 @@ public void OnEvent(in EventData eventData)
#endregion
+ #region API
+
+ public void AddDynamicWire(string switchId, string coilId)
+ {
+ var switchMapping = Table.Mappings.Data.Switches.FirstOrDefault(c => c.Id == switchId);
+ var coilMapping = Table.Mappings.Data.Coils.FirstOrDefault(c => c.Id == coilId);
+ if (switchMapping == null) {
+ Logger.Warn($"Cannot add new hardware rule for unknown switch \"{switchId}\".");
+ return;
+ }
+ if (coilMapping == null) {
+ Logger.Warn($"Cannot add new hardware rule for unknown coil \"{coilId}\".");
+ return;
+ }
+
+ var wireMapping = new MappingsWireData($"Hardware rule: {switchId} -> {coilId}", switchMapping, coilMapping);
+ _wirePlayer.AddWire(wireMapping);
+
+ // this is for showing it in the editor during runtime only
+ Table.Mappings.Data.AddWire(wireMapping);
+ }
+
+ public void RemoveDynamicWire(string switchId, string coilId)
+ {
+ var switchMapping = Table.Mappings.Data.Switches.FirstOrDefault(c => c.Id == switchId);
+ var coilMapping = Table.Mappings.Data.Coils.FirstOrDefault(c => c.Id == coilId);
+ if (switchMapping == null) {
+ Logger.Warn($"Cannot remove hardware rule for unknown switch \"{switchId}\".");
+ return;
+ }
+ if (coilMapping == null) {
+ Logger.Warn($"Cannot remove hardware rule for unknown coil \"{coilId}\".");
+ return;
+ }
+
+ var wireMapping = new MappingsWireData($"Hardware rule: {switchId} -> {coilId}", switchMapping, coilMapping);
+ _wirePlayer.RemoveWire(wireMapping);
+
+ // this is for the editor during runtime only
+ var wire = Table.Mappings.Data.Wires.FirstOrDefault(w =>
+ w.Description == wireMapping.Description &&
+ w.SourceDevice == wireMapping.SourceDevice &&
+ w.SourceDeviceItem == wireMapping.SourceDeviceItem &&
+ w.SourceInputAction == wireMapping.SourceInputAction &&
+ w.SourceInputActionMap == wireMapping.SourceInputActionMap &&
+ w.SourcePlayfieldItem == wireMapping.SourcePlayfieldItem &&
+ w.Destination == wireMapping.Destination &&
+ w.DestinationDevice == wireMapping.DestinationDevice &&
+ w.DestinationDeviceItem == wireMapping.DestinationDeviceItem &&
+ w.DestinationPlayfieldItem == wireMapping.DestinationPlayfieldItem
+ );
+ Table.Mappings.Data.RemoveWire(wire);
+ }
+
+ #endregion
+
private static void HandleInput(object obj, InputActionChange change)
{
if (obj is InputAction action && action.actionMap.name == InputConstants.MapDebug) {
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs
index 2984b2b48..39eeead81 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NLog;
using Unity.Entities;
using UnityEngine;
@@ -78,6 +79,20 @@ public void AddWireDest(WireDestConfig wireConfig)
_wires.Add(wireConfig);
}
+ ///
+ /// Removes a previously added wire.
+ ///
+ /// ID of the destination
+ public void RemoveWireDest(string destId)
+ {
+ foreach (var wire in _wires) {
+ if (wire.IsDynamic && wire.DestinationId == destId) {
+ _wires.Remove(wire);
+ return;
+ }
+ }
+ }
+
///
/// Sends the switch element to the gamelogic engine and linked wires.
///
@@ -133,8 +148,7 @@ internal void OnSwitch(bool enabled)
// if it's pulse, schedule to re-open
if (enabled && wireConfig.IsPulseSource) {
if (dest != null) {
- SimulationSystemGroup.ScheduleAction(wireConfig.PulseDelay,
- () => dest.OnChange(false));
+ SimulationSystemGroup.ScheduleAction(wireConfig.PulseDelay, () => dest.OnChange(false));
}
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs
index ce3aaebeb..b0908623b 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs
@@ -42,7 +42,8 @@ public class SwitchPlayer
internal IApiSwitch Switch(string device, string itemName) => _switchDevices.ContainsKey(device) ? _switchDevices[device].Switch(itemName) : null;
internal void RegisterSwitch(IItem item, IApiSwitch switchApi) => _switches[item.Name] = switchApi;
internal void RegisterSwitchDevice(IItem item, IApiSwitchDevice switchDeviceApi) => _switchDevices[item.Name] = switchDeviceApi;
- public void RegisterWire(MappingsWireData wireData) => _switches[wireData.SourcePlayfieldItem].AddWireDest(new WireDestConfig(wireData));
+ public void RegisterWire(MappingsWireData wireData, bool isDynamic = false) => _switches[wireData.SourcePlayfieldItem].AddWireDest(new WireDestConfig(wireData) {IsDynamic = isDynamic});
+ public void UnregisterWire(MappingsWireData wireData) => _switches[wireData.SourcePlayfieldItem].RemoveWireDest(wireData.DestinationId);
public bool SwitchExists(string name) => _switches.ContainsKey(name);
public bool SwitchDeviceExists(string name) => _switchDevices.ContainsKey(name);
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballSimulationSystemGroup.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballSimulationSystemGroup.cs
index 8466fd55d..4bb0bb9f6 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballSimulationSystemGroup.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballSimulationSystemGroup.cs
@@ -21,6 +21,7 @@
using Unity.Entities;
using Unity.Transforms;
using VisualPinball.Engine.Common;
+using Debug = UnityEngine.Debug;
namespace VisualPinball.Unity
{
@@ -51,7 +52,8 @@ internal class VisualPinballSimulationSystemGroup : ComponentSystemGroup
private UpdateAnimationsSystemGroup _updateAnimationsSystemGroup;
private TransformMeshesSystemGroup _transformMeshesSystemGroup;
- private readonly List _afterBallQueues = new List();
+ private readonly Queue _afterBallCreationQueue = new Queue();
+ private readonly Queue _beforeBallCreationQueue = new Queue();
private readonly List _scheduledActions = new List();
private const TimingMode Timing = TimingMode.UnityTime;
@@ -87,11 +89,17 @@ protected override void OnStartRunning()
protected override void OnUpdate()
{
+ lock (_beforeBallCreationQueue) {
+ while (_beforeBallCreationQueue.Count > 0) {
+ _beforeBallCreationQueue.Dequeue().Invoke();
+ }
+ }
_createBallEntityCommandBufferSystem.Update();
- foreach (var action in _afterBallQueues) {
- action();
+ lock (_afterBallCreationQueue) {
+ while (_afterBallCreationQueue.Count > 0) {
+ _afterBallCreationQueue.Dequeue().Invoke();
+ }
}
- _afterBallQueues.Clear();
//const int startTimeUsec = 0;
var initialTimeUsec = GetTargetTime();
@@ -114,10 +122,12 @@ protected override void OnUpdate()
_nextPhysicsFrameTime += PhysicsConstants.PhysicsStepTime;
// run scheduled actions
- for (var i = _scheduledActions.Count - 1; i >= 0; i--) {
- if (_currentPhysicsFrameTime > _scheduledActions[i].ScheduleAt) {
- _scheduledActions[i].Action();
- _scheduledActions.RemoveAt(i);
+ lock (_scheduledActions) {
+ for (var i = _scheduledActions.Count - 1; i >= 0; i--) {
+ if (_currentPhysicsFrameTime > _scheduledActions[i].ScheduleAt) {
+ _scheduledActions[i].Action();
+ _scheduledActions.RemoveAt(i);
+ }
}
}
}
@@ -159,16 +169,27 @@ private ulong GetTargetTime()
}
}
- public void QueueAfterBallCreation(Action action)
+ public void QueueBeforeBallCreation(Action action)
{
- _afterBallQueues.Add(action);
+ lock (_beforeBallCreationQueue) {
+ _beforeBallCreationQueue.Enqueue(action);
+ }
}
- public void ScheduleAction(int timeoutMs, Action action)
+ public void QueueAfterBallCreation(Action action)
{
- _scheduledActions.Add(new ScheduledAction(_currentPhysicsFrameTime + (ulong)timeoutMs * 1000, action));
+ lock (_afterBallCreationQueue) {
+ _afterBallCreationQueue.Enqueue(action);
+ }
}
+ public void ScheduleAction(int timeoutMs, Action action) => ScheduleAction((uint)timeoutMs, action);
+ public void ScheduleAction(uint timeoutMs, Action action)
+ {
+ lock (_scheduledActions) {
+ _scheduledActions.Add(new ScheduledAction(_currentPhysicsFrameTime + (ulong)timeoutMs * 1000, action));
+ }
+ }
private enum TimingMode
{
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs
index f4eca2584..5c2f263e6 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs
@@ -15,6 +15,7 @@
// along with this program. If not, see .
using System.Collections.Generic;
+using System.Linq;
using NLog;
using UnityEngine.InputSystem;
using VisualPinball.Engine.VPT;
@@ -29,7 +30,6 @@ public class WirePlayer
private readonly Dictionary _wireDevices = new Dictionary();
private readonly Dictionary> _keyWireAssignments = new Dictionary>();
-
private Table _table;
private InputManager _inputManager;
private SwitchPlayer _switchPlayer;
@@ -53,65 +53,125 @@ public void OnStart()
var config = _table.Mappings;
_keyWireAssignments.Clear();
foreach (var wireData in config.Data.Wires) {
- switch (wireData.Source) {
+ AddWire(wireData);
+ }
- case SwitchSource.Playfield: {
+ _inputManager.Enable(HandleKeyInput);
+ }
- if (string.IsNullOrEmpty(wireData.SourcePlayfieldItem)) {
- break;
- }
+ internal void AddWire(MappingsWireData wireData, bool isDynamic = false)
+ {
+ switch (wireData.Source) {
- if (!_switchPlayer.SwitchExists(wireData.SourcePlayfieldItem)) {
- Logger.Error($"Cannot find item \"{wireData.SourcePlayfieldItem}\" for wire source.");
- break;
- }
+ case SwitchSource.Playfield: {
+ if (string.IsNullOrEmpty(wireData.SourcePlayfieldItem)) {
+ break;
+ }
- _switchPlayer.RegisterWire(wireData);
+ if (!_switchPlayer.SwitchExists(wireData.SourcePlayfieldItem)) {
+ Logger.Error($"Cannot find item \"{wireData.SourcePlayfieldItem}\" for wire source.");
break;
}
- case SwitchSource.InputSystem:
- if (!_keyWireAssignments.ContainsKey(wireData.SourceInputAction)) {
- _keyWireAssignments[wireData.SourceInputAction] = new List();
- }
- _keyWireAssignments[wireData.SourceInputAction].Add(new WireDestConfig(wireData));
+ _switchPlayer.RegisterWire(wireData, isDynamic);
+ break;
+ }
+
+ case SwitchSource.InputSystem: {
+ if (!_keyWireAssignments.ContainsKey(wireData.SourceInputAction)) {
+ _keyWireAssignments[wireData.SourceInputAction] = new List();
+ }
+ _keyWireAssignments[wireData.SourceInputAction].Add(new WireDestConfig(wireData) { IsDynamic = isDynamic });
+ break;
+ }
+
+ case SwitchSource.Device: {
+ // mapping values must be set
+ if (string.IsNullOrEmpty(wireData.SourceDevice) || string.IsNullOrEmpty(wireData.SourceDeviceItem)) {
break;
+ }
- case SwitchSource.Device: {
+ // check if device exists
+ if (!_switchPlayer.SwitchDeviceExists(wireData.SourceDevice)) {
+ Logger.Error($"Unknown wire switch device \"{wireData.SourceDevice}\".");
+ break;
+ }
- // mapping values must be set
- if (string.IsNullOrEmpty(wireData.SourceDevice) || string.IsNullOrEmpty(wireData.SourceDeviceItem)) {
- break;
- }
+ var deviceSwitch = _switchPlayer.Switch(wireData.SourceDevice, wireData.SourceDeviceItem);
+ if (deviceSwitch != null) {
+ deviceSwitch.AddWireDest(new WireDestConfig(wireData) { IsDynamic = isDynamic });
+ Logger.Info($"Wiring device switch \"{wireData.Src}\" to \"{wireData.Dst}\"");
- // check if device exists
- if (!_switchPlayer.SwitchDeviceExists(wireData.SourceDevice)) {
- Logger.Error($"Unknown wire switch device \"{wireData.SourceDevice}\".");
- break;
- }
+ } else {
+ Logger.Warn($"Unknown switch \"{wireData.Src}\" to wire to \"{wireData.Dst}\".");
+ }
+ break;
+ }
- var deviceSwitch = _switchPlayer.Switch(wireData.SourceDevice, wireData.SourceDeviceItem);
- if (deviceSwitch != null) {
- deviceSwitch.AddWireDest(new WireDestConfig(wireData));
- Logger.Info($"Wiring device switch \"{wireData.Src}\" to \"{wireData.Dst}\"");
+ case SwitchSource.Constant:
+ break;
- } else {
- Logger.Warn($"Unknown switch \"{wireData.Src}\" to wire to \"{wireData.Dst}\".");
- }
+ default:
+ Logger.Warn($"Unknown wire switch source \"{wireData.Source}\".");
+ break;
+ }
+ }
+
+ internal void RemoveWire(MappingsWireData wireData)
+ {
+ switch (wireData.Source) {
+
+ case SwitchSource.Playfield: {
+ if (string.IsNullOrEmpty(wireData.SourcePlayfieldItem)) {
break;
}
- case SwitchSource.Constant:
+ if (!_switchPlayer.SwitchExists(wireData.SourcePlayfieldItem)) {
+ Logger.Error($"Cannot find item \"{wireData.SourcePlayfieldItem}\" for wire source.");
break;
+ }
+
+ _switchPlayer.UnregisterWire(wireData);
+ break;
+ }
- default:
- Logger.Warn($"Unknown wire switch source \"{wireData.Source}\".");
+ case SwitchSource.InputSystem: {
+ if (!_keyWireAssignments.ContainsKey(wireData.SourceInputAction)) {
+ _keyWireAssignments[wireData.SourceInputAction] = new List();
+ }
+ var assignment = _keyWireAssignments[wireData.SourceInputAction].FirstOrDefault(a => a.IsDynamic && a.DestinationId == wireData.DestinationId);
+ _keyWireAssignments[wireData.SourceInputAction].Remove(assignment);
+ break;
+ }
+
+ case SwitchSource.Device: {
+ // mapping values must be set
+ if (string.IsNullOrEmpty(wireData.SourceDevice) || string.IsNullOrEmpty(wireData.SourceDeviceItem)) {
break;
+ }
+
+ // check if device exists
+ if (!_switchPlayer.SwitchDeviceExists(wireData.SourceDevice)) {
+ Logger.Error($"Unknown wire switch device \"{wireData.SourceDevice}\".");
+ break;
+ }
+
+ var deviceSwitch = _switchPlayer.Switch(wireData.SourceDevice, wireData.SourceDeviceItem);
+ if (deviceSwitch != null) {
+ deviceSwitch.RemoveWireDest(wireData.DestinationId);
+
+ } else {
+ Logger.Warn($"Unknown switch \"{wireData.Src}\" to wire to \"{wireData.Dst}\".");
+ }
+ break;
}
- }
- if (_keyWireAssignments.Count > 0) {
- _inputManager.Enable(HandleKeyInput);
+ case SwitchSource.Constant:
+ break;
+
+ default:
+ Logger.Warn($"Unknown wire switch source \"{wireData.Source}\".");
+ break;
}
}
@@ -163,6 +223,15 @@ public struct WireDestConfig
public readonly int PulseDelay;
public bool IsPulseSource;
+ ///
+ /// If the destination is dynamic, it means it was added during
+ /// gameplay. MPF does this, and it's called a "hardware rule". We tag
+ /// it as such here so we can filter when removing the wire.
+ ///
+ public bool IsDynamic;
+
+ public string DestinationId => Destination == WireDestination.Device ? DeviceItem : PlayfieldItem;
+
public WireDestConfig(MappingsWireData data)
{
Destination = data.Destination;
@@ -171,6 +240,7 @@ public WireDestConfig(MappingsWireData data)
DeviceItem = data.DestinationDeviceItem;
PulseDelay = data.PulseDelay;
IsPulseSource = false;
+ IsDynamic = false;
}
public WireDestConfig WithPulse(bool isPulseSource)
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Rendering/RenderPipeline.cs b/VisualPinball.Unity/VisualPinball.Unity/Rendering/RenderPipeline.cs
index 09ba9c0a2..e34e6128e 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Rendering/RenderPipeline.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Rendering/RenderPipeline.cs
@@ -106,7 +106,7 @@ public static IRenderPipeline Current {
? pipelines.First()
: pipelines.First(p => p.Type != RenderPipelineType.Standard);
- Logger.Info($"Instantiated ${_current.Name}.");
+ Logger.Info($"Instantiated {_current.Name}.");
}
return _current;
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs
index e68c47c32..5ef0a1960 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs
@@ -45,6 +45,7 @@ public BumperApi(Bumper item, Entity entity, Entity parentEntity, Player player)
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig) => AddSwitchDest(switchConfig.WithPulse(Item.IsPulseSwitch));
void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch));
+ void IApiSwitch.RemoveWireDest(string destId) => RemoveWireDest(destId);
void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity);
void IApiCoil.OnCoil(bool enabled, bool _)
{
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs
index 59b925525..0873e983a 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs
@@ -95,6 +95,7 @@ public void RotateToStart()
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig) => AddSwitchDest(switchConfig.WithPulse(Item.IsPulseSwitch));
void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch));
+ void IApiSwitch.RemoveWireDest(string destId) => RemoveWireDest(destId);
void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity);
void IApiCoil.OnCoil(bool enabled, bool isHoldCoil)
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs
index 5a7b90c32..946643fb8 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs
@@ -78,6 +78,7 @@ public GateApi(Engine.VPT.Gate.Gate item, Entity entity, Entity parentEntity, Pl
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig) => AddSwitchDest(switchConfig.WithPulse(Item.IsPulseSwitch));
void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch));
+ void IApiSwitch.RemoveWireDest(string destId) => RemoveWireDest(destId);
void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity);
#region Collider Generation
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs
index 9a94e2e5b..0a16b73c3 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs
@@ -87,6 +87,7 @@ private void SetIsDropped(bool isDropped)
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig) => AddSwitchDest(switchConfig.WithPulse(Item.IsPulseSwitch));
void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch));
+ void IApiSwitch.RemoveWireDest(string destId) => RemoveWireDest(destId);
void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity);
#region Collider Generation
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs
index 6a3eae8bd..f95d32251 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs
@@ -80,6 +80,12 @@ internal interface IApiSwitch
/// Configuration which game item to link to
void AddWireDest(WireDestConfig wireConfig);
+ ///
+ /// Removes a wire destination for this switch.
+ ///
+ ///
+ void RemoveWireDest(string destId);
+
void DestroyBall(Entity ballEntity);
///
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs
index aad437725..f29ff25a2 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs
@@ -24,7 +24,7 @@ namespace VisualPinball.Unity
///
/// Base class for all item APIs.
///
- /// Item type
+ /// Item type
/// Item data type
[Api]
public abstract class ItemApi : IApi where TItem : Item where TData : ItemData
@@ -140,6 +140,7 @@ void IApi.OnDestroy()
private protected IApiSwitchStatus AddSwitchDest(SwitchConfig switchConfig) => _switchHandler.AddSwitchDest(switchConfig);
internal void AddWireDest(WireDestConfig wireConfig) => _switchHandler.AddWireDest(wireConfig);
+ internal void RemoveWireDest(string destId) => _switchHandler.RemoveWireDest(destId);
private protected void OnSwitch(bool closed) => _switchHandler.OnSwitch(closed);
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs
index 5a7624d3a..c1d9f3e20 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs
@@ -185,6 +185,7 @@ private static void KickXYZ(Table table, Entity kickerEntity, float angle, float
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig) => AddSwitchDest(switchConfig.WithPulse(Item.IsPulseSwitch));
void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch));
+ void IApiSwitch.RemoveWireDest(string destId) => RemoveWireDest(destId);
#region Collider Generation
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs
index 1dbdf0226..0dc9136a2 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs
@@ -73,6 +73,7 @@ public SpinnerApi(Engine.VPT.Spinner.Spinner item, Entity entity, Entity parentE
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig) => AddSwitchDest(switchConfig.WithPulse(Item.IsPulseSwitch));
void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch));
+ void IApiSwitch.RemoveWireDest(string destId) => RemoveWireDest(destId);
void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity);
#region Collider Generation
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs
index b7ad930da..3818b23a1 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs
@@ -276,6 +276,18 @@ public Bounds GetTableBounds()
return tableBounds;
}
+ public void RepopulateHardware(IGamelogicEngine gle)
+ {
+ Mappings.RemoveAllSwitches();
+ Table.Mappings.PopulateSwitches(gle.AvailableSwitches, Table.Switchables, Table.SwitchableDevices);
+
+ Mappings.RemoveAllCoils();
+ Table.Mappings.PopulateCoils(gle.AvailableCoils, Table.Coilables, Table.CoilableDevices);
+
+ Mappings.RemoveAllLamps();
+ Table.Mappings.PopulateLamps(gle.AvailableLamps, Table.Lightables);
+ }
+
private void Restore(Table table) where TData : ItemData
where TItem : Item
where TComp : ItemMainAuthoring
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs
index d6ab9be22..e41232f45 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs
@@ -50,6 +50,7 @@ internal TriggerApi(Engine.VPT.Trigger.Trigger item, Entity entity, Entity paren
IApiSwitchStatus IApiSwitch.AddSwitchDest(SwitchConfig switchConfig) => AddSwitchDest(switchConfig.WithPulse(Item.IsPulseSwitch));
void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch));
+ void IApiSwitch.RemoveWireDest(string destId) => RemoveWireDest(destId);
void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity);
#region Collider Generation