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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 124 additions & 1 deletion Runtime/MpfGamelogicEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using UnityEngine;
using VisualPinball.Engine.Game.Engines;
using VisualPinball.Engine.Mpf.Unity.MediaController;
using VisualPinball.Plugins;
using VisualPinball.Unity;
using Logger = NLog.Logger;

Expand All @@ -30,7 +31,11 @@ namespace VisualPinball.Engine.Mpf.Unity
/// and applying changes to coils, lights and hardware rules requested by MPF.
/// </summary>
[AddComponentMenu("Pinball/Gamelogic Engine/MPF Game Logic")]
public class MpfGamelogicEngine : MonoBehaviour, IGamelogicEngine, IGamelogicInputThreading
public class MpfGamelogicEngine
: MonoBehaviour,
IGamelogicEngine,
IGamelogicInputThreading,
IGamelogicNativeOutputs
{
[SerializeField]
private MpfWranglerOptions _wranglerOptions;
Expand Down Expand Up @@ -107,6 +112,10 @@ private MpfWrangler MpfWrangler
private Player _player;
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

private readonly MpfOutputModel _nativeOutputModel = new(MonotonicMs);
private NativeOutputSink _nativeOutputSink;
private INativeOutputListener _nativeOutputs;

public string Name => "Mission Pinball Framework";
public GamelogicInputDispatchMode SwitchDispatchMode => GamelogicInputDispatchMode.MainThread;
public GamelogicEngineSwitch[] RequestedSwitches => _requestedSwitches;
Expand Down Expand Up @@ -366,6 +375,10 @@ public async void Switch(string id, bool isClosed)
{
OnSwitchChanged?.Invoke(this, new SwitchEventArgs2(id, isClosed));

// VPE runs MPF's hardware rules as wires, so their coils only show up here.
if (_mpfSwitchNumbers.TryGetNumberByName(id, out var ruleSwitchNumber))
_nativeOutputModel.EnqueueSwitch(ruleSwitchNumber, isClosed);

if (_mpfSwitchNumbers.ContainsName(id))
{
if (MpfWrangler.MpfState == MpfState.Connected)
Expand Down Expand Up @@ -394,6 +407,96 @@ public async void Switch(string id, bool isClosed)
}
}

private void Update()
{
DispatchNativeOutputs();
}

/// <summary>
/// Reports every light and coil MPF drives by its machine config number, including
/// outputs the table does not map. See <see cref="MpfOutputModel"/> for the numbering.
/// </summary>
/// <remarks>
/// Called on the main thread, like <see cref="Update"/>. The model only queues MPF
/// commands while attached and checks that under its queue lock, so a command that races
/// with the detach cannot reach the next listener.
/// </remarks>
public void SetNativeOutputListener(INativeOutputListener listener)
{
if (listener != null)
{
_nativeOutputModel.Attach();
_nativeOutputs = listener;
}
else
{
_nativeOutputs = null;
// Release the last listener too, so a detached plugin is not kept alive by the engine.
if (_nativeOutputSink != null)
_nativeOutputSink.Listener = null;
_nativeOutputModel.Detach();
Comment thread
freezy marked this conversation as resolved.
Resolved
}
}

private void DispatchNativeOutputs()
{
var listener = _nativeOutputs;
if (listener == null)
return;
_nativeOutputSink ??= new NativeOutputSink();
_nativeOutputSink.Listener = listener;
_nativeOutputModel.Poll(_nativeOutputSink);
listener.NativeOutputPollEnd();
}

private static double MonotonicMs() =>
System.Diagnostics.Stopwatch.GetTimestamp()
* 1000d
/ System.Diagnostics.Stopwatch.Frequency;

/// <summary>
/// MPF lights are dimmable channels (brightness 0..1, faded by <see cref="MpfOutputModel"/>);
/// MPF coils are switched on and off by the VPE platform.
/// </summary>
private sealed class NativeOutputSink : IMpfOutputSink
{
public INativeOutputListener Listener;

public void OutputChanged(MpfOutputKind kind, int number, double value)
{
switch (kind)
{
case MpfOutputKind.Coil:
Listener.NativeOutputChanged(
ControllerEventKind.Coil,
number,
value,
ControllerValueSemantics.Binary,
1d
);
break;
case MpfOutputKind.GeneralIllumination:
Listener.NativeOutputChanged(
ControllerEventKind.GeneralIllumination,
number,
value,
ControllerValueSemantics.Physical,
1d
);
break;
default:
Listener.NativeOutputChanged(
ControllerEventKind.Lamp,
number,
value,
ControllerValueSemantics.Physical,
1d
);
break;
}
}
}

private void OnMpfStateChanged(object sender, StateChangedEventArgs<MpfState> args)
{
MpfStateChanged?.Invoke(this, args);
Expand Down Expand Up @@ -444,6 +547,13 @@ private MachineState CompileMachineState(Player player)

private void ExecuteMpfFadeLightRequest(object sender, FadeLightRequest request)
{
foreach (var fade in request.Fades)
_nativeOutputModel.EnqueueFade(
fade.LightNumber,
fade.TargetBrightness,
request.CommonFadeMs
);

var args = new List<LampEventArgs>();
foreach (var fade in request.Fades)
{
Expand All @@ -460,6 +570,8 @@ private void ExecuteMpfFadeLightRequest(object sender, FadeLightRequest request)

private void ExecuteMpfPulseCoilRequest(object sender, PulseCoilRequest request)
{
_nativeOutputModel.EnqueuePulse(request.CoilNumber, request.PulseMs);

if (_mpfCoilNumbers.TryGetNameByNumber(request.CoilNumber, out var coilName))
{
SetCoil(coilName, true);
Expand All @@ -474,6 +586,8 @@ private void ExecuteMpfPulseCoilRequest(object sender, PulseCoilRequest request)

private void ExecuteMpfEnableCoilRequest(object sender, EnableCoilRequest request)
{
_nativeOutputModel.EnqueueEnable(request.CoilNumber);

if (_mpfCoilNumbers.TryGetNameByNumber(request.CoilNumber, out var coilName))
SetCoil(coilName, true);
else
Expand All @@ -485,6 +599,8 @@ private void ExecuteMpfEnableCoilRequest(object sender, EnableCoilRequest reques

private void ExecuteMpfCommandDisableCoilRequest(object sender, DisableCoilRequest request)
{
_nativeOutputModel.EnqueueDisable(request.CoilNumber);

if (_mpfCoilNumbers.TryGetNameByNumber(request.CoilNumber, out var coilName))
SetCoil(coilName, false);
else
Expand All @@ -501,6 +617,12 @@ ConfigureHardwareRuleRequest request
{
var switchNumber = request.SwitchNumber;
var coilNumber = request.CoilNumber;
_nativeOutputModel.ConfigureRule(
switchNumber,
coilNumber,
request.PulseMs,
request.HoldPower
);
if (
_mpfSwitchNumbers.TryGetNameByNumber(switchNumber, out var switchName)
&& _mpfCoilNumbers.TryGetNameByNumber(coilNumber, out var coilName)
Expand All @@ -521,6 +643,7 @@ RemoveHardwareRuleRequest request
{
var switchNumber = request.SwitchNumber;
var coilNumber = request.CoilNumber;
_nativeOutputModel.RemoveRule(switchNumber, coilNumber);
if (
_mpfSwitchNumbers.TryGetNameByNumber(switchNumber, out var switchName)
&& _mpfCoilNumbers.TryGetNameByNumber(coilNumber, out var coilName)
Expand Down
Loading
Loading