diff --git a/VisualPinball.Engine.PinMAME.Unity/Runtime/PinMameGamelogicEngine.cs b/VisualPinball.Engine.PinMAME.Unity/Runtime/PinMameGamelogicEngine.cs
index 9ac44ca..8b8a090 100644
--- a/VisualPinball.Engine.PinMAME.Unity/Runtime/PinMameGamelogicEngine.cs
+++ b/VisualPinball.Engine.PinMAME.Unity/Runtime/PinMameGamelogicEngine.cs
@@ -1,23 +1,23 @@
-// Visual Pinball Engine
-// Copyright (C) 2021 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 .
-
-// ReSharper disable CheckNamespace
-// ReSharper disable InconsistentNaming
-// ReSharper disable PossibleNullReferenceException
-
+// Visual Pinball Engine
+// Copyright (C) 2021 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 .
+
+// ReSharper disable CheckNamespace
+// ReSharper disable InconsistentNaming
+// ReSharper disable PossibleNullReferenceException
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -27,44 +27,46 @@
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-using NLog;
-using PinMame;
+using NLog;
+using PinMame;
using UnityEngine;
using UnityEngine.InputSystem;
using VisualPinball.Engine.Game.Engines;
using VisualPinball.Unity;
using VisualPinball.Unity.Simulation;
using Logger = NLog.Logger;
-
-namespace VisualPinball.Engine.PinMAME
-{
+
+namespace VisualPinball.Engine.PinMAME
+{
[Serializable]
[DisallowMultipleComponent]
[RequireComponent(typeof(AudioSource))]
[PackAs("PinMameGamelogicEngine")]
[AddComponentMenu("Pinball/Gamelogic Engine/PinMAME")]
- public class PinMameGamelogicEngine : MonoBehaviour, IGamelogicEngine, IGamelogicInputThreading, IGamelogicTimeFence, IGamelogicCoilOutputFeed, IGamelogicSharedStateWriter, IGamelogicSharedStateApplier, IGamelogicPerformanceStats, IPackable
+ public class PinMameGamelogicEngine : MonoBehaviour, IGamelogicEngine, IDisplayFrameFormatPreference, IRomNameProvider, IGamelogicInputThreading, IGamelogicTimeFence, IGamelogicCoilOutputFeed, IGamelogicSharedStateWriter, IGamelogicSharedStateApplier, IGamelogicPerformanceStats, IPackable
{
public string Name { get; } = "PinMAME Gamelogic Engine";
public GamelogicInputDispatchMode SwitchDispatchMode => GamelogicInputDispatchMode.SimulationThread;
-
- public const string DmdPrefix = "dmd";
- public const string SegDispPrefix = "display";
-
- public PinMameGame Game { get => _game; set => _game = value; }
-
- #region Configuration
-
- [HideInInspector]
- public string romId = string.Empty;
-
- [Tooltip("Disable built-in mechs")]
- public bool DisableMechs = true;
-
- [Min(0f)]
- [Tooltip("Delay after startup to listen for solenoid events.")]
- public float SolenoidDelay;
-
+
+ public const string DmdPrefix = "dmd";
+ public const string SegDispPrefix = "display";
+
+ public PinMameGame Game { get => _game; set => _game = value; }
+
+ #region Configuration
+
+ [HideInInspector]
+ public string romId = string.Empty;
+
+ string IRomNameProvider.RomName => romId;
+
+ [Tooltip("Disable built-in mechs")]
+ public bool DisableMechs = true;
+
+ [Min(0f)]
+ [Tooltip("Delay after startup to listen for solenoid events.")]
+ public float SolenoidDelay;
+
[Tooltip("Disable audio")]
public bool DisableAudio;
@@ -158,34 +160,34 @@ private static PinMameGame CreateByTypeName(string gameType)
#endregion
#region IGamelogicEngine
-
- public GamelogicEngineSwitch[] RequestedSwitches {
- get {
- UpdateCaches();
- return _game?.AvailableSwitches ?? Array.Empty();
- }
- }
-
- public GamelogicEngineCoil[] RequestedCoils {
- get {
- UpdateCaches();
- return _coils.Values.ToArray();
- }
- }
-
- public GamelogicEngineLamp[] RequestedLamps {
- get {
- UpdateCaches();
- return _lamps.Values.ToArray();
- }
- }
-
- public GamelogicEngineWire[] AvailableWires => _game?.AvailableWires ?? Array.Empty();
-
- public event EventHandler OnCoilChanged;
- public event EventHandler OnSwitchChanged;
- public event EventHandler OnLampChanged;
- public event EventHandler OnLampsChanged;
+
+ public GamelogicEngineSwitch[] RequestedSwitches {
+ get {
+ UpdateCaches();
+ return _game?.AvailableSwitches ?? Array.Empty();
+ }
+ }
+
+ public GamelogicEngineCoil[] RequestedCoils {
+ get {
+ UpdateCaches();
+ return _coils.Values.ToArray();
+ }
+ }
+
+ public GamelogicEngineLamp[] RequestedLamps {
+ get {
+ UpdateCaches();
+ return _lamps.Values.ToArray();
+ }
+ }
+
+ public GamelogicEngineWire[] AvailableWires => _game?.AvailableWires ?? Array.Empty();
+
+ public event EventHandler OnCoilChanged;
+ public event EventHandler OnSwitchChanged;
+ public event EventHandler OnLampChanged;
+ public event EventHandler OnLampsChanged;
public event EventHandler OnDisplaysRequested;
public event EventHandler OnDisplayClear;
public event EventHandler OnDisplayUpdateFrame;
@@ -277,33 +279,34 @@ public void ApplySharedState(in SimulationState.Snapshot snapshot)
}
#endregion
-
- #region Internals
-
- [NonSerialized] private Player _player;
- [NonSerialized] private PinMame.PinMame _pinMame;
- [NonSerialized] private BallManager _ballManager;
- [NonSerialized] private PlayfieldComponent _playfieldComponent;
-
- [NonSerialized] private readonly List _changedLamps = new();
- [NonSerialized] private readonly List _changedGIs = new();
-
- [SerializeReference] private PinMameGame _game;
-
- private Dictionary _switches = new();
- private Dictionary _pinMameIdToSwitchIdMappings = new();
- private Dictionary _switchIdToPinMameIdMappings = new();
-
- private Dictionary _coils = new();
- private Dictionary _pinMameIdToCoilIdMapping = new();
- private Dictionary _coilIdToPinMameIdMapping = new();
-
- private Dictionary _lamps = new();
- private Dictionary _pinMameIdToLampIdMapping = new();
-
+
+ #region Internals
+
+ [NonSerialized] private Player _player;
+ [NonSerialized] private PinMame.PinMame _pinMame;
+ [NonSerialized] private BallManager _ballManager;
+ [NonSerialized] private PlayfieldComponent _playfieldComponent;
+
+ [NonSerialized] private readonly List _changedLamps = new();
+ [NonSerialized] private readonly List _changedGIs = new();
+
+ [SerializeReference] private PinMameGame _game;
+
+ private Dictionary _switches = new();
+ private Dictionary _pinMameIdToSwitchIdMappings = new();
+ private Dictionary _switchIdToPinMameIdMappings = new();
+
+ private Dictionary _coils = new();
+ private Dictionary _pinMameIdToCoilIdMapping = new();
+ private Dictionary _coilIdToPinMameIdMapping = new();
+
+ private Dictionary _lamps = new();
+ private Dictionary _pinMameIdToLampIdMapping = new();
+
private volatile bool _isRunning;
- private int _numMechs;
+ private int _numMechs;
private Dictionary _frameBuffer = new();
+ private volatile DisplayFrameFormat _requestedDmdFrameFormat = DisplayFrameFormat.Dmd8;
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static readonly Color Tint = new(1, 0.18f, 0);
@@ -326,30 +329,35 @@ public void ApplySharedState(in SimulationState.Snapshot snapshot)
private int _pinMameCallbacksInWindow;
private float _pinMameCallbackRateHz;
private readonly Dictionary _sharedCoilStates = new();
+ // Solenoid callbacks report 0/1 for binary coils, but 0-255 for PWM-integrated
+ // (modulated) coils on WPC/SAM ROMs. We can't tell from a single value whether a
+ // "1" is "binary on" or "modulated 0.4% duty"; once any coil reports a value above
+ // 1 the game is modulating, and every coil value is then normalized by 255.
+ private volatile bool _solenoidsModulated;
private readonly Dictionary _sharedLampStates = new();
private readonly Dictionary _sharedGiStates = new();
private readonly Dictionary _lastAppliedLampStates = new();
private readonly Dictionary _lastAppliedGiStates = new();
private bool _sharedLampPlaybackActive;
private readonly Queue _audioQueue = new();
-
+
private int _audioFilterChannels;
- private PinMameAudioInfo _audioInfo;
- private float[] _lastAudioFrame = {};
- private int _lastAudioFrameOffset;
- private const int _maximalQueueSize = 10;
-
- private double _audioInputStart;
- private double _audioOutputStart;
- private int _audioNumSamplesInput;
- private int _audioNumSamplesOutput;
-
- public bool _solenoidsEnabled;
- public long _solenoidDelayStart;
+ private PinMameAudioInfo _audioInfo;
+ private float[] _lastAudioFrame = {};
+ private int _lastAudioFrameOffset;
+ private const int _maximalQueueSize = 10;
+
+ private double _audioInputStart;
+ private double _audioOutputStart;
+ private int _audioNumSamplesInput;
+ private int _audioNumSamplesOutput;
+
+ public bool _solenoidsEnabled;
+ public long _solenoidDelayStart;
private Dictionary _registeredMechs = new();
private Dictionary _registeredMechNames = new();
private HashSet _mechSwitches = new();
-
+
private bool _toggleSpeed = false;
private Keyboard _keyboard;
@@ -362,23 +370,23 @@ public void ApplySharedState(in SimulationState.Snapshot snapshot)
private const int StopTimeoutMs = 10000;
#endregion
-
- #region Lifecycle
-
+
+ #region Lifecycle
+
private void Awake()
{
Logger.Info("Project audio sample rate: " + AudioSettings.outputSampleRate);
_keyboard = Keyboard.current;
}
-
- private void Start()
- {
- UpdateCaches();
-
- _lastAudioFrame = Array.Empty();
- _lastAudioFrameOffset = 0;
- }
-
+
+ private void Start()
+ {
+ UpdateCaches();
+
+ _lastAudioFrame = Array.Empty();
+ _lastAudioFrameOffset = 0;
+ }
+
private void Update()
{
if (_pinMame == null || !_isRunning) {
@@ -416,8 +424,8 @@ private void Update()
LogUnmappedLampSample("lamp", changedLamp.Id, changedLamp.Value, LampSource.Lamp);
}
}
-
- // gi
+
+ // gi
_pinMame.GetChangedGIs(_changedGIs);
foreach (var changedGi in _changedGIs) {
lock (_outputStateLock) {
@@ -431,11 +439,11 @@ private void Update()
LogUnmappedLampSample("gi", changedGi.Id, changedGi.Value, LampSource.GI);
}
}
-
+
// if (_keyboard != null && _keyboard.cKey.wasPressedThisFrame)
- // {
- // OnCoilChanged.Invoke(this, new CoilEventArgs("28", true));
- // OnCoilChanged.Invoke(this, new CoilEventArgs("28", false));
+ // {
+ // OnCoilChanged.Invoke(this, new CoilEventArgs("28", true));
+ // OnCoilChanged.Invoke(this, new CoilEventArgs("28", false));
// }
}
@@ -537,21 +545,22 @@ private void OnDestroy()
_pinMame.OnGameEnded -= OnGameEnded;
_pinMame.OnDisplayAvailable -= OnDisplayRequested;
_pinMame.OnDisplayUpdated -= OnDisplayUpdated;
-
- if (!DisableAudio)
- {
- _pinMame.OnAudioAvailable -= OnAudioAvailable;
- _pinMame.OnAudioUpdated -= OnAudioUpdated;
- }
-
- _pinMame.OnMechAvailable -= OnMechAvailable;
- _pinMame.OnMechUpdated -= OnMechUpdated;
- _pinMame.OnSolenoidUpdated -= OnSolenoidUpdated;
- _pinMame.IsKeyPressed -= IsKeyPressed;
+
+ if (!DisableAudio)
+ {
+ _pinMame.OnAudioAvailable -= OnAudioAvailable;
+ _pinMame.OnAudioUpdated -= OnAudioUpdated;
+ }
+
+ _pinMame.OnMechAvailable -= OnMechAvailable;
+ _pinMame.OnMechUpdated -= OnMechUpdated;
+ _pinMame.OnSolenoidUpdated -= OnSolenoidUpdated;
+ _pinMame.IsKeyPressed -= IsKeyPressed;
}
_frameBuffer.Clear();
lock (_outputStateLock) {
_sharedCoilStates.Clear();
+ _solenoidsModulated = false;
_sharedLampStates.Clear();
_sharedGiStates.Clear();
}
@@ -681,7 +690,7 @@ private void RequestStopGame(string reason)
}
});
}
-
+
public async Task OnInit(Player player, TableApi tableApi, BallManager ballManager, CancellationToken ct)
{
if (Interlocked.Exchange(ref _onInitCalled, 1) != 0) {
@@ -692,29 +701,29 @@ public async Task OnInit(Player player, TableApi tableApi, BallManager ballManag
string vpmPath = null;
_ballManager = ballManager;
_playfieldComponent = GetComponentInChildren();
-
- #if (UNITY_IOS || UNITY_ANDROID) && !UNITY_EDITOR
- vpmPath = Path.Combine(Application.persistentDataPath, "pinmame");
- Directory.CreateDirectory(Path.Combine(vpmPath, "roms"));
-
- byte[] data = null;
-
- #if UNITY_IOS
- data = File.ReadAllBytes(Path.Combine(Application.streamingAssetsPath, $"{romId}.zip"));
- #else
- UnityWebRequest webRequest = new UnityWebRequest(Path.Combine(Application.streamingAssetsPath, $"{romId}.zip"));
-
- webRequest.downloadHandler = new DownloadHandlerBuffer();
- webRequest.SendWebRequest();
-
- while (!webRequest.isDone) { };
-
- data = webRequest.downloadHandler.data;
- #endif
-
- File.WriteAllBytes(Path.Combine(vpmPath, "roms", $"{romId}.zip"), data);
- #endif
-
+
+ #if (UNITY_IOS || UNITY_ANDROID) && !UNITY_EDITOR
+ vpmPath = Path.Combine(Application.persistentDataPath, "pinmame");
+ Directory.CreateDirectory(Path.Combine(vpmPath, "roms"));
+
+ byte[] data = null;
+
+ #if UNITY_IOS
+ data = File.ReadAllBytes(Path.Combine(Application.streamingAssetsPath, $"{romId}.zip"));
+ #else
+ UnityWebRequest webRequest = new UnityWebRequest(Path.Combine(Application.streamingAssetsPath, $"{romId}.zip"));
+
+ webRequest.downloadHandler = new DownloadHandlerBuffer();
+ webRequest.SendWebRequest();
+
+ while (!webRequest.isDone) { };
+
+ data = webRequest.downloadHandler.data;
+ #endif
+
+ File.WriteAllBytes(Path.Combine(vpmPath, "roms", $"{romId}.zip"), data);
+ #endif
+
await PinMameStartStopGate.WaitAsync(ct);
try {
Logger.Info($"[PinMAME] OnInit {name}: romId={romId}, sampleRate={AudioSettings.outputSampleRate}");
@@ -796,6 +805,7 @@ public async Task OnInit(Player player, TableApi tableApi, BallManager ballManag
_pinMame.SetHandleKeyboard(false);
_pinMame.SetHandleMechanics(DisableMechs ? 0 : 0xFF);
+ ApplyDmdModePreference();
SetTimeFence(0.01);
_pinMame.OnGameStarted += OnGameStarted;
@@ -841,15 +851,15 @@ public async Task OnInit(Player player, TableApi tableApi, BallManager ballManag
PinMameStartStopGate.Release();
}
}
-
- public void ToggleSpeed()
- {
+
+ public void ToggleSpeed()
+ {
Logger.Info("[PinMAME] Toggle speed.");
-
- _pinMame.SetHandleKeyboard(true);
- _toggleSpeed = true;
- }
-
+
+ _pinMame.SetHandleKeyboard(true);
+ _toggleSpeed = true;
+ }
+
private void OnGameStarted()
{
Logger.Info("[PinMAME] Game started.");
@@ -857,19 +867,19 @@ private void OnGameStarted()
_solenoidDelayStartTimestampUsec = TimestampUsec;
_solenoidDelayStart = _solenoidDelayStartTimestampUsec / 1000;
-
- try {
- SendInitialSwitches();
- SendMechs();
- }
-
- catch(Exception e) {
+
+ try {
+ SendInitialSwitches();
+ SendMechs();
+ }
+
+ catch(Exception e) {
Logger.Error($"[PinMAME] OnGameStarted: {e.Message}");
- }
-
+ }
+
EnqueueMainThreadDispatch(() => OnStarted?.Invoke(this, EventArgs.Empty));
- }
-
+ }
+
private void OnGameEnded()
{
// Native can report ended more than once during teardown; keep it idempotent.
@@ -884,6 +894,7 @@ private void OnGameEnded()
}
lock (_outputStateLock) {
_sharedCoilStates.Clear();
+ _solenoidsModulated = false;
_sharedLampStates.Clear();
_sharedGiStates.Clear();
}
@@ -891,90 +902,108 @@ private void OnGameEnded()
_lastAppliedGiStates.Clear();
_sharedLampPlaybackActive = false;
}
-
- private void UpdateCaches()
- {
- if (_game == null) {
- return;
- }
-
- _lamps.Clear();
- _pinMameIdToLampIdMapping.Clear();
-
- _coils.Clear();
- _pinMameIdToCoilIdMapping.Clear();
-
- _switches.Clear();
- _pinMameIdToSwitchIdMappings.Clear();
- _switchIdToPinMameIdMappings.Clear();
-
- // check aliases first (the switches/coils that aren't an integer)
- foreach (var alias in _game.AvailableAliases) {
- switch (alias.AliasType) {
- case AliasType.Switch:
- _pinMameIdToSwitchIdMappings[alias.Id] = alias.Name;
- _switchIdToPinMameIdMappings[alias.Name] = alias.Id;
- break;
-
-
- case AliasType.Coil:
- _pinMameIdToCoilIdMapping[alias.Id] = alias.Name;
- _coilIdToPinMameIdMapping[alias.Name] = alias.Id;
- break;
-
- case AliasType.Lamp:
- _pinMameIdToLampIdMapping[alias.Id] = alias.Name;
- break;
- }
- }
-
- // retrieve the game's switches
- foreach (var @switch in _game.AvailableSwitches) {
- _switches[@switch.Id] = @switch;
-
- if (int.TryParse(@switch.Id, out var pinMameId)) {
- _pinMameIdToSwitchIdMappings[pinMameId] = @switch.Id;
- _switchIdToPinMameIdMappings[@switch.Id] = pinMameId;
-
- // add mappings with prefixed 0.
- if (pinMameId < 10) {
- _switchIdToPinMameIdMappings["0" + @switch.Id] = pinMameId;
- _switchIdToPinMameIdMappings["00" + @switch.Id] = pinMameId;
-
- _switches["0" + @switch.Id] = @switch;
- _switches["00" + @switch.Id] = @switch;
- }
- if (pinMameId < 100) {
- _switchIdToPinMameIdMappings["0" + @switch.Id] = pinMameId;
- _switches["0" + @switch.Id] = @switch;
- }
- }
- }
-
- // retrieve the game's coils
- foreach (var coil in _game.AvailableCoils) {
- _coils[coil.Id] = coil;
-
- if (int.TryParse(coil.Id, out int pinMameId)) {
- _pinMameIdToCoilIdMapping[pinMameId] = coil.Id;
- _coilIdToPinMameIdMapping[coil.Id] = pinMameId;
- }
- }
-
- // retrieve the game's lamps
- foreach (var lamp in _game.AvailableLamps) {
- _lamps[lamp.Id] = lamp;
-
- if (int.TryParse(lamp.Id, out int pinMameId)) {
- _pinMameIdToLampIdMapping[pinMameId] = lamp.Id;
- }
- }
- }
-
- #endregion
-
- #region Displays
-
+
+ private void UpdateCaches()
+ {
+ if (_game == null) {
+ return;
+ }
+
+ _lamps.Clear();
+ _pinMameIdToLampIdMapping.Clear();
+
+ _coils.Clear();
+ _pinMameIdToCoilIdMapping.Clear();
+
+ _switches.Clear();
+ _pinMameIdToSwitchIdMappings.Clear();
+ _switchIdToPinMameIdMappings.Clear();
+
+ // check aliases first (the switches/coils that aren't an integer)
+ foreach (var alias in _game.AvailableAliases) {
+ switch (alias.AliasType) {
+ case AliasType.Switch:
+ _pinMameIdToSwitchIdMappings[alias.Id] = alias.Name;
+ _switchIdToPinMameIdMappings[alias.Name] = alias.Id;
+ break;
+
+
+ case AliasType.Coil:
+ _pinMameIdToCoilIdMapping[alias.Id] = alias.Name;
+ _coilIdToPinMameIdMapping[alias.Name] = alias.Id;
+ break;
+
+ case AliasType.Lamp:
+ _pinMameIdToLampIdMapping[alias.Id] = alias.Name;
+ break;
+ }
+ }
+
+ // retrieve the game's switches
+ foreach (var @switch in _game.AvailableSwitches) {
+ _switches[@switch.Id] = @switch;
+
+ if (int.TryParse(@switch.Id, out var pinMameId)) {
+ _pinMameIdToSwitchIdMappings[pinMameId] = @switch.Id;
+ _switchIdToPinMameIdMappings[@switch.Id] = pinMameId;
+
+ // add mappings with prefixed 0.
+ if (pinMameId < 10) {
+ _switchIdToPinMameIdMappings["0" + @switch.Id] = pinMameId;
+ _switchIdToPinMameIdMappings["00" + @switch.Id] = pinMameId;
+
+ _switches["0" + @switch.Id] = @switch;
+ _switches["00" + @switch.Id] = @switch;
+ }
+ if (pinMameId < 100) {
+ _switchIdToPinMameIdMappings["0" + @switch.Id] = pinMameId;
+ _switches["0" + @switch.Id] = @switch;
+ }
+ }
+ }
+
+ // retrieve the game's coils
+ foreach (var coil in _game.AvailableCoils) {
+ _coils[coil.Id] = coil;
+
+ if (int.TryParse(coil.Id, out int pinMameId)) {
+ _pinMameIdToCoilIdMapping[pinMameId] = coil.Id;
+ _coilIdToPinMameIdMapping[coil.Id] = pinMameId;
+ }
+ }
+
+ // retrieve the game's lamps
+ foreach (var lamp in _game.AvailableLamps) {
+ _lamps[lamp.Id] = lamp;
+
+ if (int.TryParse(lamp.Id, out int pinMameId)) {
+ _pinMameIdToLampIdMapping[pinMameId] = lamp.Id;
+ }
+ }
+ }
+
+ #endregion
+
+ #region Displays
+
+ public void RequestDisplayFrameFormat(string displayId, DisplayFrameFormat format)
+ {
+ if (!string.IsNullOrWhiteSpace(displayId) && !displayId.StartsWith(DmdPrefix, StringComparison.OrdinalIgnoreCase)) {
+ return;
+ }
+
+ if (format != DisplayFrameFormat.Dmd2 && format != DisplayFrameFormat.Dmd4 && format != DisplayFrameFormat.Dmd8) {
+ return;
+ }
+
+ if (_requestedDmdFrameFormat == format) {
+ return;
+ }
+
+ _requestedDmdFrameFormat = format;
+ ApplyDmdModePreference();
+ }
+
private void OnDisplayRequested(int index, int displayCount, PinMameDisplayLayout displayLayout)
{
if (displayLayout.IsDmd) {
@@ -997,7 +1026,7 @@ private void OnDisplayRequested(int index, int displayCount, PinMameDisplayLayou
Logger.Info($"[PinMAME] Display {SegDispPrefix}{index} is of type {displayLayout.Type} at {displayLayout.Length} wide.");
}
}
-
+
private void OnDisplayUpdated(int index, IntPtr framePtr, PinMameDisplayLayout displayLayout)
{
MarkPinMameCallbackActivity();
@@ -1006,12 +1035,17 @@ private void OnDisplayUpdated(int index, IntPtr framePtr, PinMameDisplayLayout d
UpdateDmd(index, displayLayout, framePtr);
} else {
- UpdateSegDisp(index, displayLayout, framePtr);
- }
- }
-
+ UpdateSegDisp(index, displayLayout, framePtr);
+ }
+ }
+
private void UpdateDmd(int index, PinMameDisplayLayout displayLayout, IntPtr framePtr)
{
+ if (framePtr == IntPtr.Zero) {
+ Logger.Warn($"[PinMAME] Dropping DMD frame for index {index} because frame pointer is null (layout: {displayLayout}).");
+ return;
+ }
+
byte[] frameBuffer;
lock (_displayStateLock) {
if (!_frameBuffer.TryGetValue(index, out frameBuffer)) {
@@ -1020,14 +1054,23 @@ private void UpdateDmd(int index, PinMameDisplayLayout displayLayout, IntPtr fra
}
}
- Marshal.Copy(framePtr, frameBuffer, 0, frameBuffer.Length);
+ // Snapshot into a fresh array: the native PinMAME thread can fire UpdateDmd again before
+ // the queued dispatch runs on the main thread, so the frame handed to subscribers must not
+ // be the reused _frameBuffer (otherwise it can be overwritten / torn mid-read).
+ var frame = new byte[frameBuffer.Length];
+ Marshal.Copy(framePtr, frame, 0, frame.Length);
EnqueueMainThreadDispatch(() => OnDisplayUpdateFrame?.Invoke(this,
- new DisplayFrameData($"{DmdPrefix}{index}", GetDisplayFrameFormat(displayLayout), frameBuffer)));
+ new DisplayFrameData($"{DmdPrefix}{index}", GetRequestedDmdDisplayFrameFormat(displayLayout), frame)));
}
private void UpdateSegDisp(int index, PinMameDisplayLayout displayLayout, IntPtr framePtr)
{
+ if (framePtr == IntPtr.Zero) {
+ Logger.Warn($"[PinMAME] Dropping segmented display frame for index {index} because frame pointer is null (layout: {displayLayout}).");
+ return;
+ }
+
byte[] frameBuffer;
lock (_displayStateLock) {
if (!_frameBuffer.TryGetValue(index, out frameBuffer)) {
@@ -1036,196 +1079,236 @@ private void UpdateSegDisp(int index, PinMameDisplayLayout displayLayout, IntPtr
}
}
- Marshal.Copy(framePtr, frameBuffer, 0, displayLayout.Length * 2);
+ // Snapshot into a fresh array (see UpdateDmd) so a follow-up native callback can't
+ // overwrite a frame already queued for the main thread.
+ var frame = new byte[frameBuffer.Length];
+ Marshal.Copy(framePtr, frame, 0, displayLayout.Length * 2);
- //Logger.Info($"[PinMAME] Seg data ({index}): {BitConverter.ToString(_frameBuffer[index])}" );
EnqueueMainThreadDispatch(() => OnDisplayUpdateFrame?.Invoke(this,
- new DisplayFrameData($"{SegDispPrefix}{index}", GetDisplayFrameFormat(displayLayout), frameBuffer)));
+ new DisplayFrameData($"{SegDispPrefix}{index}", GetDisplayFrameFormat(displayLayout), frame)));
}
-
+
public static DisplayFrameFormat GetDisplayFrameFormat(PinMameDisplayLayout layout)
{
if (layout.IsDmd) {
return DisplayFrameFormat.Dmd8;
}
-
- switch (layout.Type) {
- case PinMameDisplayType.Seg8: // 7 segments and comma
- case PinMameDisplayType.Seg7SC: // 7 segments, small, with comma
- case PinMameDisplayType.Seg8D: // 7 segments and period
- case PinMameDisplayType.Seg7: // 7 segments
- case PinMameDisplayType.Seg7S: // 7 segments, small
- case PinMameDisplayType.Seg87: // 7 segments, comma every three
- case PinMameDisplayType.Seg87F: // 7 segments, forced comma every three
- case PinMameDisplayType.Seg10: // 9 segments and comma
- case PinMameDisplayType.Seg9: // 9 segments
- case PinMameDisplayType.Seg98: // 9 segments, comma every three
- case PinMameDisplayType.Seg98F: // 9 segments, forced comma every three
- case PinMameDisplayType.Seg16: // 16 segments
- case PinMameDisplayType.Seg16R: // 16 segments with comma and period reversed
- case PinMameDisplayType.Seg16N: // 16 segments without commas
- case PinMameDisplayType.Seg16D: // 16 segments with periods only
- case PinMameDisplayType.Seg16S: // 16 segments with split top and bottom line
- case PinMameDisplayType.Seg8H:
- case PinMameDisplayType.Seg7H:
- case PinMameDisplayType.Seg87H:
- case PinMameDisplayType.Seg87FH:
- case PinMameDisplayType.Seg7SH:
- case PinMameDisplayType.Seg7SCH:
- case PinMameDisplayType.Seg7 | PinMameDisplayType.NoDisp:
- return DisplayFrameFormat.Segment;
-
- case PinMameDisplayType.Video:
- break;
-
- case PinMameDisplayType.SegAll:
- case PinMameDisplayType.Import:
- case PinMameDisplayType.SegMask:
- case PinMameDisplayType.SegHiBit:
- case PinMameDisplayType.SegRev:
- case PinMameDisplayType.DmdNoAA:
- case PinMameDisplayType.NoDisp:
- throw new ArgumentOutOfRangeException(nameof(layout), layout, null);
-
- default:
- throw new ArgumentOutOfRangeException(nameof(layout), layout, null);
- }
-
- throw new NotImplementedException($"Still unsupported segmented display format: {layout}.");
- }
-
- public void DisplayChanged(DisplayFrameData displayFrameData)
- {
- }
-
- #endregion
-
- #region Audio
-
- private int OnAudioAvailable(PinMameAudioInfo audioInfo)
- {
- Logger.Info("Game audio available: " + audioInfo);
-
- _audioInfo = audioInfo;
- return _audioInfo.SamplesPerFrame;
- }
-
+
+ switch (layout.Type) {
+ case PinMameDisplayType.Seg8: // 7 segments and comma
+ case PinMameDisplayType.Seg7SC: // 7 segments, small, with comma
+ case PinMameDisplayType.Seg8D: // 7 segments and period
+ case PinMameDisplayType.Seg7: // 7 segments
+ case PinMameDisplayType.Seg7S: // 7 segments, small
+ case PinMameDisplayType.Seg87: // 7 segments, comma every three
+ case PinMameDisplayType.Seg87F: // 7 segments, forced comma every three
+ case PinMameDisplayType.Seg10: // 9 segments and comma
+ case PinMameDisplayType.Seg9: // 9 segments
+ case PinMameDisplayType.Seg98: // 9 segments, comma every three
+ case PinMameDisplayType.Seg98F: // 9 segments, forced comma every three
+ case PinMameDisplayType.Seg16: // 16 segments
+ case PinMameDisplayType.Seg16R: // 16 segments with comma and period reversed
+ case PinMameDisplayType.Seg16N: // 16 segments without commas
+ case PinMameDisplayType.Seg16D: // 16 segments with periods only
+ case PinMameDisplayType.Seg16S: // 16 segments with split top and bottom line
+ case PinMameDisplayType.Seg8H:
+ case PinMameDisplayType.Seg7H:
+ case PinMameDisplayType.Seg87H:
+ case PinMameDisplayType.Seg87FH:
+ case PinMameDisplayType.Seg7SH:
+ case PinMameDisplayType.Seg7SCH:
+ case PinMameDisplayType.Seg7 | PinMameDisplayType.NoDisp:
+ return DisplayFrameFormat.Segment;
+
+ case PinMameDisplayType.Video:
+ break;
+
+ case PinMameDisplayType.SegAll:
+ case PinMameDisplayType.Import:
+ case PinMameDisplayType.SegMask:
+ case PinMameDisplayType.SegHiBit:
+ case PinMameDisplayType.SegRev:
+ case PinMameDisplayType.DmdNoAA:
+ case PinMameDisplayType.NoDisp:
+ throw new ArgumentOutOfRangeException(nameof(layout), layout, null);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(layout), layout, null);
+ }
+
+ throw new NotImplementedException($"Still unsupported segmented display format: {layout}.");
+ }
+
+ private DisplayFrameFormat GetRequestedDmdDisplayFrameFormat(PinMameDisplayLayout layout)
+ {
+ if (_requestedDmdFrameFormat == DisplayFrameFormat.Dmd8) {
+ return DisplayFrameFormat.Dmd8;
+ }
+
+ return GetRawDmdDisplayFrameFormat(layout);
+ }
+
+ private static DisplayFrameFormat GetRawDmdDisplayFrameFormat(PinMameDisplayLayout layout)
+ {
+ var levels = layout.Levels?.Count ?? 0;
+ if ((levels > 0 && levels <= 4) || (layout.Depth > 0 && layout.Depth <= 2)) {
+ return DisplayFrameFormat.Dmd2;
+ }
+
+ if ((levels > 0 && levels <= 16) || (layout.Depth > 0 && layout.Depth <= 4)) {
+ return DisplayFrameFormat.Dmd4;
+ }
+
+ return DisplayFrameFormat.Dmd8;
+ }
+
+ private void ApplyDmdModePreference()
+ {
+ if (_pinMame == null) {
+ return;
+ }
+
+ var rawMode = _requestedDmdFrameFormat == DisplayFrameFormat.Dmd2 || _requestedDmdFrameFormat == DisplayFrameFormat.Dmd4;
+ try {
+ _pinMame.SetDmdMode(rawMode ? PinMameDmdMode.Raw : PinMameDmdMode.Brightness);
+ Logger.Info($"[PinMAME] DMD frame mode set to {(rawMode ? "raw" : "brightness")} for {_requestedDmdFrameFormat} output.");
+ } catch (Exception exception) {
+ Logger.Warn(exception, "[PinMAME] Could not set DMD frame mode.");
+ }
+ }
+
+ public void DisplayChanged(DisplayFrameData displayFrameData)
+ {
+ }
+
+ #endregion
+
+ #region Audio
+
+ private int OnAudioAvailable(PinMameAudioInfo audioInfo)
+ {
+ Logger.Info("Game audio available: " + audioInfo);
+
+ _audioInfo = audioInfo;
+ return _audioInfo.SamplesPerFrame;
+ }
+
private int OnAudioUpdated(IntPtr framePtr, int frameSize)
{
if (_audioFilterChannels == 0) {
// don't know how many channels yet
return _audioInfo.SamplesPerFrame;
- }
-
- _audioNumSamplesInput += frameSize;
- if (_audioNumSamplesInput > 100000) {
- // var delta = AudioSettings.dspTime - _audioInputStart;
- // var queueMs = System.Math.Round(_audioQueue.Count * (double)_audioInfo.SamplesPerFrame / _audioInfo.SampleRate * 1000);
- // Logger.Info($"INPUT: {System.Math.Round(_audioNumSamplesInput / delta)} - {_audioQueue.Count} in queue ({queueMs}ms)");
- _audioInputStart = AudioSettings.dspTime;
- _audioNumSamplesInput = 0;
- }
-
- float[] frame;
- if (_audioFilterChannels == _audioInfo.Channels) { // n channels -> n channels
- frame = new float[frameSize * 2];
- unsafe {
- var src = (float*)framePtr;
- for (var i = 0; i < frameSize * 2; i++) {
- frame[i] = src[i];
- }
- }
- } else if (_audioFilterChannels > _audioInfo.Channels) { // 1 channel -> 2 channels
- frame = new float[frameSize * 2];
- unsafe {
- var src = (float*)framePtr;
- for (var i = 0; i < frameSize; i++) {
- frame[i * 2] = src[i];
- frame[i * 2 + 1] = frame[i * 2];
- }
- }
- } else { // 2 channels -> 1 channel
- frame = new float[frameSize / 2];
- unsafe {
- var src = (float*)framePtr;
- for (var i = 0; i < frameSize; i += 2) {
- frame[i] = src[i];
- }
- }
- }
-
- lock (_audioQueue) {
- if (_audioQueue.Count >= _maximalQueueSize) {
- _audioQueue.Clear();
- Logger.Error("Clearing full audio frame queue.");
- }
- _audioQueue.Enqueue(frame);
- }
-
- return _audioInfo.SamplesPerFrame;
- }
-
- private void OnAudioFilterRead(float[] data, int channels)
- {
- _audioNumSamplesOutput += data.Length / channels;
- if (_audioNumSamplesOutput > 100000) {
- //var delta = AudioSettings.dspTime - _audioOutputStart;
- //Logger.Info($"OUTPUT: {System.Math.Round(_audioNumSamplesOutput / delta)}");
- _audioOutputStart = AudioSettings.dspTime;
- _audioNumSamplesOutput = 0;
- }
-
- if (_audioFilterChannels == 0) {
- _audioFilterChannels = channels;
- Logger.Info($"Creating audio on {channels} channels.");
- return;
- }
-
- if (_audioQueue.Count == 0) {
- if (!DisableAudio) {
- Logger.Info("Filtering audio but nothing to de-queue.");
- }
- return;
- }
-
-
- const int size = sizeof(float);
- var dataOffset = 0;
- var lastFrameSize = _lastAudioFrame.Length - _lastAudioFrameOffset;
- if (data.Length >= lastFrameSize) {
- if (lastFrameSize > 0) {
- Buffer.BlockCopy(_lastAudioFrame, _lastAudioFrameOffset * size, data, 0, lastFrameSize * size);
- dataOffset += lastFrameSize;
- }
- _lastAudioFrame = Array.Empty();
- _lastAudioFrameOffset = 0;
-
- lock (_audioQueue) {
- while (dataOffset < data.Length && _audioQueue.Count > 0) {
- var frame = _audioQueue.Dequeue();
- if (frame.Length <= data.Length - dataOffset) {
- Buffer.BlockCopy(frame, 0, data, dataOffset * size, frame.Length * size);
- dataOffset += frame.Length;
-
- } else {
- Buffer.BlockCopy(frame, 0, data, dataOffset * size, (data.Length - dataOffset) * size);
- _lastAudioFrame = frame;
- _lastAudioFrameOffset = data.Length - dataOffset;
- dataOffset = data.Length;
- }
- }
- }
-
- } else {
- Buffer.BlockCopy(_lastAudioFrame, _lastAudioFrameOffset * size, data, 0, data.Length * size);
- _lastAudioFrameOffset += data.Length;
- }
- }
-
- #endregion
-
- #region Coils
-
+ }
+
+ _audioNumSamplesInput += frameSize;
+ if (_audioNumSamplesInput > 100000) {
+ // var delta = AudioSettings.dspTime - _audioInputStart;
+ // var queueMs = System.Math.Round(_audioQueue.Count * (double)_audioInfo.SamplesPerFrame / _audioInfo.SampleRate * 1000);
+ // Logger.Info($"INPUT: {System.Math.Round(_audioNumSamplesInput / delta)} - {_audioQueue.Count} in queue ({queueMs}ms)");
+ _audioInputStart = AudioSettings.dspTime;
+ _audioNumSamplesInput = 0;
+ }
+
+ float[] frame;
+ if (_audioFilterChannels == _audioInfo.Channels) { // n channels -> n channels
+ frame = new float[frameSize * 2];
+ unsafe {
+ var src = (float*)framePtr;
+ for (var i = 0; i < frameSize * 2; i++) {
+ frame[i] = src[i];
+ }
+ }
+ } else if (_audioFilterChannels > _audioInfo.Channels) { // 1 channel -> 2 channels
+ frame = new float[frameSize * 2];
+ unsafe {
+ var src = (float*)framePtr;
+ for (var i = 0; i < frameSize; i++) {
+ frame[i * 2] = src[i];
+ frame[i * 2 + 1] = frame[i * 2];
+ }
+ }
+ } else { // 2 channels -> 1 channel
+ frame = new float[frameSize / 2];
+ unsafe {
+ var src = (float*)framePtr;
+ for (var i = 0; i < frameSize; i += 2) {
+ frame[i] = src[i];
+ }
+ }
+ }
+
+ lock (_audioQueue) {
+ if (_audioQueue.Count >= _maximalQueueSize) {
+ _audioQueue.Clear();
+ Logger.Error("Clearing full audio frame queue.");
+ }
+ _audioQueue.Enqueue(frame);
+ }
+
+ return _audioInfo.SamplesPerFrame;
+ }
+
+ private void OnAudioFilterRead(float[] data, int channels)
+ {
+ _audioNumSamplesOutput += data.Length / channels;
+ if (_audioNumSamplesOutput > 100000) {
+ //var delta = AudioSettings.dspTime - _audioOutputStart;
+ //Logger.Info($"OUTPUT: {System.Math.Round(_audioNumSamplesOutput / delta)}");
+ _audioOutputStart = AudioSettings.dspTime;
+ _audioNumSamplesOutput = 0;
+ }
+
+ if (_audioFilterChannels == 0) {
+ _audioFilterChannels = channels;
+ Logger.Info($"Creating audio on {channels} channels.");
+ return;
+ }
+
+ if (_audioQueue.Count == 0) {
+ if (!DisableAudio) {
+ Logger.Info("Filtering audio but nothing to de-queue.");
+ }
+ return;
+ }
+
+
+ const int size = sizeof(float);
+ var dataOffset = 0;
+ var lastFrameSize = _lastAudioFrame.Length - _lastAudioFrameOffset;
+ if (data.Length >= lastFrameSize) {
+ if (lastFrameSize > 0) {
+ Buffer.BlockCopy(_lastAudioFrame, _lastAudioFrameOffset * size, data, 0, lastFrameSize * size);
+ dataOffset += lastFrameSize;
+ }
+ _lastAudioFrame = Array.Empty();
+ _lastAudioFrameOffset = 0;
+
+ lock (_audioQueue) {
+ while (dataOffset < data.Length && _audioQueue.Count > 0) {
+ var frame = _audioQueue.Dequeue();
+ if (frame.Length <= data.Length - dataOffset) {
+ Buffer.BlockCopy(frame, 0, data, dataOffset * size, frame.Length * size);
+ dataOffset += frame.Length;
+
+ } else {
+ Buffer.BlockCopy(frame, 0, data, dataOffset * size, (data.Length - dataOffset) * size);
+ _lastAudioFrame = frame;
+ _lastAudioFrameOffset = data.Length - dataOffset;
+ dataOffset = data.Length;
+ }
+ }
+ }
+
+ } else {
+ Buffer.BlockCopy(_lastAudioFrame, _lastAudioFrameOffset * size, data, 0, data.Length * size);
+ _lastAudioFrameOffset += data.Length;
+ }
+ }
+
+ #endregion
+
+ #region Coils
+
public void SetCoil(string n, bool value)
{
if (int.TryParse(n, out var coilId)) {
@@ -1235,16 +1318,25 @@ public void SetCoil(string n, bool value)
}
OnCoilChanged?.Invoke(this, new CoilEventArgs(n, value));
}
-
- public bool GetCoil(string id)
- {
- return _player != null && _player.CoilStatuses.ContainsKey(id) && _player.CoilStatuses[id];
- }
-
- private void OnSolenoidUpdated(int id, bool isActive)
+
+ public bool GetCoil(string id)
+ {
+ return _player != null && _player.CoilStatuses.ContainsKey(id) && _player.CoilStatuses[id];
+ }
+
+ private void OnSolenoidUpdated(int id, int value)
{
MarkPinMameCallbackActivity();
+ // value is 0/1 for binary coils, 0-255 for PWM-integrated (modulated) coils
+ if (value > 1) {
+ _solenoidsModulated = true;
+ }
+ var isActive = value > 0;
+ // normalized strength for the (main-thread) coil pipeline; the sim-thread path
+ // and the shared snapshot stay boolean on/off
+ var intensity = _solenoidsModulated ? System.Math.Min(value, 255) / 255f : (isActive ? 1f : 0f);
+
if (_pinMameIdToCoilIdMapping.ContainsKey(id)) {
if (!_solenoidsEnabled) {
_solenoidsEnabled = TimestampUsec - _solenoidDelayStartTimestampUsec >= (long)(SolenoidDelay * 1000f);
@@ -1253,7 +1345,7 @@ private void OnSolenoidUpdated(int id, bool isActive)
Logger.Info($"Solenoids enabled, {SolenoidDelay}ms passed");
}
}
-
+
var coil = _coils[_pinMameIdToCoilIdMapping[id]];
if (_solenoidsEnabled) {
@@ -1262,7 +1354,7 @@ private void OnSolenoidUpdated(int id, bool isActive)
}
if (Logger.IsDebugEnabled) {
- Logger.Debug($"[PinMAME] <= coil {coil.Id} : {isActive} | {coil.Description}");
+ Logger.Debug($"[PinMAME] <= coil {coil.Id} : {value} | {coil.Description}");
}
if (ShouldDispatchSimulationCoil(coil.Id)) {
_simulationCoilDispatchQueue.Enqueue(new CoilEventArgs(coil.Id, isActive));
@@ -1284,23 +1376,23 @@ private void OnSolenoidUpdated(int id, bool isActive)
}
}
- EnqueueMainThreadDispatch(() => OnCoilChanged?.Invoke(this, new CoilEventArgs(coil.Id, isActive)));
+ EnqueueMainThreadDispatch(() => OnCoilChanged?.Invoke(this, new CoilEventArgs(coil.Id, intensity)));
}
else {
if (Logger.IsDebugEnabled) {
- Logger.Debug($"[PinMAME] <= solenoids disabled, coil {coil.Id} : {isActive} | {coil.Description}");
+ Logger.Debug($"[PinMAME] <= solenoids disabled, coil {coil.Id} : {value} | {coil.Description}");
}
}
- }
- else {
- Logger.Warn($"[PinMAME] <= coil UNMAPPED {id}: {isActive}");
- }
- }
-
- #endregion
-
- #region Lamps
-
+ }
+ else {
+ Logger.Warn($"[PinMAME] <= coil UNMAPPED {id}: {isActive}");
+ }
+ }
+
+ #endregion
+
+ #region Lamps
+
public void SetLamp(string id, float value, bool isCoil = false, LampSource source = LampSource.Lamp)
{
if (int.TryParse(id, out var lampId)) {
@@ -1314,34 +1406,34 @@ public void SetLamp(string id, float value, bool isCoil = false, LampSource sour
}
OnLampChanged?.Invoke(this, new LampEventArgs(id, value, isCoil, source));
}
-
- public LampState GetLamp(string id)
- {
- return _player != null && _player.LampStatuses.ContainsKey(id) ? _player.LampStatuses[id] : LampState.Default;
- }
-
- #endregion
-
- #region Switches
-
- public void SendInitialSwitches()
- {
- var switches = _player.SwitchStatuses;
+
+ public LampState GetLamp(string id)
+ {
+ return _player != null && _player.LampStatuses.ContainsKey(id) ? _player.LampStatuses[id] : LampState.Default;
+ }
+
+ #endregion
+
+ #region Switches
+
+ public void SendInitialSwitches()
+ {
+ var switches = _player.SwitchStatuses;
Logger.Info("[PinMAME] Sending initial switch statuses...");
- foreach (var id in switches.Keys) {
- var isClosed = switches[id].IsSwitchClosed;
- // skip open switches
- if (!isClosed) {
- continue;
- }
- if (_switches.ContainsKey(id) && !_mechSwitches.Contains(_switchIdToPinMameIdMappings[_switches[id].Id])) {
- Logger.Info($"[PinMAME] => sw {id} ({_switches[id].Id}): {true} | {_switches[id].Description}");
-
- _pinMame.SetSwitch(_switchIdToPinMameIdMappings[_switches[id].Id], true);
- }
- }
- }
-
+ foreach (var id in switches.Keys) {
+ var isClosed = switches[id].IsSwitchClosed;
+ // skip open switches
+ if (!isClosed) {
+ continue;
+ }
+ if (_switches.ContainsKey(id) && !_mechSwitches.Contains(_switchIdToPinMameIdMappings[_switches[id].Id])) {
+ Logger.Info($"[PinMAME] => sw {id} ({_switches[id].Id}): {true} | {_switches[id].Description}");
+
+ _pinMame.SetSwitch(_switchIdToPinMameIdMappings[_switches[id].Id], true);
+ }
+ }
+ }
+
public void Switch(string id, bool isClosed)
{
if (_switches.TryGetValue(id, out var sw)) {
@@ -1369,16 +1461,16 @@ public void Switch(string id, bool isClosed)
OnSwitchChanged?.Invoke(this, new SwitchEventArgs2(id, isClosed));
}
-
- public bool GetSwitch(string id)
- {
- return _player != null && _player.SwitchStatuses.ContainsKey(id) && _player.SwitchStatuses[id].IsSwitchEnabled;
- }
-
- #endregion
-
- #region Mechs
-
+
+ public bool GetSwitch(string id)
+ {
+ return _player != null && _player.SwitchStatuses.ContainsKey(id) && _player.SwitchStatuses[id].IsSwitchEnabled;
+ }
+
+ #endregion
+
+ #region Mechs
+
public void RegisterMech(PinMameMechComponent mechComponent)
{
// PinMAME mech numbers are 1-based.
@@ -1386,22 +1478,22 @@ public void RegisterMech(PinMameMechComponent mechComponent)
_registeredMechs[id] = mechComponent;
_registeredMechNames[id] = mechComponent ? mechComponent.name : "";
}
-
- private void OnMechAvailable(int mechNo, PinMameMechInfo mechInfo)
- {
- Logger.Info($"[PinMAME] <= mech available: mechNo={mechNo}, mechInfo={mechInfo}");
- }
-
- private void OnMechUpdated(int mechNo, PinMameMechInfo mechInfo)
- {
+
+ private void OnMechAvailable(int mechNo, PinMameMechInfo mechInfo)
+ {
+ Logger.Info($"[PinMAME] <= mech available: mechNo={mechNo}, mechInfo={mechInfo}");
+ }
+
+ private void OnMechUpdated(int mechNo, PinMameMechInfo mechInfo)
+ {
if (_registeredMechs.ContainsKey(mechNo)) {
EnqueueMainThreadDispatch(() => _registeredMechs[mechNo].UpdateMech(mechInfo));
-
- } else {
- Logger.Info($"[PinMAME] <= mech updated: mechNo={mechNo}, mechInfo={mechInfo}");
- }
- }
-
+
+ } else {
+ Logger.Info($"[PinMAME] <= mech updated: mechNo={mechNo}, mechInfo={mechInfo}");
+ }
+ }
+
private void SendMechs()
{
var max = _pinMame.GetMaxMechs();
@@ -1419,7 +1511,7 @@ private void SendMechs()
}
}
}
-
+
#endregion
private int IsKeyPressed(PinMameKeycode keycode)
diff --git a/VisualPinball.Engine.PinMAME/VisualPinball.Engine.PinMAME.csproj b/VisualPinball.Engine.PinMAME/VisualPinball.Engine.PinMAME.csproj
index 041653a..c1c22b9 100644
--- a/VisualPinball.Engine.PinMAME/VisualPinball.Engine.PinMAME.csproj
+++ b/VisualPinball.Engine.PinMAME/VisualPinball.Engine.PinMAME.csproj
@@ -5,7 +5,7 @@
false
true
9.0
- 1.0.2
+ 1.0.4
3.7.0-beta1