From c17f4d6beba3281fc3c189c76350330a10b33047 Mon Sep 17 00:00:00 2001 From: Eli Curtz Date: Fri, 20 Nov 2020 16:54:06 -0800 Subject: [PATCH 01/23] All the boilerplate required for the Lamp Manager. Mostly copied from Coil Manager. --- .../Game/Engines/GamelogicEngineLamp.cs | 25 ++ .../Game/Engines/GamelogicEngineLamp.cs.meta | 11 + VisualPinball.Engine/Game/ILightable.cs | 23 ++ VisualPinball.Engine/Game/ILightable.cs.meta | 11 + .../IO/BiffMappingsLampAttribute.cs | 47 ++++ .../IO/BiffMappingsLampAttribute.cs.meta | 11 + VisualPinball.Engine/VPT/Enums.cs | 6 + VisualPinball.Engine/VPT/Flasher/Flasher.cs | 3 +- VisualPinball.Engine/VPT/Light/Light.cs | 2 +- VisualPinball.Engine/VPT/Mappings/Mappings.cs | 82 ++++++ .../VPT/Mappings/MappingsData.cs | 22 ++ .../VPT/Mappings/MappingsLampData.cs | 80 ++++++ .../VPT/Mappings/MappingsLampData.cs.meta | 11 + VisualPinball.Engine/VPT/Table/Table.cs | 4 + .../Managers/Lamp.meta | 8 + .../Managers/Lamp/LampListData.cs | 62 +++++ .../Managers/Lamp/LampListData.cs.meta | 11 + .../Managers/Lamp/LampListViewItemRenderer.cs | 188 ++++++++++++++ .../Lamp/LampListViewItemRenderer.cs.meta | 11 + .../Managers/Lamp/LampManager.cs | 233 ++++++++++++++++++ .../Managers/Lamp/LampManager.cs.meta | 11 + .../Game/Engine/DefaultGamelogicEngine.cs | 8 +- .../Game/Engine/IGamelogicEngineWithLamps.cs | 59 +++++ .../Engine/IGamelogicEngineWithLamps.cs.meta | 11 + .../VisualPinball.Unity/VPT/ILampAuthoring.cs | 23 ++ .../VPT/ILampAuthoring.cs.meta | 11 + 26 files changed, 971 insertions(+), 3 deletions(-) create mode 100644 VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs create mode 100644 VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs.meta create mode 100644 VisualPinball.Engine/Game/ILightable.cs create mode 100644 VisualPinball.Engine/Game/ILightable.cs.meta create mode 100644 VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs create mode 100644 VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs.meta create mode 100644 VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs create mode 100644 VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs.meta diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs new file mode 100644 index 000000000..f3493ffb2 --- /dev/null +++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs @@ -0,0 +1,25 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +namespace VisualPinball.Engine.Game.Engines +{ + public struct GamelogicEngineLamp + { + public string Id; + public string Description; + public string PlayfieldItemHint; + } +} diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs.meta b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs.meta new file mode 100644 index 000000000..850a7ce34 --- /dev/null +++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 359865907fa734abd8d0543d3f670a1a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Engine/Game/ILightable.cs b/VisualPinball.Engine/Game/ILightable.cs new file mode 100644 index 000000000..9ec20ad9c --- /dev/null +++ b/VisualPinball.Engine/Game/ILightable.cs @@ -0,0 +1,23 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +namespace VisualPinball.Engine.Game +{ + public interface ILightable + { + string Name { get; } + } +} diff --git a/VisualPinball.Engine/Game/ILightable.cs.meta b/VisualPinball.Engine/Game/ILightable.cs.meta new file mode 100644 index 000000000..7f3f191f8 --- /dev/null +++ b/VisualPinball.Engine/Game/ILightable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4297c2291ecb74623b898c9acf230dac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs b/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs new file mode 100644 index 000000000..967120820 --- /dev/null +++ b/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs @@ -0,0 +1,47 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.IO; +using VisualPinball.Engine.VPT.Mappings; +using VisualPinball.Engine.VPT.Table; + +namespace VisualPinball.Engine.IO +{ + public class BiffMappingsLampAttribute : BiffAttribute + { + public BiffMappingsLampAttribute(string name) : base(name) { } + + public override void Parse(T obj, BinaryReader reader, int len) + { + ParseValue(obj, reader, len, ReadMappingsLamp); + } + + public override void Write(TItem obj, BinaryWriter writer, HashWriter hashWriter) + { + WriteValue(obj, writer, (w, v) => WriteMappingsLamp(w, v, hashWriter), hashWriter, x => 0); + } + + private static void WriteMappingsLamp(BinaryWriter writer, BiffData value, HashWriter hashWriter) + { + value.Write(writer, hashWriter); + } + + private static MappingsLampData ReadMappingsLamp(BinaryReader reader, int len) + { + return new MappingsLampData(reader); + } + } +} diff --git a/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs.meta b/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs.meta new file mode 100644 index 000000000..853608b78 --- /dev/null +++ b/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 52ca1e80f328c42089e3f035bb833401 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Engine/VPT/Enums.cs b/VisualPinball.Engine/VPT/Enums.cs index 1a3fa6b7e..fe957d775 100644 --- a/VisualPinball.Engine/VPT/Enums.cs +++ b/VisualPinball.Engine/VPT/Enums.cs @@ -190,4 +190,10 @@ public static class WireType public const int OnOff = 0; public const int Pulse = 1; } + + public static class LampDestination + { + public const int Playfield = 0; + public const int Device = 1; + } } diff --git a/VisualPinball.Engine/VPT/Flasher/Flasher.cs b/VisualPinball.Engine/VPT/Flasher/Flasher.cs index 10fd3552f..ce730ad03 100644 --- a/VisualPinball.Engine/VPT/Flasher/Flasher.cs +++ b/VisualPinball.Engine/VPT/Flasher/Flasher.cs @@ -15,10 +15,11 @@ // along with this program. If not, see . using System.IO; +using VisualPinball.Engine.Game; namespace VisualPinball.Engine.VPT.Flasher { - public class Flasher : Item + public class Flasher : Item, ILightable { public override string ItemName { get; } = "Flasher"; public override string ItemGroupName { get; } = "Flashers"; diff --git a/VisualPinball.Engine/VPT/Light/Light.cs b/VisualPinball.Engine/VPT/Light/Light.cs index 18d9457a2..ce2c2bbbc 100644 --- a/VisualPinball.Engine/VPT/Light/Light.cs +++ b/VisualPinball.Engine/VPT/Light/Light.cs @@ -20,7 +20,7 @@ namespace VisualPinball.Engine.VPT.Light { - public class Light : Item, IRenderable + public class Light : Item, IRenderable, ILightable { public override string ItemName { get; } = "Light"; public override string ItemGroupName { get; } = "Lights"; diff --git a/VisualPinball.Engine/VPT/Mappings/Mappings.cs b/VisualPinball.Engine/VPT/Mappings/Mappings.cs index 421215593..17e46b356 100644 --- a/VisualPinball.Engine/VPT/Mappings/Mappings.cs +++ b/VisualPinball.Engine/VPT/Mappings/Mappings.cs @@ -329,5 +329,87 @@ private static ICoilable GuessPlayfieldCoil(Dictionary coils, #endregion + + #region Lamp Population + + /// + /// Auto-matches the lamps provided by the gamelogic engine with the + /// lamps on the playfield. + /// + /// List of lamps provided by the gamelogic engine + /// List of lamps on the playfield + public void PopulateLamps(GamelogicEngineLamp[] engineLamps, IEnumerable tableLamps) + { + var lamps = tableLamps + .GroupBy(x => x.Name.ToLower()) + .ToDictionary(x => x.Key, x => x.First()); + + foreach (var engineLamp in GetLamps(engineLamps)) { + + var lampMapping = Data.Lamps.FirstOrDefault(mappingsLampData => mappingsLampData.Id == engineLamp.Id); + if (lampMapping == null) { + + var description = string.IsNullOrEmpty(engineLamp.Description) ? string.Empty : engineLamp.Description; + var playfieldItem = GuessPlayfieldLamp(lamps, engineLamp); + + Data.AddLamp(new MappingsLampData + { + Id = engineLamp.Id, + Description = description, + Destination = LampDestination.Playfield, + PlayfieldItem = playfieldItem != null ? playfieldItem.Name : string.Empty, + }); + } + } + } + + /// + /// Returns a sorted list of lamp names from the gamelogic engine, + /// appended with the additional names in the lamp mapping. In short, + /// the list of lamp names to choose from. + /// + /// Lamp names provided by the gamelogic engine + /// All lamp names + public IEnumerable GetLamps(GamelogicEngineLamp[] engineLamps) + { + var lamps = new List(); + + // first, add lamps from the gamelogic engine + if (engineLamps != null) { + lamps.AddRange(engineLamps); + } + + // then add lamp ids that were added manually + foreach (var mappingsLampData in Data.Lamps) { + if (!lamps.Exists(entry => entry.Id == mappingsLampData.Id)) { + lamps.Add(new GamelogicEngineLamp + { + Id = mappingsLampData.Id + }); + + } + } + + lamps.Sort((s1, s2) => s1.Id.CompareTo(s2.Id)); + return lamps; + } + + private static ILightable GuessPlayfieldLamp(Dictionary lamps, GamelogicEngineLamp lamp) + { + // first, match by regex if hint provided + if (!string.IsNullOrEmpty(lamp.PlayfieldItemHint)) { + foreach (var lampName in lamps.Keys) { + var regex = new Regex(lamp.PlayfieldItemHint.ToLower()); + if (regex.Match(lampName).Success) { + return lamps[lampName]; + } + } + } + + // second, match by id + return lamps.ContainsKey(lamp.Id) ? lamps[lamp.Id] : null; + } + + #endregion } } diff --git a/VisualPinball.Engine/VPT/Mappings/MappingsData.cs b/VisualPinball.Engine/VPT/Mappings/MappingsData.cs index 0e36c3515..6effdd201 100644 --- a/VisualPinball.Engine/VPT/Mappings/MappingsData.cs +++ b/VisualPinball.Engine/VPT/Mappings/MappingsData.cs @@ -48,6 +48,9 @@ public class MappingsData : ItemData [BiffMappingsWireAttribute("MWIR", TagAll = true, Pos = 1002)] public MappingsWireData[] Wires = Array.Empty(); + [BiffMappingsLampAttribute("MLMP", TagAll = true, Pos = 1003)] + public MappingsLampData[] Lamps = Array.Empty(); + #region BIFF static MappingsData() @@ -131,5 +134,24 @@ public void RemoveAllWires() } #endregion + + #region Lamps + + public void AddLamp(MappingsLampData data) + { + Lamps = Lamps.Append(data).ToArray(); + } + + public void RemoveLamp(MappingsLampData data) + { + Lamps = Lamps.Except(new[] { data }).ToArray(); + } + + public void RemoveAllLamps() + { + Lamps = Array.Empty(); + } + + #endregion } } diff --git a/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs new file mode 100644 index 000000000..7ba61b162 --- /dev/null +++ b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs @@ -0,0 +1,80 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#region ReSharper +// ReSharper disable UnassignedField.Global +// ReSharper disable StringLiteralTypo +// ReSharper disable FieldCanBeMadeReadOnly.Global +// ReSharper disable ConvertToConstant.Global +// ReSharper disable CompareOfFloatsByEqualityOperator +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using VisualPinball.Engine.IO; +using VisualPinball.Engine.VPT.Table; + +namespace VisualPinball.Engine.VPT.Mappings +{ + [Serializable] + public class MappingsLampData : BiffData + { + [BiffString("MCID", IsWideString = true, Pos = 1)] + public string Id = string.Empty; + + [BiffString("DESC", IsWideString = true, Pos = 2)] + public string Description = string.Empty; + + [BiffInt("DEST", Pos = 3)] + public int Destination = LampDestination.Playfield; + + [BiffString("PITM", IsWideString = true, Pos = 4)] + public string PlayfieldItem = string.Empty; + + [BiffString("DEVC", IsWideString = true, Pos = 5)] + public string Device = string.Empty; + + [BiffString("DITM", IsWideString = true, Pos = 6)] + public string DeviceItem = string.Empty; + + #region BIFF + + static MappingsLampData() + { + Init(typeof(MappingsLampData), Attributes); + } + + public MappingsLampData() : base(null) + { + } + + public MappingsLampData(BinaryReader reader) : base(null) + { + Load(this, reader, Attributes); + } + + public override void Write(BinaryWriter writer, HashWriter hashWriter) + { + WriteRecord(writer, Attributes, hashWriter); + WriteEnd(writer, hashWriter); + } + + private static readonly Dictionary> Attributes = new Dictionary>(); + + #endregion + } +} diff --git a/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs.meta b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs.meta new file mode 100644 index 000000000..4316bf6c2 --- /dev/null +++ b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: daf52ad04f73240db80698c7780505c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Engine/VPT/Table/Table.cs b/VisualPinball.Engine/VPT/Table/Table.cs index f5b92d1fc..d2554d1e2 100644 --- a/VisualPinball.Engine/VPT/Table/Table.cs +++ b/VisualPinball.Engine/VPT/Table/Table.cs @@ -249,6 +249,10 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable) public IEnumerable CoilableDevices => new ICoilableDevice[0] .Concat(_troughs.Values); + public IEnumerable Lightables => new ILightable[0] + .Concat(_lights.Values) + .Concat(_flashers.Values); + private void AddItem(string name, TItem item, IDictionary d, bool updateStorageIndices) where TItem : IItem { if (updateStorageIndices) { diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp.meta new file mode 100644 index 000000000..4a6519ce5 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4b3361184c24541a3aa5e00b5899cbf1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs new file mode 100644 index 000000000..a9f3999c2 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs @@ -0,0 +1,62 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using VisualPinball.Engine.VPT.Mappings; + +namespace VisualPinball.Unity.Editor +{ + public class LampListData : IManagerListData + { + [ManagerListColumn(Order = 0, HeaderName = "ID", Width = 135)] + public string Name => Id; + + [ManagerListColumn(Order = 1, HeaderName = "Description", Width = 150)] + public string Description; + + [ManagerListColumn(Order = 2, HeaderName = "Destination", Width = 150)] + public int Destination; + + [ManagerListColumn(Order = 3, HeaderName = "Element", Width = 200)] + public string Element; + + public string Id; + public string PlayfieldItem; + public string Device; + public string DeviceItem; + + public MappingsLampData MappingsLampData; + + public LampListData(MappingsLampData mappingsLampData) + { + Id = mappingsLampData.Id; + Description = mappingsLampData.Description; + PlayfieldItem = mappingsLampData.PlayfieldItem; + Device = mappingsLampData.Device; + DeviceItem = mappingsLampData.DeviceItem; + + MappingsLampData = mappingsLampData; + } + + public void Update() + { + MappingsLampData.Id = Id; + MappingsLampData.Description = Description; + MappingsLampData.PlayfieldItem = PlayfieldItem; + MappingsLampData.Device = Device; + MappingsLampData.DeviceItem = DeviceItem; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs.meta new file mode 100644 index 000000000..a3204a554 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9beff7bbb35646e5a988a07d97bccba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs new file mode 100644 index 000000000..3aa2754da --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs @@ -0,0 +1,188 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using UnityEngine; +using UnityEditor; +using System; +using System.Collections.Generic; +using UnityEditor.IMGUI.Controls; +using VisualPinball.Engine.VPT; +using System.Linq; +using VisualPinball.Engine.Game.Engines; + +namespace VisualPinball.Unity.Editor +{ + public class LampListViewItemRenderer + { + private readonly string[] OPTIONS_LAMP_DESTINATION = { "Playfield" }; + + private enum LampListColumn + { + Id = 0, + Description = 1, + Destination = 2, + Element = 3, + } + + private readonly List _gleLamps; + private readonly Dictionary _lamps; + + private AdvancedDropdownState _itemPickDropdownState; + + public LampListViewItemRenderer(List gleLamps, Dictionary lamps) + { + _gleLamps = gleLamps; + _lamps = lamps; + } + + public void Render(TableAuthoring tableAuthoring, LampListData data, Rect cellRect, int column, Action updateAction) + { + switch ((LampListColumn)column) { + case LampListColumn.Id: + RenderId(ref data.Id, id => data.Id = id, data, cellRect, updateAction); + break; + case LampListColumn.Description: + RenderDescription(data, cellRect, updateAction); + break; + case LampListColumn.Destination: + RenderDestination(data, cellRect, updateAction); + break; + case LampListColumn.Element: + RenderElement(tableAuthoring, data, cellRect, updateAction); + break; + } + } + + private void RenderId(ref string id, Action setId, LampListData lampListData, Rect cellRect, Action updateAction) + { + // add some padding + cellRect.x += 2; + cellRect.width -= 4; + + var options = new List(_gleLamps.Select(entry => entry.Id).ToArray()); + + if (options.Count > 0) { + options.Add(""); + } + + options.Add("Add..."); + + EditorGUI.BeginChangeCheck(); + var index = EditorGUI.Popup(cellRect, options.IndexOf(id), options.ToArray()); + if (EditorGUI.EndChangeCheck()) { + if (index == options.Count - 1) { + PopupWindow.Show(cellRect, new ManagerListTextFieldPopup("ID", "", newId => { + if (_gleLamps.Exists(entry => entry.Id == newId)) { + _gleLamps.Add(new GamelogicEngineLamp + { + Id = newId + }); + } + + setId(newId); + updateAction(lampListData); + })); + + } + else { + setId(_gleLamps[index].Id); + updateAction(lampListData); + } + } + } + + private void RenderDescription(LampListData lampListData, Rect cellRect, Action updateAction) + { + EditorGUI.BeginChangeCheck(); + var value = EditorGUI.TextField(cellRect, lampListData.Description); + if (EditorGUI.EndChangeCheck()) { + lampListData.Description = value; + updateAction(lampListData); + } + } + + private void RenderDestination(LampListData lampListData, Rect cellRect, Action updateAction) + { + EditorGUI.BeginChangeCheck(); + var index = EditorGUI.Popup(cellRect, lampListData.Destination, OPTIONS_LAMP_DESTINATION); + if (EditorGUI.EndChangeCheck()) { + if (lampListData.Destination != index) { + lampListData.Destination = index; + updateAction(lampListData); + } + } + } + + private void RenderElement(TableAuthoring tableAuthoring, LampListData lampListData, Rect cellRect, Action updateAction) + { + var icon = GetIcon(lampListData); + + if (icon != null) { + var iconRect = cellRect; + iconRect.width = 20; + var guiColor = GUI.color; + GUI.color = Color.clear; + EditorGUI.DrawTextureTransparent(iconRect, icon, ScaleMode.ScaleToFit); + GUI.color = guiColor; + } + + cellRect.x += 25; + cellRect.width -= 25; + + switch (lampListData.Destination) { + case LampDestination.Playfield: + RenderPlayfieldElement(tableAuthoring, lampListData, cellRect, updateAction); + break; + } + } + + private void RenderPlayfieldElement(TableAuthoring tableAuthoring, LampListData lampListData, Rect cellRect, Action updateAction) + { + if (GUI.Button(cellRect, lampListData.PlayfieldItem, EditorStyles.objectField) || GUI.Button(cellRect, "", GUI.skin.GetStyle("IN ObjectField"))) { + if (_itemPickDropdownState == null) { + _itemPickDropdownState = new AdvancedDropdownState(); + } + + var dropdown = new ItemSearchableDropdown( + _itemPickDropdownState, + tableAuthoring, + "Lamp Items", + item => { + lampListData.PlayfieldItem = item.Name; + updateAction(lampListData); + } + ); + dropdown.Show(cellRect); + } + } + + private UnityEngine.Texture GetIcon(LampListData lampListData) + { + Texture2D icon = null; + + switch (lampListData.Destination) { + case LampDestination.Playfield: { + if (_lamps.ContainsKey(lampListData.PlayfieldItem.ToLower())) { + icon = Icons.ByComponent(_lamps[lampListData.PlayfieldItem.ToLower()], size: IconSize.Small); + } + break; + } + } + + return icon; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs.meta new file mode 100644 index 000000000..96047366e --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ffa24be5be8864c54ae501901eadbf60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs new file mode 100644 index 000000000..c39fd4da0 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs @@ -0,0 +1,233 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.Collections.Generic; +using System.Linq; +using NLog; +using UnityEditor; +using UnityEngine; +using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT.Mappings; +using Logger = NLog.Logger; + +namespace VisualPinball.Unity.Editor +{ + /// + /// Editor UI for VPE lamps + /// + /// + class LampManager : ManagerWindow + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + protected override string DataTypeName => "Lamp"; + + protected override bool DetailsEnabled => false; + protected override bool ListViewItemRendererEnabled => true; + + private readonly List _gleLamps = new List(); + private readonly Dictionary _lamps = new Dictionary(); + + private LampListViewItemRenderer _listViewItemRenderer; + + private class SerializedMappings : ScriptableObject + { + public TableAuthoring Table; + public MappingsData Mappings; + } + private SerializedMappings _recordMappings; + + [MenuItem("Visual Pinball/Lamp Manager", false, 107)] + public static void ShowWindow() + { + GetWindow(); + } + + public override void OnEnable() + { + titleContent = new GUIContent("Lamp Manager", Icons.Light(IconSize.Small)); + RowHeight = 22; + + base.OnEnable(); + } + + private void OnFocus() + { + _listViewItemRenderer = new LampListViewItemRenderer(_gleLamps, _lamps); + } + + protected override bool SetupCompleted() + { + if (_tableAuthoring == null) { + DisplayMessage("No table set."); + return false; + } + + var gle = _tableAuthoring.gameObject.GetComponent(); + + if (gle == null) { + DisplayMessage("No gamelogic engine set."); + return false; + } + + return true; + } + + protected override void OnButtonBarGUI() + { + if (GUILayout.Button("Populate All", GUILayout.ExpandWidth(false))) { + if (_tableAuthoring != null) { + RecordUndo("Populate all lamp mappings"); + _tableAuthoring.Table.Mappings.PopulateLamps(GetAvailableEngineLamps(), _tableAuthoring.Table.Lightables); + Reload(); + } + } + + if (GUILayout.Button("Remove All", GUILayout.ExpandWidth(false))) { + if (_tableAuthoring != null) { + if (EditorUtility.DisplayDialog("Lamp Manager", "Are you sure want to remove all lamp mappings?", "Yes", "Cancel")) { + RecordUndo("Remove all lamp mappings"); + _tableAuthoring.Mappings.RemoveAllLamps(); + } + Reload(); + } + } + } + + protected override void OnListViewItemRenderer(LampListData data, Rect cellRect, int column) + { + _listViewItemRenderer.Render(_tableAuthoring, data, cellRect, column, lampListData => { + RecordUndo(DataTypeName + " Data Change"); + + lampListData.Update(); + + var lamp = _tableAuthoring.Table.Lightables.FirstOrDefault(c => c.Name == lampListData.PlayfieldItem); + }); + } + + #region Data management + protected override List CollectData() + { + List data = new List(); + + foreach (var mappingsLampData in _tableAuthoring.Mappings.Lamps) { + data.Add(new LampListData(mappingsLampData)); + } + + RefreshLamps(); + RefreshLampIds(); + + return data; + } + + protected override void AddNewData(string undoName, string newName) + { + RecordUndo(undoName); + + _tableAuthoring.Mappings.AddLamp(new MappingsLampData()); + } + + protected override void RemoveData(string undoName, LampListData data) + { + RecordUndo(undoName); + + _tableAuthoring.Mappings.RemoveLamp(data.MappingsLampData); + } + + protected override void CloneData(string undoName, string newName, LampListData data) + { + RecordUndo(undoName); + + _tableAuthoring.Mappings.AddLamp(new MappingsLampData + { + Id = data.Id, + Description = data.Description, + Destination = data.Destination, + PlayfieldItem = data.PlayfieldItem, + Device = data.Device, + DeviceItem = data.DeviceItem, + }); + } + #endregion + + #region Helper methods + private void DisplayMessage(string message) + { + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + var style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter }; + EditorGUILayout.LabelField(message, style, GUILayout.ExpandWidth(true)); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + private void RefreshLamps() + { + _lamps.Clear(); + + if (_tableAuthoring != null) { + foreach (var item in _tableAuthoring.GetComponentsInChildren()) { + _lamps.Add(item.Name.ToLower(), item); + } + } + } + + private void RefreshLampIds() + { + _gleLamps.Clear(); + _gleLamps.AddRange(_tableAuthoring.Table.Mappings.GetLamps(GetAvailableEngineLamps())); + } + + private GamelogicEngineLamp[] GetAvailableEngineLamps() + { + var gle = _tableAuthoring.gameObject.GetComponent(); + return gle == null ? new GamelogicEngineLamp[0] : ((IGamelogicEngineWithLamps)gle.GameEngine).AvailableLamps; + } + + #endregion + + #region Undo Redo + private void RestoreMappings() + { + if (_recordMappings == null) { return; } + if (_tableAuthoring == null) { return; } + if (_recordMappings.Table == _tableAuthoring) { + _tableAuthoring.RestoreMappings(_recordMappings.Mappings); + } + } + + protected override void UndoPerformed() + { + RestoreMappings(); + base.UndoPerformed(); + } + + private void RecordUndo(string undoName) + { + if (_tableAuthoring == null) { return; } + if (_recordMappings == null) { + _recordMappings = CreateInstance(); + } + _recordMappings.Table = _tableAuthoring; + _recordMappings.Mappings = _tableAuthoring.Mappings; + + Undo.RecordObjects(new Object[] { this, _recordMappings }, undoName); + } + #endregion + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs.meta new file mode 100644 index 000000000..0239793b0 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c341f3ff525a4da48384d8fbc41b025 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index 959e4b548..24b085604 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -30,11 +30,12 @@ namespace VisualPinball.Unity /// them up to the switches. /// [Serializable] - public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwitches, IGamelogicEngineWithCoils + public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwitches, IGamelogicEngineWithCoils, IGamelogicEngineWithLamps { public string Name { get; } = "Default Game Engine"; public event EventHandler OnCoilChanged; + public event EventHandler OnLampChanged; private const string SwLeftFlipper = "s_left_flipper"; private const string SwLeftFlipperEos = "s_left_flipper_eos"; @@ -81,6 +82,11 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit new GamelogicEngineCoil { Id = CoilTroughEntry, Description = "Trough Entry", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = Trough.EntryCoilId}, }; + public GamelogicEngineLamp[] AvailableLamps { get; } = + { + new GamelogicEngineLamp { Id = "l_11", Description = "Matrix Lamp 11" } + }; + private TableApi _tableApi; private BallManager _ballManager; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs new file mode 100644 index 000000000..d3f2e109f --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs @@ -0,0 +1,59 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using VisualPinball.Engine.Game.Engines; + +namespace VisualPinball.Unity +{ + /// + /// A game logic engine working with lamps

+ /// + /// It provides a list of available lamps and an event handler to trigger + /// them. + ///

+ public interface IGamelogicEngineWithLamps + { + /// + /// A list of available lamps. + /// + GamelogicEngineLamp[] AvailableLamps { get; } + + /// + /// Triggered when a lamp is turned on or off. + /// + event EventHandler OnLampChanged; + } + + public readonly struct LampEventArgs + { + /// + /// Id of the lamp, as defined by . + /// + public readonly string Id; + + /// + /// State of the lamp, true if the lamp is illuminated, false if not. + /// + public readonly bool IsOn; + + public LampEventArgs(string id, bool isOn) + { + Id = id; + IsOn = isOn; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs.meta new file mode 100644 index 000000000..1f7369630 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 17c472a5cc0f74c4ba0d741649f50198 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs new file mode 100644 index 000000000..1f14110ad --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs @@ -0,0 +1,23 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + + +namespace VisualPinball.Unity +{ + public interface ILampAuthoring : IIdentifiableItemAuthoring + { + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs.meta new file mode 100644 index 000000000..125a7746e --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 466fa2b1cbde64c5abd7d457611e1a7f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 598d1bb8bc69fdc6afab5709891d409688338014 Mon Sep 17 00:00:00 2001 From: freezy Date: Sat, 9 Jan 2021 23:56:25 +0100 Subject: [PATCH 02/23] lamps: Wire them up! --- .../VPT/MaterialFileTests.cs | 2 +- .../Game/Engines/GamelogicEngineLamp.cs | 2 +- VisualPinball.Engine/Game/ILightable.cs | 2 +- .../IO/BiffMappingsLampAttribute.cs | 2 +- VisualPinball.Engine/VPT/Mappings/Mappings.cs | 24 +- .../VPT/Mappings/MappingsLampData.cs | 3 +- VisualPinball.Engine/VPT/MaterialLoader.cs | 2 +- .../Managers/Lamp/LampListData.cs | 92 ++--- .../Managers/Lamp/LampListViewItemRenderer.cs | 340 +++++++++--------- .../Managers/Lamp/LampManager.cs | 4 +- .../Managers/Wire/WireListViewItemRenderer.cs | 21 +- .../Managers/Wire/WireManager.cs | 15 +- .../Utils/LayoutUtility.cs | 2 +- .../Game/Engine/DefaultGamelogicEngine.cs | 46 ++- .../Game/Engine/IGamelogicEngineWithLamps.cs | 2 +- .../VisualPinball.Unity/Game/Player.cs | 60 ++++ .../VisualPinball.Unity/VPT/IApi.cs | 7 + .../VisualPinball.Unity/VPT/ICoilAuthoring.cs | 2 +- .../VisualPinball.Unity/VPT/ILampAuthoring.cs | 7 +- .../VPT/IWireableAuthoring.cs | 23 ++ .../VPT/IWireableAuthoring.cs.meta | 11 + .../VisualPinball.Unity/VPT/Light/LightApi.cs | 97 +++++ .../VPT/Light/LightApi.cs.meta | 11 + .../VPT/Light/LightAuthoring.cs | 99 ++++- .../VisualPinball.Unity/VPT/Table/TableApi.cs | 8 + .../VPT/Trough/TroughAuthoring.cs | 1 + 26 files changed, 636 insertions(+), 249 deletions(-) create mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/IWireableAuthoring.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/IWireableAuthoring.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs.meta diff --git a/VisualPinball.Engine.Test/VPT/MaterialFileTests.cs b/VisualPinball.Engine.Test/VPT/MaterialFileTests.cs index 4cd3b6605..7b088b9d6 100644 --- a/VisualPinball.Engine.Test/VPT/MaterialFileTests.cs +++ b/VisualPinball.Engine.Test/VPT/MaterialFileTests.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs index f3493ffb2..d9334ba8d 100644 --- a/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs +++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 diff --git a/VisualPinball.Engine/Game/ILightable.cs b/VisualPinball.Engine/Game/ILightable.cs index 9ec20ad9c..368d15dc0 100644 --- a/VisualPinball.Engine/Game/ILightable.cs +++ b/VisualPinball.Engine/Game/ILightable.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 diff --git a/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs b/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs index 967120820..7315070e1 100644 --- a/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs +++ b/VisualPinball.Engine/IO/BiffMappingsLampAttribute.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 diff --git a/VisualPinball.Engine/VPT/Mappings/Mappings.cs b/VisualPinball.Engine/VPT/Mappings/Mappings.cs index 17e46b356..e21c1d5c3 100644 --- a/VisualPinball.Engine/VPT/Mappings/Mappings.cs +++ b/VisualPinball.Engine/VPT/Mappings/Mappings.cs @@ -329,7 +329,6 @@ private static ICoilable GuessPlayfieldCoil(Dictionary coils, #endregion - #region Lamp Population /// @@ -337,7 +336,7 @@ private static ICoilable GuessPlayfieldCoil(Dictionary coils, /// lamps on the playfield. /// /// List of lamps provided by the gamelogic engine - /// List of lamps on the playfield + /// List of lamps on the playfield public void PopulateLamps(GamelogicEngineLamp[] engineLamps, IEnumerable tableLamps) { var lamps = tableLamps @@ -352,8 +351,7 @@ public void PopulateLamps(GamelogicEngineLamp[] engineLamps, IEnumerable GetLamps(GamelogicEngineLamp[] engineLam // then add lamp ids that were added manually foreach (var mappingsLampData in Data.Lamps) { if (!lamps.Exists(entry => entry.Id == mappingsLampData.Id)) { - lamps.Add(new GamelogicEngineLamp - { + lamps.Add(new GamelogicEngineLamp { Id = mappingsLampData.Id }); - } } @@ -394,20 +390,24 @@ public IEnumerable GetLamps(GamelogicEngineLamp[] engineLam return lamps; } - private static ILightable GuessPlayfieldLamp(Dictionary lamps, GamelogicEngineLamp lamp) + private static ILightable GuessPlayfieldLamp(Dictionary lamps, GamelogicEngineLamp engineLamp) { // first, match by regex if hint provided - if (!string.IsNullOrEmpty(lamp.PlayfieldItemHint)) { + if (!string.IsNullOrEmpty(engineLamp.PlayfieldItemHint)) { foreach (var lampName in lamps.Keys) { - var regex = new Regex(lamp.PlayfieldItemHint.ToLower()); + var regex = new Regex(engineLamp.PlayfieldItemHint.ToLower()); if (regex.Match(lampName).Success) { return lamps[lampName]; } } } - // second, match by id - return lamps.ContainsKey(lamp.Id) ? lamps[lamp.Id] : null; + // second, match by "lXX" or name + var matchKey = int.TryParse(engineLamp.Id, out var numericLampId) + ? $"l{numericLampId}" + : engineLamp.Id; + + return lamps.ContainsKey(matchKey) ? lamps[matchKey] : null; } #endregion diff --git a/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs index 7ba61b162..ba454bbff 100644 --- a/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs +++ b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 @@ -26,6 +26,7 @@ using System.Collections.Generic; using System.IO; using VisualPinball.Engine.IO; +using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT.Table; namespace VisualPinball.Engine.VPT.Mappings diff --git a/VisualPinball.Engine/VPT/MaterialLoader.cs b/VisualPinball.Engine/VPT/MaterialLoader.cs index 1cdb680ae..cf571835d 100644 --- a/VisualPinball.Engine/VPT/MaterialLoader.cs +++ b/VisualPinball.Engine/VPT/MaterialLoader.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs index a9f3999c2..30d82f288 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 @@ -14,49 +14,49 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -using VisualPinball.Engine.VPT.Mappings; - -namespace VisualPinball.Unity.Editor -{ - public class LampListData : IManagerListData - { - [ManagerListColumn(Order = 0, HeaderName = "ID", Width = 135)] - public string Name => Id; - - [ManagerListColumn(Order = 1, HeaderName = "Description", Width = 150)] - public string Description; - - [ManagerListColumn(Order = 2, HeaderName = "Destination", Width = 150)] - public int Destination; - - [ManagerListColumn(Order = 3, HeaderName = "Element", Width = 200)] - public string Element; - - public string Id; - public string PlayfieldItem; - public string Device; - public string DeviceItem; - - public MappingsLampData MappingsLampData; - +using VisualPinball.Engine.VPT.Mappings; + +namespace VisualPinball.Unity.Editor +{ + public class LampListData : IManagerListData + { + [ManagerListColumn(Order = 0, HeaderName = "ID", Width = 135)] + public string Name => Id; + + [ManagerListColumn(Order = 1, HeaderName = "Description", Width = 150)] + public string Description; + + [ManagerListColumn(Order = 2, HeaderName = "Destination", Width = 150)] + public int Destination; + + [ManagerListColumn(Order = 3, HeaderName = "Element", Width = 200)] + public string Element; + + public string Id; + public string PlayfieldItem; + public string Device; + public string DeviceItem; + + public MappingsLampData MappingsLampData; + public LampListData(MappingsLampData mappingsLampData) - { - Id = mappingsLampData.Id; - Description = mappingsLampData.Description; - PlayfieldItem = mappingsLampData.PlayfieldItem; - Device = mappingsLampData.Device; - DeviceItem = mappingsLampData.DeviceItem; - - MappingsLampData = mappingsLampData; - } - - public void Update() - { - MappingsLampData.Id = Id; - MappingsLampData.Description = Description; - MappingsLampData.PlayfieldItem = PlayfieldItem; - MappingsLampData.Device = Device; - MappingsLampData.DeviceItem = DeviceItem; - } - } -} + { + Id = mappingsLampData.Id; + Description = mappingsLampData.Description; + PlayfieldItem = mappingsLampData.PlayfieldItem; + Device = mappingsLampData.Device; + DeviceItem = mappingsLampData.DeviceItem; + + MappingsLampData = mappingsLampData; + } + + public void Update() + { + MappingsLampData.Id = Id; + MappingsLampData.Description = Description; + MappingsLampData.PlayfieldItem = PlayfieldItem; + MappingsLampData.Device = Device; + MappingsLampData.DeviceItem = DeviceItem; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs index 3aa2754da..46bf4bb51 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 @@ -14,175 +14,175 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -using UnityEngine; -using UnityEditor; -using System; -using System.Collections.Generic; -using UnityEditor.IMGUI.Controls; -using VisualPinball.Engine.VPT; -using System.Linq; -using VisualPinball.Engine.Game.Engines; - -namespace VisualPinball.Unity.Editor -{ - public class LampListViewItemRenderer - { - private readonly string[] OPTIONS_LAMP_DESTINATION = { "Playfield" }; - - private enum LampListColumn - { - Id = 0, - Description = 1, - Destination = 2, - Element = 3, - } - - private readonly List _gleLamps; - private readonly Dictionary _lamps; - - private AdvancedDropdownState _itemPickDropdownState; - - public LampListViewItemRenderer(List gleLamps, Dictionary lamps) - { - _gleLamps = gleLamps; - _lamps = lamps; - } - - public void Render(TableAuthoring tableAuthoring, LampListData data, Rect cellRect, int column, Action updateAction) - { - switch ((LampListColumn)column) { - case LampListColumn.Id: - RenderId(ref data.Id, id => data.Id = id, data, cellRect, updateAction); - break; - case LampListColumn.Description: - RenderDescription(data, cellRect, updateAction); - break; - case LampListColumn.Destination: - RenderDestination(data, cellRect, updateAction); - break; - case LampListColumn.Element: - RenderElement(tableAuthoring, data, cellRect, updateAction); - break; - } - } - - private void RenderId(ref string id, Action setId, LampListData lampListData, Rect cellRect, Action updateAction) +using UnityEngine; +using UnityEditor; +using System; +using System.Collections.Generic; +using UnityEditor.IMGUI.Controls; +using VisualPinball.Engine.VPT; +using System.Linq; +using VisualPinball.Engine.Game.Engines; + +namespace VisualPinball.Unity.Editor +{ + public class LampListViewItemRenderer + { + private readonly string[] OPTIONS_LAMP_DESTINATION = { "Playfield" }; + + private enum LampListColumn + { + Id = 0, + Description = 1, + Destination = 2, + Element = 3, + } + + private readonly List _gleLamps; + private readonly Dictionary _lamps; + + private AdvancedDropdownState _itemPickDropdownState; + + public LampListViewItemRenderer(List gleLamps, Dictionary lamps) + { + _gleLamps = gleLamps; + _lamps = lamps; + } + + public void Render(TableAuthoring tableAuthoring, LampListData data, Rect cellRect, int column, Action updateAction) + { + switch ((LampListColumn)column) { + case LampListColumn.Id: + RenderId(ref data.Id, id => data.Id = id, data, cellRect, updateAction); + break; + case LampListColumn.Description: + RenderDescription(data, cellRect, updateAction); + break; + case LampListColumn.Destination: + RenderDestination(data, cellRect, updateAction); + break; + case LampListColumn.Element: + RenderElement(tableAuthoring, data, cellRect, updateAction); + break; + } + } + + private void RenderId(ref string id, Action setId, LampListData lampListData, Rect cellRect, Action updateAction) { // add some padding - cellRect.x += 2; - cellRect.width -= 4; - - var options = new List(_gleLamps.Select(entry => entry.Id).ToArray()); - - if (options.Count > 0) { - options.Add(""); - } - - options.Add("Add..."); - - EditorGUI.BeginChangeCheck(); - var index = EditorGUI.Popup(cellRect, options.IndexOf(id), options.ToArray()); - if (EditorGUI.EndChangeCheck()) { - if (index == options.Count - 1) { - PopupWindow.Show(cellRect, new ManagerListTextFieldPopup("ID", "", newId => { - if (_gleLamps.Exists(entry => entry.Id == newId)) { - _gleLamps.Add(new GamelogicEngineLamp - { - Id = newId - }); - } - - setId(newId); - updateAction(lampListData); - })); - + cellRect.x += 2; + cellRect.width -= 4; + + var options = new List(_gleLamps.Select(entry => entry.Id).ToArray()); + + if (options.Count > 0) { + options.Add(""); + } + + options.Add("Add..."); + + EditorGUI.BeginChangeCheck(); + var index = EditorGUI.Popup(cellRect, options.IndexOf(id), options.ToArray()); + if (EditorGUI.EndChangeCheck()) { + if (index == options.Count - 1) { + PopupWindow.Show(cellRect, new ManagerListTextFieldPopup("ID", "", newId => { + if (_gleLamps.Exists(entry => entry.Id == newId)) { + _gleLamps.Add(new GamelogicEngineLamp + { + Id = newId + }); + } + + setId(newId); + updateAction(lampListData); + })); + } - else { - setId(_gleLamps[index].Id); - updateAction(lampListData); - } - } - } - - private void RenderDescription(LampListData lampListData, Rect cellRect, Action updateAction) - { - EditorGUI.BeginChangeCheck(); - var value = EditorGUI.TextField(cellRect, lampListData.Description); - if (EditorGUI.EndChangeCheck()) { - lampListData.Description = value; - updateAction(lampListData); - } - } - - private void RenderDestination(LampListData lampListData, Rect cellRect, Action updateAction) - { - EditorGUI.BeginChangeCheck(); - var index = EditorGUI.Popup(cellRect, lampListData.Destination, OPTIONS_LAMP_DESTINATION); - if (EditorGUI.EndChangeCheck()) { - if (lampListData.Destination != index) { - lampListData.Destination = index; - updateAction(lampListData); - } - } - } - - private void RenderElement(TableAuthoring tableAuthoring, LampListData lampListData, Rect cellRect, Action updateAction) - { - var icon = GetIcon(lampListData); - - if (icon != null) { - var iconRect = cellRect; - iconRect.width = 20; - var guiColor = GUI.color; - GUI.color = Color.clear; - EditorGUI.DrawTextureTransparent(iconRect, icon, ScaleMode.ScaleToFit); - GUI.color = guiColor; - } - - cellRect.x += 25; - cellRect.width -= 25; - - switch (lampListData.Destination) { - case LampDestination.Playfield: - RenderPlayfieldElement(tableAuthoring, lampListData, cellRect, updateAction); - break; - } - } - - private void RenderPlayfieldElement(TableAuthoring tableAuthoring, LampListData lampListData, Rect cellRect, Action updateAction) - { - if (GUI.Button(cellRect, lampListData.PlayfieldItem, EditorStyles.objectField) || GUI.Button(cellRect, "", GUI.skin.GetStyle("IN ObjectField"))) { - if (_itemPickDropdownState == null) { - _itemPickDropdownState = new AdvancedDropdownState(); - } - - var dropdown = new ItemSearchableDropdown( - _itemPickDropdownState, - tableAuthoring, - "Lamp Items", - item => { - lampListData.PlayfieldItem = item.Name; - updateAction(lampListData); - } - ); - dropdown.Show(cellRect); - } - } - - private UnityEngine.Texture GetIcon(LampListData lampListData) - { - Texture2D icon = null; - - switch (lampListData.Destination) { - case LampDestination.Playfield: { - if (_lamps.ContainsKey(lampListData.PlayfieldItem.ToLower())) { - icon = Icons.ByComponent(_lamps[lampListData.PlayfieldItem.ToLower()], size: IconSize.Small); - } - break; - } - } - - return icon; - } - } -} + else { + setId(_gleLamps[index].Id); + updateAction(lampListData); + } + } + } + + private void RenderDescription(LampListData lampListData, Rect cellRect, Action updateAction) + { + EditorGUI.BeginChangeCheck(); + var value = EditorGUI.TextField(cellRect, lampListData.Description); + if (EditorGUI.EndChangeCheck()) { + lampListData.Description = value; + updateAction(lampListData); + } + } + + private void RenderDestination(LampListData lampListData, Rect cellRect, Action updateAction) + { + EditorGUI.BeginChangeCheck(); + var index = EditorGUI.Popup(cellRect, lampListData.Destination, OPTIONS_LAMP_DESTINATION); + if (EditorGUI.EndChangeCheck()) { + if (lampListData.Destination != index) { + lampListData.Destination = index; + updateAction(lampListData); + } + } + } + + private void RenderElement(TableAuthoring tableAuthoring, LampListData lampListData, Rect cellRect, Action updateAction) + { + var icon = GetIcon(lampListData); + + if (icon != null) { + var iconRect = cellRect; + iconRect.width = 20; + var guiColor = GUI.color; + GUI.color = Color.clear; + EditorGUI.DrawTextureTransparent(iconRect, icon, ScaleMode.ScaleToFit); + GUI.color = guiColor; + } + + cellRect.x += 25; + cellRect.width -= 25; + + switch (lampListData.Destination) { + case LampDestination.Playfield: + RenderPlayfieldElement(tableAuthoring, lampListData, cellRect, updateAction); + break; + } + } + + private void RenderPlayfieldElement(TableAuthoring tableAuthoring, LampListData lampListData, Rect cellRect, Action updateAction) + { + if (GUI.Button(cellRect, lampListData.PlayfieldItem, EditorStyles.objectField) || GUI.Button(cellRect, "", GUI.skin.GetStyle("IN ObjectField"))) { + if (_itemPickDropdownState == null) { + _itemPickDropdownState = new AdvancedDropdownState(); + } + + var dropdown = new ItemSearchableDropdown( + _itemPickDropdownState, + tableAuthoring, + "Lamp Items", + item => { + lampListData.PlayfieldItem = item.Name; + updateAction(lampListData); + } + ); + dropdown.Show(cellRect); + } + } + + private UnityEngine.Texture GetIcon(LampListData lampListData) + { + Texture2D icon = null; + + switch (lampListData.Destination) { + case LampDestination.Playfield: { + if (_lamps.ContainsKey(lampListData.PlayfieldItem.ToLower())) { + icon = Icons.ByComponent(_lamps[lampListData.PlayfieldItem.ToLower()], size: IconSize.Small); + } + break; + } + } + + return icon; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs index c39fd4da0..cc3ed6d41 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 @@ -50,7 +50,7 @@ private class SerializedMappings : ScriptableObject } private SerializedMappings _recordMappings; - [MenuItem("Visual Pinball/Lamp Manager", false, 107)] + [MenuItem("Visual Pinball/Lamp Manager", false, 304)] public static void ShowWindow() { GetWindow(); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireListViewItemRenderer.cs index b30ec499d..2b59e2d79 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireListViewItemRenderer.cs @@ -54,9 +54,16 @@ private enum WireListColumn private readonly Dictionary _coils; private readonly Dictionary _coilDevices; + + private readonly Dictionary _lamps; private AdvancedDropdownState _destinationElementDeviceDropdownState; - public WireListViewItemRenderer(Dictionary switches, Dictionary switchDevices, InputManager inputManager, Dictionary coils, Dictionary coilDevices) + public WireListViewItemRenderer(Dictionary switches, + Dictionary switchDevices, + Dictionary coils, + Dictionary coilDevices, + Dictionary lamps, + InputManager inputManager) { _switches = switches; _switchDevices = switchDevices; @@ -64,6 +71,8 @@ public WireListViewItemRenderer(Dictionary switches, D _coils = coils; _coilDevices = coilDevices; + + _lamps = lamps; } public void Render(TableAuthoring tableAuthoring, WireListData data, Rect cellRect, int column, Action updateAction) @@ -337,10 +346,10 @@ private void RenderDestinationElementPlayfield(TableAuthoring tableAuthoring, Wi _destinationElementDeviceDropdownState = new AdvancedDropdownState(); } - var dropdown = new ItemSearchableDropdown( + var dropdown = new ItemSearchableDropdown( _destinationElementDeviceDropdownState, tableAuthoring, - "Coil Items", + "Wireable Items", item => { wireListData.DestinationPlayfieldItem = item != null ? item.Name : string.Empty; updateAction(wireListData); @@ -466,7 +475,11 @@ private UnityEngine.Texture GetDestinationIcon(WireListData wireListData) { case CoilDestination.Playfield: if (_coils.ContainsKey(wireListData.DestinationPlayfieldItem.ToLower())) { - icon = Icons.ByComponent(_coils[wireListData.DestinationPlayfieldItem.ToLower()], size: IconSize.Small); + icon = Icons.ByComponent(_coils[wireListData.DestinationPlayfieldItem.ToLower()], IconSize.Small); + } + + if (_lamps.ContainsKey(wireListData.DestinationPlayfieldItem.ToLower())) { + icon = Icons.ByComponent(_lamps[wireListData.DestinationPlayfieldItem.ToLower()], IconSize.Small); } break; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireManager.cs index c2cf617d1..eec55e543 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireManager.cs @@ -45,6 +45,8 @@ class WireManager : ManagerWindow private readonly Dictionary _coils = new Dictionary(); private readonly Dictionary _coilDevices = new Dictionary(); + private readonly Dictionary _lamps = new Dictionary(); + private InputManager _inputManager; private bool _needsAssetRefresh; @@ -75,7 +77,7 @@ public override void OnEnable() private void OnFocus() { _inputManager = new InputManager(RESOURCE_PATH); - _listViewItemRenderer = new WireListViewItemRenderer(_switches, _switchDevices, _inputManager, _coils, _coilDevices); + _listViewItemRenderer = new WireListViewItemRenderer(_switches, _switchDevices, _coils, _coilDevices, _lamps, _inputManager); _needsAssetRefresh = true; } @@ -137,6 +139,7 @@ protected override List CollectData() RefreshSwitches(); RefreshCoils(); + RefreshLamps(); return data; } @@ -215,6 +218,16 @@ private void RefreshCoils() } } } + + private void RefreshLamps() + { + _lamps.Clear(); + if (_tableAuthoring != null) { + foreach (var item in _tableAuthoring.GetComponentsInChildren()) { + _lamps.Add(item.Name.ToLower(), item); + } + } + } #endregion #region Undo Redo diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/LayoutUtility.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/LayoutUtility.cs index 4b312e495..1589c0ee6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/LayoutUtility.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/LayoutUtility.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index 24b085604..ba72e90e8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -48,6 +48,7 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit private const string SwTrough4 = "s_trough4"; private const string SwPlunger = "s_plunger"; private const string SwCreateBall = "s_create_ball"; + private const string SwRedBumper = "s_red_bumper"; public GamelogicEngineSwitch[] AvailableSwitches { get; } = { new GamelogicEngineSwitch { Id = SwLeftFlipper, Description = "Left Flipper (button)", InputActionHint = InputConstants.ActionLeftFlipper }, @@ -61,7 +62,8 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit new GamelogicEngineSwitch { Id = SwTrough3, Description = "Trough 3", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "3"}, new GamelogicEngineSwitch { Id = SwTrough4, Description = "Trough 4", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "4"}, new GamelogicEngineSwitch { Id = SwTrough4, Description = "Trough 4", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "4"}, - new GamelogicEngineSwitch { Id = SwCreateBall, Description = "Create Debug Ball", InputActionHint = InputConstants.ActionCreateBall, InputMapHint = InputConstants.MapDebug } + new GamelogicEngineSwitch { Id = SwCreateBall, Description = "Create Debug Ball", InputActionHint = InputConstants.ActionCreateBall, InputMapHint = InputConstants.MapDebug }, + new GamelogicEngineSwitch { Id = SwRedBumper, Description = "Red Bumper", PlayfieldItemHint = "^Bumper1$" } }; private const string CoilLeftFlipperMain = "c_flipper_left_main"; @@ -82,9 +84,45 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit new GamelogicEngineCoil { Id = CoilTroughEntry, Description = "Trough Entry", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = Trough.EntryCoilId}, }; + private const string GiSlingshotRightLower = "gi_1"; + private const string GiSlingshotRightUpper = "gi_2"; + private const string GiSlingshotLeftLower = "gi_3"; + private const string GiSlingshotLeftUpper = "gi_4"; + private const string GiDropTargetsRightLower = "gi_5"; + private const string GiDropTargetsLeftLower = "gi_6"; + private const string GiDropTargetsLeftUpper = "gi_7"; + private const string GiDropTargetsRightUpper = "gi_8"; + private const string GiTop3 = "gi_9"; + private const string GiTop2 = "gi_10"; + private const string GiTop4 = "gi_11"; + private const string GiTop5 = "gi_12"; + private const string GiTop1 = "gi_13"; + private const string GiLowerRamp = "gi_14"; + private const string GiUpperRamp = "gi_15"; + private const string GiTopLeftPlastic = "gi_16"; + + private const string LampRedBumper = "l_bumper"; + + public GamelogicEngineLamp[] AvailableLamps { get; } = { - new GamelogicEngineLamp { Id = "l_11", Description = "Matrix Lamp 11" } + new GamelogicEngineLamp { Id = GiSlingshotRightLower, Description = "Right Slingshot (lower)", PlayfieldItemHint = "gi1$" }, + new GamelogicEngineLamp { Id = GiSlingshotRightUpper, Description = "Right Slingshot (upper)", PlayfieldItemHint = "gi2$" }, + new GamelogicEngineLamp { Id = GiSlingshotLeftLower, Description = "Left Slingshot (lower)", PlayfieldItemHint = "gi3$" }, + new GamelogicEngineLamp { Id = GiSlingshotLeftUpper, Description = "Left Slingshot (upper)", PlayfieldItemHint = "gi4$" }, + new GamelogicEngineLamp { Id = GiDropTargetsRightLower, Description = "Right Drop Targets (lower)", PlayfieldItemHint = "gi5$" }, + new GamelogicEngineLamp { Id = GiDropTargetsRightUpper, Description = "Right Drop Targets (upper)", PlayfieldItemHint = "gi8$" }, + new GamelogicEngineLamp { Id = GiDropTargetsLeftLower, Description = "Left Drop Targets (lower)", PlayfieldItemHint = "gi6$" }, + new GamelogicEngineLamp { Id = GiDropTargetsLeftUpper, Description = "Left Drop Targets (upper)", PlayfieldItemHint = "gi7$" }, + new GamelogicEngineLamp { Id = GiTop1, Description = "Top 1 (left)", PlayfieldItemHint = "gi13$" }, + new GamelogicEngineLamp { Id = GiTop2, Description = "Top 2", PlayfieldItemHint = "gi10$" }, + new GamelogicEngineLamp { Id = GiTop3, Description = "Top 3", PlayfieldItemHint = "gi9$" }, + new GamelogicEngineLamp { Id = GiTop4, Description = "Top 4", PlayfieldItemHint = "gi11$" }, + new GamelogicEngineLamp { Id = GiTop5, Description = "Top 5 (right)", PlayfieldItemHint = "gi12$" }, + new GamelogicEngineLamp { Id = GiLowerRamp, Description = "Ramp (lower)", PlayfieldItemHint = "gi14$" }, + new GamelogicEngineLamp { Id = GiUpperRamp, Description = "Ramp (upper)", PlayfieldItemHint = "gi15$" }, + new GamelogicEngineLamp { Id = GiTopLeftPlastic, Description = "Top Left Plastics", PlayfieldItemHint = "gi16$" }, + new GamelogicEngineLamp { Id = LampRedBumper, Description = "Red Bumper", PlayfieldItemHint = "^b1l2$" } }; private TableApi _tableApi; @@ -184,6 +222,10 @@ public void Switch(string id, bool isClosed) } break; + case SwRedBumper: + OnLampChanged?.Invoke(this, new LampEventArgs(LampRedBumper, isClosed)); + break; + case SwCreateBall: { if (isClosed) { _ballManager.CreateBall(new DebugBallCreator()); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs index d3f2e109f..8fd95b6f5 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 49cce00e5..341d824e7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -38,6 +38,7 @@ using VisualPinball.Engine.VPT.Table; using VisualPinball.Engine.VPT.Trigger; using VisualPinball.Engine.VPT.Trough; +using Light = VisualPinball.Engine.VPT.Light.Light; using Logger = NLog.Logger; namespace VisualPinball.Unity @@ -70,6 +71,7 @@ public class Player : MonoBehaviour private readonly Dictionary _switches = new Dictionary(); private readonly Dictionary _switchDevices = new Dictionary(); private readonly Dictionary _coils = new Dictionary(); + private readonly Dictionary _lamps = new Dictionary(); private readonly Dictionary _coilDevices = new Dictionary(); private readonly Dictionary _wires = new Dictionary(); private readonly Dictionary _wireDevices = new Dictionary(); @@ -81,8 +83,12 @@ public class Player : MonoBehaviour [NonSerialized] private readonly Dictionary> _keySwitchAssignments = new Dictionary>(); + [NonSerialized] private readonly Dictionary> _keyWireAssignments = new Dictionary>(); + [NonSerialized] // tuple: itemName, isHoldCoil, deviceName private readonly Dictionary>> _coilAssignments = new Dictionary>>(); + [NonSerialized] + private readonly Dictionary> _lampAssignments = new Dictionary>(); public Player() { @@ -137,6 +143,7 @@ private void Start() // hook up mapping configuration SetupSwitchMapping(); SetupCoilMapping(); + SetupLampMapping(); SetupWireMapping(); GameEngine?.OnInit(TableApi, BallManager); @@ -155,6 +162,9 @@ private void OnDestroy() if (_coilAssignments.Count > 0 && GameEngine is IGamelogicEngineWithCoils gamelogicEngineWithCoils) { gamelogicEngineWithCoils.OnCoilChanged -= HandleCoilEvent; } + if (_lampAssignments.Count > 0 && GameEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { + gamelogicEngineWithLamps.OnLampChanged -= HandleLampEvent; + } foreach (var i in _apis) { i.OnDestroy(); @@ -230,6 +240,16 @@ public void RegisterKicker(Kicker kicker, Entity entity, GameObject go) _wires[kicker.Name] = kickerApi; } + public void RegisterLamp(Light lamp, GameObject go) + { + var lightApi = new LightApi(lamp, go, this); + TableApi.Lights[lamp.Name] = lightApi; + _apis.Add(lightApi); + _initializables.Add(lightApi); + _lamps[lamp.Name] = lightApi; + _wires[lamp.Name] = lightApi; + } + public void RegisterPlunger(Plunger plunger, Entity entity, GameObject go) { var plungerApi = new PlungerApi(plunger, entity, this); @@ -424,6 +444,28 @@ private void SetupSwitchMapping() } } + private void SetupLampMapping() + { + if (GameEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { + var config = Table.Mappings; + _lampAssignments.Clear(); + foreach (var lampData in config.Data.Lamps) { + switch (lampData.Destination) { + case LampDestination.Playfield: + if (!_lampAssignments.ContainsKey(lampData.Id)) { + _lampAssignments[lampData.Id] = new List(); + } + _lampAssignments[lampData.Id].Add(lampData.PlayfieldItem); + break; + } + } + + if (_lampAssignments.Count > 0) { + gamelogicEngineWithLamps.OnLampChanged += HandleLampEvent; + } + } + } + private void SetupWireMapping() { var config = Table.Mappings; @@ -549,6 +591,24 @@ private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) } } + private void HandleLampEvent(object sender, LampEventArgs lampEvent) + { + if (_lampAssignments.ContainsKey(lampEvent.Id)) { + foreach (var itemName in _lampAssignments[lampEvent.Id]) { + if (_lamps.ContainsKey(itemName)) { + _lamps[itemName].OnLamp(lampEvent.IsOn); + + } else { + Logger.Warn($"Cannot trigger unknown lamp {itemName}."); + } + } + + } else { + var what = lampEvent.IsOn ? "turn on" : "turn off"; + Logger.Warn($"Should {what} unassigned coil {lampEvent.Id}."); + } + } + #endregion #region Events diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index ea4b7c3dd..d2f70e505 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -16,6 +16,7 @@ using System; using Unity.Entities; +using VisualPinball.Engine.Math; namespace VisualPinball.Unity { @@ -99,6 +100,12 @@ internal interface IApiCoil : IApiWireDest void OnCoil(bool enabled, bool isHoldCoil); } + internal interface IApiLamp : IApiWireDest + { + void OnLamp(bool enabled); + void OnLamp(bool enabled, Color color); + } + internal interface IApiWireDest { void OnChange(bool enabled); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilAuthoring.cs index 11bd22ed1..c7c9ae5cf 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilAuthoring.cs @@ -17,7 +17,7 @@ namespace VisualPinball.Unity { - public interface ICoilAuthoring : IIdentifiableItemAuthoring + public interface ICoilAuthoring : IWireableAuthoring { } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs index 1f14110ad..c2040322a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ILampAuthoring.cs @@ -1,5 +1,5 @@ // Visual Pinball Engine -// Copyright (C) 2020 freezy and VPE Team +// 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 @@ -15,9 +15,12 @@ // along with this program. If not, see . +using VisualPinball.Engine.Game; + namespace VisualPinball.Unity { - public interface ILampAuthoring : IIdentifiableItemAuthoring + public interface ILampAuthoring : IWireableAuthoring { + ILightable Lightable { get; } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IWireableAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IWireableAuthoring.cs new file mode 100644 index 000000000..c1a39a337 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IWireableAuthoring.cs @@ -0,0 +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 . + +namespace VisualPinball.Unity +{ + public interface IWireableAuthoring : IIdentifiableItemAuthoring + { + + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IWireableAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/IWireableAuthoring.cs.meta new file mode 100644 index 000000000..5a9d1e2a2 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IWireableAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a7a06d0a5dfa8b343956da529e83f630 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs new file mode 100644 index 000000000..a6ed5c06d --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs @@ -0,0 +1,97 @@ +// 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 . + +using System; +using UnityEngine; +using VisualPinball.Engine.VPT; +using VisualPinball.Engine.VPT.Light; +using Color = VisualPinball.Engine.Math.Color; +using Light = VisualPinball.Engine.VPT.Light.Light; + +namespace VisualPinball.Unity +{ + public class LightApi : ItemApi, IApiInitializable, IApiLamp + { + /// + /// Event emitted when the table is started. + /// + public event EventHandler Init; + + public int State { get => _state; set => Set(value); } + + private int _state; + private readonly LightAuthoring _lightAuthoring; + + void IApiWireDest.OnChange(bool enabled) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff); + void IApiLamp.OnLamp(bool enabled) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff); + void IApiLamp.OnLamp(bool enabled, Color color) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff, color); + + internal LightApi(Light item, GameObject go, Player player) : base(item, player) + { + _lightAuthoring = go.GetComponentInChildren(); + _state = item.Data.State; + } + + private void Set(int lightStatus) + { + switch (lightStatus) { + case LightStatus.LightStateOff: { + if (Data.FadeSpeedDown > 0) { + _lightAuthoring.FadeOut(Data.FadeSpeedDown); + + } else { + _lightAuthoring.Enabled = false; + } + break; + } + + case LightStatus.LightStateOn: { + if (Data.FadeSpeedUp > 0) { + _lightAuthoring.FadeIn(Data.FadeSpeedUp); + + } else { + _lightAuthoring.Enabled = true; + } + break; + } + + case LightStatus.LightStateBlinking: { + throw new NotImplementedException("Blinking lights not implemented yet."); + } + + default: + throw new ArgumentOutOfRangeException(); + } + _state = lightStatus; + } + + private void Set(int lightStatus, Color color) + { + _lightAuthoring.Color = color; + Set(lightStatus); + } + + #region Events + + void IApiInitializable.OnInit(BallManager ballManager) + { + base.OnInit(ballManager); + Init?.Invoke(this, EventArgs.Empty); + } + + #endregion + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs.meta new file mode 100644 index 000000000..cf1c65479 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00ac33dcc0cd52b4786f94e1bf049fee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs index bc168ff07..1b50a3dc9 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs @@ -21,28 +21,125 @@ #endregion using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using NLog; using UnityEngine; +using VisualPinball.Engine.Game; using VisualPinball.Engine.VPT.Light; +using Color = VisualPinball.Engine.Math.Color; using Light = VisualPinball.Engine.VPT.Light.Light; +using Logger = NLog.Logger; namespace VisualPinball.Unity { [AddComponentMenu("Visual Pinball/Game Item/Light")] - public class LightAuthoring : ItemMainRenderableAuthoring + public class LightAuthoring : ItemMainRenderableAuthoring, ILampAuthoring { + public ILightable Lightable => Item; + + public bool Enabled { + set { + StopAllCoroutines(); + _unityLight.enabled = value; + } + } + public Color Color { + set { + StopAllCoroutines(); + _unityLight.color = value.ToUnityColor(); + } + } + private UnityEngine.Light _unityLight; + private float _fullIntensity; protected override Light InstantiateItem(LightData data) => new Light(data); protected override Type MeshAuthoringType { get; } = typeof(ItemMeshAuthoring); protected override Type ColliderAuthoringType { get; } = null; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + public override IEnumerable ValidParents => LightBulbMeshAuthoring.ValidParentTypes .Concat(LightSocketMeshAuthoring.ValidParentTypes) .Distinct(); + private void Start() + { + var player = GetComponentInParent(); + if (player == null) { + Logger.Error($"Cannot find player for lamp {Name}."); + return; + } + + player.RegisterLamp(Item, gameObject); + _unityLight = GetComponentInChildren(); + _fullIntensity = _unityLight.intensity; + } + + + public void FadeIn(float seconds) + { + StopAllCoroutines(); + StartCoroutine(nameof(Fade), true); + } + + public void FadeOut(float seconds) + { + StopAllCoroutines(); + StartCoroutine(nameof(Fade), false); + } + + public void StartBlinking() + { + StopAllCoroutines(); + StartCoroutine(nameof(Blink)); + } + + private IEnumerator Blink() + { + // parse blink sequence + var sequence = Data.BlinkPattern.ToCharArray().Select(c => c == '1').ToArray(); + + // step time is stored in ms but we need seconds + var stepTime = Data.BlinkInterval / 1000f; + + while (true) { + foreach (var on in sequence) { + yield return Fade(on); + var timeFading = on ? Data.FadeSpeedUp : Data.FadeSpeedDown; + if (timeFading < stepTime) { + yield return new WaitForSeconds(stepTime - timeFading); + } + } + } + } + + private IEnumerator Fade(bool fadeIn) + { + var counter = 0f; + + float a, b, duration; + + if (fadeIn) { + a = _unityLight.intensity; + b = _fullIntensity; + duration = _data.FadeSpeedUp * (_fullIntensity - a) / _fullIntensity; + } else { + a = _unityLight.intensity; + b = 0f; + duration = _data.FadeSpeedDown * (1 - (_fullIntensity - a) / _fullIntensity); + } + + while (counter < duration) { + counter += Time.deltaTime; + _unityLight.intensity = Mathf.Lerp(a, b, counter / duration); + yield return null; + } + } + public override void Restore() { // update the name diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs index 894d9b2ed..2dfa4d0fe 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableApi.cs @@ -28,6 +28,7 @@ public class TableApi : IApiInitializable internal readonly Dictionary Gates = new Dictionary(); internal readonly Dictionary HitTargets = new Dictionary(); internal readonly Dictionary Kickers = new Dictionary(); + internal readonly Dictionary Lights = new Dictionary(); internal readonly Dictionary Plungers = new Dictionary(); internal readonly Dictionary Ramps = new Dictionary(); internal readonly Dictionary Rubbers = new Dictionary(); @@ -84,6 +85,13 @@ public TableApi(Player player) /// Kicker or `null` if no kicker with that name exists. public KickerApi Kicker(string name) => Kickers.ContainsKey(name) ? Kickers[name] : null; + /// + /// Returns a light by name. + /// + /// Name of the light + /// Light or `null` if no light with that name exists. + public LightApi Light(string name) => Lights.ContainsKey(name) ? Lights[name] : null; + /// /// Returns a plunger by name. /// diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs index c6c21a733..60368e411 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs @@ -31,6 +31,7 @@ namespace VisualPinball.Unity { [AddComponentMenu("Visual Pinball/Trough")] + [HelpURL("https://docs.visualpinball.org/creators-guide/manual/mechanisms/troughs.html")] public class TroughAuthoring : ItemMainAuthoring, ISwitchDeviceAuthoring, ICoilDeviceAuthoring { public IEnumerable AvailableSwitches => Item.AvailableSwitches; From f1d7094e2dbaf52412d89abe9e06daa221c9c907 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 11 Jan 2021 00:09:19 +0100 Subject: [PATCH 03/23] lamps: Add types. --- .../Game/Engines/GamelogicEngineLamp.cs | 3 ++ VisualPinball.Engine/VPT/Enums.cs | 7 ++++ VisualPinball.Engine/VPT/Mappings/Mappings.cs | 35 +++++++++++++++++-- .../VPT/Mappings/MappingsLampData.cs | 12 ++++++- .../Managers/Lamp/LampListData.cs | 13 +++++++ .../Managers/Lamp/LampListViewItemRenderer.cs | 34 ++++++++++++++++++ .../Managers/Lamp/LampManager.cs | 3 ++ 7 files changed, 104 insertions(+), 3 deletions(-) diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs index d9334ba8d..dfdb186d2 100644 --- a/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs +++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineLamp.cs @@ -21,5 +21,8 @@ public struct GamelogicEngineLamp public string Id; public string Description; public string PlayfieldItemHint; + public int TypeHint; + public string MainLampIdOfGreen; + public string MainLampIdOfBlue; } } diff --git a/VisualPinball.Engine/VPT/Enums.cs b/VisualPinball.Engine/VPT/Enums.cs index fe957d775..a228e063f 100644 --- a/VisualPinball.Engine/VPT/Enums.cs +++ b/VisualPinball.Engine/VPT/Enums.cs @@ -196,4 +196,11 @@ public static class LampDestination public const int Playfield = 0; public const int Device = 1; } + + public static class LampType + { + public const int SingleOnOff = 0; + public const int SingleFading = 1; + public const int Rgb = 2; + } } diff --git a/VisualPinball.Engine/VPT/Mappings/Mappings.cs b/VisualPinball.Engine/VPT/Mappings/Mappings.cs index e21c1d5c3..ddbca7be7 100644 --- a/VisualPinball.Engine/VPT/Mappings/Mappings.cs +++ b/VisualPinball.Engine/VPT/Mappings/Mappings.cs @@ -238,11 +238,12 @@ public void PopulateCoils(GamelogicEngineCoil[] engineCoils, IEnumerable x.Name.ToLower()) .ToDictionary(x => x.Key, x => x.First()); + var gbLamps = new List(); foreach (var engineLamp in GetLamps(engineLamps)) { var lampMapping = Data.Lamps.FirstOrDefault(mappingsLampData => mappingsLampData.Id == engineLamp.Id); if (lampMapping == null) { + // we'll handle those in a second loop when all the R lamps are added + if (!string.IsNullOrEmpty(engineLamp.MainLampIdOfGreen) || !string.IsNullOrEmpty(engineLamp.MainLampIdOfBlue)) { + gbLamps.Add(engineLamp); + continue; + } + var description = string.IsNullOrEmpty(engineLamp.Description) ? string.Empty : engineLamp.Description; var playfieldItem = GuessPlayfieldLamp(lamps, engineLamp); @@ -359,6 +367,29 @@ public void PopulateLamps(GamelogicEngineLamp[] engineLamps, IEnumerable c.Id == rLampId); + if (rLamp == null) { + var playfieldItem = GuessPlayfieldLamp(lamps, gbLamp); + rLamp = new MappingsLampData { + Id = gbLamp.Id, + Description = string.IsNullOrEmpty(gbLamp.Description) ? string.Empty : gbLamp.Description, + Destination = LampDestination.Playfield, + PlayfieldItem = playfieldItem != null ? playfieldItem.Name : string.Empty, + }; + Data.AddLamp(rLamp); + } + + rLamp.Type = LampType.Rgb; + if (!string.IsNullOrEmpty(gbLamp.MainLampIdOfGreen)) { + rLamp.Green = gbLamp.Id; + + } else { + rLamp.Blue = gbLamp.Id; + } + } } /// diff --git a/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs index ba454bbff..5848a9087 100644 --- a/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs +++ b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs @@ -15,18 +15,19 @@ // along with this program. If not, see . #region ReSharper + // ReSharper disable UnassignedField.Global // ReSharper disable StringLiteralTypo // ReSharper disable FieldCanBeMadeReadOnly.Global // ReSharper disable ConvertToConstant.Global // ReSharper disable CompareOfFloatsByEqualityOperator + #endregion using System; using System.Collections.Generic; using System.IO; using VisualPinball.Engine.IO; -using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT.Table; namespace VisualPinball.Engine.VPT.Mappings @@ -52,6 +53,15 @@ public class MappingsLampData : BiffData [BiffString("DITM", IsWideString = true, Pos = 6)] public string DeviceItem = string.Empty; + [BiffInt("LTYP", Pos = 7)] + public int Type = LampType.SingleOnOff; + + [BiffString("RGBG", IsWideString = true, Pos = 8)] + public string Green = string.Empty; + + [BiffString("RGBB", IsWideString = true, Pos = 9)] + public string Blue = string.Empty; + #region BIFF static MappingsLampData() diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs index 30d82f288..d6708a32d 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs @@ -32,6 +32,13 @@ public class LampListData : IManagerListData [ManagerListColumn(Order = 3, HeaderName = "Element", Width = 200)] public string Element; + [ManagerListColumn(Order = 4, HeaderName = "Type", Width = 110)] + public int Type; + + [ManagerListColumn(Order = 5, HeaderName = "R G B", Width = 135)] + public string Green; + public string Blue; + public string Id; public string PlayfieldItem; public string Device; @@ -46,6 +53,9 @@ public LampListData(MappingsLampData mappingsLampData) PlayfieldItem = mappingsLampData.PlayfieldItem; Device = mappingsLampData.Device; DeviceItem = mappingsLampData.DeviceItem; + Type = mappingsLampData.Type; + Green = mappingsLampData.Green; + Blue = mappingsLampData.Blue; MappingsLampData = mappingsLampData; } @@ -57,6 +67,9 @@ public void Update() MappingsLampData.PlayfieldItem = PlayfieldItem; MappingsLampData.Device = Device; MappingsLampData.DeviceItem = DeviceItem; + MappingsLampData.Type = Type; + MappingsLampData.Green = Green; + MappingsLampData.Blue = Blue; } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs index 46bf4bb51..c32b4d4db 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs @@ -28,6 +28,7 @@ namespace VisualPinball.Unity.Editor public class LampListViewItemRenderer { private readonly string[] OPTIONS_LAMP_DESTINATION = { "Playfield" }; + private readonly string[] OPTIONS_LAMP_TYPE = { "Single On|Off", "Single Fading", "RGB" }; private enum LampListColumn { @@ -35,6 +36,8 @@ private enum LampListColumn Description = 1, Destination = 2, Element = 3, + Type = 4, + Color = 5, } private readonly List _gleLamps; @@ -63,6 +66,14 @@ public void Render(TableAuthoring tableAuthoring, LampListData data, Rect cellRe case LampListColumn.Element: RenderElement(tableAuthoring, data, cellRect, updateAction); break; + case LampListColumn.Type: + RenderType(data, cellRect, updateAction); + break; + case LampListColumn.Color: + if (data.Type == LampType.Rgb) { + RenderRgb(data, cellRect, updateAction); + } + break; } } @@ -169,6 +180,29 @@ private void RenderPlayfieldElement(TableAuthoring tableAuthoring, LampListData } } + private void RenderType(LampListData lampListData, Rect cellRect, Action updateAction) + { + EditorGUI.BeginChangeCheck(); + var index = EditorGUI.Popup(cellRect, (int)lampListData.Type, OPTIONS_LAMP_TYPE); + if (EditorGUI.EndChangeCheck()) { + lampListData.Type = index; + updateAction(lampListData); + } + } + + private void RenderRgb(LampListData data, Rect cellRect, Action updateAction) + { + var pad = 2; + var width = cellRect.width / 3; + var c = cellRect; + c.width = width - pad; + RenderId(ref data.Id, id => data.Id = id, data, c, updateAction); + c.x += width + pad; + RenderId(ref data.Green, id => data.Green = id, data, c, updateAction); + c.x += width + pad; + RenderId(ref data.Blue, id => data.Blue = id, data, c, updateAction); + } + private UnityEngine.Texture GetIcon(LampListData lampListData) { Texture2D icon = null; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs index cc3ed6d41..da91b46a0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs @@ -159,6 +159,9 @@ protected override void CloneData(string undoName, string newName, LampListData PlayfieldItem = data.PlayfieldItem, Device = data.Device, DeviceItem = data.DeviceItem, + Type = data.Type, + Blue = data.Blue, + Green = data.Green, }); } #endregion From 4fd6238f0f76cf6808f2ba87506744511372c309 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 12 Jan 2021 00:01:44 +0100 Subject: [PATCH 04/23] lamps: Add RGB output (untested). --- .../Game/Engine/DefaultGamelogicEngine.cs | 2 +- .../Game/Engine/IGamelogicEngineWithLamps.cs | 27 ++++++++++++- .../VisualPinball.Unity/Game/Player.cs | 40 +++++++++++++++++-- .../VisualPinball.Unity/VPT/IApi.cs | 1 + .../VisualPinball.Unity/VPT/Light/LightApi.cs | 13 ++++-- .../VPT/Light/LightAuthoring.cs | 30 +++++--------- 6 files changed, 82 insertions(+), 31 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index ba72e90e8..a4dc5b6b2 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -223,7 +223,7 @@ public void Switch(string id, bool isClosed) break; case SwRedBumper: - OnLampChanged?.Invoke(this, new LampEventArgs(LampRedBumper, isClosed)); + OnLampChanged?.Invoke(this, new LampEventArgs(LampRedBumper, isClosed)); break; case SwCreateBall: { diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs index 8fd95b6f5..6fd36648f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs @@ -40,6 +40,11 @@ public interface IGamelogicEngineWithLamps public readonly struct LampEventArgs { + public enum ValueType + { + Bool, Int + } + /// /// Id of the lamp, as defined by . /// @@ -48,12 +53,30 @@ public readonly struct LampEventArgs /// /// State of the lamp, true if the lamp is illuminated, false if not. /// - public readonly bool IsOn; + public readonly bool BoolValue; + + /// + /// Value of the lamp. Depending on its type, it can be 0/1 for on/off, or 0-255 for + /// a fading light. + /// + public readonly int IntValue; + + public readonly ValueType Type; public LampEventArgs(string id, bool isOn) { Id = id; - IsOn = isOn; + BoolValue = isOn; + IntValue = 0; + Type = ValueType.Bool; + } + + public LampEventArgs(string id, int value) + { + Id = id; + BoolValue = false; + IntValue = value; + Type = ValueType.Int; } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 341d824e7..83ad1da35 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -29,6 +29,7 @@ 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; @@ -89,6 +90,7 @@ public class Player : MonoBehaviour private readonly Dictionary>> _coilAssignments = new Dictionary>>(); [NonSerialized] private readonly Dictionary> _lampAssignments = new Dictionary>(); + private readonly Dictionary _lampMappings = new Dictionary(); public Player() { @@ -449,6 +451,7 @@ private void SetupLampMapping() if (GameEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { var config = Table.Mappings; _lampAssignments.Clear(); + _lampMappings.Clear(); foreach (var lampData in config.Data.Lamps) { switch (lampData.Destination) { case LampDestination.Playfield: @@ -456,6 +459,7 @@ private void SetupLampMapping() _lampAssignments[lampData.Id] = new List(); } _lampAssignments[lampData.Id].Add(lampData.PlayfieldItem); + _lampMappings[lampData.Id] = lampData; break; } } @@ -594,9 +598,40 @@ private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) private void HandleLampEvent(object sender, LampEventArgs lampEvent) { if (_lampAssignments.ContainsKey(lampEvent.Id)) { + var mapping = _lampMappings[lampEvent.Id]; foreach (var itemName in _lampAssignments[lampEvent.Id]) { if (_lamps.ContainsKey(itemName)) { - _lamps[itemName].OnLamp(lampEvent.IsOn); + switch (mapping.Type) { + case LampType.SingleOnOff: + switch (lampEvent.Type) { + case LampEventArgs.ValueType.Bool: + _lamps[itemName].OnLamp(lampEvent.BoolValue); + break; + case LampEventArgs.ValueType.Int: + _lamps[itemName].OnLamp(lampEvent.IntValue == 1); + break; + default: + throw new ArgumentOutOfRangeException(); + } + break; + + case LampType.SingleFading: + switch (lampEvent.Type) { + case LampEventArgs.ValueType.Bool: + _lamps[itemName].OnLamp(lampEvent.BoolValue ? 1f : 0f); + break; + case LampEventArgs.ValueType.Int: + _lamps[itemName].OnLamp(lampEvent.IntValue / 255f); + break; + default: + throw new ArgumentOutOfRangeException(); + } + break; + + case LampType.Rgb: + + break; + } } else { Logger.Warn($"Cannot trigger unknown lamp {itemName}."); @@ -604,8 +639,7 @@ private void HandleLampEvent(object sender, LampEventArgs lampEvent) } } else { - var what = lampEvent.IsOn ? "turn on" : "turn off"; - Logger.Warn($"Should {what} unassigned coil {lampEvent.Id}."); + Logger.Warn($"Should update unassigned lamp {lampEvent.Id}."); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index d2f70e505..3827363cb 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -103,6 +103,7 @@ internal interface IApiCoil : IApiWireDest internal interface IApiLamp : IApiWireDest { void OnLamp(bool enabled); + void OnLamp(float value); void OnLamp(bool enabled, Color color); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs index a6ed5c06d..a496f6cb3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs @@ -37,6 +37,10 @@ public class LightApi : ItemApi, IApiInitializable, IApiLamp void IApiWireDest.OnChange(bool enabled) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff); void IApiLamp.OnLamp(bool enabled) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff); + void IApiLamp.OnLamp(float value) + { + throw new NotImplementedException(); + } void IApiLamp.OnLamp(bool enabled, Color color) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff, color); internal LightApi(Light item, GameObject go, Player player) : base(item, player) @@ -45,12 +49,12 @@ internal LightApi(Light item, GameObject go, Player player) : base(item, player) _state = item.Data.State; } - private void Set(int lightStatus) + private void Set(int lightStatus, float value = 1f) { switch (lightStatus) { case LightStatus.LightStateOff: { if (Data.FadeSpeedDown > 0) { - _lightAuthoring.FadeOut(Data.FadeSpeedDown); + _lightAuthoring.FadeTo(Data.FadeSpeedDown, 0f); } else { _lightAuthoring.Enabled = false; @@ -60,7 +64,7 @@ private void Set(int lightStatus) case LightStatus.LightStateOn: { if (Data.FadeSpeedUp > 0) { - _lightAuthoring.FadeIn(Data.FadeSpeedUp); + _lightAuthoring.FadeTo(Data.FadeSpeedUp, value); } else { _lightAuthoring.Enabled = true; @@ -69,7 +73,8 @@ private void Set(int lightStatus) } case LightStatus.LightStateBlinking: { - throw new NotImplementedException("Blinking lights not implemented yet."); + _lightAuthoring.StartBlinking(); + break; } default: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs index 1b50a3dc9..71d61b4fe 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs @@ -79,18 +79,12 @@ private void Start() _fullIntensity = _unityLight.intensity; } - - public void FadeIn(float seconds) + public void FadeTo(float seconds, float value) { StopAllCoroutines(); - StartCoroutine(nameof(Fade), true); + StartCoroutine(nameof(Fade), value); } - public void FadeOut(float seconds) - { - StopAllCoroutines(); - StartCoroutine(nameof(Fade), false); - } public void StartBlinking() { @@ -108,7 +102,7 @@ private IEnumerator Blink() while (true) { foreach (var on in sequence) { - yield return Fade(on); + yield return Fade(on ? 1 : 0); var timeFading = on ? Data.FadeSpeedUp : Data.FadeSpeedDown; if (timeFading < stepTime) { yield return new WaitForSeconds(stepTime - timeFading); @@ -117,21 +111,15 @@ private IEnumerator Blink() } } - private IEnumerator Fade(bool fadeIn) + private IEnumerator Fade(float value) { var counter = 0f; - float a, b, duration; - - if (fadeIn) { - a = _unityLight.intensity; - b = _fullIntensity; - duration = _data.FadeSpeedUp * (_fullIntensity - a) / _fullIntensity; - } else { - a = _unityLight.intensity; - b = 0f; - duration = _data.FadeSpeedDown * (1 - (_fullIntensity - a) / _fullIntensity); - } + var a = _unityLight.intensity; + var b = value; + var duration = a < b + ? _data.FadeSpeedUp * (_fullIntensity - a) / _fullIntensity + : _data.FadeSpeedDown * (1 - (_fullIntensity - a) / _fullIntensity); while (counter < duration) { counter += Time.deltaTime; From 18bba6b929bf420b8515b3e6ec7d872b92705956 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 13 Jan 2021 00:27:07 +0100 Subject: [PATCH 05/23] lamps: Fix RGB output, add multi-lamp API and unit tests. --- .../VPT/Mappings/CoilPopulationTests.cs | 6 +- .../VPT/Mappings/LampPopulationTests.cs | 182 ++++++++++++++++++ VisualPinball.Engine/Math/Color.cs | 5 + VisualPinball.Engine/VPT/Mappings/Mappings.cs | 3 +- .../VPT/Table/TableBuilder.cs | 7 + .../Game/Engine/DefaultGamelogicEngine.cs | 3 +- .../Game/Engine/IGamelogicEngineWithLamps.cs | 42 ++-- .../VisualPinball.Unity/Game/Player.cs | 115 +++++++---- .../VisualPinball.Unity/VPT/IApi.cs | 5 +- .../VisualPinball.Unity/VPT/Light/LightApi.cs | 56 ++++-- .../VPT/Light/LightAuthoring.cs | 8 +- 11 files changed, 343 insertions(+), 89 deletions(-) create mode 100644 VisualPinball.Engine.Test/VPT/Mappings/LampPopulationTests.cs diff --git a/VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs b/VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs index 08611e317..cc9602226 100644 --- a/VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs +++ b/VisualPinball.Engine.Test/VPT/Mappings/CoilPopulationTests.cs @@ -104,7 +104,7 @@ public void ShouldMapAHoldCoilByHint() } [Test] - public void ShouldMapAHoldCoilAsMainCoilIfHintedMainCoilNotFound() + public void ShouldCreateMainCoilIfNotFoundByHoldCoil() { var table = new TableBuilder() .AddFlipper("left_flipper") @@ -123,9 +123,9 @@ public void ShouldMapAHoldCoilAsMainCoilIfHintedMainCoilNotFound() table.Mappings.Data.Coils[0].PlayfieldItem.Should().Be("left_flipper"); table.Mappings.Data.Coils[0].HoldCoilId.Should().BeEmpty(); - table.Mappings.Data.Coils[1].Id.Should().Be("left_flipper_hold"); + table.Mappings.Data.Coils[1].Id.Should().Be("foobar"); table.Mappings.Data.Coils[1].PlayfieldItem.Should().BeEmpty(); - table.Mappings.Data.Coils[1].HoldCoilId.Should().BeEmpty(); + table.Mappings.Data.Coils[1].HoldCoilId.Should().Be("left_flipper_hold"); } [Test] diff --git a/VisualPinball.Engine.Test/VPT/Mappings/LampPopulationTests.cs b/VisualPinball.Engine.Test/VPT/Mappings/LampPopulationTests.cs new file mode 100644 index 000000000..6bbb741cc --- /dev/null +++ b/VisualPinball.Engine.Test/VPT/Mappings/LampPopulationTests.cs @@ -0,0 +1,182 @@ +// 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 . + +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT; +using VisualPinball.Engine.VPT.Mappings; +using VisualPinball.Engine.VPT.Table; + +namespace VisualPinball.Engine.Test.VPT.Mappings +{ + public class LampPopulationTests + { + [Test] + public void ShouldMapALampWithTheSameName() + { + var table = new TableBuilder() + .AddLight("some_light") + .Build(); + + var gameEngineLamps = new[] { + new GamelogicEngineLamp {Id = "some_light", Description = "Some Light"} + }; + + table.Mappings.PopulateLamps(gameEngineLamps, table.Lightables); + + table.Mappings.Data.Lamps.Should().HaveCount(1); + table.Mappings.Data.Lamps[0].Destination.Should().Be(CoilDestination.Playfield); + table.Mappings.Data.Lamps[0].Id.Should().Be("some_light"); + table.Mappings.Data.Lamps[0].Description.Should().Be("Some Light"); + table.Mappings.Data.Lamps[0].PlayfieldItem.Should().Be("some_light"); + } + + [Test] + public void ShouldMapALampWithTheSameNumericalId() + { + var table = new TableBuilder() + .AddLight("l42") + .Build(); + + var gameEngineLamps = new[] { + new GamelogicEngineLamp {Id = "42", Description = "Light 42"} + }; + + table.Mappings.PopulateLamps(gameEngineLamps, table.Lightables); + + table.Mappings.Data.Lamps.Should().HaveCount(1); + table.Mappings.Data.Lamps[0].Destination.Should().Be(CoilDestination.Playfield); + table.Mappings.Data.Lamps[0].Id.Should().Be("42"); + table.Mappings.Data.Lamps[0].Description.Should().Be("Light 42"); + table.Mappings.Data.Lamps[0].PlayfieldItem.Should().Be("l42"); + } + + [Test] + public void ShouldMapALampWithViaRegex() + { + var table = new TableBuilder() + .AddLight("lamp_foobar_name") + .Build(); + + var gameEngineLamps = new[] { + new GamelogicEngineLamp {Id = "11", Description = "Foobar", PlayfieldItemHint = "_foobar_"} + }; + + table.Mappings.PopulateLamps(gameEngineLamps, table.Lightables); + + table.Mappings.Data.Lamps.Should().HaveCount(1); + table.Mappings.Data.Lamps[0].Destination.Should().Be(CoilDestination.Playfield); + table.Mappings.Data.Lamps[0].Id.Should().Be("11"); + table.Mappings.Data.Lamps[0].Description.Should().Be("Foobar"); + table.Mappings.Data.Lamps[0].PlayfieldItem.Should().Be("lamp_foobar_name"); + } + + [Test] + public void ShouldNotMapALampWithViaRegex() + { + var table = new TableBuilder() + .AddLight("lamp_foobar_name") + .Build(); + + var gameEngineLamps = new[] { + new GamelogicEngineLamp {Id = "12", Description = "Foobar", PlayfieldItemHint = "^_foobar_$"} + }; + + table.Mappings.PopulateLamps(gameEngineLamps, table.Lightables); + + table.Mappings.Data.Lamps.Should().HaveCount(1); + table.Mappings.Data.Lamps[0].Destination.Should().Be(CoilDestination.Playfield); + table.Mappings.Data.Lamps[0].Id.Should().Be("12"); + table.Mappings.Data.Lamps[0].Description.Should().Be("Foobar"); + table.Mappings.Data.Lamps[0].PlayfieldItem.Should().BeEmpty(); + } + + [Test] + public void ShouldMapAnRgbLamp() + { + var table = new TableBuilder() + .AddLight("my_rgb_light") + .Build(); + + var gameEngineLamps = new[] { + new GamelogicEngineLamp {Id = "rgb", Description = "RGB", PlayfieldItemHint = "rgb"}, + new GamelogicEngineLamp {Id = "g", MainLampIdOfGreen = "rgb"}, + new GamelogicEngineLamp {Id = "b", MainLampIdOfBlue = "rgb"} + }; + + table.Mappings.PopulateLamps(gameEngineLamps, table.Lightables); + + table.Mappings.Data.Lamps.Should().HaveCount(1); + table.Mappings.Data.Lamps[0].Destination.Should().Be(CoilDestination.Playfield); + table.Mappings.Data.Lamps[0].Id.Should().Be("rgb"); + table.Mappings.Data.Lamps[0].Green.Should().Be("g"); + table.Mappings.Data.Lamps[0].Blue.Should().Be("b"); + table.Mappings.Data.Lamps[0].Description.Should().Be("RGB"); + table.Mappings.Data.Lamps[0].PlayfieldItem.Should().Be("my_rgb_light"); + } + + [Test] + public void ShouldCreateMapAnRgbLampIfRisMissing() + { + var table = new TableBuilder() + .AddLight("my_rgb_light") + .Build(); + + var gameEngineLamps = new[] { + new GamelogicEngineLamp {Id = "g", Description = "RGB", MainLampIdOfGreen = "rgb"}, + new GamelogicEngineLamp {Id = "b", MainLampIdOfBlue = "rgb"} + }; + + table.Mappings.PopulateLamps(gameEngineLamps, table.Lightables); + + table.Mappings.Data.Lamps.Should().HaveCount(1); + table.Mappings.Data.Lamps[0].Destination.Should().Be(CoilDestination.Playfield); + table.Mappings.Data.Lamps[0].Id.Should().Be("rgb"); + table.Mappings.Data.Lamps[0].Green.Should().Be("g"); + table.Mappings.Data.Lamps[0].Blue.Should().Be("b"); + table.Mappings.Data.Lamps[0].Description.Should().BeEmpty(); + table.Mappings.Data.Lamps[0].PlayfieldItem.Should().BeEmpty(); + } + + [Test] + public void ShouldReturnAllLampIds() + { + var table = new TableBuilder() + .AddLight("l11") + .AddLight("l12") + .Build(); + + var gameEngineLamps = new[] { + new GamelogicEngineLamp {Id = "11" }, + }; + + table.Mappings.PopulateLamps(gameEngineLamps, table.Lightables); + table.Mappings.Data.AddLamp(new MappingsLampData { + Id = "12", + Destination = LampDestination.Playfield, + PlayfieldItem = "l12" + }); + + var lampIds = table.Mappings.GetLamps(gameEngineLamps).ToArray(); + + lampIds.Length.Should().Be(2); + lampIds[0].Id.Should().Be("11"); + lampIds[1].Id.Should().Be("12"); + } + } +} diff --git a/VisualPinball.Engine/Math/Color.cs b/VisualPinball.Engine/Math/Color.cs index bb7e2a31c..6338d511c 100644 --- a/VisualPinball.Engine/Math/Color.cs +++ b/VisualPinball.Engine/Math/Color.cs @@ -90,6 +90,11 @@ public enum ColorFormat Bgr, Argb } + public enum ColorChannel + { + Red, Green, Blue, Alpha + } + public static class Colors { public static readonly Color Black = new Color(0, 0, 0, 255); diff --git a/VisualPinball.Engine/VPT/Mappings/Mappings.cs b/VisualPinball.Engine/VPT/Mappings/Mappings.cs index ddbca7be7..94a6f7be4 100644 --- a/VisualPinball.Engine/VPT/Mappings/Mappings.cs +++ b/VisualPinball.Engine/VPT/Mappings/Mappings.cs @@ -374,8 +374,7 @@ public void PopulateLamps(GamelogicEngineLamp[] engineLamps, IEnumerable OnCoilChanged; public event EventHandler OnLampChanged; + public event EventHandler OnLampsChanged; private const string SwLeftFlipper = "s_left_flipper"; private const string SwLeftFlipperEos = "s_left_flipper_eos"; @@ -223,7 +224,7 @@ public void Switch(string id, bool isClosed) break; case SwRedBumper: - OnLampChanged?.Invoke(this, new LampEventArgs(LampRedBumper, isClosed)); + OnLampChanged?.Invoke(this, new LampEventArgs(LampRedBumper, isClosed ? 1 : 0)); break; case SwCreateBall: { diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs index 6fd36648f..44002b34c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs @@ -36,47 +36,45 @@ public interface IGamelogicEngineWithLamps /// Triggered when a lamp is turned on or off. /// event EventHandler OnLampChanged; + + /// + /// Triggered when multiple lamps are turned on or off at once. + /// + /// + /// + /// This also allows to to group RGB updates, i.e. updating the color + /// at once instead of each channel individually. + /// + event EventHandler OnLampsChanged; } public readonly struct LampEventArgs { - public enum ValueType - { - Bool, Int - } - /// /// Id of the lamp, as defined by . /// public readonly string Id; - /// - /// State of the lamp, true if the lamp is illuminated, false if not. - /// - public readonly bool BoolValue; - /// /// Value of the lamp. Depending on its type, it can be 0/1 for on/off, or 0-255 for /// a fading light. /// - public readonly int IntValue; - - public readonly ValueType Type; + public readonly int Value; - public LampEventArgs(string id, bool isOn) + public LampEventArgs(string id, int value) { Id = id; - BoolValue = isOn; - IntValue = 0; - Type = ValueType.Bool; + Value = value; } + } - public LampEventArgs(string id, int value) + public readonly struct LampsEventArgs + { + public readonly LampEventArgs[] LampsChanged; + + public LampsEventArgs(LampEventArgs[] lampsChanged) { - Id = id; - BoolValue = false; - IntValue = value; - Type = ValueType.Int; + LampsChanged = lampsChanged; } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 83ad1da35..1d32cb805 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -17,12 +17,14 @@ using System; using System.Collections.Generic; using NLog; +using NLog.Fluent; using Unity.Entities; using Unity.Mathematics; using UnityEngine; using UnityEngine.InputSystem; using VisualPinball.Engine.Common; using VisualPinball.Engine.Game; +using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Bumper; using VisualPinball.Engine.VPT.Flipper; @@ -39,6 +41,7 @@ using VisualPinball.Engine.VPT.Table; using VisualPinball.Engine.VPT.Trigger; using VisualPinball.Engine.VPT.Trough; +using Color = UnityEngine.Color; using Light = VisualPinball.Engine.VPT.Light.Light; using Logger = NLog.Logger; @@ -166,6 +169,7 @@ private void OnDestroy() } if (_lampAssignments.Count > 0 && GameEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { gamelogicEngineWithLamps.OnLampChanged -= HandleLampEvent; + gamelogicEngineWithLamps.OnLampsChanged -= HandleLampsEvent; } foreach (var i in _apis) { @@ -343,15 +347,9 @@ private void SetupCoilMapping() foreach (var coilData in config.Data.Coils) { switch (coilData.Destination) { case CoilDestination.Playfield: - if (!_coilAssignments.ContainsKey(coilData.Id)) { - _coilAssignments[coilData.Id] = new List>(); - } - _coilAssignments[coilData.Id].Add(new Tuple(coilData.PlayfieldItem, false, null)); + AssignCoilMapping(coilData.Id, coilData); if (coilData.Type == CoilType.DualWound) { - if (!_coilAssignments.ContainsKey(coilData.HoldCoilId)) { - _coilAssignments[coilData.HoldCoilId] = new List>(); - } - _coilAssignments[coilData.HoldCoilId].Add(new Tuple(coilData.PlayfieldItem, true, null)); + AssignCoilMapping(coilData.HoldCoilId, coilData, true); } break; @@ -360,10 +358,7 @@ private void SetupCoilMapping() var device = _coilDevices[coilData.Device]; var coil = device.Coil(coilData.DeviceItem); if (coil != null) { - if (!_coilAssignments.ContainsKey(coilData.Id)) { - _coilAssignments[coilData.Id] = new List>(); - } - _coilAssignments[coilData.Id].Add(new Tuple(coilData.DeviceItem, false, coilData.Device)); + AssignCoilMapping(coilData.Id, coilData, false, coilData.Device); } else { Logger.Warn($"Unknown coil \"{coilData.DeviceItem}\" in coil device \"{coilData.Device}\"."); @@ -379,6 +374,14 @@ private void SetupCoilMapping() } } + private void AssignCoilMapping(string id, MappingsCoilData coilData, bool isHoldCoil = false, string deviceName = null) + { + if (!_coilAssignments.ContainsKey(id)) { + _coilAssignments[id] = new List>(); + } + _coilAssignments[id].Add(new Tuple(coilData.PlayfieldItem, isHoldCoil, deviceName)); + } + private void SetupSwitchMapping() { // hook-up game switches @@ -455,21 +458,33 @@ private void SetupLampMapping() foreach (var lampData in config.Data.Lamps) { switch (lampData.Destination) { case LampDestination.Playfield: - if (!_lampAssignments.ContainsKey(lampData.Id)) { - _lampAssignments[lampData.Id] = new List(); + AssignLampMapping(lampData.Id, lampData); + if (!string.IsNullOrEmpty(lampData.Green)) { + AssignLampMapping(lampData.Green, lampData); + } + if (!string.IsNullOrEmpty(lampData.Blue)) { + AssignLampMapping(lampData.Blue, lampData); } - _lampAssignments[lampData.Id].Add(lampData.PlayfieldItem); - _lampMappings[lampData.Id] = lampData; break; } } if (_lampAssignments.Count > 0) { gamelogicEngineWithLamps.OnLampChanged += HandleLampEvent; + gamelogicEngineWithLamps.OnLampsChanged += HandleLampsEvent; } } } + private void AssignLampMapping(string id, MappingsLampData lampData) + { + if (!_lampAssignments.ContainsKey(id)) { + _lampAssignments[id] = new List(); + } + _lampAssignments[id].Add(lampData.PlayfieldItem); + _lampMappings[id] = lampData; + } + private void SetupWireMapping() { var config = Table.Mappings; @@ -595,51 +610,75 @@ private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) } } + private void HandleLampsEvent(object sender, LampsEventArgs lampsEvent) + { + foreach (var lampEvent in lampsEvent.LampsChanged) { + HandleLampEvent(lampEvent, (lamp, data, itemName) => { + + }); + } + } + private void HandleLampEvent(object sender, LampEventArgs lampEvent) + { + var colors = new Dictionary(); + var lamps = new Dictionary(); + + HandleLampEvent(lampEvent, (lamp, mapping, itemName) => { + var color = colors.ContainsKey(mapping.Id) ? colors[mapping.Id] : lamp.Color; + if (lampEvent.Id == mapping.Id) { + color.r = lampEvent.Value / 255f; + + } else if (lampEvent.Id == mapping.Green) { + color.g = lampEvent.Value / 255f; + + } else if (lampEvent.Id == mapping.Blue) { + color.b = lampEvent.Value / 255f; + + } else { + Logger.Error($"Cannot assign lamp {lampEvent.Id} to an RGB value of light {itemName}"); + } + colors[mapping.Id] = color; + lamps[mapping.Id] = lamp; + }); + + foreach (var mappingId in colors.Keys) { + lamps[mappingId].Color = colors[mappingId]; + } + } + + private void HandleLampEvent(LampEventArgs lampEvent, Action handleRgb) { if (_lampAssignments.ContainsKey(lampEvent.Id)) { var mapping = _lampMappings[lampEvent.Id]; foreach (var itemName in _lampAssignments[lampEvent.Id]) { if (_lamps.ContainsKey(itemName)) { + var lamp = _lamps[itemName]; switch (mapping.Type) { case LampType.SingleOnOff: - switch (lampEvent.Type) { - case LampEventArgs.ValueType.Bool: - _lamps[itemName].OnLamp(lampEvent.BoolValue); - break; - case LampEventArgs.ValueType.Int: - _lamps[itemName].OnLamp(lampEvent.IntValue == 1); - break; - default: - throw new ArgumentOutOfRangeException(); - } + lamp.OnLamp(lampEvent.Value > 0 ? 1f : 0f, ColorChannel.Alpha); break; case LampType.SingleFading: - switch (lampEvent.Type) { - case LampEventArgs.ValueType.Bool: - _lamps[itemName].OnLamp(lampEvent.BoolValue ? 1f : 0f); - break; - case LampEventArgs.ValueType.Int: - _lamps[itemName].OnLamp(lampEvent.IntValue / 255f); - break; - default: - throw new ArgumentOutOfRangeException(); - } + lamp.OnLamp(lampEvent.Value / 255f, ColorChannel.Alpha); break; case LampType.Rgb: + handleRgb(lamp, mapping, itemName); + break; + default: + Logger.Error($"Unknown mapping type \"{mapping.Type}\" of lamp ID {lampEvent.Id} for light {itemName}."); break; } } else { - Logger.Warn($"Cannot trigger unknown lamp {itemName}."); + Logger.Error($"Cannot trigger unknown lamp {itemName}."); } } } else { - Logger.Warn($"Should update unassigned lamp {lampEvent.Id}."); + Logger.Error($"Should update unassigned lamp {lampEvent.Id}."); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index 3827363cb..c1284d4d7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -102,9 +102,8 @@ internal interface IApiCoil : IApiWireDest internal interface IApiLamp : IApiWireDest { - void OnLamp(bool enabled); - void OnLamp(float value); - void OnLamp(bool enabled, Color color); + UnityEngine.Color Color { get; set; } + void OnLamp(float value, ColorChannel channel); } internal interface IApiWireDest diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs index a496f6cb3..dd7f43e5b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs @@ -16,9 +16,10 @@ using System; using UnityEngine; +using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Light; -using Color = VisualPinball.Engine.Math.Color; +using Color = UnityEngine.Color; using Light = VisualPinball.Engine.VPT.Light.Light; namespace VisualPinball.Unity @@ -30,18 +31,49 @@ public class LightApi : ItemApi, IApiInitializable, IApiLamp /// public event EventHandler Init; - public int State { get => _state; set => Set(value); } + public int State { + get => _state; + set => Set(value, value == LightStatus.LightStateOn ? 1.0f : 0f); + } private int _state; private readonly LightAuthoring _lightAuthoring; - void IApiWireDest.OnChange(bool enabled) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff); - void IApiLamp.OnLamp(bool enabled) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff); - void IApiLamp.OnLamp(float value) + void IApiWireDest.OnChange(bool enabled) => Set( + enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff, + enabled ? 1.0f : 0f); + + public Color Color { get => _lightAuthoring.Color; set => _lightAuthoring.Color = value; } + + void IApiLamp.OnLamp(float value, ColorChannel channel) { - throw new NotImplementedException(); + switch (channel) { + case ColorChannel.Alpha: { + Set(value == 0 ? LightStatus.LightStateOff : LightStatus.LightStateOn, value); + break; + } + case ColorChannel.Red: { + var color = _lightAuthoring.Color; + color.r = value; + _lightAuthoring.Color = color; + break; + } + case ColorChannel.Green: { + var color = _lightAuthoring.Color; + color.g = value; + _lightAuthoring.Color = color; + break; + } + case ColorChannel.Blue: { + var color = _lightAuthoring.Color; + color.b = value; + _lightAuthoring.Color = color; + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(channel), channel, null); + } } - void IApiLamp.OnLamp(bool enabled, Color color) => Set(enabled ? LightStatus.LightStateOn : LightStatus.LightStateOff, color); internal LightApi(Light item, GameObject go, Player player) : base(item, player) { @@ -49,12 +81,12 @@ internal LightApi(Light item, GameObject go, Player player) : base(item, player) _state = item.Data.State; } - private void Set(int lightStatus, float value = 1f) + private void Set(int lightStatus, float value) { switch (lightStatus) { case LightStatus.LightStateOff: { if (Data.FadeSpeedDown > 0) { - _lightAuthoring.FadeTo(Data.FadeSpeedDown, 0f); + _lightAuthoring.FadeTo(Data.FadeSpeedDown, 0); } else { _lightAuthoring.Enabled = false; @@ -83,12 +115,6 @@ private void Set(int lightStatus, float value = 1f) _state = lightStatus; } - private void Set(int lightStatus, Color color) - { - _lightAuthoring.Color = color; - Set(lightStatus); - } - #region Events void IApiInitializable.OnInit(BallManager ballManager) diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs index 71d61b4fe..22142a96f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs @@ -28,7 +28,6 @@ using UnityEngine; using VisualPinball.Engine.Game; using VisualPinball.Engine.VPT.Light; -using Color = VisualPinball.Engine.Math.Color; using Light = VisualPinball.Engine.VPT.Light.Light; using Logger = NLog.Logger; @@ -45,11 +44,10 @@ public bool Enabled { _unityLight.enabled = value; } } + public Color Color { - set { - StopAllCoroutines(); - _unityLight.color = value.ToUnityColor(); - } + get => _unityLight.color; + set => _unityLight.color = value; } private UnityEngine.Light _unityLight; From 949f4225eaa971b18992aa20fedb6a6764cf4587 Mon Sep 17 00:00:00 2001 From: freezy Date: Thu, 14 Jan 2021 00:04:20 +0100 Subject: [PATCH 06/23] lamps: Add some documentation. --- .../creators-guide/editor/lamp-manager.md | 47 +++++++++++++++++++ .../creators-guide/manual/gamelogic-engine.md | 8 +++- .../Documentation~/creators-guide/toc.yml | 8 ++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md new file mode 100644 index 000000000..d7136a7e5 --- /dev/null +++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md @@ -0,0 +1,47 @@ +--- +description: The lamp manager lets you connect and configure lights, flashers and GIs of the playfield to the gamelogic engine. +--- +# Lamp Manager + +There are many types of lamps a real pinball machine might use, and there are different ways a gamelogic engine might be addressing them. VPE uses the Unity game engine to accurately simulate lights on the playfield. Those lights have a standardized set of parameters, which you can tweak in the editor. However, lights in a game are dynamic, so the gamelogic engine will toggle them, fade them, or even change their color. + +In order to link each the playfield light to the gamelogic engine and configure how they react during gameplay, the *Lamp Manager* is used. You can find it under *Visual Pinball -> Lamp Manager*. + +[TODO: Screenshot] + +> [!note] +> We use the terms *lights* and *lamps* as follows: +> - With **light** we're referring to the render engine's [light](https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@10.2/manual/Light-Component.html). It's a simulated light source and doesn't have to be a physical element on the table, but can also refer to the sun, some directional scene light, or other types of lighting used in the simulation. +> - With **lamp** we're referring to a "bulb" that is "screwed" into the table. It's more of a logical component VPE has to deal with during gameplay, decoupled for the rendering aspect. + +## About Lamps + +Physical machines have a bunch of different concepts when it comes to lighting. The vast majority of solid state machines from the eighties until the early 2010s used a **lamp matrix**, where lamps were addressed by row/column, and they only could be turned on or off. Historically, incandescent light bulbs were used, which resulted in a warm-up period until they reached full illuminosity (and a cool-down period when turned off). For this, VPE adopted the fade-in and fade-out properties from Visual Pinball that can be set on a light. + +Later machines used single colored **LEDs** that were each directly connected to the controller board (see also: [Lights vs LEDs](https://docs.missionpinball.org/en/latest/mechs/lights/lights_versus_leds.html)). Contrarily to matrix lamps, the intensity here could be set more fine grained by the game software. + +More recently, games started using **RGB-LEDs** that are also able to change the color during gameplay. In VPE, they can be handled in two different ways: +- As three single inputs from the gamelogic engine (e.g. that's what PinMAME provides) +- With a single RGB input, where the gamelogic engine always provides the full color (e.g. MPF, or custom table logic) + +Additionally, most pinball machines come with **GI strips**, which are a set of bulbs used for global illumination of the playfield. All lights from a strip are addressed at once, so one gamelogic GI strip maps to multiple lamps on the playfield. + +Finally, high-powered lamps such as flashers might appear under the gamelogic engine's **coil outputs**, since those lamps operate with the same voltage and have the same properties as coils. + +[TBD how to handle solenoids] + +## Setup + +[TODO] + +## Inserts + +[TODO] + +## Flashers + +[TODO] + +## Editor vs Runtime + +While editing the table in the Unity editor, you can and probably should disable lights you're not editing. During runtime, VPE first turns all lights off, then turns on the constant lights, and then waits for the gamelogic engine for further instructions. diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/gamelogic-engine.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/gamelogic-engine.md index ed928c234..f03a003c7 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/manual/gamelogic-engine.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/gamelogic-engine.md @@ -16,6 +16,12 @@ Classic examples of gamelogic engines are [MPF](https://missionpinball.org/) and In Visual Pinball, the gamelogic engine is part of the table script, which in most cases uses VPM to drive the game. So a part of the table script is about piping data into VPM and handling its outputs (lamp changes, coil changes, and so on). -Since VPE defines a clear API (like a contract) between the table and the gamelogic engine, we will provide tools to make this easy for you. Currently there is a [Switch Manager](~/creators-guide/editor/switch-manager.md) which manages switches. Soon there will be additional managers for lamps and coils, where you can connect your playfield elements to the gamelogic engine using a UI. +Since VPE defines a clear API (like a contract) between the table and the gamelogic engine, we can provide tools to make this easy for you. Currently, VPE provides: + +- A [Switch Manager](~/creators-guide/editor/switch-manager.md) +- A [Lamp Manager](~/creators-guide/editor/lamp-manager.md) +- A [Coil Manager](~/creators-guide/editor/coil-manager.md) + +These tools provide a graphical user interface where you can link playfield elements to the gamelogic engine and configure them. Ultimately, that means if your table uses an existing gamelogic engine like MPF or PinMAME, and the table doesn't contain any exotic game mechanics, that's all you need to do. You can set up your table without a single line of code! diff --git a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml index a70efcd06..3e5d68a4d 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml +++ b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml @@ -1,5 +1,5 @@ - name: Visual Pinball Engine - items: + items: - name: Overview href: introduction/overview.md - name: Features @@ -22,11 +22,13 @@ href: editor/switch-manager.md - name: Coil Manager href: editor/coil-manager.md + - name: Lamp Manager + href: editor/lamp-manager.md - name: Wire Manager href: editor/wire-manager.md - name: Multiple Tables href: editor/multiple-tables.md - - name: Advanced Topics + - name: Advanced Topics items: - name: Camera Settings href: editor/advanced/camera-settings.md @@ -40,4 +42,4 @@ - name: Troughs / Ball Drains href: manual/mechanisms/troughs.md - name: Flippers - href: manual/mechanisms/flippers.md \ No newline at end of file + href: manual/mechanisms/flippers.md From c2324da8069e912233f3f9761eb6ed215d65528d Mon Sep 17 00:00:00 2001 From: freezy Date: Thu, 14 Jan 2021 22:17:16 +0100 Subject: [PATCH 07/23] lamps: Add lamp destination to coils. --- VisualPinball.Engine/VPT/Enums.cs | 7 +++++ .../VPT/Mappings/MappingsLampData.cs | 19 +++++++----- .../Managers/Coil/CoilListViewItemRenderer.cs | 29 +++++++++++++++++-- .../Managers/Coil/CoilManager.cs | 2 +- .../Managers/Lamp/LampListData.cs | 3 ++ .../Managers/Lamp/LampListViewItemRenderer.cs | 29 +++++++++++++++++-- .../Managers/Lamp/LampManager.cs | 2 -- .../Managers/ManagerWindow.cs | 2 +- .../VisualPinball.Unity/Game/Player.cs | 3 +- 9 files changed, 78 insertions(+), 18 deletions(-) diff --git a/VisualPinball.Engine/VPT/Enums.cs b/VisualPinball.Engine/VPT/Enums.cs index a228e063f..c4e36f7fa 100644 --- a/VisualPinball.Engine/VPT/Enums.cs +++ b/VisualPinball.Engine/VPT/Enums.cs @@ -171,6 +171,7 @@ public static class CoilDestination { public const int Playfield = 0; public const int Device = 1; + public const int Lamp = 2; } public static class CoilType @@ -197,6 +198,12 @@ public static class LampDestination public const int Device = 1; } + public static class LampSource + { + public const int Lamps = 0; + public const int Coils = 1; + } + public static class LampType { public const int SingleOnOff = 0; diff --git a/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs index 5848a9087..5584bcdf5 100644 --- a/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs +++ b/VisualPinball.Engine/VPT/Mappings/MappingsLampData.cs @@ -38,28 +38,31 @@ public class MappingsLampData : BiffData [BiffString("MCID", IsWideString = true, Pos = 1)] public string Id = string.Empty; - [BiffString("DESC", IsWideString = true, Pos = 2)] + [BiffInt("LSRC", Pos = 2)] + public int Source = LampSource.Lamps; + + [BiffString("DESC", IsWideString = true, Pos = 3)] public string Description = string.Empty; - [BiffInt("DEST", Pos = 3)] + [BiffInt("DEST", Pos = 4)] public int Destination = LampDestination.Playfield; - [BiffString("PITM", IsWideString = true, Pos = 4)] + [BiffString("PITM", IsWideString = true, Pos = 5)] public string PlayfieldItem = string.Empty; - [BiffString("DEVC", IsWideString = true, Pos = 5)] + [BiffString("DEVC", IsWideString = true, Pos = 6)] public string Device = string.Empty; - [BiffString("DITM", IsWideString = true, Pos = 6)] + [BiffString("DITM", IsWideString = true, Pos = 7)] public string DeviceItem = string.Empty; - [BiffInt("LTYP", Pos = 7)] + [BiffInt("LTYP", Pos = 8)] public int Type = LampType.SingleOnOff; - [BiffString("RGBG", IsWideString = true, Pos = 8)] + [BiffString("RGBG", IsWideString = true, Pos = 9)] public string Green = string.Empty; - [BiffString("RGBB", IsWideString = true, Pos = 9)] + [BiffString("RGBB", IsWideString = true, Pos = 10)] public string Blue = string.Empty; #region BIFF diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs index e795395b9..b1429f329 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs @@ -22,12 +22,13 @@ using VisualPinball.Engine.VPT; using System.Linq; using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT.Mappings; namespace VisualPinball.Unity.Editor { public class CoilListViewItemRenderer { - private readonly string[] OPTIONS_COIL_DESTINATION = { "Playfield", "Device" }; + private readonly string[] OPTIONS_COIL_DESTINATION = { "Playfield", "Device", "Lamp" }; private readonly string[] OPTIONS_COIL_TYPE = { "Single-Wound", "Dual-Wound" }; private enum CoilListColumn @@ -40,14 +41,16 @@ private enum CoilListColumn HoldCoilId = 5, } + private readonly TableAuthoring _tableAuthoring; private readonly List _gleCoils; private readonly Dictionary _coils; private readonly Dictionary _coilDevices; private AdvancedDropdownState _itemPickDropdownState; - public CoilListViewItemRenderer(List gleCoils, Dictionary coils, Dictionary coilDevices) + public CoilListViewItemRenderer(TableAuthoring tableAuthoring, List gleCoils, Dictionary coils, Dictionary coilDevices) { + _tableAuthoring = tableAuthoring; _gleCoils = gleCoils; _coils = coils; _coilDevices = coilDevices; @@ -137,6 +140,22 @@ private void RenderDestination(CoilListData coilListData, Rect cellRect, Action< { if (coilListData.Destination != index) { + if (coilListData.Destination == CoilDestination.Lamp) { + + var lampEntry = _tableAuthoring.Mappings.Lamps.FirstOrDefault(l => l.Id == coilListData.Id && l.Source == LampSource.Coils); + if (lampEntry != null) { + _tableAuthoring.Mappings.RemoveLamp(lampEntry); + EditorWindow.GetWindow().Reload(); + } + + } else if (index == CoilDestination.Lamp) { + _tableAuthoring.Mappings.AddLamp(new MappingsLampData { + Id = coilListData.Id, + Source = LampSource.Coils, + Description = coilListData.Description + }); + EditorWindow.GetWindow().Reload(); + } coilListData.Destination = index; updateAction(coilListData); } @@ -172,6 +191,12 @@ private void RenderElement(TableAuthoring tableAuthoring, CoilListData coilListD cellRect.x += cellRect.width + 10f; RenderDeviceItemElement(coilListData, cellRect, updateAction); break; + + case CoilDestination.Lamp: + cellRect.x -= 25; + cellRect.width += 25; + EditorGUI.LabelField(cellRect, "Configure in Lamp Manager", new GUIStyle(GUI.skin.label) { fontStyle = FontStyle.Italic }); + break; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs index 292c1ed49..06bb32933 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs @@ -70,7 +70,7 @@ public override void OnEnable() private void OnFocus() { - _listViewItemRenderer = new CoilListViewItemRenderer(_gleCoils, _coils, _coilDevices); + _listViewItemRenderer = new CoilListViewItemRenderer(_tableAuthoring, _gleCoils, _coils, _coilDevices); } protected override bool SetupCompleted() diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs index d6708a32d..402b7ef03 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs @@ -43,12 +43,14 @@ public class LampListData : IManagerListData public string PlayfieldItem; public string Device; public string DeviceItem; + public int Source; public MappingsLampData MappingsLampData; public LampListData(MappingsLampData mappingsLampData) { Id = mappingsLampData.Id; + Source = mappingsLampData.Source; Description = mappingsLampData.Description; PlayfieldItem = mappingsLampData.PlayfieldItem; Device = mappingsLampData.Device; @@ -63,6 +65,7 @@ public LampListData(MappingsLampData mappingsLampData) public void Update() { MappingsLampData.Id = Id; + MappingsLampData.Source = Source; MappingsLampData.Description = Description; MappingsLampData.PlayfieldItem = PlayfieldItem; MappingsLampData.Device = Device; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs index c32b4d4db..9c7a7d150 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs @@ -55,7 +55,11 @@ public void Render(TableAuthoring tableAuthoring, LampListData data, Rect cellRe { switch ((LampListColumn)column) { case LampListColumn.Id: - RenderId(ref data.Id, id => data.Id = id, data, cellRect, updateAction); + if (data.Source == LampSource.Coils) { + RenderCoilId(data, cellRect); + } else { + RenderId(ref data.Id, id => data.Id = id, data, cellRect, updateAction); + } break; case LampListColumn.Description: RenderDescription(data, cellRect, updateAction); @@ -77,6 +81,27 @@ public void Render(TableAuthoring tableAuthoring, LampListData data, Rect cellRe } } + private void RenderCoilId(LampListData lampListData, Rect cellRect) + { + // add some padding + cellRect.x += 2; + cellRect.width -= 4; + + var icon = Icons.Coil(IconSize.Small); + if (icon != null) { + var iconRect = cellRect; + iconRect.width = 20; + var guiColor = GUI.color; + GUI.color = Color.clear; + EditorGUI.DrawTextureTransparent(iconRect, icon, ScaleMode.ScaleToFit); + GUI.color = guiColor; + } + cellRect.x += 20; + cellRect.width -= 20; + + EditorGUI.LabelField(cellRect, lampListData.Id); + } + private void RenderId(ref string id, Action setId, LampListData lampListData, Rect cellRect, Action updateAction) { // add some padding @@ -88,7 +113,6 @@ private void RenderId(ref string id, Action setId, LampListData lampList if (options.Count > 0) { options.Add(""); } - options.Add("Add..."); EditorGUI.BeginChangeCheck(); @@ -140,7 +164,6 @@ private void RenderDestination(LampListData lampListData, Rect cellRect, Action< private void RenderElement(TableAuthoring tableAuthoring, LampListData lampListData, Rect cellRect, Action updateAction) { var icon = GetIcon(lampListData); - if (icon != null) { var iconRect = cellRect; iconRect.width = 20; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs index da91b46a0..4be254207 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs @@ -136,14 +136,12 @@ protected override List CollectData() protected override void AddNewData(string undoName, string newName) { RecordUndo(undoName); - _tableAuthoring.Mappings.AddLamp(new MappingsLampData()); } protected override void RemoveData(string undoName, LampListData data) { RecordUndo(undoName); - _tableAuthoring.Mappings.RemoveLamp(data.MappingsLampData); } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ManagerWindow.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ManagerWindow.cs index 02cf957b9..7f9144e53 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ManagerWindow.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ManagerWindow.cs @@ -79,7 +79,7 @@ protected float RowHeight { private bool _isImplRenameExistingItem; private Vector2 _scrollPos = Vector2.zero; - protected void Reload() + public void Reload() { if (_tableAuthoring != null) { _data = CollectData(); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 1d32cb805..5de657d5a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -17,7 +17,6 @@ using System; using System.Collections.Generic; using NLog; -using NLog.Fluent; using Unity.Entities; using Unity.Mathematics; using UnityEngine; @@ -91,6 +90,7 @@ public class Player : MonoBehaviour private readonly Dictionary> _keyWireAssignments = new Dictionary>(); [NonSerialized] // tuple: itemName, isHoldCoil, deviceName private readonly Dictionary>> _coilAssignments = new Dictionary>>(); + private readonly Dictionary _coilMappings = new Dictionary(); [NonSerialized] private readonly Dictionary> _lampAssignments = new Dictionary>(); private readonly Dictionary _lampMappings = new Dictionary(); @@ -380,6 +380,7 @@ private void AssignCoilMapping(string id, MappingsCoilData coilData, bool isHold _coilAssignments[id] = new List>(); } _coilAssignments[id].Add(new Tuple(coilData.PlayfieldItem, isHoldCoil, deviceName)); + _coilMappings[id] = coilData; } private void SetupSwitchMapping() From 4f6c70f477be5a581b5dea44c92e5ca98cfd45a2 Mon Sep 17 00:00:00 2001 From: freezy Date: Thu, 14 Jan 2021 22:32:32 +0100 Subject: [PATCH 08/23] lamps: Update coil ID when coil lamp ID is updated. --- .../Managers/Coil/CoilListViewItemRenderer.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs index b1429f329..d01aec8a7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs @@ -61,7 +61,7 @@ public void Render(TableAuthoring tableAuthoring, CoilListData data, Rect cellRe switch ((CoilListColumn)column) { case CoilListColumn.Id: - RenderId(ref data.Id, id => data.Id = id, data, cellRect, updateAction); + RenderId(ref data.Id, id => UpdateId(data, id), data, cellRect, updateAction); break; case CoilListColumn.Description: RenderDescription(data, cellRect, updateAction); @@ -83,6 +83,18 @@ public void Render(TableAuthoring tableAuthoring, CoilListData data, Rect cellRe } } + private void UpdateId(CoilListData data, string id) + { + if (data.Destination == CoilDestination.Lamp) { + var lampEntry = _tableAuthoring.Mappings.Lamps.FirstOrDefault(l => l.Id == data.Id && l.Source == LampSource.Coils); + if (lampEntry != null) { + lampEntry.Id = id; + EditorWindow.GetWindow().Reload(); + } + } + data.Id = id; + } + private void RenderId(ref string id, Action setId, CoilListData coilListData, Rect cellRect, Action updateAction) { // add some padding @@ -102,10 +114,8 @@ private void RenderId(ref string id, Action setId, CoilListData coilList if (EditorGUI.EndChangeCheck()) { if (index == options.Count - 1) { PopupWindow.Show(cellRect, new ManagerListTextFieldPopup("ID", "", newId => { - if (_gleCoils.Exists(entry => entry.Id == newId)) - { - _gleCoils.Add(new GamelogicEngineCoil - { + if (_gleCoils.Exists(entry => entry.Id == newId)) { + _gleCoils.Add(new GamelogicEngineCoil { Id = newId }); } From d225cb2b6bb84dfc55dbd1b2c13201b127eabec8 Mon Sep 17 00:00:00 2001 From: freezy Date: Thu, 14 Jan 2021 23:45:21 +0100 Subject: [PATCH 09/23] lamps: Handle coil lamps during runtime. --- .../Game/Engine/IGamelogicEngineWithLamps.cs | 14 +++++ .../VisualPinball.Unity/Game/Player.cs | 58 ++++++++++++++----- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs index 44002b34c..0fb08cfed 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs @@ -16,6 +16,7 @@ using System; using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT; namespace VisualPinball.Unity { @@ -61,10 +62,23 @@ public readonly struct LampEventArgs /// public readonly int Value; + /// + /// Source which triggered the lamp. + /// + public readonly int Source; + public LampEventArgs(string id, int value) { Id = id; Value = value; + Source = LampSource.Lamps; + } + + public LampEventArgs(string id, int value, int source) + { + Id = id; + Value = value; + Source = source; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 5de657d5a..fcec7c379 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -88,9 +88,8 @@ public class Player : MonoBehaviour private readonly Dictionary> _keySwitchAssignments = new Dictionary>(); [NonSerialized] private readonly Dictionary> _keyWireAssignments = new Dictionary>(); - [NonSerialized] // tuple: itemName, isHoldCoil, deviceName - private readonly Dictionary>> _coilAssignments = new Dictionary>>(); - private readonly Dictionary _coilMappings = new Dictionary(); + [NonSerialized] + private readonly Dictionary> _coilAssignments = new Dictionary>(); [NonSerialized] private readonly Dictionary> _lampAssignments = new Dictionary>(); private readonly Dictionary _lampMappings = new Dictionary(); @@ -358,13 +357,17 @@ private void SetupCoilMapping() var device = _coilDevices[coilData.Device]; var coil = device.Coil(coilData.DeviceItem); if (coil != null) { - AssignCoilMapping(coilData.Id, coilData, false, coilData.Device); + AssignCoilMapping(coilData.Id, coilData, deviceName: coilData.Device); } else { Logger.Warn($"Unknown coil \"{coilData.DeviceItem}\" in coil device \"{coilData.Device}\"."); } } break; + + case CoilDestination.Lamp: + AssignCoilMapping(coilData.Id, coilData, isLampCoil: true); + break; } } @@ -374,13 +377,12 @@ private void SetupCoilMapping() } } - private void AssignCoilMapping(string id, MappingsCoilData coilData, bool isHoldCoil = false, string deviceName = null) + private void AssignCoilMapping(string id, MappingsCoilData coilData, bool isHoldCoil = false, bool isLampCoil = false, string deviceName = null) { if (!_coilAssignments.ContainsKey(id)) { - _coilAssignments[id] = new List>(); + _coilAssignments[id] = new List(); } - _coilAssignments[id].Add(new Tuple(coilData.PlayfieldItem, isHoldCoil, deviceName)); - _coilMappings[id] = coilData; + _coilAssignments[id].Add(new CoilDestConfig(coilData.PlayfieldItem, isHoldCoil, isLampCoil, deviceName)); } private void SetupSwitchMapping() @@ -593,15 +595,20 @@ private void HandleKeyInput(object obj, InputActionChange change) private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) { if (_coilAssignments.ContainsKey(coilEvent.Id)) { - foreach (var (itemName, isHoldCoil, deviceName) in _coilAssignments[coilEvent.Id]) { - if (deviceName != null && _coilDevices.ContainsKey(deviceName)) { - _coilDevices[deviceName].Coil(itemName).OnCoil(coilEvent.IsEnabled, isHoldCoil); + foreach (var destConfig in _coilAssignments[coilEvent.Id]) { + if (destConfig.DeviceName != null && _coilDevices.ContainsKey(destConfig.DeviceName)) { + _coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName).OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); - } else if (_coils.ContainsKey(itemName)) { - _coils[itemName].OnCoil(coilEvent.IsEnabled, isHoldCoil); + } else if (_coils.ContainsKey(destConfig.ItemName)) { + if (destConfig.IsLampCoil) { + HandleLampEvent(null, new LampEventArgs(coilEvent.Id, coilEvent.IsEnabled ? 1 : 0, LampSource.Coils)); + + } else { + _coils[destConfig.ItemName].OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); + } } else { - Logger.Warn($"Cannot trigger unknown coil item {itemName}."); + Logger.Warn($"Cannot trigger unknown coil item {destConfig.ItemName}."); } } @@ -615,7 +622,7 @@ private void HandleLampsEvent(object sender, LampsEventArgs lampsEvent) { foreach (var lampEvent in lampsEvent.LampsChanged) { HandleLampEvent(lampEvent, (lamp, data, itemName) => { - + // todo }); } } @@ -653,6 +660,11 @@ private void HandleLampEvent(LampEventArgs lampEvent, Action Date: Sun, 17 Jan 2021 21:57:38 +0100 Subject: [PATCH 10/23] player: Move coil-, switch-, lamp-, and wire handling into separate classes. --- .../VisualPinball.Unity/Game/CoilPlayer.cs | 143 +++++ ...eDestConfig.cs.meta => CoilPlayer.cs.meta} | 2 +- .../VisualPinball.Unity/Game/LampPlayer.cs | 187 +++++++ .../Game/LampPlayer.cs.meta | 11 + .../VisualPinball.Unity/Game/Player.cs | 491 ++---------------- .../VisualPinball.Unity/Game/SwitchPlayer.cs | 145 ++++++ .../Game/SwitchPlayer.cs.meta | 11 + .../Game/VisualPinballScript.cs | 53 -- .../Game/WireDestConfig.cs | 46 -- .../VisualPinball.Unity/Game/WirePlayer.cs | 176 +++++++ .../Game/WirePlayer.cs.meta | 11 + 11 files changed, 725 insertions(+), 551 deletions(-) create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs rename VisualPinball.Unity/VisualPinball.Unity/Game/{WireDestConfig.cs.meta => CoilPlayer.cs.meta} (83%) create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs.meta delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballScript.cs delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/WireDestConfig.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs.meta diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs new file mode 100644 index 000000000..926bcde23 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs @@ -0,0 +1,143 @@ +// 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 . + +using System.Collections.Generic; +using NLog; +using VisualPinball.Engine.VPT; +using VisualPinball.Engine.VPT.Mappings; +using VisualPinball.Engine.VPT.Table; + +namespace VisualPinball.Unity +{ + public class CoilPlayer + { + private readonly Dictionary _coils = new Dictionary(); + private readonly Dictionary _coilDevices = new Dictionary(); + private readonly Dictionary> _coilAssignments = new Dictionary>(); + + private readonly Table _table; + private readonly IGamelogicEngine _gamelogicEngine; + private readonly LampPlayer _lampPlayer; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + internal void RegisterCoil(IItem item, IApiCoil coilApi) => _coils[item.Name] = coilApi; + internal void RegisterCoilDevice(IItem item, IApiCoilDevice coilDeviceApi) => _coilDevices[item.Name] = coilDeviceApi; + + public CoilPlayer(Table table, IGamelogicEngine gamelogicEngine, LampPlayer lampPlayer) + { + _table = table; + _gamelogicEngine = gamelogicEngine; + _lampPlayer = lampPlayer; + } + + public void OnStart() + { + if (_gamelogicEngine is IGamelogicEngineWithCoils gamelogicEngineWithCoils) { + var config = _table.Mappings; + _coilAssignments.Clear(); + foreach (var coilData in config.Data.Coils) { + switch (coilData.Destination) { + case CoilDestination.Playfield: + AssignCoilMapping(coilData.Id, coilData); + if (coilData.Type == CoilType.DualWound) { + AssignCoilMapping(coilData.HoldCoilId, coilData, true); + } + break; + + case CoilDestination.Device: + if (_coilDevices.ContainsKey(coilData.Device)) { + var device = _coilDevices[coilData.Device]; + var coil = device.Coil(coilData.DeviceItem); + if (coil != null) { + AssignCoilMapping(coilData.Id, coilData, deviceName: coilData.Device); + + } else { + Logger.Warn($"Unknown coil \"{coilData.DeviceItem}\" in coil device \"{coilData.Device}\"."); + } + } + break; + + case CoilDestination.Lamp: + AssignCoilMapping(coilData.Id, coilData, isLampCoil: true); + break; + } + } + + if (_coilAssignments.Count > 0) { + gamelogicEngineWithCoils.OnCoilChanged += HandleCoilEvent; + } + } + } + + private void AssignCoilMapping(string id, MappingsCoilData coilData, bool isHoldCoil = false, bool isLampCoil = false, string deviceName = null) + { + if (!_coilAssignments.ContainsKey(id)) { + _coilAssignments[id] = new List(); + } + _coilAssignments[id].Add(new CoilDestConfig(coilData.PlayfieldItem, isHoldCoil, isLampCoil, deviceName)); + } + + private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) + { + if (_coilAssignments.ContainsKey(coilEvent.Id)) { + foreach (var destConfig in _coilAssignments[coilEvent.Id]) { + if (destConfig.DeviceName != null && _coilDevices.ContainsKey(destConfig.DeviceName)) { + _coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName).OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); + + } else if (_coils.ContainsKey(destConfig.ItemName)) { + if (destConfig.IsLampCoil) { + _lampPlayer.HandleLampEvent(new LampEventArgs(coilEvent.Id, coilEvent.IsEnabled ? 1 : 0, LampSource.Coils)); + + } else { + _coils[destConfig.ItemName].OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); + } + + } else { + Logger.Warn($"Cannot trigger unknown coil item {destConfig.ItemName}."); + } + } + + } else { + var what = coilEvent.IsEnabled ? "turn on" : "turn off"; + Logger.Warn($"Should {what} unassigned coil {coilEvent.Id}."); + } + } + + public void OnDestroy() + { + if (_coilAssignments.Count > 0 && _gamelogicEngine is IGamelogicEngineWithCoils gamelogicEngineWithCoils) { + gamelogicEngineWithCoils.OnCoilChanged -= HandleCoilEvent; + } + } + } + + internal readonly struct CoilDestConfig + { + public readonly string ItemName; + public readonly bool IsHoldCoil; + public readonly bool IsLampCoil; + public readonly string DeviceName; + + public CoilDestConfig(string itemName, bool isHoldCoil, bool isLampCoil, string deviceName) + { + ItemName = itemName; + IsHoldCoil = isHoldCoil; + IsLampCoil = isLampCoil; + DeviceName = deviceName; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/WireDestConfig.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs.meta similarity index 83% rename from VisualPinball.Unity/VisualPinball.Unity/Game/WireDestConfig.cs.meta rename to VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs.meta index 8a8f1a663..fe8bffc74 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/WireDestConfig.cs.meta +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: cf8ea31819f4e2b47b6a5ccd3cfce295 +guid: a9d3730a4e826534c855eee8061e99d8 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs new file mode 100644 index 000000000..6aea1bcb9 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs @@ -0,0 +1,187 @@ +// 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 . + +using System; +using System.Collections.Generic; +using NLog; +using VisualPinball.Engine.Math; +using VisualPinball.Engine.VPT; +using VisualPinball.Engine.VPT.Mappings; +using VisualPinball.Engine.VPT.Table; +using Color = UnityEngine.Color; +using Logger = NLog.Logger; + +namespace VisualPinball.Unity +{ + public class LampPlayer + { + private readonly Dictionary _lamps = new Dictionary(); + private readonly Dictionary> _lampAssignments = new Dictionary>(); + private readonly Dictionary _lampMappings = new Dictionary(); + + private Table _table; + private readonly IGamelogicEngine _gamelogicEngine; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + internal void RegisterLamp(IItem item, IApiLamp lampApi) => _lamps[item.Name] = lampApi; + + public LampPlayer(Table table, IGamelogicEngine gamelogicEngine) + { + _table = table; + _gamelogicEngine = gamelogicEngine; + } + + public void OnStart() + { + if (_gamelogicEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { + var config = _table.Mappings; + _lampAssignments.Clear(); + _lampMappings.Clear(); + foreach (var lampData in config.Data.Lamps) { + switch (lampData.Destination) { + case LampDestination.Playfield: + AssignLampMapping(lampData.Id, lampData); + if (!string.IsNullOrEmpty(lampData.Green)) { + AssignLampMapping(lampData.Green, lampData); + } + if (!string.IsNullOrEmpty(lampData.Blue)) { + AssignLampMapping(lampData.Blue, lampData); + } + break; + } + } + + if (_lampAssignments.Count > 0) { + gamelogicEngineWithLamps.OnLampChanged += HandleLampEvent; + gamelogicEngineWithLamps.OnLampsChanged += HandleLampsEvent; + } + } + } + + private void AssignLampMapping(string id, MappingsLampData lampData) + { + if (!_lampAssignments.ContainsKey(id)) { + _lampAssignments[id] = new List(); + } + _lampAssignments[id].Add(lampData.PlayfieldItem); + _lampMappings[id] = lampData; + } + + + + private void HandleLampsEvent(object sender, LampsEventArgs lampsEvent) + { + var colors = new Dictionary(); + var lamps = new Dictionary(); + foreach (var lampEvent in lampsEvent.LampsChanged) { + HandleLampEvent(lampEvent, (lamp, mapping, itemName) => { + var color = colors.ContainsKey(mapping.Id) ? colors[mapping.Id] : lamp.Color; + if (lampEvent.Id == mapping.Id) { + color.r = lampEvent.Value / 255f; + + } else if (lampEvent.Id == mapping.Green) { + color.g = lampEvent.Value / 255f; + + } else if (lampEvent.Id == mapping.Blue) { + color.b = lampEvent.Value / 255f; + + } else { + Logger.Error($"Cannot assign lamp {lampEvent.Id} to an RGB value of light {itemName}"); + } + colors[mapping.Id] = color; + lamps[mapping.Id] = lamp; + }); + } + + foreach (var mappingId in colors.Keys) { + lamps[mappingId].Color = colors[mappingId]; + } + } + + public void HandleLampEvent(LampEventArgs lampEvent) + { + HandleLampEvent(null, lampEvent); + } + + private void HandleLampEvent(object sender, LampEventArgs lampEvent) + { + HandleLampEvent(lampEvent, (lamp, mapping, itemName) => { + if (lampEvent.Id == mapping.Id) { + lamp.OnLamp(lampEvent.Value / 255f, ColorChannel.Red); + + } else if (lampEvent.Id == mapping.Green) { + lamp.OnLamp(lampEvent.Value / 255f, ColorChannel.Green); + + } else if (lampEvent.Id == mapping.Blue) { + lamp.OnLamp(lampEvent.Value / 255f, ColorChannel.Blue); + + } else { + Logger.Error($"Cannot assign lamp {lampEvent.Id} to an RGB value of light {itemName}"); + } + }); + } + + private void HandleLampEvent(LampEventArgs lampEvent, Action handleRgb) + { + if (_lampAssignments.ContainsKey(lampEvent.Id)) { + var mapping = _lampMappings[lampEvent.Id]; + foreach (var itemName in _lampAssignments[lampEvent.Id]) { + if (mapping.Source != lampEvent.Source) { + // so, if we have a coil here that happens to have the same name as a lamp, + // skip if the source isn't the same. + continue; + } + if (_lamps.ContainsKey(itemName)) { + var lamp = _lamps[itemName]; + switch (mapping.Type) { + case LampType.SingleOnOff: + lamp.OnLamp(lampEvent.Value > 0 ? 1f : 0f, ColorChannel.Alpha); + break; + + case LampType.SingleFading: + lamp.OnLamp(lampEvent.Value / 255f, ColorChannel.Alpha); + break; + + case LampType.Rgb: + handleRgb(lamp, mapping, itemName); + break; + + default: + Logger.Error($"Unknown mapping type \"{mapping.Type}\" of lamp ID {lampEvent.Id} for light {itemName}."); + break; + } + + } else { + Logger.Error($"Cannot trigger unknown lamp {itemName}."); + } + } + + } else { + Logger.Error($"Should update unassigned lamp {lampEvent.Id}."); + } + } + + + public void OnDestroy() + { + if (_lampAssignments.Count > 0 && _gamelogicEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { + gamelogicEngineWithLamps.OnLampChanged -= HandleLampEvent; + gamelogicEngineWithLamps.OnLampsChanged -= HandleLampsEvent; + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs.meta new file mode 100644 index 000000000..8247c7bfa --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c31c8b5b6e7f19e4880f9148f78e1c4b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index fcec7c379..b0e0875d7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -16,21 +16,16 @@ using System; using System.Collections.Generic; -using NLog; using Unity.Entities; using Unity.Mathematics; using UnityEngine; -using UnityEngine.InputSystem; using VisualPinball.Engine.Common; using VisualPinball.Engine.Game; -using VisualPinball.Engine.Math; -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; @@ -40,9 +35,7 @@ using VisualPinball.Engine.VPT.Table; using VisualPinball.Engine.VPT.Trigger; using VisualPinball.Engine.VPT.Trough; -using Color = UnityEngine.Color; using Light = VisualPinball.Engine.VPT.Light.Light; -using Logger = NLog.Logger; namespace VisualPinball.Unity { @@ -71,28 +64,12 @@ public class Player : MonoBehaviour private readonly Dictionary _collidables = new Dictionary(); private readonly Dictionary _spinnables = new Dictionary(); private readonly Dictionary _slingshots = new Dictionary(); - private readonly Dictionary _switches = new Dictionary(); - private readonly Dictionary _switchDevices = new Dictionary(); - private readonly Dictionary _coils = new Dictionary(); - private readonly Dictionary _lamps = new Dictionary(); - private readonly Dictionary _coilDevices = new Dictionary(); - private readonly Dictionary _wires = new Dictionary(); - private readonly Dictionary _wireDevices = new Dictionary(); - private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - - // input related private InputManager _inputManager; - - [NonSerialized] - private readonly Dictionary> _keySwitchAssignments = new Dictionary>(); - [NonSerialized] - private readonly Dictionary> _keyWireAssignments = new Dictionary>(); - [NonSerialized] - private readonly Dictionary> _coilAssignments = new Dictionary>(); - [NonSerialized] - private readonly Dictionary> _lampAssignments = new Dictionary>(); - private readonly Dictionary _lampMappings = new Dictionary(); + [NonSerialized] private CoilPlayer _coilPlayer; + [NonSerialized] private SwitchPlayer _switchPlayer; + [NonSerialized] private LampPlayer _lampPlayer; + [NonSerialized] private WirePlayer _wirePlayer; public Player() { @@ -102,9 +79,9 @@ public Player() #region Access - internal IApiSwitch Switch(string n) => _switches.ContainsKey(n) ? _switches[n] : null; - internal IApiWireDest Wire(string n) => _wires.ContainsKey(n) ? _wires[n] : null; - internal IApiWireDeviceDest WireDevice(string n) => _wireDevices.ContainsKey(n) ? _wireDevices[n] : null; + internal IApiSwitch Switch(string n) => _switchPlayer?.Switch(n); + internal IApiWireDest Wire(string n) => _wirePlayer?.Wire(n); + internal IApiWireDeviceDest WireDevice(string n) => _wirePlayer?.WireDevice(n); #endregion @@ -121,6 +98,10 @@ private void Awake() if (engineComponent != null) { GameEngine = engineComponent.GameEngine; + _lampPlayer = new LampPlayer(Table, GameEngine); + _coilPlayer = new CoilPlayer(Table, GameEngine, _lampPlayer); + _switchPlayer = new SwitchPlayer(Table, GameEngine, _inputManager); + _wirePlayer = new WirePlayer(Table, _inputManager, _switchPlayer); } EngineProvider.Set(physicsEngineId); @@ -132,23 +113,15 @@ private void Awake() private void Start() { - - // bootstrap table script(s) - var tableScripts = GetComponents(); - foreach (var tableScript in tableScripts) { - tableScript.OnAwake(TableApi, BallManager); - } - // trigger init events now foreach (var i in _initializables) { i.OnInit(BallManager); } - // hook up mapping configuration - SetupSwitchMapping(); - SetupCoilMapping(); - SetupLampMapping(); - SetupWireMapping(); + _coilPlayer?.OnStart(); + _switchPlayer?.OnStart(); + _lampPlayer?.OnStart(); + _wirePlayer?.OnStart(); GameEngine?.OnInit(TableApi, BallManager); } @@ -160,21 +133,14 @@ private void Update() private void OnDestroy() { - if (_keySwitchAssignments.Count > 0) { - _inputManager.Disable(HandleKeyInput); - } - if (_coilAssignments.Count > 0 && GameEngine is IGamelogicEngineWithCoils gamelogicEngineWithCoils) { - gamelogicEngineWithCoils.OnCoilChanged -= HandleCoilEvent; - } - if (_lampAssignments.Count > 0 && GameEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { - gamelogicEngineWithLamps.OnLampChanged -= HandleLampEvent; - gamelogicEngineWithLamps.OnLampsChanged -= HandleLampsEvent; - } - foreach (var i in _apis) { i.OnDestroy(); } + _coilPlayer?.OnDestroy(); + _switchPlayer?.OnDestroy(); + _lampPlayer?.OnDestroy(); + _wirePlayer?.OnDestroy(); GameEngine?.OnDestroy(); } @@ -189,9 +155,9 @@ public void RegisterBumper(Bumper bumper, Entity entity, GameObject go) _apis.Add(bumperApi); _initializables.Add(bumperApi); _hittables[entity] = bumperApi; - _switches[bumper.Name] = bumperApi; - _coils[bumper.Name] = bumperApi; - _wires[bumper.Name] = bumperApi; + _switchPlayer?.RegisterSwitch(bumper, bumperApi); + _coilPlayer?.RegisterCoil(bumper, bumperApi); + _wirePlayer?.RegisterWire(bumper, bumperApi); } public void RegisterFlipper(Flipper flipper, Entity entity, GameObject go) @@ -203,9 +169,9 @@ public void RegisterFlipper(Flipper flipper, Entity entity, GameObject go) _hittables[entity] = flipperApi; _rotatables[entity] = flipperApi; _collidables[entity] = flipperApi; - _switches[flipper.Name] = flipperApi; - _coils[flipper.Name] = flipperApi; - _wires[flipper.Name] = flipperApi; + _switchPlayer?.RegisterSwitch(flipper, flipperApi); + _coilPlayer?.RegisterCoil(flipper, flipperApi); + _wirePlayer?.RegisterWire(flipper, flipperApi); if (EngineProvider.Exists) { EngineProvider.Get().OnRegisterFlipper(entity, flipper.Name); @@ -220,7 +186,7 @@ public void RegisterGate(Gate gate, Entity entity, GameObject go) _initializables.Add(gateApi); _hittables[entity] = gateApi; _rotatables[entity] = gateApi; - _switches[gate.Name] = gateApi; + _switchPlayer?.RegisterSwitch(gate, gateApi); } public void RegisterHitTarget(HitTarget hitTarget, Entity entity, GameObject go) @@ -230,7 +196,7 @@ public void RegisterHitTarget(HitTarget hitTarget, Entity entity, GameObject go) _apis.Add(hitTargetApi); _initializables.Add(hitTargetApi); _hittables[entity] = hitTargetApi; - _switches[hitTarget.Name] = hitTargetApi; + _switchPlayer?.RegisterSwitch(hitTarget, hitTargetApi); } public void RegisterKicker(Kicker kicker, Entity entity, GameObject go) @@ -240,9 +206,9 @@ public void RegisterKicker(Kicker kicker, Entity entity, GameObject go) _apis.Add(kickerApi); _initializables.Add(kickerApi); _hittables[entity] = kickerApi; - _switches[kicker.Name] = kickerApi; - _coils[kicker.Name] = kickerApi; - _wires[kicker.Name] = kickerApi; + _switchPlayer?.RegisterSwitch(kicker, kickerApi); + _coilPlayer?.RegisterCoil(kicker, kickerApi); + _wirePlayer?.RegisterWire(kicker, kickerApi); } public void RegisterLamp(Light lamp, GameObject go) @@ -251,8 +217,8 @@ public void RegisterLamp(Light lamp, GameObject go) TableApi.Lights[lamp.Name] = lightApi; _apis.Add(lightApi); _initializables.Add(lightApi); - _lamps[lamp.Name] = lightApi; - _wires[lamp.Name] = lightApi; + _lampPlayer?.RegisterLamp(lamp, lightApi); + _wirePlayer?.RegisterWire(lamp, lightApi); } public void RegisterPlunger(Plunger plunger, Entity entity, GameObject go) @@ -262,8 +228,8 @@ public void RegisterPlunger(Plunger plunger, Entity entity, GameObject go) _apis.Add(plungerApi); _initializables.Add(plungerApi); _rotatables[entity] = plungerApi; - _coils[plunger.Name] = plungerApi; - _wires[plunger.Name] = plungerApi; + _coilPlayer?.RegisterCoil(plunger, plungerApi); + _wirePlayer?.RegisterWire(plunger, plungerApi); } public void RegisterPrimitive(Primitive primitive, Entity entity, GameObject go) @@ -310,7 +276,7 @@ public void RegisterSpinner(Spinner spinner, Entity entity, GameObject go) _initializables.Add(spinnerApi); _spinnables[entity] = spinnerApi; _rotatables[entity] = spinnerApi; - _switches[spinner.Name] = spinnerApi; + _switchPlayer?.RegisterSwitch(spinner, spinnerApi); } public void RegisterTrigger(Trigger trigger, Entity entity, GameObject go) @@ -320,7 +286,7 @@ public void RegisterTrigger(Trigger trigger, Entity entity, GameObject go) _apis.Add(triggerApi); _initializables.Add(triggerApi); _hittables[entity] = triggerApi; - _switches[trigger.Name] = triggerApi; + _switchPlayer?.RegisterSwitch(trigger, triggerApi); } public void RegisterTrough(Trough trough, GameObject go) @@ -329,370 +295,9 @@ public void RegisterTrough(Trough trough, GameObject go) TableApi.Troughs[trough.Name] = troughApi; _apis.Add(troughApi); _initializables.Add(troughApi); - _switchDevices[trough.Name] = troughApi; - _coilDevices[trough.Name] = troughApi; - _wireDevices[trough.Name] = troughApi; - } - - #endregion - - #region Mappings - - private void SetupCoilMapping() - { - if (GameEngine is IGamelogicEngineWithCoils gamelogicEngineWithCoils) { - var config = Table.Mappings; - _coilAssignments.Clear(); - foreach (var coilData in config.Data.Coils) { - switch (coilData.Destination) { - case CoilDestination.Playfield: - AssignCoilMapping(coilData.Id, coilData); - if (coilData.Type == CoilType.DualWound) { - AssignCoilMapping(coilData.HoldCoilId, coilData, true); - } - break; - - case CoilDestination.Device: - if (_coilDevices.ContainsKey(coilData.Device)) { - var device = _coilDevices[coilData.Device]; - var coil = device.Coil(coilData.DeviceItem); - if (coil != null) { - AssignCoilMapping(coilData.Id, coilData, deviceName: coilData.Device); - - } else { - Logger.Warn($"Unknown coil \"{coilData.DeviceItem}\" in coil device \"{coilData.Device}\"."); - } - } - break; - - case CoilDestination.Lamp: - AssignCoilMapping(coilData.Id, coilData, isLampCoil: true); - break; - } - } - - if (_coilAssignments.Count > 0) { - gamelogicEngineWithCoils.OnCoilChanged += HandleCoilEvent; - } - } - } - - private void AssignCoilMapping(string id, MappingsCoilData coilData, bool isHoldCoil = false, bool isLampCoil = false, string deviceName = null) - { - if (!_coilAssignments.ContainsKey(id)) { - _coilAssignments[id] = new List(); - } - _coilAssignments[id].Add(new CoilDestConfig(coilData.PlayfieldItem, isHoldCoil, isLampCoil, deviceName)); - } - - private void SetupSwitchMapping() - { - // hook-up game switches - if (GameEngine is IGamelogicEngineWithSwitches) { - - var config = Table.Mappings; - _keySwitchAssignments.Clear(); - foreach (var switchData in config.Data.Switches) { - switch (switchData.Source) { - - case SwitchSource.Playfield - when !string.IsNullOrEmpty(switchData.PlayfieldItem) - && _switches.ContainsKey(switchData.PlayfieldItem): - { - var element = _switches[switchData.PlayfieldItem]; - element.AddSwitchId(new SwitchConfig(switchData)); - break; - } - - case SwitchSource.InputSystem: - if (!_keySwitchAssignments.ContainsKey(switchData.InputAction)) { - _keySwitchAssignments[switchData.InputAction] = new List(); - } - _keySwitchAssignments[switchData.InputAction].Add(switchData.Id); - break; - - case SwitchSource.Playfield: - Logger.Warn($"Cannot find switch \"{switchData.PlayfieldItem}\" on playfield!"); - break; - - case SwitchSource.Device - when !string.IsNullOrEmpty(switchData.Device) - && _switchDevices.ContainsKey(switchData.Device): - { - var device = _switchDevices[switchData.Device]; - var deviceSwitch = device.Switch(switchData.DeviceItem); - if (deviceSwitch != null) { - deviceSwitch.AddSwitchId(new SwitchConfig(switchData)); - - } else { - Logger.Warn($"Unknown switch \"{switchData.DeviceItem}\" in switch device \"{switchData.Device}\"."); - } - break; - } - case SwitchSource.Device when string.IsNullOrEmpty(switchData.Device): - Logger.Warn($"Switch device not set for switch \"{switchData.Id}\"."); - break; - - case SwitchSource.Device when !_switchDevices.ContainsKey(switchData.Device): - Logger.Warn($"Unknown switch device \"{switchData.Device}\" for switch \"{switchData.Id}\"."); - break; - - case SwitchSource.Constant: - break; - - default: - Logger.Warn($"Unknown switch source \"{switchData.Source}\"."); - break; - } - } - - if (_keySwitchAssignments.Count > 0) { - _inputManager.Enable(HandleKeyInput); - } - } - } - - private void SetupLampMapping() - { - if (GameEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { - var config = Table.Mappings; - _lampAssignments.Clear(); - _lampMappings.Clear(); - foreach (var lampData in config.Data.Lamps) { - switch (lampData.Destination) { - case LampDestination.Playfield: - AssignLampMapping(lampData.Id, lampData); - if (!string.IsNullOrEmpty(lampData.Green)) { - AssignLampMapping(lampData.Green, lampData); - } - if (!string.IsNullOrEmpty(lampData.Blue)) { - AssignLampMapping(lampData.Blue, lampData); - } - break; - } - } - - if (_lampAssignments.Count > 0) { - gamelogicEngineWithLamps.OnLampChanged += HandleLampEvent; - gamelogicEngineWithLamps.OnLampsChanged += HandleLampsEvent; - } - } - } - - private void AssignLampMapping(string id, MappingsLampData lampData) - { - if (!_lampAssignments.ContainsKey(id)) { - _lampAssignments[id] = new List(); - } - _lampAssignments[id].Add(lampData.PlayfieldItem); - _lampMappings[id] = lampData; - } - - private void SetupWireMapping() - { - var config = Table.Mappings; - _keyWireAssignments.Clear(); - foreach (var wireData in config.Data.Wires) { - switch (wireData.Source) { - - case SwitchSource.Playfield - when !string.IsNullOrEmpty(wireData.SourcePlayfieldItem) - && _switches.ContainsKey(wireData.SourcePlayfieldItem): - { - _switches[wireData.SourcePlayfieldItem].AddWireDest(new WireDestConfig(wireData)); - break; - } - - case SwitchSource.InputSystem: - if (!_keyWireAssignments.ContainsKey(wireData.SourceInputAction)) { - _keyWireAssignments[wireData.SourceInputAction] = new List(); - } - _keyWireAssignments[wireData.SourceInputAction].Add(new WireDestConfig(wireData)); - break; - - case SwitchSource.Playfield: - Logger.Warn($"Cannot find wire switch \"{wireData.Src}\" on playfield!"); - break; - - case SwitchSource.Device - when !string.IsNullOrEmpty(wireData.SourceDevice) - && _switchDevices.ContainsKey(wireData.SourceDevice): - { - var device = _switchDevices[wireData.SourceDevice]; - var deviceSwitch = device.Switch(wireData.SourceDeviceItem); - if (deviceSwitch != null) { - deviceSwitch.AddWireDest(new WireDestConfig(wireData)); - Logger.Info($"Wiring device switch \"{wireData.Src}\" to \"{wireData.Dst}\""); - - } else { - Logger.Warn($"Unknown switch \"{wireData.Src}\" to wire to \"{wireData.Dst}\"."); - } - break; - } - case SwitchSource.Device when string.IsNullOrEmpty(wireData.SourceDevice): - Logger.Warn($"Switch device not set for switch \"{wireData.Src}\"."); - break; - - case SwitchSource.Device when !_switchDevices.ContainsKey(wireData.SourceDevice): - Logger.Warn($"Unknown switch device \"{wireData.SourceDevice}\" to wire to \"{wireData.Dst}\"."); - break; - - case SwitchSource.Constant: - break; - - default: - Logger.Warn($"Unknown wire switch source \"{wireData.Source}\"."); - break; - } - } - - if (_keySwitchAssignments.Count > 0) { - _inputManager.Enable(HandleKeyInput); - } - } - - private void HandleKeyInput(object obj, InputActionChange change) - { - switch (change) { - case InputActionChange.ActionStarted: - case InputActionChange.ActionCanceled: - var action = (InputAction) obj; - if (_keySwitchAssignments.ContainsKey(action.name)) { - if (GameEngine is IGamelogicEngineWithSwitches engineWithSwitches) { - foreach (var switchId in _keySwitchAssignments[action.name]) { - engineWithSwitches.Switch(switchId, change == InputActionChange.ActionStarted); - } - } - } else { - Logger.Info($"Unmapped input command \"{action.name}\"."); - } - - if (_keyWireAssignments != null && _keyWireAssignments.ContainsKey(action.name)) { - foreach (var wireConfig in _keyWireAssignments[action.name]) { - switch (wireConfig.Destination) { - case WireDestination.Playfield: - Wire(wireConfig.PlayfieldItem)?.OnChange(change == InputActionChange.ActionStarted); - break; - - case WireDestination.Device: - if (_wireDevices.ContainsKey(wireConfig.Device)) { - var device = _wireDevices[wireConfig.Device]; - var wire = device.Wire(wireConfig.DeviceItem); - if (wire != null) { - wire.OnChange(change == InputActionChange.ActionStarted); - } else { - Logger.Warn($"Unknown wire \"{wireConfig.DeviceItem}\" in wire device \"{wireConfig.Device}\"."); - } - } - break; - } - } - } - break; - } - } - - private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) - { - if (_coilAssignments.ContainsKey(coilEvent.Id)) { - foreach (var destConfig in _coilAssignments[coilEvent.Id]) { - if (destConfig.DeviceName != null && _coilDevices.ContainsKey(destConfig.DeviceName)) { - _coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName).OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); - - } else if (_coils.ContainsKey(destConfig.ItemName)) { - if (destConfig.IsLampCoil) { - HandleLampEvent(null, new LampEventArgs(coilEvent.Id, coilEvent.IsEnabled ? 1 : 0, LampSource.Coils)); - - } else { - _coils[destConfig.ItemName].OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); - } - - } else { - Logger.Warn($"Cannot trigger unknown coil item {destConfig.ItemName}."); - } - } - - } else { - var what = coilEvent.IsEnabled ? "turn on" : "turn off"; - Logger.Warn($"Should {what} unassigned coil {coilEvent.Id}."); - } - } - - private void HandleLampsEvent(object sender, LampsEventArgs lampsEvent) - { - foreach (var lampEvent in lampsEvent.LampsChanged) { - HandleLampEvent(lampEvent, (lamp, data, itemName) => { - // todo - }); - } - } - - private void HandleLampEvent(object sender, LampEventArgs lampEvent) - { - var colors = new Dictionary(); - var lamps = new Dictionary(); - - HandleLampEvent(lampEvent, (lamp, mapping, itemName) => { - var color = colors.ContainsKey(mapping.Id) ? colors[mapping.Id] : lamp.Color; - if (lampEvent.Id == mapping.Id) { - color.r = lampEvent.Value / 255f; - - } else if (lampEvent.Id == mapping.Green) { - color.g = lampEvent.Value / 255f; - - } else if (lampEvent.Id == mapping.Blue) { - color.b = lampEvent.Value / 255f; - - } else { - Logger.Error($"Cannot assign lamp {lampEvent.Id} to an RGB value of light {itemName}"); - } - colors[mapping.Id] = color; - lamps[mapping.Id] = lamp; - }); - - foreach (var mappingId in colors.Keys) { - lamps[mappingId].Color = colors[mappingId]; - } - } - - private void HandleLampEvent(LampEventArgs lampEvent, Action handleRgb) - { - if (_lampAssignments.ContainsKey(lampEvent.Id)) { - var mapping = _lampMappings[lampEvent.Id]; - foreach (var itemName in _lampAssignments[lampEvent.Id]) { - if (mapping.Source != lampEvent.Source) { - // so, if we have a coil here that happens to have the same name as a lamp, - // skip if the source isn't the same. - continue; - } - if (_lamps.ContainsKey(itemName)) { - var lamp = _lamps[itemName]; - switch (mapping.Type) { - case LampType.SingleOnOff: - lamp.OnLamp(lampEvent.Value > 0 ? 1f : 0f, ColorChannel.Alpha); - break; - - case LampType.SingleFading: - lamp.OnLamp(lampEvent.Value / 255f, ColorChannel.Alpha); - break; - - case LampType.Rgb: - handleRgb(lamp, mapping, itemName); - break; - - default: - Logger.Error($"Unknown mapping type \"{mapping.Type}\" of lamp ID {lampEvent.Id} for light {itemName}."); - break; - } - - } else { - Logger.Error($"Cannot trigger unknown lamp {itemName}."); - } - } - - } else { - Logger.Error($"Should update unassigned lamp {lampEvent.Id}."); - } + _switchPlayer?.RegisterSwitchDevice(trough, troughApi); + _coilPlayer?.RegisterCoilDevice(trough, troughApi); + _wirePlayer?.RegisterWireDevice(trough, troughApi); } #endregion @@ -744,23 +349,7 @@ public float3 GetGravity() { var slope = Table.Data.AngleTiltMin + (Table.Data.AngleTiltMax - Table.Data.AngleTiltMin) * Table.Data.GlobalDifficulty; var strength = Table.Data.OverridePhysics != 0 ? PhysicsConstants.DefaultTableGravity : Table.Data.Gravity; - return new float3(0, math.sin(math.radians(slope)) * strength, -math.cos(math.radians(slope)) * strength); - } - } - - internal struct CoilDestConfig - { - public readonly string ItemName; - public readonly bool IsHoldCoil; - public readonly bool IsLampCoil; - public readonly string DeviceName; - - public CoilDestConfig(string itemName, bool isHoldCoil, bool isLampCoil, string deviceName) - { - ItemName = itemName; - IsHoldCoil = isHoldCoil; - IsLampCoil = isLampCoil; - DeviceName = deviceName; + return new float3(0, math.sin(math.radians(slope)) * strength, -math.cos(math.radians(slope)) * strength); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs new file mode 100644 index 000000000..6ee5eb265 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs @@ -0,0 +1,145 @@ +// 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 . + +using System.Collections.Generic; +using NLog; +using UnityEngine.InputSystem; +using VisualPinball.Engine.VPT; +using VisualPinball.Engine.VPT.Mappings; +using VisualPinball.Engine.VPT.Table; + +namespace VisualPinball.Unity +{ + public class SwitchPlayer + { + + private readonly Dictionary _switches = new Dictionary(); + private readonly Dictionary _switchDevices = new Dictionary(); + private readonly Dictionary> _keySwitchAssignments = new Dictionary>(); + + private Table _table; + private readonly IGamelogicEngine _gamelogicEngine; + private readonly InputManager _inputManager; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + internal IApiSwitch Switch(string itemName) => _switches.ContainsKey(itemName) ? _switches[itemName] : null; + 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 bool SwitchExists(string name) => _switches.ContainsKey(name); + public bool SwitchDeviceExists(string name) => _switchDevices.ContainsKey(name); + + public SwitchPlayer(Table table, IGamelogicEngine gamelogicEngine, InputManager inputManager) + { + _table = table; + _gamelogicEngine = gamelogicEngine; + _inputManager = inputManager; + } + + public void OnStart() + { + // hook-up game switches + if (_gamelogicEngine is IGamelogicEngineWithSwitches) { + + var config = _table.Mappings; + _keySwitchAssignments.Clear(); + foreach (var switchData in config.Data.Switches) { + switch (switchData.Source) { + + case SwitchSource.Playfield + when !string.IsNullOrEmpty(switchData.PlayfieldItem) + && _switches.ContainsKey(switchData.PlayfieldItem): { + var element = _switches[switchData.PlayfieldItem]; + element.AddSwitchId(new SwitchConfig(switchData)); + break; + } + + case SwitchSource.InputSystem: + if (!_keySwitchAssignments.ContainsKey(switchData.InputAction)) { + _keySwitchAssignments[switchData.InputAction] = new List(); + } + _keySwitchAssignments[switchData.InputAction].Add(switchData.Id); + break; + + case SwitchSource.Playfield: + Logger.Warn($"Cannot find switch \"{switchData.PlayfieldItem}\" on playfield!"); + break; + + case SwitchSource.Device + when !string.IsNullOrEmpty(switchData.Device) + && _switchDevices.ContainsKey(switchData.Device): { + var device = _switchDevices[switchData.Device]; + var deviceSwitch = device.Switch(switchData.DeviceItem); + if (deviceSwitch != null) { + deviceSwitch.AddSwitchId(new SwitchConfig(switchData)); + + } else { + Logger.Warn($"Unknown switch \"{switchData.DeviceItem}\" in switch device \"{switchData.Device}\"."); + } + break; + } + case SwitchSource.Device when string.IsNullOrEmpty(switchData.Device): + Logger.Warn($"Switch device not set for switch \"{switchData.Id}\"."); + break; + + case SwitchSource.Device when !_switchDevices.ContainsKey(switchData.Device): + Logger.Warn($"Unknown switch device \"{switchData.Device}\" for switch \"{switchData.Id}\"."); + break; + + case SwitchSource.Constant: + break; + + default: + Logger.Warn($"Unknown switch source \"{switchData.Source}\"."); + break; + } + } + + if (_keySwitchAssignments.Count > 0) { + _inputManager.Enable(HandleKeyInput); + } + } + } + + private void HandleKeyInput(object obj, InputActionChange change) + { + switch (change) { + case InputActionChange.ActionStarted: + case InputActionChange.ActionCanceled: + var action = (InputAction)obj; + if (_keySwitchAssignments.ContainsKey(action.name)) { + if (_gamelogicEngine is IGamelogicEngineWithSwitches engineWithSwitches) { + foreach (var switchId in _keySwitchAssignments[action.name]) { + engineWithSwitches.Switch(switchId, change == InputActionChange.ActionStarted); + } + } + } else { + Logger.Info($"Unmapped input command \"{action.name}\"."); + } + break; + } + } + + public void OnDestroy() + { + if (_keySwitchAssignments.Count > 0) { + _inputManager.Disable(HandleKeyInput); + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs.meta new file mode 100644 index 000000000..a50973a95 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 36759aa706930fc40b80fb7fdf9153cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballScript.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballScript.cs deleted file mode 100644 index edacf9a51..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballScript.cs +++ /dev/null @@ -1,53 +0,0 @@ -// 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 . - -using UnityEngine; - -namespace VisualPinball.Unity -{ - public class VisualPinballScript : MonoBehaviour - { - public virtual void OnAwake(TableApi table, BallManager ballManager) - { - // table.Plunger("Plunger").Init += (sender, args) => { - // KickNewBallToPlunger(table); - // }; - // - // table.Kicker("Drain").Hit += (sender, args) => { - // ((KickerApi)sender).DestroyBall(); - // KickNewBallToPlunger(table); - // }; - - table.Init += (sender, args) => { - - ballManager.CreateBall(new DebugBallCreator(200f, 620f)); - ballManager.CreateBall(new DebugBallCreator(330f, 360f)); - ballManager.CreateBall(new DebugBallCreator(400f, 700f)); - ballManager.CreateBall(new DebugBallCreator(620f, 820f)); - ballManager.CreateBall(new DebugBallCreator(720f, 400f)); - ballManager.CreateBall(new DebugBallCreator(830f, 870f)); - ballManager.CreateBall(new DebugBallCreator(470f, 230f)); - ballManager.CreateBall(new DebugBallCreator(620f, 1200f)); - }; - } - - public void KickNewBallToPlunger(TableApi table) - { - table.Kicker("BallRelease").CreateBall(); - table.Kicker("BallRelease").Kick(90, 7); - } - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/WireDestConfig.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/WireDestConfig.cs deleted file mode 100644 index 2e9983cb5..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/WireDestConfig.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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 . - -using VisualPinball.Engine.VPT.Mappings; - -namespace VisualPinball.Unity -{ - public struct WireDestConfig - { - public readonly int Destination; - public readonly string PlayfieldItem; - public readonly string Device; - public readonly string DeviceItem; - public readonly int PulseDelay; - public bool IsPulseSource; - - public WireDestConfig(MappingsWireData data) - { - Destination = data.Destination; - PlayfieldItem = data.DestinationPlayfieldItem; - Device = data.DestinationDevice; - DeviceItem = data.DestinationDeviceItem; - PulseDelay = data.PulseDelay; - IsPulseSource = false; - } - - public WireDestConfig WithPulse(bool isPulseSource) - { - IsPulseSource = isPulseSource; - return this; - } - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs new file mode 100644 index 000000000..8fd8c2459 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs @@ -0,0 +1,176 @@ +// 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 . + +using System.Collections.Generic; +using NLog; +using UnityEngine.InputSystem; +using VisualPinball.Engine.VPT; +using VisualPinball.Engine.VPT.Mappings; +using VisualPinball.Engine.VPT.Table; + +namespace VisualPinball.Unity +{ + public class WirePlayer + { + private readonly Dictionary _wires = new Dictionary(); + private readonly Dictionary _wireDevices = new Dictionary(); + private readonly Dictionary> _keyWireAssignments = new Dictionary>(); + + + private readonly Table _table; + private readonly InputManager _inputManager; + private readonly SwitchPlayer _switchPlayer; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + internal IApiWireDest Wire(string n) => _wires.ContainsKey(n) ? _wires[n] : null; + internal IApiWireDeviceDest WireDevice(string n) => _wireDevices.ContainsKey(n) ? _wireDevices[n] : null; + internal void RegisterWire(IItem item, IApiWireDest wireApi) => _wires[item.Name] = wireApi; + internal void RegisterWireDevice(IItem item, IApiWireDeviceDest wireDeviceApi) => _wireDevices[item.Name] = wireDeviceApi; + + public WirePlayer(Table table, InputManager inputManager, SwitchPlayer switchPlayer) + { + _table = table; + _inputManager = inputManager; + _switchPlayer = switchPlayer; + } + + public void OnStart() + { + var config = _table.Mappings; + _keyWireAssignments.Clear(); + foreach (var wireData in config.Data.Wires) { + switch (wireData.Source) { + + case SwitchSource.Playfield + when !string.IsNullOrEmpty(wireData.SourcePlayfieldItem) + && _switchPlayer.SwitchExists(wireData.SourcePlayfieldItem): { + _switchPlayer.RegisterWire(wireData); + + break; + } + + case SwitchSource.InputSystem: + if (!_keyWireAssignments.ContainsKey(wireData.SourceInputAction)) { + _keyWireAssignments[wireData.SourceInputAction] = new List(); + } + _keyWireAssignments[wireData.SourceInputAction].Add(new WireDestConfig(wireData)); + break; + + case SwitchSource.Playfield: + Logger.Warn($"Cannot find wire switch \"{wireData.Src}\" on playfield!"); + break; + + case SwitchSource.Device + when !string.IsNullOrEmpty(wireData.SourceDevice) + && _switchPlayer.SwitchDeviceExists(wireData.SourceDevice): { + 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}\""); + + } else { + Logger.Warn($"Unknown switch \"{wireData.Src}\" to wire to \"{wireData.Dst}\"."); + } + break; + } + case SwitchSource.Device when string.IsNullOrEmpty(wireData.SourceDevice): + Logger.Warn($"Switch device not set for switch \"{wireData.Src}\"."); + break; + + case SwitchSource.Device when !_switchPlayer.SwitchDeviceExists(wireData.SourceDevice): + Logger.Warn($"Unknown switch device \"{wireData.SourceDevice}\" to wire to \"{wireData.Dst}\"."); + break; + + case SwitchSource.Constant: + break; + + default: + Logger.Warn($"Unknown wire switch source \"{wireData.Source}\"."); + break; + } + } + + if (_keyWireAssignments.Count > 0) { + _inputManager.Enable(HandleKeyInput); + } + } + + private void HandleKeyInput(object obj, InputActionChange change) + { + switch (change) { + case InputActionChange.ActionStarted: + case InputActionChange.ActionCanceled: + var action = (InputAction)obj; + if (_keyWireAssignments != null && _keyWireAssignments.ContainsKey(action.name)) { + foreach (var wireConfig in _keyWireAssignments[action.name]) { + switch (wireConfig.Destination) { + case WireDestination.Playfield: + Wire(wireConfig.PlayfieldItem)?.OnChange(change == InputActionChange.ActionStarted); + break; + + case WireDestination.Device: + if (_wireDevices.ContainsKey(wireConfig.Device)) { + var device = _wireDevices[wireConfig.Device]; + var wire = device.Wire(wireConfig.DeviceItem); + if (wire != null) { + wire.OnChange(change == InputActionChange.ActionStarted); + } else { + Logger.Warn($"Unknown wire \"{wireConfig.DeviceItem}\" in wire device \"{wireConfig.Device}\"."); + } + } + break; + } + } + } + break; + } + } + + public void OnDestroy() + { + if (_keyWireAssignments.Count > 0) { + _inputManager.Disable(HandleKeyInput); + } + } + } + + public struct WireDestConfig + { + public readonly int Destination; + public readonly string PlayfieldItem; + public readonly string Device; + public readonly string DeviceItem; + public readonly int PulseDelay; + public bool IsPulseSource; + + public WireDestConfig(MappingsWireData data) + { + Destination = data.Destination; + PlayfieldItem = data.DestinationPlayfieldItem; + Device = data.DestinationDevice; + DeviceItem = data.DestinationDeviceItem; + PulseDelay = data.PulseDelay; + IsPulseSource = false; + } + + public WireDestConfig WithPulse(bool isPulseSource) + { + IsPulseSource = isPulseSource; + return this; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs.meta new file mode 100644 index 000000000..8d3a6c40e --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 47ef89abae693fe43acad81109183617 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 3587aadd511e30f3a3be1b77cddd19062c843089 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 17 Jan 2021 22:08:07 +0100 Subject: [PATCH 11/23] player: Fix calling order. --- .../VisualPinball.Unity/Game/CoilPlayer.cs | 8 +- .../VisualPinball.Unity/Game/LampPlayer.cs | 4 +- .../VisualPinball.Unity/Game/Player.cs | 77 +++++++++---------- .../VisualPinball.Unity/Game/SwitchPlayer.cs | 6 +- .../Game/VisualPinballScript.cs.meta | 3 - .../VisualPinball.Unity/Game/WirePlayer.cs | 8 +- 6 files changed, 51 insertions(+), 55 deletions(-) delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballScript.cs.meta diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs index 926bcde23..c2a418d94 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs @@ -28,16 +28,16 @@ public class CoilPlayer private readonly Dictionary _coilDevices = new Dictionary(); private readonly Dictionary> _coilAssignments = new Dictionary>(); - private readonly Table _table; - private readonly IGamelogicEngine _gamelogicEngine; - private readonly LampPlayer _lampPlayer; + private Table _table; + private IGamelogicEngine _gamelogicEngine; + private LampPlayer _lampPlayer; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); internal void RegisterCoil(IItem item, IApiCoil coilApi) => _coils[item.Name] = coilApi; internal void RegisterCoilDevice(IItem item, IApiCoilDevice coilDeviceApi) => _coilDevices[item.Name] = coilDeviceApi; - public CoilPlayer(Table table, IGamelogicEngine gamelogicEngine, LampPlayer lampPlayer) + public void Awake(Table table, IGamelogicEngine gamelogicEngine, LampPlayer lampPlayer) { _table = table; _gamelogicEngine = gamelogicEngine; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs index 6aea1bcb9..8d7f468e7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs @@ -33,13 +33,13 @@ public class LampPlayer private readonly Dictionary _lampMappings = new Dictionary(); private Table _table; - private readonly IGamelogicEngine _gamelogicEngine; + private IGamelogicEngine _gamelogicEngine; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); internal void RegisterLamp(IItem item, IApiLamp lampApi) => _lamps[item.Name] = lampApi; - public LampPlayer(Table table, IGamelogicEngine gamelogicEngine) + public void Awake(Table table, IGamelogicEngine gamelogicEngine) { _table = table; _gamelogicEngine = gamelogicEngine; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index b0e0875d7..38687131c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -66,10 +66,10 @@ public class Player : MonoBehaviour private readonly Dictionary _slingshots = new Dictionary(); private InputManager _inputManager; - [NonSerialized] private CoilPlayer _coilPlayer; - [NonSerialized] private SwitchPlayer _switchPlayer; - [NonSerialized] private LampPlayer _lampPlayer; - [NonSerialized] private WirePlayer _wirePlayer; + [NonSerialized] private readonly LampPlayer _lampPlayer = new LampPlayer(); + [NonSerialized] private readonly CoilPlayer _coilPlayer = new CoilPlayer(); + [NonSerialized] private readonly SwitchPlayer _switchPlayer = new SwitchPlayer(); + [NonSerialized] private readonly WirePlayer _wirePlayer = new WirePlayer(); public Player() { @@ -79,9 +79,9 @@ public Player() #region Access - internal IApiSwitch Switch(string n) => _switchPlayer?.Switch(n); - internal IApiWireDest Wire(string n) => _wirePlayer?.Wire(n); - internal IApiWireDeviceDest WireDevice(string n) => _wirePlayer?.WireDevice(n); + internal IApiSwitch Switch(string n) => _switchPlayer.Switch(n); + internal IApiWireDest Wire(string n) => _wirePlayer.Wire(n); + internal IApiWireDeviceDest WireDevice(string n) => _wirePlayer.WireDevice(n); #endregion @@ -98,10 +98,10 @@ private void Awake() if (engineComponent != null) { GameEngine = engineComponent.GameEngine; - _lampPlayer = new LampPlayer(Table, GameEngine); - _coilPlayer = new CoilPlayer(Table, GameEngine, _lampPlayer); - _switchPlayer = new SwitchPlayer(Table, GameEngine, _inputManager); - _wirePlayer = new WirePlayer(Table, _inputManager, _switchPlayer); + _lampPlayer.Awake(Table, GameEngine); + _coilPlayer.Awake(Table, GameEngine, _lampPlayer); + _switchPlayer.Awake(Table, GameEngine, _inputManager); + _wirePlayer.Awake(Table, _inputManager, _switchPlayer); } EngineProvider.Set(physicsEngineId); @@ -118,10 +118,10 @@ private void Start() i.OnInit(BallManager); } - _coilPlayer?.OnStart(); - _switchPlayer?.OnStart(); - _lampPlayer?.OnStart(); - _wirePlayer?.OnStart(); + _coilPlayer.OnStart(); + _switchPlayer.OnStart(); + _lampPlayer.OnStart(); + _wirePlayer.OnStart(); GameEngine?.OnInit(TableApi, BallManager); } @@ -137,10 +137,10 @@ private void OnDestroy() i.OnDestroy(); } - _coilPlayer?.OnDestroy(); - _switchPlayer?.OnDestroy(); - _lampPlayer?.OnDestroy(); - _wirePlayer?.OnDestroy(); + _coilPlayer.OnDestroy(); + _switchPlayer.OnDestroy(); + _lampPlayer.OnDestroy(); + _wirePlayer.OnDestroy(); GameEngine?.OnDestroy(); } @@ -155,9 +155,9 @@ public void RegisterBumper(Bumper bumper, Entity entity, GameObject go) _apis.Add(bumperApi); _initializables.Add(bumperApi); _hittables[entity] = bumperApi; - _switchPlayer?.RegisterSwitch(bumper, bumperApi); - _coilPlayer?.RegisterCoil(bumper, bumperApi); - _wirePlayer?.RegisterWire(bumper, bumperApi); + _switchPlayer.RegisterSwitch(bumper, bumperApi); + _coilPlayer.RegisterCoil(bumper, bumperApi); + _wirePlayer.RegisterWire(bumper, bumperApi); } public void RegisterFlipper(Flipper flipper, Entity entity, GameObject go) @@ -169,9 +169,9 @@ public void RegisterFlipper(Flipper flipper, Entity entity, GameObject go) _hittables[entity] = flipperApi; _rotatables[entity] = flipperApi; _collidables[entity] = flipperApi; - _switchPlayer?.RegisterSwitch(flipper, flipperApi); - _coilPlayer?.RegisterCoil(flipper, flipperApi); - _wirePlayer?.RegisterWire(flipper, flipperApi); + _switchPlayer.RegisterSwitch(flipper, flipperApi); + _coilPlayer.RegisterCoil(flipper, flipperApi); + _wirePlayer.RegisterWire(flipper, flipperApi); if (EngineProvider.Exists) { EngineProvider.Get().OnRegisterFlipper(entity, flipper.Name); @@ -186,7 +186,7 @@ public void RegisterGate(Gate gate, Entity entity, GameObject go) _initializables.Add(gateApi); _hittables[entity] = gateApi; _rotatables[entity] = gateApi; - _switchPlayer?.RegisterSwitch(gate, gateApi); + _switchPlayer.RegisterSwitch(gate, gateApi); } public void RegisterHitTarget(HitTarget hitTarget, Entity entity, GameObject go) @@ -196,7 +196,7 @@ public void RegisterHitTarget(HitTarget hitTarget, Entity entity, GameObject go) _apis.Add(hitTargetApi); _initializables.Add(hitTargetApi); _hittables[entity] = hitTargetApi; - _switchPlayer?.RegisterSwitch(hitTarget, hitTargetApi); + _switchPlayer.RegisterSwitch(hitTarget, hitTargetApi); } public void RegisterKicker(Kicker kicker, Entity entity, GameObject go) @@ -206,9 +206,9 @@ public void RegisterKicker(Kicker kicker, Entity entity, GameObject go) _apis.Add(kickerApi); _initializables.Add(kickerApi); _hittables[entity] = kickerApi; - _switchPlayer?.RegisterSwitch(kicker, kickerApi); - _coilPlayer?.RegisterCoil(kicker, kickerApi); - _wirePlayer?.RegisterWire(kicker, kickerApi); + _switchPlayer.RegisterSwitch(kicker, kickerApi); + _coilPlayer.RegisterCoil(kicker, kickerApi); + _wirePlayer.RegisterWire(kicker, kickerApi); } public void RegisterLamp(Light lamp, GameObject go) @@ -217,8 +217,8 @@ public void RegisterLamp(Light lamp, GameObject go) TableApi.Lights[lamp.Name] = lightApi; _apis.Add(lightApi); _initializables.Add(lightApi); - _lampPlayer?.RegisterLamp(lamp, lightApi); - _wirePlayer?.RegisterWire(lamp, lightApi); + _lampPlayer.RegisterLamp(lamp, lightApi); + _wirePlayer.RegisterWire(lamp, lightApi); } public void RegisterPlunger(Plunger plunger, Entity entity, GameObject go) @@ -228,8 +228,8 @@ public void RegisterPlunger(Plunger plunger, Entity entity, GameObject go) _apis.Add(plungerApi); _initializables.Add(plungerApi); _rotatables[entity] = plungerApi; - _coilPlayer?.RegisterCoil(plunger, plungerApi); - _wirePlayer?.RegisterWire(plunger, plungerApi); + _coilPlayer.RegisterCoil(plunger, plungerApi); + _wirePlayer.RegisterWire(plunger, plungerApi); } public void RegisterPrimitive(Primitive primitive, Entity entity, GameObject go) @@ -276,7 +276,7 @@ public void RegisterSpinner(Spinner spinner, Entity entity, GameObject go) _initializables.Add(spinnerApi); _spinnables[entity] = spinnerApi; _rotatables[entity] = spinnerApi; - _switchPlayer?.RegisterSwitch(spinner, spinnerApi); + _switchPlayer.RegisterSwitch(spinner, spinnerApi); } public void RegisterTrigger(Trigger trigger, Entity entity, GameObject go) @@ -286,7 +286,7 @@ public void RegisterTrigger(Trigger trigger, Entity entity, GameObject go) _apis.Add(triggerApi); _initializables.Add(triggerApi); _hittables[entity] = triggerApi; - _switchPlayer?.RegisterSwitch(trigger, triggerApi); + _switchPlayer.RegisterSwitch(trigger, triggerApi); } public void RegisterTrough(Trough trough, GameObject go) @@ -295,9 +295,8 @@ public void RegisterTrough(Trough trough, GameObject go) TableApi.Troughs[trough.Name] = troughApi; _apis.Add(troughApi); _initializables.Add(troughApi); - _switchPlayer?.RegisterSwitchDevice(trough, troughApi); - _coilPlayer?.RegisterCoilDevice(trough, troughApi); - _wirePlayer?.RegisterWireDevice(trough, troughApi); + _switchPlayer.RegisterSwitchDevice(trough, troughApi); + _coilPlayer.RegisterCoilDevice(trough, troughApi); } #endregion diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs index 6ee5eb265..b150e8ba4 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs @@ -31,8 +31,8 @@ public class SwitchPlayer private readonly Dictionary> _keySwitchAssignments = new Dictionary>(); private Table _table; - private readonly IGamelogicEngine _gamelogicEngine; - private readonly InputManager _inputManager; + private IGamelogicEngine _gamelogicEngine; + private InputManager _inputManager; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -44,7 +44,7 @@ public class SwitchPlayer public bool SwitchExists(string name) => _switches.ContainsKey(name); public bool SwitchDeviceExists(string name) => _switchDevices.ContainsKey(name); - public SwitchPlayer(Table table, IGamelogicEngine gamelogicEngine, InputManager inputManager) + public void Awake(Table table, IGamelogicEngine gamelogicEngine, InputManager inputManager) { _table = table; _gamelogicEngine = gamelogicEngine; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballScript.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballScript.cs.meta deleted file mode 100644 index 829ce27f8..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballScript.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: cc915bbd672b42529c0f79b68cd02027 -timeCreated: 1583591575 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs index 8fd8c2459..cfd0917e6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs @@ -30,9 +30,9 @@ public class WirePlayer private readonly Dictionary> _keyWireAssignments = new Dictionary>(); - private readonly Table _table; - private readonly InputManager _inputManager; - private readonly SwitchPlayer _switchPlayer; + private Table _table; + private InputManager _inputManager; + private SwitchPlayer _switchPlayer; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -41,7 +41,7 @@ public class WirePlayer internal void RegisterWire(IItem item, IApiWireDest wireApi) => _wires[item.Name] = wireApi; internal void RegisterWireDevice(IItem item, IApiWireDeviceDest wireDeviceApi) => _wireDevices[item.Name] = wireDeviceApi; - public WirePlayer(Table table, InputManager inputManager, SwitchPlayer switchPlayer) + public void Awake(Table table, InputManager inputManager, SwitchPlayer switchPlayer) { _table = table; _inputManager = inputManager; From 2fcca2acec91af0b1e88ca14f54aad38f149bf91 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 17 Jan 2021 22:56:27 +0100 Subject: [PATCH 12/23] player: Better error logging when assiging coils, lamps, switches and wires. --- .../VisualPinball.Unity/Game/CoilPlayer.cs | 61 +++++++++++++------ .../VisualPinball.Unity/Game/LampPlayer.cs | 6 ++ .../VisualPinball.Unity/Game/SwitchPlayer.cs | 46 ++++++++------ .../VisualPinball.Unity/Game/WirePlayer.cs | 40 ++++++------ 4 files changed, 99 insertions(+), 54 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs index c2a418d94..a3c0341bc 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs @@ -17,7 +17,6 @@ using System.Collections.Generic; using NLog; using VisualPinball.Engine.VPT; -using VisualPinball.Engine.VPT.Mappings; using VisualPinball.Engine.VPT.Table; namespace VisualPinball.Unity @@ -52,27 +51,44 @@ public void OnStart() foreach (var coilData in config.Data.Coils) { switch (coilData.Destination) { case CoilDestination.Playfield: - AssignCoilMapping(coilData.Id, coilData); + + if (string.IsNullOrEmpty(coilData.PlayfieldItem)) { + Logger.Warn($"Ignoring unassigned coil \"{coilData.Id}\"."); + break; + } + + AssignCoilMapping(coilData.Id, coilData.PlayfieldItem); if (coilData.Type == CoilType.DualWound) { - AssignCoilMapping(coilData.HoldCoilId, coilData, true); + AssignCoilMapping(coilData.HoldCoilId, coilData.PlayfieldItem, true); } break; case CoilDestination.Device: - if (_coilDevices.ContainsKey(coilData.Device)) { - var device = _coilDevices[coilData.Device]; - var coil = device.Coil(coilData.DeviceItem); - if (coil != null) { - AssignCoilMapping(coilData.Id, coilData, deviceName: coilData.Device); - - } else { - Logger.Warn($"Unknown coil \"{coilData.DeviceItem}\" in coil device \"{coilData.Device}\"."); - } + + // mapping values must be set + if (string.IsNullOrEmpty(coilData.Device) || string.IsNullOrEmpty(coilData.DeviceItem)) { + Logger.Warn($"Ignoring unassigned device coil \"{coilData.Id}\"."); + break; + } + + // check if device exists + if (!_coilDevices.ContainsKey(coilData.Device)) { + Logger.Error($"Unknown coil device \"{coilData.Device}\"."); + break; + } + + var device = _coilDevices[coilData.Device]; + var coil = device.Coil(coilData.DeviceItem); + if (coil != null) { + AssignCoilMapping(coilData.Id, coilData.DeviceItem, deviceName: coilData.Device); + + } else { + Logger.Error($"Unknown coil \"{coilData.DeviceItem}\" in coil device \"{coilData.Device}\"."); } break; case CoilDestination.Lamp: - AssignCoilMapping(coilData.Id, coilData, isLampCoil: true); + AssignCoilMapping(coilData.Id, coilData.PlayfieldItem, isLampCoil: true); break; } } @@ -83,12 +99,12 @@ public void OnStart() } } - private void AssignCoilMapping(string id, MappingsCoilData coilData, bool isHoldCoil = false, bool isLampCoil = false, string deviceName = null) + private void AssignCoilMapping(string id, string playfieldItem, bool isHoldCoil = false, bool isLampCoil = false, string deviceName = null) { if (!_coilAssignments.ContainsKey(id)) { _coilAssignments[id] = new List(); } - _coilAssignments[id].Add(new CoilDestConfig(coilData.PlayfieldItem, isHoldCoil, isLampCoil, deviceName)); + _coilAssignments[id].Add(new CoilDestConfig(playfieldItem, isHoldCoil, isLampCoil, deviceName)); } private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) @@ -96,7 +112,12 @@ private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) if (_coilAssignments.ContainsKey(coilEvent.Id)) { foreach (var destConfig in _coilAssignments[coilEvent.Id]) { if (destConfig.DeviceName != null && _coilDevices.ContainsKey(destConfig.DeviceName)) { - _coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName).OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); + if (_coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName) != null) { + _coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName).OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); + + } else { + Logger.Error($"Cannot trigger non-existing coil \"{destConfig.ItemName}\" in coil device \"{destConfig.DeviceName}\" for {coilEvent.Id}."); + } } else if (_coils.ContainsKey(destConfig.ItemName)) { if (destConfig.IsLampCoil) { @@ -107,13 +128,12 @@ private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) } } else { - Logger.Warn($"Cannot trigger unknown coil item {destConfig.ItemName}."); + Logger.Error($"Cannot trigger unknown coil item {destConfig.ItemName}."); } } } else { - var what = coilEvent.IsEnabled ? "turn on" : "turn off"; - Logger.Warn($"Should {what} unassigned coil {coilEvent.Id}."); + Logger.Info($"Ignoring unassigned coil {coilEvent.Id}."); } } @@ -127,6 +147,9 @@ public void OnDestroy() internal readonly struct CoilDestConfig { + /// + /// Playfield item if is null or device item otherwise. + /// public readonly string ItemName; public readonly bool IsHoldCoil; public readonly bool IsLampCoil; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs index 8d7f468e7..33b83eaae 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs @@ -54,6 +54,12 @@ public void OnStart() foreach (var lampData in config.Data.Lamps) { switch (lampData.Destination) { case LampDestination.Playfield: + + if (string.IsNullOrEmpty(lampData.PlayfieldItem)) { + Logger.Warn($"Ignoring unassigned lamp \"{lampData.Id}\"."); + break; + } + AssignLampMapping(lampData.Id, lampData); if (!string.IsNullOrEmpty(lampData.Green)) { AssignLampMapping(lampData.Green, lampData); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs index b150e8ba4..d182e8fc9 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs @@ -61,9 +61,18 @@ public void OnStart() foreach (var switchData in config.Data.Switches) { switch (switchData.Source) { - case SwitchSource.Playfield - when !string.IsNullOrEmpty(switchData.PlayfieldItem) - && _switches.ContainsKey(switchData.PlayfieldItem): { + case SwitchSource.Playfield: { + + if (string.IsNullOrEmpty(switchData.PlayfieldItem)) { + Logger.Warn($"Ignoring unassigned switch \"{switchData.Id}\"."); + break; + } + + if (!_switches.ContainsKey(switchData.PlayfieldItem)) { + Logger.Error($"Cannot find item \"{switchData.PlayfieldItem}\" for switch \"{switchData.Id}\"."); + break; + } + var element = _switches[switchData.PlayfieldItem]; element.AddSwitchId(new SwitchConfig(switchData)); break; @@ -76,36 +85,37 @@ public void OnStart() _keySwitchAssignments[switchData.InputAction].Add(switchData.Id); break; - case SwitchSource.Playfield: - Logger.Warn($"Cannot find switch \"{switchData.PlayfieldItem}\" on playfield!"); - break; + case SwitchSource.Device: { + + // mapping values must be set + if (string.IsNullOrEmpty(switchData.Device) || string.IsNullOrEmpty(switchData.DeviceItem)) { + Logger.Warn($"Ignoring unassigned device switch \"{switchData.Id}\"."); + break; + } + + // check if device exists + if (!_switchDevices.ContainsKey(switchData.Device)) { + Logger.Error($"Unknown switch device \"{switchData.Device}\"."); + break; + } - case SwitchSource.Device - when !string.IsNullOrEmpty(switchData.Device) - && _switchDevices.ContainsKey(switchData.Device): { var device = _switchDevices[switchData.Device]; var deviceSwitch = device.Switch(switchData.DeviceItem); if (deviceSwitch != null) { deviceSwitch.AddSwitchId(new SwitchConfig(switchData)); } else { - Logger.Warn($"Unknown switch \"{switchData.DeviceItem}\" in switch device \"{switchData.Device}\"."); + Logger.Error($"Unknown switch \"{switchData.DeviceItem}\" in switch device \"{switchData.Device}\"."); } - break; - } - case SwitchSource.Device when string.IsNullOrEmpty(switchData.Device): - Logger.Warn($"Switch device not set for switch \"{switchData.Id}\"."); - break; - case SwitchSource.Device when !_switchDevices.ContainsKey(switchData.Device): - Logger.Warn($"Unknown switch device \"{switchData.Device}\" for switch \"{switchData.Id}\"."); break; + } case SwitchSource.Constant: break; default: - Logger.Warn($"Unknown switch source \"{switchData.Source}\"."); + Logger.Error($"Unknown switch source \"{switchData.Source}\"."); break; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs index cfd0917e6..f4eca2584 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/WirePlayer.cs @@ -55,11 +55,18 @@ public void OnStart() foreach (var wireData in config.Data.Wires) { switch (wireData.Source) { - case SwitchSource.Playfield - when !string.IsNullOrEmpty(wireData.SourcePlayfieldItem) - && _switchPlayer.SwitchExists(wireData.SourcePlayfieldItem): { - _switchPlayer.RegisterWire(wireData); + case SwitchSource.Playfield: { + + if (string.IsNullOrEmpty(wireData.SourcePlayfieldItem)) { + break; + } + if (!_switchPlayer.SwitchExists(wireData.SourcePlayfieldItem)) { + Logger.Error($"Cannot find item \"{wireData.SourcePlayfieldItem}\" for wire source."); + break; + } + + _switchPlayer.RegisterWire(wireData); break; } @@ -70,13 +77,19 @@ public void OnStart() _keyWireAssignments[wireData.SourceInputAction].Add(new WireDestConfig(wireData)); break; - case SwitchSource.Playfield: - Logger.Warn($"Cannot find wire switch \"{wireData.Src}\" on playfield!"); - 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; + } - case SwitchSource.Device - when !string.IsNullOrEmpty(wireData.SourceDevice) - && _switchPlayer.SwitchDeviceExists(wireData.SourceDevice): { var deviceSwitch = _switchPlayer.Switch(wireData.SourceDevice, wireData.SourceDeviceItem); if (deviceSwitch != null) { deviceSwitch.AddWireDest(new WireDestConfig(wireData)); @@ -87,13 +100,6 @@ public void OnStart() } break; } - case SwitchSource.Device when string.IsNullOrEmpty(wireData.SourceDevice): - Logger.Warn($"Switch device not set for switch \"{wireData.Src}\"."); - break; - - case SwitchSource.Device when !_switchPlayer.SwitchDeviceExists(wireData.SourceDevice): - Logger.Warn($"Unknown switch device \"{wireData.SourceDevice}\" to wire to \"{wireData.Dst}\"."); - break; case SwitchSource.Constant: break; From 8f5e80a8783089388cf145372ecf410cdc2a2224 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 17 Jan 2021 23:03:33 +0100 Subject: [PATCH 13/23] gle: Make default gamelogic engine a component. --- .../Managers/Coil/CoilManager.cs | 6 ++--- .../Managers/Lamp/LampManager.cs | 6 ++--- .../Managers/Switch/SwitchManager.cs | 6 ++--- .../Managers/Wire/WireManager.cs | 2 +- .../VisualPinball.Unity.Editor/Utils/Icons.cs | 2 +- .../Game/DefaultGameEngineAuthoring.cs | 10 -------- .../Game/DefaultGameEngineAuthoring.cs.meta | 11 --------- .../Game/Engine/DefaultGamelogicEngine.cs | 7 ++++-- .../Engine/DefaultGamelogicEngine.cs.meta | 2 +- .../VisualPinball.Unity/Game/Player.cs | 4 ++-- .../Import/VpxConverter.cs | 6 ++--- .../VPT/IGameEngineAuthoring.cs | 24 ------------------- .../VPT/IGameEngineAuthoring.cs.meta | 11 --------- 13 files changed, 22 insertions(+), 75 deletions(-) delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/DefaultGameEngineAuthoring.cs delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/Game/DefaultGameEngineAuthoring.cs.meta delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/IGameEngineAuthoring.cs delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/VPT/IGameEngineAuthoring.cs.meta diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs index 06bb32933..7b2e3a11e 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs @@ -81,7 +81,7 @@ protected override bool SetupCompleted() return false; } - var gle = _tableAuthoring.gameObject.GetComponent(); + var gle = _tableAuthoring.gameObject.GetComponent(); if (gle == null) { @@ -206,8 +206,8 @@ private void RefreshCoilIds() private GamelogicEngineCoil[] GetAvailableEngineCoils() { - var gle = _tableAuthoring.gameObject.GetComponent(); - return gle == null ? new GamelogicEngineCoil[0] : ((IGamelogicEngineWithCoils) gle.GameEngine).AvailableCoils; + var gle = _tableAuthoring.gameObject.GetComponent(); + return gle == null ? new GamelogicEngineCoil[0] : ((IGamelogicEngineWithCoils) gle).AvailableCoils; } #endregion diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs index 4be254207..1dd162375 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs @@ -76,7 +76,7 @@ protected override bool SetupCompleted() return false; } - var gle = _tableAuthoring.gameObject.GetComponent(); + var gle = _tableAuthoring.gameObject.GetComponent(); if (gle == null) { DisplayMessage("No gamelogic engine set."); @@ -196,8 +196,8 @@ private void RefreshLampIds() private GamelogicEngineLamp[] GetAvailableEngineLamps() { - var gle = _tableAuthoring.gameObject.GetComponent(); - return gle == null ? new GamelogicEngineLamp[0] : ((IGamelogicEngineWithLamps)gle.GameEngine).AvailableLamps; + var gle = _tableAuthoring.gameObject.GetComponent(); + return gle == null ? new GamelogicEngineLamp[0] : ((IGamelogicEngineWithLamps)gle).AvailableLamps; } #endregion diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchManager.cs index ef449db20..40202e2e7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchManager.cs @@ -86,7 +86,7 @@ protected override bool SetupCompleted() return false; } - var gle = _tableAuthoring.gameObject.GetComponent(); + var gle = _tableAuthoring.gameObject.GetComponent(); if (gle == null) { @@ -216,8 +216,8 @@ private void RefreshSwitchIds() private GamelogicEngineSwitch[] GetAvailableEngineSwitches() { - var gle = _tableAuthoring.gameObject.GetComponent(); - return gle == null ? new GamelogicEngineSwitch[0] : ((IGamelogicEngineWithSwitches) gle.GameEngine).AvailableSwitches; + var gle = _tableAuthoring.gameObject.GetComponent(); + return gle == null ? new GamelogicEngineSwitch[0] : ((IGamelogicEngineWithSwitches) gle).AvailableSwitches; } #endregion diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireManager.cs index eec55e543..7b7ae5509 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Wire/WireManager.cs @@ -89,7 +89,7 @@ protected override bool SetupCompleted() return false; } - var gle = _tableAuthoring.gameObject.GetComponent(); + var gle = _tableAuthoring.gameObject.GetComponent(); if (gle == null) { diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs index bc5efe2b6..acf3613b0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs @@ -150,7 +150,7 @@ public static void DisableGizmoIcons() DisableGizmo(); DisableGizmo(); DisableGizmo(); - DisableGizmo(); + DisableGizmo(); DisableGizmo(); DisableGizmo(); DisableGizmo(); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DefaultGameEngineAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DefaultGameEngineAuthoring.cs deleted file mode 100644 index 130d3830a..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DefaultGameEngineAuthoring.cs +++ /dev/null @@ -1,10 +0,0 @@ -using UnityEngine; - -namespace VisualPinball.Unity -{ - [AddComponentMenu("Visual Pinball/Game Logic Engine/Default Game Logic")] - public class DefaultGameEngineAuthoring : MonoBehaviour, IGameEngineAuthoring - { - public IGamelogicEngine GameEngine => new DefaultGamelogicEngine(); - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DefaultGameEngineAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/DefaultGameEngineAuthoring.cs.meta deleted file mode 100644 index 5687a8b9d..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DefaultGameEngineAuthoring.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3d509f22d6cc7324b8de9bd704baeb75 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 5d2f099f5d6bcdd47ad8b5cd9f62e65c, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index 11c8189bb..d3de232ed 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -18,9 +18,11 @@ using System.Collections.Generic; using System.Diagnostics; using NLog; +using UnityEngine; using VisualPinball.Engine.Common; using VisualPinball.Engine.Game.Engines; using VisualPinball.Engine.VPT.Trough; +using Logger = NLog.Logger; namespace VisualPinball.Unity { @@ -29,8 +31,9 @@ namespace VisualPinball.Unity /// gamelogic engine. For now it just tries to find the flippers and hook /// them up to the switches. /// - [Serializable] - public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwitches, IGamelogicEngineWithCoils, IGamelogicEngineWithLamps + [AddComponentMenu("Visual Pinball/Game Logic Engine/Default Game Logic")] + public class DefaultGamelogicEngine : MonoBehaviour, IGamelogicEngine, + IGamelogicEngineWithSwitches, IGamelogicEngineWithCoils, IGamelogicEngineWithLamps { public string Name { get; } = "Default Game Engine"; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs.meta index 3975f1a82..75c23a296 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs.meta +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs.meta @@ -5,7 +5,7 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 5d2f099f5d6bcdd47ad8b5cd9f62e65c, type: 3} userData: assetBundleName: assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 38687131c..83d3f7030 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -90,14 +90,14 @@ public Player() private void Awake() { var tableComponent = gameObject.GetComponent(); - var engineComponent = GetComponent(); + var engineComponent = GetComponent(); Table = tableComponent.CreateTable(tableComponent.Data); BallManager = new BallManager(Table, TableToWorld); _inputManager = new InputManager(); if (engineComponent != null) { - GameEngine = engineComponent.GameEngine; + GameEngine = engineComponent; _lampPlayer.Awake(Table, GameEngine); _coilPlayer.Awake(Table, GameEngine, _lampPlayer); _switchPlayer.Awake(Table, GameEngine, _inputManager); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs index fc6c5f838..eeada5f8a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs @@ -101,7 +101,7 @@ public void Convert(string fileName, Table table, bool applyPatch = true, string // add the player script and default game engine go.AddComponent(); - var dga = go.AddComponent(); + var dga = go.AddComponent(); // add trough if none available if (!_table.HasTrough) { @@ -110,8 +110,8 @@ public void Convert(string fileName, Table table, bool applyPatch = true, string // populate mappings if (_table.Mappings.IsEmpty()) { - _table.Mappings.PopulateSwitches((dga.GameEngine as IGamelogicEngineWithSwitches).AvailableSwitches, table.Switchables, table.SwitchableDevices); - _table.Mappings.PopulateCoils((dga.GameEngine as IGamelogicEngineWithCoils).AvailableCoils, table.Coilables, table.CoilableDevices); + _table.Mappings.PopulateSwitches(((IGamelogicEngineWithSwitches)dga).AvailableSwitches, table.Switchables, table.SwitchableDevices); + _table.Mappings.PopulateCoils(((IGamelogicEngineWithCoils)dga).AvailableCoils, table.Coilables, table.CoilableDevices); } // don't need that anymore. diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IGameEngineAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IGameEngineAuthoring.cs deleted file mode 100644 index 1970b9e62..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IGameEngineAuthoring.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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 . - - -namespace VisualPinball.Unity -{ - public interface IGameEngineAuthoring - { - IGamelogicEngine GameEngine { get; } - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IGameEngineAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/IGameEngineAuthoring.cs.meta deleted file mode 100644 index f6b23cfec..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IGameEngineAuthoring.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e961a77fb54254a69b2968b8c3ab3532 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: From bffeb76a4028ff8913475efcdca578171a4c66e5 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 17 Jan 2021 23:33:10 +0100 Subject: [PATCH 14/23] lamps: Fix intensity. --- .../Game/DefaultGamelogicEngineInspector.cs | 58 +++++++++++++++++++ .../Game/Engine/DefaultGamelogicEngine.cs | 21 +++++++ .../VisualPinball.Unity/Game/LampPlayer.cs | 12 ++-- .../VisualPinball.Unity/VPT/IApi.cs | 9 +++ .../VisualPinball.Unity/VPT/Light/LightApi.cs | 2 +- .../VPT/Light/LightAuthoring.cs | 3 +- 6 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs new file mode 100644 index 000000000..d0241f2ce --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs @@ -0,0 +1,58 @@ +// 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 . + +using UnityEditor; +using UnityEngine; + +namespace VisualPinball.Unity.Editor +{ + + [CustomEditor(typeof(DefaultGamelogicEngine)), CanEditMultipleObjects] + public class DefaultGamelogicEngineInspector : BaseEditor + { + private DefaultGamelogicEngine _gamelogicEngine; + + private int _value1; + private int _value2; + private int _value3; + + private void OnEnable() + { + _gamelogicEngine = target as DefaultGamelogicEngine; + } + + public override void OnInspectorGUI() + { + _value1 = EditorGUILayout.IntSlider("Value 1", _value1, 0, 255); + _value2 = EditorGUILayout.IntSlider("Value 2", _value2, 0, 255); + _value3 = EditorGUILayout.IntSlider("Value 3", _value3, 0, 255); + + if (GUILayout.Button("Apply Each")) { + _gamelogicEngine.SetLamp("gi_1", _value1); + _gamelogicEngine.SetLamp("gi_2", _value2); + _gamelogicEngine.SetLamp("gi_3", _value3); + } + + if (GUILayout.Button("Apply At Once")) { + _gamelogicEngine.SetLamps(new LampEventArgs[] { + new LampEventArgs("gi_1", _value1), + new LampEventArgs("gi_2", _value2), + new LampEventArgs("gi_3", _value3), + }); + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index d3de232ed..74e7cfd59 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -31,6 +31,7 @@ namespace VisualPinball.Unity /// gamelogic engine. For now it just tries to find the flippers and hook /// them up to the switches. /// + [DisallowMultipleComponent] [AddComponentMenu("Visual Pinball/Game Logic Engine/Default Game Logic")] public class DefaultGamelogicEngine : MonoBehaviour, IGamelogicEngine, IGamelogicEngineWithSwitches, IGamelogicEngineWithCoils, IGamelogicEngineWithLamps @@ -137,6 +138,11 @@ public class DefaultGamelogicEngine : MonoBehaviour, IGamelogicEngine, private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + public DefaultGamelogicEngine() + { + Logger.Info("New Gamelogic engine instantiated."); + } + public void OnInit(TableApi tableApi, BallManager ballManager) { _tableApi = tableApi; @@ -239,6 +245,21 @@ public void Switch(string id, bool isClosed) } } + public void SetCoil(string n, bool value) + { + OnCoilChanged?.Invoke(this, new CoilEventArgs(n, value)); + } + + public void SetLamp(string n, int value) + { + OnLampChanged?.Invoke(this, new LampEventArgs(n, value)); + } + + public void SetLamps(LampEventArgs[] values) + { + OnLampsChanged?.Invoke(this, new LampsEventArgs(values)); + } + private void DebugPrintCoil(object sender, CoilEventArgs e) { Logger.Info("Coil {0} set to {1}.", e.Id, e.IsEnabled); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs index 33b83eaae..5dc80155b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs @@ -78,6 +78,11 @@ public void OnStart() } } + public void HandleLampEvent(LampEventArgs lampEvent) + { + HandleLampEvent(null, lampEvent); + } + private void AssignLampMapping(string id, MappingsLampData lampData) { if (!_lampAssignments.ContainsKey(id)) { @@ -87,8 +92,6 @@ private void AssignLampMapping(string id, MappingsLampData lampData) _lampMappings[id] = lampData; } - - private void HandleLampsEvent(object sender, LampsEventArgs lampsEvent) { var colors = new Dictionary(); @@ -118,11 +121,6 @@ private void HandleLampsEvent(object sender, LampsEventArgs lampsEvent) } } - public void HandleLampEvent(LampEventArgs lampEvent) - { - HandleLampEvent(null, lampEvent); - } - private void HandleLampEvent(object sender, LampEventArgs lampEvent) { HandleLampEvent(lampEvent, (lamp, mapping, itemName) => { diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index c1284d4d7..96f614cb2 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -102,7 +102,16 @@ internal interface IApiCoil : IApiWireDest internal interface IApiLamp : IApiWireDest { + /// + /// Sets the color of the light. + /// UnityEngine.Color Color { get; set; } + + /// + /// Sets the light intensity to a given value between 0 and 1. + /// + /// 0.0 = off, 1.0 = full intensity + /// If channel is , change intensity, otherwise update color. void OnLamp(float value, ColorChannel channel); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs index dd7f43e5b..47d506043 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs @@ -49,7 +49,7 @@ void IApiLamp.OnLamp(float value, ColorChannel channel) { switch (channel) { case ColorChannel.Alpha: { - Set(value == 0 ? LightStatus.LightStateOff : LightStatus.LightStateOn, value); + Set(value == 0.0f ? LightStatus.LightStateOff : LightStatus.LightStateOn, value); break; } case ColorChannel.Red: { diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs index 22142a96f..38a933670 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs @@ -75,6 +75,7 @@ private void Start() player.RegisterLamp(Item, gameObject); _unityLight = GetComponentInChildren(); _fullIntensity = _unityLight.intensity; + Debug.Log($"Full intensity of {name} is {_fullIntensity}."); } public void FadeTo(float seconds, float value) @@ -114,7 +115,7 @@ private IEnumerator Fade(float value) var counter = 0f; var a = _unityLight.intensity; - var b = value; + var b = _fullIntensity * value; var duration = a < b ? _data.FadeSpeedUp * (_fullIntensity - a) / _fullIntensity : _data.FadeSpeedDown * (1 - (_fullIntensity - a) / _fullIntensity); From 040f31f6ec759bff2c095ba173cdbb86befc885e Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 17 Jan 2021 23:56:45 +0100 Subject: [PATCH 15/23] lamp: Fix coil hook-up. --- .../Managers/ItemSearchableDropdown.cs | 2 +- .../Managers/Lamp/LampListViewItemRenderer.cs | 2 +- .../VisualPinball.Unity/Game/CoilPlayer.cs | 35 ++++++++++++------- .../VPT/Light/LightAuthoring.cs | 1 - 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ItemSearchableDropdown.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ItemSearchableDropdown.cs index 3d38362a6..0481f342f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ItemSearchableDropdown.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/ItemSearchableDropdown.cs @@ -50,7 +50,7 @@ protected override AdvancedDropdownItem BuildRoot() protected override void ItemSelected(AdvancedDropdownItem item) { var elementItem = (ElementDropdownItem) item; - _onElementSelected?.Invoke(elementItem.Item); + _onElementSelected?.Invoke(elementItem?.Item); } private class ElementDropdownItem : AdvancedDropdownItem where TItem : class, IIdentifiableItemAuthoring diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs index 9c7a7d150..577f34399 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs @@ -195,7 +195,7 @@ private void RenderPlayfieldElement(TableAuthoring tableAuthoring, LampListData tableAuthoring, "Lamp Items", item => { - lampListData.PlayfieldItem = item.Name; + lampListData.PlayfieldItem = item?.Name ?? string.Empty; updateAction(lampListData); } ); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs index a3c0341bc..d19b3ffd0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CoilPlayer.cs @@ -111,29 +111,40 @@ private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) { if (_coilAssignments.ContainsKey(coilEvent.Id)) { foreach (var destConfig in _coilAssignments[coilEvent.Id]) { - if (destConfig.DeviceName != null && _coilDevices.ContainsKey(destConfig.DeviceName)) { - if (_coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName) != null) { - _coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName).OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); - } else { + if (destConfig.IsLampCoil) { + _lampPlayer.HandleLampEvent(new LampEventArgs(coilEvent.Id, coilEvent.IsEnabled ? 1 : 0, LampSource.Coils)); + continue; + } + + // device coil? + if (destConfig.DeviceName != null) { + + // check device + if (!_coilDevices.ContainsKey(destConfig.DeviceName)) { + Logger.Error($"Cannot trigger coil on non-existing device \"{destConfig.DeviceName}\" for {coilEvent.Id}."); + continue; + } + + // check coil in device + var coil = _coilDevices[destConfig.DeviceName].Coil(destConfig.ItemName); + if (coil == null) { Logger.Error($"Cannot trigger non-existing coil \"{destConfig.ItemName}\" in coil device \"{destConfig.DeviceName}\" for {coilEvent.Id}."); + continue; } - } else if (_coils.ContainsKey(destConfig.ItemName)) { - if (destConfig.IsLampCoil) { - _lampPlayer.HandleLampEvent(new LampEventArgs(coilEvent.Id, coilEvent.IsEnabled ? 1 : 0, LampSource.Coils)); + coil.OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); - } else { - _coils[destConfig.ItemName].OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); - } + } else if (_coils.ContainsKey(destConfig.ItemName)) { + _coils[destConfig.ItemName].OnCoil(coilEvent.IsEnabled, destConfig.IsHoldCoil); } else { - Logger.Error($"Cannot trigger unknown coil item {destConfig.ItemName}."); + Logger.Error($"Cannot trigger unknown coil item \"{destConfig.ItemName}\" for {coilEvent.Id}."); } } } else { - Logger.Info($"Ignoring unassigned coil {coilEvent.Id}."); + Logger.Info($"Ignoring unassigned coil \"{coilEvent.Id}\"."); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs index 38a933670..1702836ea 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs @@ -75,7 +75,6 @@ private void Start() player.RegisterLamp(Item, gameObject); _unityLight = GetComponentInChildren(); _fullIntensity = _unityLight.intensity; - Debug.Log($"Full intensity of {name} is {_fullIntensity}."); } public void FadeTo(float seconds, float value) From 24268a8cea7cde2be38da2e97e1ddb593cdcda74 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 18 Jan 2021 00:12:25 +0100 Subject: [PATCH 16/23] coils: Remove lamp entry when removing lamp coil. --- .../Managers/Coil/CoilManager.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs index 7b2e3a11e..3e860d9f3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs @@ -16,10 +16,12 @@ using System; using System.Collections.Generic; +using System.Linq; using NLog; using UnityEditor; using UnityEngine; using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Mappings; using Logger = NLog.Logger; using Object = UnityEngine.Object; @@ -148,6 +150,15 @@ protected override void RemoveData(string undoName, CoilListData data) RecordUndo(undoName); _tableAuthoring.Mappings.RemoveCoil(data.MappingsCoilData); + + // if it's a lamp, also delete the lamp entry. + if (data.MappingsCoilData.Destination == CoilDestination.Lamp) { + var lampEntry = _tableAuthoring.Mappings.Lamps.FirstOrDefault(l => l.Id == data.Id && l.Source == LampSource.Coils); + if (lampEntry != null) { + _tableAuthoring.Mappings.RemoveLamp(lampEntry); + GetWindow().Reload(); + } + } } protected override void CloneData(string undoName, string newName, CoilListData data) From 4f13aca02c87a4cd37d0e1c497634bcb12b12510 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 18 Jan 2021 00:39:16 +0100 Subject: [PATCH 17/23] lamps: Add support for single-wire RGB lamps. --- VisualPinball.Engine/VPT/Enums.cs | 3 +- VisualPinball.Engine/VPT/Mappings/Mappings.cs | 2 +- .../creators-guide/editor/lamp-manager.md | 8 ++--- .../Game/DefaultGamelogicEngineInspector.cs | 7 ++++ .../Managers/Lamp/LampListViewItemRenderer.cs | 8 +++-- .../Game/Engine/DefaultGamelogicEngine.cs | 6 ++++ .../Game/Engine/IGamelogicEngineWithLamps.cs | 25 ++++++++++++++ .../VisualPinball.Unity/Game/LampPlayer.cs | 33 +++++++++++++++++-- .../VisualPinball.Unity/VPT/IApi.cs | 6 ++++ .../VisualPinball.Unity/VPT/Light/LightApi.cs | 5 +++ 10 files changed, 92 insertions(+), 11 deletions(-) diff --git a/VisualPinball.Engine/VPT/Enums.cs b/VisualPinball.Engine/VPT/Enums.cs index c4e36f7fa..64a595360 100644 --- a/VisualPinball.Engine/VPT/Enums.cs +++ b/VisualPinball.Engine/VPT/Enums.cs @@ -208,6 +208,7 @@ public static class LampType { public const int SingleOnOff = 0; public const int SingleFading = 1; - public const int Rgb = 2; + public const int RgbMulti = 2; + public const int Rgb = 3; } } diff --git a/VisualPinball.Engine/VPT/Mappings/Mappings.cs b/VisualPinball.Engine/VPT/Mappings/Mappings.cs index 94a6f7be4..78ecafe4c 100644 --- a/VisualPinball.Engine/VPT/Mappings/Mappings.cs +++ b/VisualPinball.Engine/VPT/Mappings/Mappings.cs @@ -381,7 +381,7 @@ public void PopulateLamps(GamelogicEngineLamp[] engineLamps, IEnumerable Lamp Manager*. +In order to link each of the playfield lights to the gamelogic engine and configure how they react during gameplay, the *Lamp Manager* is used. You can find it under *Visual Pinball -> Lamp Manager*. [TODO: Screenshot] @@ -20,9 +20,9 @@ Physical machines have a bunch of different concepts when it comes to lighting. Later machines used single colored **LEDs** that were each directly connected to the controller board (see also: [Lights vs LEDs](https://docs.missionpinball.org/en/latest/mechs/lights/lights_versus_leds.html)). Contrarily to matrix lamps, the intensity here could be set more fine grained by the game software. -More recently, games started using **RGB-LEDs** that are also able to change the color during gameplay. In VPE, they can be handled in two different ways: -- As three single inputs from the gamelogic engine (e.g. that's what PinMAME provides) -- With a single RGB input, where the gamelogic engine always provides the full color (e.g. MPF, or custom table logic) +More recently, games started using **RGB-LEDs** that are additionally able to change the color during gameplay. In VPE, these can be handled in two different ways: +- As three single connections from the gamelogic engine (e.g. that's what PinMAME provides) +- With a single RGB connection, where the gamelogic engine always provides the full color (e.g. MPF, or custom table logic) Additionally, most pinball machines come with **GI strips**, which are a set of bulbs used for global illumination of the playfield. All lights from a strip are addressed at once, so one gamelogic GI strip maps to multiple lamps on the playfield. diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs index d0241f2ce..dd59b474d 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs @@ -29,6 +29,8 @@ public class DefaultGamelogicEngineInspector : BaseEditor OnCoilChanged; public event EventHandler OnLampChanged; public event EventHandler OnLampsChanged; + public event EventHandler OnLampColorChanged; private const string SwLeftFlipper = "s_left_flipper"; private const string SwLeftFlipperEos = "s_left_flipper_eos"; @@ -255,6 +256,11 @@ public void SetLamp(string n, int value) OnLampChanged?.Invoke(this, new LampEventArgs(n, value)); } + public void SetLampColor(string n, Color color) + { + OnLampColorChanged?.Invoke(this, new LampColorEventArgs(n, color)); + } + public void SetLamps(LampEventArgs[] values) { OnLampsChanged?.Invoke(this, new LampsEventArgs(values)); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs index 0fb08cfed..f63d5c181 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngineWithLamps.cs @@ -15,6 +15,7 @@ // along with this program. If not, see . using System; +using UnityEngine; using VisualPinball.Engine.Game.Engines; using VisualPinball.Engine.VPT; @@ -47,6 +48,11 @@ public interface IGamelogicEngineWithLamps /// at once instead of each channel individually. /// event EventHandler OnLampsChanged; + + /// + /// Triggered when the an RGB lamp changes color. + /// + event EventHandler OnLampColorChanged; } public readonly struct LampEventArgs @@ -82,6 +88,25 @@ public LampEventArgs(string id, int value, int source) } } + public readonly struct LampColorEventArgs + { + /// + /// Id of the lamp, as defined by . + /// + public readonly string Id; + + /// + /// New color + /// + public readonly Color Color; + + public LampColorEventArgs(string id, Color color) + { + Id = id; + Color = color; + } + } + public readonly struct LampsEventArgs { public readonly LampEventArgs[] LampsChanged; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs index 5dc80155b..3d973ebff 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs @@ -74,6 +74,7 @@ public void OnStart() if (_lampAssignments.Count > 0) { gamelogicEngineWithLamps.OnLampChanged += HandleLampEvent; gamelogicEngineWithLamps.OnLampsChanged += HandleLampsEvent; + gamelogicEngineWithLamps.OnLampColorChanged += HandleLampColorEvent; } } } @@ -92,6 +93,33 @@ private void AssignLampMapping(string id, MappingsLampData lampData) _lampMappings[id] = lampData; } + private void HandleLampColorEvent(object sender, LampColorEventArgs lampEvent) + { + if (_lampAssignments.ContainsKey(lampEvent.Id)) { + var mapping = _lampMappings[lampEvent.Id]; + foreach (var itemName in _lampAssignments[lampEvent.Id]) { + if (_lamps.ContainsKey(itemName)) { + var lamp = _lamps[itemName]; + switch (mapping.Type) { + case LampType.Rgb: + lamp.OnLampColor(lampEvent.Color); + break; + + default: + Logger.Error($"Received an RGB event for lamp {itemName} but lamp mapping type is {mapping.Type} ({mapping.Id})"); + break; + } + + } else { + Logger.Error($"Cannot trigger unknown lamp {itemName}."); + } + } + + } else { + Logger.Error($"Should update unassigned lamp {lampEvent.Id}."); + } + } + private void HandleLampsEvent(object sender, LampsEventArgs lampsEvent) { var colors = new Dictionary(); @@ -156,11 +184,12 @@ private void HandleLampEvent(LampEventArgs lampEvent, Action 0 ? 1f : 0f, ColorChannel.Alpha); break; + case LampType.Rgb: case LampType.SingleFading: lamp.OnLamp(lampEvent.Value / 255f, ColorChannel.Alpha); break; - case LampType.Rgb: + case LampType.RgbMulti: handleRgb(lamp, mapping, itemName); break; @@ -179,10 +208,10 @@ private void HandleLampEvent(LampEventArgs lampEvent, Action 0 && _gamelogicEngine is IGamelogicEngineWithLamps gamelogicEngineWithLamps) { + gamelogicEngineWithLamps.OnLampColorChanged -= HandleLampColorEvent; gamelogicEngineWithLamps.OnLampChanged -= HandleLampEvent; gamelogicEngineWithLamps.OnLampsChanged -= HandleLampsEvent; } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index 96f614cb2..24f5724be 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -113,6 +113,12 @@ internal interface IApiLamp : IApiWireDest /// 0.0 = off, 1.0 = full intensity /// If channel is , change intensity, otherwise update color. void OnLamp(float value, ColorChannel channel); + + /// + /// Sets the light color of the lamp. + /// + /// New color to set + void OnLampColor(UnityEngine.Color color); } internal interface IApiWireDest diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs index 47d506043..677846720 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightApi.cs @@ -75,6 +75,11 @@ void IApiLamp.OnLamp(float value, ColorChannel channel) } } + void IApiLamp.OnLampColor(Color color) + { + _lightAuthoring.Color = color; + } + internal LightApi(Light item, GameObject go, Player player) : base(item, player) { _lightAuthoring = go.GetComponentInChildren(); From d749283c70d4186ae78d3dec81256b8e858806e4 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 18 Jan 2021 01:20:08 +0100 Subject: [PATCH 18/23] player: Split into multiple classes. --- .../creators-guide/editor/lamp-manager.md | 32 +++++++++++-- .../DefaultGamelogicEngineInspector.cs.meta | 11 +++++ .../Managers/Coil/CoilListViewItemRenderer.cs | 29 ++++++++++-- .../Managers/Lamp/LampListData.cs | 2 +- .../Managers/Lamp/LampListViewItemRenderer.cs | 43 +++++++++++++---- .../Managers/Lamp/LampManager.cs | 2 +- .../Switch/SwitchListViewItemRenderer.cs | 33 +++++++++---- .../VisualPinball.Unity/Game/CoilPlayer.cs | 11 +++++ .../VisualPinball.Unity/Game/DeviceSwitch.cs | 1 + .../Game/Engine/DefaultGamelogicEngine.cs | 35 +++++++------- .../Game/Engine/IGamelogicEngine.cs | 2 +- .../VisualPinball.Unity/Game/LampPlayer.cs | 47 ++++++++++++------- .../VisualPinball.Unity/Game/Player.cs | 9 +++- .../VisualPinball.Unity/Game/SwitchHandler.cs | 23 ++++++--- .../VisualPinball.Unity/Game/SwitchPlayer.cs | 41 +++++++++++++--- .../VisualPinballSimulationSystemGroup.cs | 28 +++++------ .../VPT/Bumper/BumperApi.cs | 1 + .../VPT/Flipper/FlipperApi.cs | 1 + .../VisualPinball.Unity/VPT/Gate/GateApi.cs | 1 + .../VPT/HitTarget/HitTargetApi.cs | 1 + .../VisualPinball.Unity/VPT/IApi.cs | 7 ++- .../VisualPinball.Unity/VPT/ItemApi.cs | 2 + .../VPT/Kicker/KickerApi.cs | 1 + .../VPT/Spinner/SpinnerApi.cs | 1 + .../VPT/Trigger/TriggerApi.cs | 1 + 25 files changed, 272 insertions(+), 93 deletions(-) create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs.meta diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md index 0fb32f32d..31e9a1529 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md @@ -28,11 +28,37 @@ Additionally, most pinball machines come with **GI strips**, which are a set of Finally, high-powered lamps such as flashers might appear under the gamelogic engine's **coil outputs**, since those lamps operate with the same voltage and have the same properties as coils. -[TBD how to handle solenoids] - ## Setup -[TODO] +Every row in the lamp manager corresponds to a logical connection between the gamelogic engine and the lamp on the playfield. A lamp can be linked to multiple outputs, and an output can be linked to multiple lamps. + +### IDs + +The first column, **ID** shows the name that the gamelogic engine exports for each lamp. + +> [!note] +> As we cannot be 100% sure that the gamelogic engine has accurate data about the lamp names, you can also add lamp IDs manually, but that should be the exception. + +### Description + +The **Description** column is optional. If you're setting up a re-creation, you would typically use this for the lamp name from the game manual. It's purely for your own benefit, and you can keep this empty if you want. + +### Destination + +The **Destination** defines where the lamp is located. Currently, *Playfield* is the only option. + +### Element + +Under the **Element** column, you choose which lamp among the game items on the playfield should be controlled. + +### Type + +The **Type** column defines how the signal is interpreted by the lamp. This is important, because the gamelogic engine typically sends integer values to the lamp. There are four types: + +- *Single On|Off* - Typically lamps from the lamp matrix. They can only be on or off. Receiving `0` will turn the lamp off, any other value will tell it on. +- *Single Fading* - Individually connected lamps that can be dimmed by the gamelogic engine. Received values can be `0` to `255`, where `0` turns the lamp off, and `255` sets it to full intensity. +- *RGB Multi* - An RGB lamp that can change its color during gameplay. Lamps of this type received three inputs from red, green and blue, which can be set in the next column. +- *RGB* - An RGB lamp that receives its data from a single channel. This is the only mode where the lamp doesn't receive an integer, but an entire color. ## Inserts diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs.meta new file mode 100644 index 000000000..6a7813f51 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Game/DefaultGamelogicEngineInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf71671eddfe1234aa2dd13a4c772c11 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs index d01aec8a7..1428b8fc3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs @@ -58,10 +58,15 @@ public CoilListViewItemRenderer(TableAuthoring tableAuthoring, List updateAction) { + EditorGUI.BeginDisabledGroup(Application.isPlaying); + var coilStatuses = Application.isPlaying + ? tableAuthoring.gameObject.GetComponent()?.CoilStatuses + : null; + switch ((CoilListColumn)column) { case CoilListColumn.Id: - RenderId(ref data.Id, id => UpdateId(data, id), data, cellRect, updateAction); + RenderId(coilStatuses, ref data.Id, id => UpdateId(data, id), data, cellRect, updateAction); break; case CoilListColumn.Description: RenderDescription(data, cellRect, updateAction); @@ -77,10 +82,11 @@ public void Render(TableAuthoring tableAuthoring, CoilListData data, Rect cellRe break; case CoilListColumn.HoldCoilId: if (data.Type == CoilType.DualWound) { - RenderId(ref data.HoldCoilId, id => data.HoldCoilId = id, data, cellRect, updateAction); + RenderId(coilStatuses, ref data.HoldCoilId, id => data.HoldCoilId = id, data, cellRect, updateAction); } break; } + EditorGUI.EndDisabledGroup(); } private void UpdateId(CoilListData data, string id) @@ -95,20 +101,33 @@ private void UpdateId(CoilListData data, string id) data.Id = id; } - private void RenderId(ref string id, Action setId, CoilListData coilListData, Rect cellRect, Action updateAction) + private void RenderId(Dictionary coilStatuses, ref string id, Action setId, CoilListData coilListData, Rect cellRect, Action updateAction) { // add some padding cellRect.x += 2; cellRect.width -= 4; var options = new List(_gleCoils.Select(entry => entry.Id).ToArray()); - if (options.Count > 0) { options.Add(""); } - options.Add("Add..."); + if (Application.isPlaying && coilStatuses != null) { + var iconRect = cellRect; + iconRect.width = 20; + cellRect.x += 25; + cellRect.width -= 25; + if (coilStatuses.ContainsKey(id)) { + var coilStatus = coilStatuses[id]; + var icon = Icons.Bolt(IconSize.Small, coilStatus ? IconColor.Orange : IconColor.Gray); + var guiColor = GUI.color; + GUI.color = Color.clear; + EditorGUI.DrawTextureTransparent(iconRect, icon, ScaleMode.ScaleToFit); + GUI.color = guiColor; + } + } + EditorGUI.BeginChangeCheck(); var index = EditorGUI.Popup(cellRect, options.IndexOf(id), options.ToArray()); if (EditorGUI.EndChangeCheck()) { diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs index 402b7ef03..02a192600 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListData.cs @@ -35,7 +35,7 @@ public class LampListData : IManagerListData [ManagerListColumn(Order = 4, HeaderName = "Type", Width = 110)] public int Type; - [ManagerListColumn(Order = 5, HeaderName = "R G B", Width = 135)] + [ManagerListColumn(Order = 5, HeaderName = "R G B", Width = 300)] public string Green; public string Blue; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs index 3ec31a9c9..431583ac3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampListViewItemRenderer.cs @@ -53,12 +53,17 @@ public LampListViewItemRenderer(List gleLamps, Dictionary updateAction) { + EditorGUI.BeginDisabledGroup(Application.isPlaying); + var lampStatuses = Application.isPlaying + ? tableAuthoring.gameObject.GetComponent()?.LampStatuses + : null; + switch ((LampListColumn)column) { case LampListColumn.Id: if (data.Source == LampSource.Coils) { - RenderCoilId(data, cellRect); + RenderCoilId(lampStatuses, data, cellRect); } else { - RenderId(ref data.Id, id => data.Id = id, data, cellRect, updateAction); + RenderId(lampStatuses, ref data.Id, id => data.Id = id, data, cellRect, updateAction); } break; case LampListColumn.Description: @@ -76,20 +81,23 @@ public void Render(TableAuthoring tableAuthoring, LampListData data, Rect cellRe case LampListColumn.Color: switch (data.Type) { case LampType.RgbMulti: - RenderRgb(data, cellRect, updateAction); + RenderRgb(lampStatuses, data, cellRect, updateAction); break; } break; } + EditorGUI.EndDisabledGroup(); } - private void RenderCoilId(LampListData lampListData, Rect cellRect) + private void RenderCoilId(Dictionary lampStatuses, LampListData lampListData, Rect cellRect) { // add some padding cellRect.x += 2; cellRect.width -= 4; - var icon = Icons.Coil(IconSize.Small); + + var statusAvail = Application.isPlaying && lampStatuses != null && lampStatuses.ContainsKey(lampListData.Id); + var icon = Icons.Coil(IconSize.Small, statusAvail && lampStatuses[lampListData.Id] > 0 ? IconColor.Orange : IconColor.Gray); if (icon != null) { var iconRect = cellRect; iconRect.width = 20; @@ -104,7 +112,7 @@ private void RenderCoilId(LampListData lampListData, Rect cellRect) EditorGUI.LabelField(cellRect, lampListData.Id); } - private void RenderId(ref string id, Action setId, LampListData lampListData, Rect cellRect, Action updateAction) + private void RenderId(IReadOnlyDictionary lampStatuses, ref string id, Action setId, LampListData lampListData, Rect cellRect, Action updateAction) { // add some padding cellRect.x += 2; @@ -117,6 +125,21 @@ private void RenderId(ref string id, Action setId, LampListData lampList } options.Add("Add..."); + if (Application.isPlaying && lampStatuses != null) { + var iconRect = cellRect; + iconRect.width = 20; + cellRect.x += 25; + cellRect.width -= 25; + if (lampStatuses.ContainsKey(id)) { + var lampStatus = lampStatuses[id]; + var icon = Icons.Light(IconSize.Small, lampStatus > 0 ? IconColor.Orange : IconColor.Gray); + var guiColor = GUI.color; + GUI.color = Color.clear; + EditorGUI.DrawTextureTransparent(iconRect, icon, ScaleMode.ScaleToFit); + GUI.color = guiColor; + } + } + EditorGUI.BeginChangeCheck(); var index = EditorGUI.Popup(cellRect, options.IndexOf(id), options.ToArray()); if (EditorGUI.EndChangeCheck()) { @@ -215,17 +238,17 @@ private void RenderType(LampListData lampListData, Rect cellRect, Action updateAction) + private void RenderRgb(IReadOnlyDictionary lampStatuses, LampListData data, Rect cellRect, Action updateAction) { var pad = 2; var width = cellRect.width / 3; var c = cellRect; c.width = width - pad; - RenderId(ref data.Id, id => data.Id = id, data, c, updateAction); + RenderId(lampStatuses, ref data.Id, id => data.Id = id, data, c, updateAction); c.x += width + pad; - RenderId(ref data.Green, id => data.Green = id, data, c, updateAction); + RenderId(lampStatuses, ref data.Green, id => data.Green = id, data, c, updateAction); c.x += width + pad; - RenderId(ref data.Blue, id => data.Blue = id, data, c, updateAction); + RenderId(lampStatuses, ref data.Blue, id => data.Blue = id, data, c, updateAction); } private UnityEngine.Texture GetIcon(LampListData lampListData) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs index 1dd162375..f105e581b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Lamp/LampManager.cs @@ -29,7 +29,7 @@ namespace VisualPinball.Unity.Editor /// Editor UI for VPE lamps /// /// - class LampManager : ManagerWindow + internal class LampManager : ManagerWindow { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs index 0d77e3f80..0d89af1c8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs @@ -22,6 +22,8 @@ using VisualPinball.Engine.VPT; using System.Linq; using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.Math; +using Color = UnityEngine.Color; namespace VisualPinball.Unity.Editor { @@ -62,10 +64,14 @@ public SwitchListViewItemRenderer(List gleSwitches, Dicti public void Render(TableAuthoring tableAuthoring, SwitchListData data, Rect cellRect, int column, Action updateAction) { + EditorGUI.BeginDisabledGroup(Application.isPlaying); + var switchStatuses = Application.isPlaying + ? tableAuthoring.gameObject.GetComponent()?.SwitchStatuses + : null; switch ((SwitchListColumn)column) { case SwitchListColumn.Id: - RenderId(data, cellRect, updateAction); + RenderId(switchStatuses, data, cellRect, updateAction); break; case SwitchListColumn.Description: RenderDescription(data, cellRect, updateAction); @@ -80,23 +86,36 @@ public void Render(TableAuthoring tableAuthoring, SwitchListData data, Rect cell RenderPulseDelay(data, cellRect, updateAction); break; } + EditorGUI.EndDisabledGroup(); } - private void RenderId(SwitchListData switchListData, Rect cellRect, Action updateAction) + private void RenderId(Dictionary switchStatuses, SwitchListData switchListData, Rect cellRect, Action updateAction) { // add some padding cellRect.x += 2; cellRect.width -= 4; var options = new List(_gleSwitches.Select(entry => entry.Id).ToArray()); - - if (options.Count > 0) - { + if (options.Count > 0) { options.Add(""); } - options.Add("Add..."); + if (Application.isPlaying && switchStatuses != null) { + var iconRect = cellRect; + iconRect.width = 20; + cellRect.x += 25; + cellRect.width -= 25; + if (switchStatuses.ContainsKey(switchListData.Id)) { + var switchStatus = switchStatuses[switchListData.Id]; + var icon = Icons.Switch(switchStatus, IconSize.Small, switchStatus ? IconColor.Orange : IconColor.Gray); + var guiColor = GUI.color; + GUI.color = Color.clear; + EditorGUI.DrawTextureTransparent(iconRect, icon, ScaleMode.ScaleToFit); + GUI.color = guiColor; + } + } + EditorGUI.BeginChangeCheck(); var index = EditorGUI.Popup(cellRect, options.IndexOf(switchListData.Id), options.ToArray()); if (EditorGUI.EndChangeCheck()) @@ -114,14 +133,12 @@ private void RenderId(SwitchListData switchListData, Rect cellRect, Action CoilStatuses { get; } = new Dictionary(); internal void RegisterCoil(IItem item, IApiCoil coilApi) => _coils[item.Name] = coilApi; internal void RegisterCoilDevice(IItem item, IApiCoilDevice coilDeviceApi) => _coilDevices[item.Name] = coilDeviceApi; @@ -105,11 +108,15 @@ private void AssignCoilMapping(string id, string playfieldItem, bool isHoldCoil _coilAssignments[id] = new List(); } _coilAssignments[id].Add(new CoilDestConfig(playfieldItem, isHoldCoil, isLampCoil, deviceName)); + CoilStatuses[id] = false; } private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) { if (_coilAssignments.ContainsKey(coilEvent.Id)) { + CoilStatuses[coilEvent.Id] = coilEvent.IsEnabled; + Debug.LogWarning($"Setting coil {coilEvent.Id} to {coilEvent.IsEnabled}."); + foreach (var destConfig in _coilAssignments[coilEvent.Id]) { if (destConfig.IsLampCoil) { @@ -143,6 +150,10 @@ private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) } } +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif + } else { Logger.Info($"Ignoring unassigned coil \"{coilEvent.Id}\"."); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs index 1da08c7fe..9e75b5928 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -34,6 +34,7 @@ public class DeviceSwitch : IApiSwitch /// Indicates whether the switch is currently opened or closed. /// public bool IsClosed => _switchHandler.IsClosed; + public bool IsSwitchClosed => _switchHandler.IsClosed; /// /// Indicates whether the switch is currently enabled. diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs index b9c74178b..ae6a02e26 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -131,11 +131,10 @@ public class DefaultGamelogicEngine : MonoBehaviour, IGamelogicEngine, new GamelogicEngineLamp { Id = LampRedBumper, Description = "Red Bumper", PlayfieldItemHint = "^b1l2$" } }; - private TableApi _tableApi; + private Player _player; private BallManager _ballManager; - private Dictionary _switchStatus = new Dictionary(); - private Dictionary _switchTime = new Dictionary(); + private readonly Dictionary _switchTime = new Dictionary(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -144,23 +143,17 @@ public DefaultGamelogicEngine() Logger.Info("New Gamelogic engine instantiated."); } - public void OnInit(TableApi tableApi, BallManager ballManager) + public void OnInit(Player player, TableApi tableApi, BallManager ballManager) { - _tableApi = tableApi; + _player = player; _ballManager = ballManager; // debug print stuff OnCoilChanged += DebugPrintCoil; - _switchStatus[SwLeftFlipper] = false; - _switchStatus[SwLeftFlipperEos] = false; - _switchStatus[SwRightFlipper] = false; - _switchStatus[SwRightFlipperEos] = false; - _switchStatus[SwPlunger] = false; - _switchStatus[SwCreateBall] = false; - // eject ball onto playfield OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); + _player.ScheduleAction(100, () => OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, false))); } public void OnUpdate() @@ -174,7 +167,6 @@ public void OnDestroy() public void Switch(string id, bool isClosed) { - _switchStatus[id] = isClosed; if (!_switchTime.ContainsKey(id)) { _switchTime[id] = new Stopwatch(); } @@ -194,7 +186,7 @@ public void Switch(string id, bool isClosed) } else { OnCoilChanged?.Invoke(this, - _switchStatus[SwLeftFlipperEos] + _player.SwitchStatuses[SwLeftFlipperEos] ? new CoilEventArgs(CoilLeftFlipperHold, false) : new CoilEventArgs(CoilLeftFlipperMain, false) ); @@ -202,8 +194,10 @@ public void Switch(string id, bool isClosed) break; case SwLeftFlipperEos: - OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilLeftFlipperMain, false)); - OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilLeftFlipperHold, true)); + if (isClosed) { + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilLeftFlipperMain, false)); + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilLeftFlipperHold, true)); + } break; case SwRightFlipper: @@ -211,7 +205,7 @@ public void Switch(string id, bool isClosed) OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilRightFlipperMain, true)); } else { OnCoilChanged?.Invoke(this, - _switchStatus[SwRightFlipperEos] + _player.SwitchStatuses[SwRightFlipperEos] ? new CoilEventArgs(CoilRightFlipperHold, false) : new CoilEventArgs(CoilRightFlipperMain, false) ); @@ -219,8 +213,10 @@ public void Switch(string id, bool isClosed) break; case SwRightFlipperEos: - OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilRightFlipperMain, false)); - OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilRightFlipperHold, true)); + if (isClosed) { + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilRightFlipperMain, false)); + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilRightFlipperHold, true)); + } break; case SwPlunger: @@ -230,6 +226,7 @@ public void Switch(string id, bool isClosed) case SwTrough4: if (isClosed) { OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); + _player.ScheduleAction(100, () => OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, false))); } break; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngine.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngine.cs index 4eac6381f..72224d1dc 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/IGamelogicEngine.cs @@ -25,7 +25,7 @@ public interface IGamelogicEngine { string Name { get; } - void OnInit(TableApi tableApi, BallManager ballManager); + void OnInit(Player player, TableApi tableApi, BallManager ballManager); void OnUpdate(); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs index 3d973ebff..2c06a6ade 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/LampPlayer.cs @@ -30,13 +30,14 @@ public class LampPlayer { private readonly Dictionary _lamps = new Dictionary(); private readonly Dictionary> _lampAssignments = new Dictionary>(); - private readonly Dictionary _lampMappings = new Dictionary(); + private readonly Dictionary> _lampMappings = new Dictionary>(); private Table _table; private IGamelogicEngine _gamelogicEngine; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + internal Dictionary LampStatuses { get; } = new Dictionary(); internal void RegisterLamp(IItem item, IApiLamp lampApi) => _lamps[item.Name] = lampApi; public void Awake(Table table, IGamelogicEngine gamelogicEngine) @@ -61,12 +62,16 @@ public void OnStart() } AssignLampMapping(lampData.Id, lampData); - if (!string.IsNullOrEmpty(lampData.Green)) { - AssignLampMapping(lampData.Green, lampData); - } - if (!string.IsNullOrEmpty(lampData.Blue)) { - AssignLampMapping(lampData.Blue, lampData); + + if (lampData.Type == LampType.RgbMulti) { + if (!string.IsNullOrEmpty(lampData.Green)) { + AssignLampMapping(lampData.Green, lampData); + } + if (!string.IsNullOrEmpty(lampData.Blue)) { + AssignLampMapping(lampData.Blue, lampData); + } } + break; } } @@ -88,16 +93,18 @@ private void AssignLampMapping(string id, MappingsLampData lampData) { if (!_lampAssignments.ContainsKey(id)) { _lampAssignments[id] = new List(); + _lampMappings[id] = new Dictionary(); } _lampAssignments[id].Add(lampData.PlayfieldItem); - _lampMappings[id] = lampData; + _lampMappings[id][lampData.PlayfieldItem] = lampData; + LampStatuses[id] = 0f; } private void HandleLampColorEvent(object sender, LampColorEventArgs lampEvent) { if (_lampAssignments.ContainsKey(lampEvent.Id)) { - var mapping = _lampMappings[lampEvent.Id]; foreach (var itemName in _lampAssignments[lampEvent.Id]) { + var mapping = _lampMappings[lampEvent.Id][itemName]; if (_lamps.ContainsKey(itemName)) { var lamp = _lamps[itemName]; switch (mapping.Type) { @@ -146,6 +153,7 @@ private void HandleLampsEvent(object sender, LampsEventArgs lampsEvent) foreach (var mappingId in colors.Keys) { lamps[mappingId].Color = colors[mappingId]; + LampStatuses[mappingId] = colors[mappingId].grayscale; } } @@ -164,14 +172,15 @@ private void HandleLampEvent(object sender, LampEventArgs lampEvent) } else { Logger.Error($"Cannot assign lamp {lampEvent.Id} to an RGB value of light {itemName}"); } + LampStatuses[lampEvent.Id] = lamp.Color.grayscale; }); } private void HandleLampEvent(LampEventArgs lampEvent, Action handleRgb) { if (_lampAssignments.ContainsKey(lampEvent.Id)) { - var mapping = _lampMappings[lampEvent.Id]; foreach (var itemName in _lampAssignments[lampEvent.Id]) { + var mapping = _lampMappings[lampEvent.Id][itemName]; if (mapping.Source != lampEvent.Source) { // so, if we have a coil here that happens to have the same name as a lamp, // skip if the source isn't the same. @@ -180,14 +189,20 @@ private void HandleLampEvent(LampEventArgs lampEvent, Action 0 ? 1f : 0f, ColorChannel.Alpha); + case LampType.SingleOnOff: { + var value = lampEvent.Value > 0 ? 1f : 0f; + lamp.OnLamp(value, ColorChannel.Alpha); + LampStatuses[lampEvent.Id] = value; break; + } case LampType.Rgb: - case LampType.SingleFading: - lamp.OnLamp(lampEvent.Value / 255f, ColorChannel.Alpha); + case LampType.SingleFading: { + var value = lampEvent.Value / 255f; + lamp.OnLamp(value, ColorChannel.Alpha); + LampStatuses[lampEvent.Id] = value; break; + } case LampType.RgbMulti: handleRgb(lamp, mapping, itemName); @@ -202,9 +217,9 @@ private void HandleLampEvent(LampEventArgs lampEvent, Action _slingshots = new Dictionary(); private InputManager _inputManager; + private VisualPinballSimulationSystemGroup _simulationSystemGroup; [NonSerialized] private readonly LampPlayer _lampPlayer = new LampPlayer(); [NonSerialized] private readonly CoilPlayer _coilPlayer = new CoilPlayer(); [NonSerialized] private readonly SwitchPlayer _switchPlayer = new SwitchPlayer(); @@ -82,6 +83,9 @@ public Player() internal IApiSwitch Switch(string n) => _switchPlayer.Switch(n); internal IApiWireDest Wire(string n) => _wirePlayer.Wire(n); internal IApiWireDeviceDest WireDevice(string n) => _wirePlayer.WireDevice(n); + public Dictionary SwitchStatuses => _switchPlayer.SwitchStatuses; + public Dictionary CoilStatuses => _coilPlayer.CoilStatuses; + public Dictionary LampStatuses => _lampPlayer.LampStatuses; #endregion @@ -109,6 +113,7 @@ private void Awake() if (!string.IsNullOrEmpty(debugUiId)) { EngineProvider.Set(debugUiId); } + _simulationSystemGroup = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem(); } private void Start() @@ -123,7 +128,7 @@ private void Start() _lampPlayer.OnStart(); _wirePlayer.OnStart(); - GameEngine?.OnInit(TableApi, BallManager); + GameEngine?.OnInit(this, TableApi, BallManager); } private void Update() @@ -303,6 +308,8 @@ public void RegisterTrough(Trough trough, GameObject go) #region Events + public void ScheduleAction(int timeMs, Action action) => _simulationSystemGroup.ScheduleAction(timeMs, action); + public void OnEvent(in EventData eventData) { switch (eventData.eventId) { diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs index 5b3b1fba7..735d01453 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchHandler.cs @@ -82,8 +82,14 @@ internal void OnSwitch(bool closed) // if it's pulse, schedule to re-open if (closed && switchConfig.IsPulseSwitch) { - SimulationSystemGroup.ScheduleSwitch(switchConfig.PulseDelay, - () => Engine.Switch(switchConfig.SwitchId, false)); + SimulationSystemGroup.ScheduleAction(switchConfig.PulseDelay, + () => { + Engine.Switch(switchConfig.SwitchId, false); + IsClosed = false; +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif + }); } } } @@ -114,7 +120,7 @@ internal void OnSwitch(bool closed) // if it's pulse, schedule to re-open if (closed && wireConfig.IsPulseSource) { if (dest != null) { - SimulationSystemGroup.ScheduleSwitch(wireConfig.PulseDelay, + SimulationSystemGroup.ScheduleAction(wireConfig.PulseDelay, () => dest.OnChange(false)); } } @@ -123,6 +129,10 @@ internal void OnSwitch(bool closed) // handle own status IsClosed = closed; + +#if UNITY_EDITOR + UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); +#endif } internal void ScheduleSwitch(bool closed, int delay, Action onSwitched) @@ -130,7 +140,7 @@ internal void ScheduleSwitch(bool closed, int delay, Action onSwitched) // handle switch -> gamelogic engine if (Engine != null && _switchIds != null) { foreach (var switchConfig in _switchIds) { - SimulationSystemGroup.ScheduleSwitch(delay, + SimulationSystemGroup.ScheduleAction(delay, () => Engine.Switch(switchConfig.SwitchId, closed)); } } else { @@ -158,14 +168,13 @@ internal void ScheduleSwitch(bool closed, int delay, Action onSwitched) } if (dest != null) { - SimulationSystemGroup.ScheduleSwitch(delay, - () => dest.OnChange(closed)); + SimulationSystemGroup.ScheduleAction(delay, () => dest.OnChange(closed)); } } } // handle own status - SimulationSystemGroup.ScheduleSwitch(delay, () => { + SimulationSystemGroup.ScheduleAction(delay, () => { Debug.Log($"Setting scheduled switch {_name} to {closed}."); IsClosed = closed; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs index d182e8fc9..4c54bce2b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchPlayer.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; @@ -25,16 +26,17 @@ namespace VisualPinball.Unity { public class SwitchPlayer { - private readonly Dictionary _switches = new Dictionary(); + private readonly Dictionary _switchStatuses = new Dictionary(); private readonly Dictionary _switchDevices = new Dictionary(); - private readonly Dictionary> _keySwitchAssignments = new Dictionary>(); + private readonly Dictionary> _keySwitchAssignments = new Dictionary>(); private Table _table; private IGamelogicEngine _gamelogicEngine; private InputManager _inputManager; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + internal Dictionary SwitchStatuses => _switchStatuses.ToDictionary(s => s.Key, s => s.Value.IsSwitchClosed); internal IApiSwitch Switch(string itemName) => _switches.ContainsKey(itemName) ? _switches[itemName] : null; internal IApiSwitch Switch(string device, string itemName) => _switchDevices.ContainsKey(device) ? _switchDevices[device].Switch(itemName) : null; @@ -75,14 +77,17 @@ public void OnStart() var element = _switches[switchData.PlayfieldItem]; element.AddSwitchId(new SwitchConfig(switchData)); + _switchStatuses[switchData.Id] = element; break; } case SwitchSource.InputSystem: if (!_keySwitchAssignments.ContainsKey(switchData.InputAction)) { - _keySwitchAssignments[switchData.InputAction] = new List(); + _keySwitchAssignments[switchData.InputAction] = new List(); } - _keySwitchAssignments[switchData.InputAction].Add(switchData.Id); + var keyboardSwitch = new KeyboardSwitch(switchData.Id); + _keySwitchAssignments[switchData.InputAction].Add(keyboardSwitch); + _switchStatuses[switchData.Id] = keyboardSwitch; break; case SwitchSource.Device: { @@ -103,6 +108,7 @@ public void OnStart() var deviceSwitch = device.Switch(switchData.DeviceItem); if (deviceSwitch != null) { deviceSwitch.AddSwitchId(new SwitchConfig(switchData)); + _switchStatuses[switchData.Id] = deviceSwitch; } else { Logger.Error($"Unknown switch \"{switchData.DeviceItem}\" in switch device \"{switchData.Device}\"."); @@ -112,6 +118,7 @@ public void OnStart() } case SwitchSource.Constant: + _switchStatuses[switchData.Id] = new ConstantSwitch(switchData.Constant == SwitchConstant.NormallyClosed); break; default: @@ -134,8 +141,9 @@ private void HandleKeyInput(object obj, InputActionChange change) var action = (InputAction)obj; if (_keySwitchAssignments.ContainsKey(action.name)) { if (_gamelogicEngine is IGamelogicEngineWithSwitches engineWithSwitches) { - foreach (var switchId in _keySwitchAssignments[action.name]) { - engineWithSwitches.Switch(switchId, change == InputActionChange.ActionStarted); + foreach (var sw in _keySwitchAssignments[action.name]) { + sw.IsSwitchClosed = change == InputActionChange.ActionStarted; + engineWithSwitches.Switch(sw.SwitchId, sw.IsSwitchClosed); } } } else { @@ -152,4 +160,25 @@ public void OnDestroy() } } } + + internal class KeyboardSwitch : IApiSwitchStatus + { + public readonly string SwitchId; + public bool IsSwitchClosed { get; set; } + + public KeyboardSwitch(string switchId) + { + SwitchId = switchId; + } + } + + internal class ConstantSwitch : IApiSwitchStatus + { + public bool IsSwitchClosed { get; } + + public ConstantSwitch(bool isSwitchClosed) + { + IsSwitchClosed = isSwitchClosed; + } + } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballSimulationSystemGroup.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballSimulationSystemGroup.cs index 705d12f02..8466fd55d 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballSimulationSystemGroup.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/VisualPinballSimulationSystemGroup.cs @@ -52,7 +52,7 @@ internal class VisualPinballSimulationSystemGroup : ComponentSystemGroup private TransformMeshesSystemGroup _transformMeshesSystemGroup; private readonly List _afterBallQueues = new List(); - private readonly List _switchQueues = new List(); + private readonly List _scheduledActions = new List(); private const TimingMode Timing = TimingMode.UnityTime; @@ -113,11 +113,11 @@ protected override void OnUpdate() // advance physics position _nextPhysicsFrameTime += PhysicsConstants.PhysicsStepTime; - // close switches - for (var i = _switchQueues.Count - 1; i >= 0; i--) { - if (_currentPhysicsFrameTime > _switchQueues[i].SwitchAt) { - _switchQueues[i].Close(); - _switchQueues.RemoveAt(i); + // run scheduled actions + for (var i = _scheduledActions.Count - 1; i >= 0; i--) { + if (_currentPhysicsFrameTime > _scheduledActions[i].ScheduleAt) { + _scheduledActions[i].Action(); + _scheduledActions.RemoveAt(i); } } } @@ -164,9 +164,9 @@ public void QueueAfterBallCreation(Action action) _afterBallQueues.Add(action); } - public void ScheduleSwitch(int timeoutMs, Action action) + public void ScheduleAction(int timeoutMs, Action action) { - _switchQueues.Add(new SwitchAction(_currentPhysicsFrameTime + (ulong)timeoutMs * 1000, action)); + _scheduledActions.Add(new ScheduledAction(_currentPhysicsFrameTime + (ulong)timeoutMs * 1000, action)); } @@ -178,15 +178,15 @@ private enum TimingMode Locked60 } - private class SwitchAction + private class ScheduledAction { - public readonly ulong SwitchAt; - public readonly Action Close; + public readonly ulong ScheduleAt; + public readonly Action Action; - public SwitchAction(ulong switchAt, Action close) + public ScheduledAction(ulong scheduleAt, Action action) { - SwitchAt = switchAt; - Close = close; + ScheduleAt = scheduleAt; + Action = action; } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs index ffeebbf0b..5061f4706 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs @@ -41,6 +41,7 @@ public BumperApi(Bumper item, Entity entity, Player player) : base(item, entity, { } + bool IApiSwitchStatus.IsSwitchClosed => SwitchClosed; void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs index 4cdf25faf..a3f55040b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs @@ -90,6 +90,7 @@ public void RotateToStart() EngineProvider.Get().FlipperRotateToStart(Entity); } + bool IApiSwitchStatus.IsSwitchClosed => SwitchClosed; void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs index 5bb2e2acd..729060ed8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateApi.cs @@ -74,6 +74,7 @@ public GateApi(Engine.VPT.Gate.Gate item, Entity entity, Player player) : base(i { } + bool IApiSwitchStatus.IsSwitchClosed => SwitchClosed; void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs index ab23fab4a..ec6f9ed9a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs @@ -82,6 +82,7 @@ private void SetIsDropped(bool isDropped) EntityManager.SetComponentData(Entity, data); } + bool IApiSwitchStatus.IsSwitchClosed => SwitchClosed; void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index 24f5724be..3091e6d6a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -57,7 +57,7 @@ internal interface IApiSlingshot void OnSlingshot(Entity ballEntity); } - internal interface IApiSwitch + internal interface IApiSwitch : IApiSwitchStatus { /// /// Set up this switch to send its status to the gamelogic engine with the given ID. @@ -85,6 +85,11 @@ internal interface IApiSwitch event EventHandler Switch; } + internal interface IApiSwitchStatus + { + bool IsSwitchClosed { get; } + } + internal interface IApiSwitchDevice { IApiSwitch Switch(string switchId); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs index 21b632280..4457147b0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs @@ -42,6 +42,8 @@ public abstract class ItemApi : IApi where T : Item where TData private protected EntityManager EntityManager; + private protected bool SwitchClosed => _switchHandler.IsClosed; + internal VisualPinballSimulationSystemGroup SimulationSystemGroup => World.DefaultGameObjectInjectionWorld.GetOrCreateSystem(); private readonly Player _player; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs index 96b1477d2..053a1650f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs @@ -181,6 +181,7 @@ private static void KickXYZ(Table table, Entity kickerEntity, float angle, float } } + bool IApiSwitchStatus.IsSwitchClosed => SwitchClosed; void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs index cdba9462b..109c656fe 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerApi.cs @@ -69,6 +69,7 @@ public SpinnerApi(Engine.VPT.Spinner.Spinner item, Entity entity, Player player) { } + bool IApiSwitchStatus.IsSwitchClosed => SwitchClosed; void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs index 66fb8d966..febf8dcd1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs @@ -46,6 +46,7 @@ internal TriggerApi(Engine.VPT.Trigger.Trigger item, Entity entity, Player playe { } + bool IApiSwitchStatus.IsSwitchClosed => SwitchClosed; void IApiSwitch.AddSwitchId(SwitchConfig switchConfig) => AddSwitchId(switchConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.AddWireDest(WireDestConfig wireConfig) => AddWireDest(wireConfig.WithPulse(Item.IsPulseSwitch)); void IApiSwitch.DestroyBall(Entity ballEntity) => DestroyBall(ballEntity); From bf645deff4a96d5f07cd2b56a8795b9a08f618d9 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 18 Jan 2021 21:09:26 +0100 Subject: [PATCH 19/23] lamps: Add more documentation. --- .../editor/coil-manager-lamp.png | Bin 0 -> 21580 bytes .../editor/lamp-manager-coil.png | Bin 0 -> 22323 bytes .../editor/lamp-manager-gameplay.gif | Bin 0 -> 171915 bytes .../creators-guide/editor/lamp-manager.md | 32 +++++++++++++----- .../creators-guide/editor/lamp-manager.png | Bin 0 -> 86787 bytes 5 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager-lamp.png create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager-coil.png create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager-gameplay.gif create mode 100644 VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.png diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager-lamp.png b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager-lamp.png new file mode 100644 index 0000000000000000000000000000000000000000..6b875cb46c372ea260a9468f86afdb0da239bf3f GIT binary patch literal 21580 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 literal 0 HcmV?d00001 diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager-coil.png b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager-coil.png new file mode 100644 index 0000000000000000000000000000000000000000..c3b798344e28a9fa1128f1e5ef5a4f6dfd65d771 GIT binary patch literal 22323 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 VBZIb`zrmiKf8Y1F^4-Cw{{x)ThRFZ` literal 0 HcmV?d00001 diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager-gameplay.gif b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager-gameplay.gif new file mode 100644 index 0000000000000000000000000000000000000000..be280938e1bb49d7dfb60d01d31f60ecddc843cd GIT binary patch literal 171915 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℘+XBrA-I!FdaTcjKxZve#TAYD4dgU@32?c5Ve zGRgXZQPhro*+(&(@PdIf#~UvE=cnIB3puS@7NB`#Qd6Dk+*yRA)|iBy>G;lQ#Y#0j zi~w{Iuyo2i~BM|8Vzdu=o{MCBB!&}SfX>_v@Aa-R48 z(96qTt@7OeMeP2be&_K{<>yg_ea%m~A(lM(B3Y7p4Mb+y{PcX94TlyMnjWcE-@#m> za8@5Ktp(R6t&3a2jI60^jmYydYvgm>n9Wpqx$$3#+8W+=3fkNPB}M|iWpE|ZI%{a}y6BjZ}IQ%`+ZOGwfrde_gQTnGU%06`#ZB5p_I{_X2Xd%ngQ!tzk z;{*)bXTy_iOL*#)7gnmXHhOR@1b9=fkJ&=JWIk42?k>4ZpqBE| zQ75ZR$;s_T-d8XibR@M)5H8Wp(&>UGr%2;-^jHml;uO#F#9j@F5wCoIezGI5jO${0 zm`ao*kM7(PEY9qOVQPA%!j|NW`Sx0rFGFk+>i{BeY0&7fJa)(X|1MG|HD5*XPVII}sfT=jay zy?+<$@h_Qb$2(-k!JLRc*MP_~J9)eTEweHC3V~;Ko1-hNPNRPEnm&@{Qk$9AT#`y^pQlX36<|RF;@Iu#b)J9BcUjF%~82KG+vf*OPqCuz$MRWpX+>a5Fh4 zN$!;Tn07jvV<;WKHVS}g-vSykJ1|P_anaNx9P$jzk{`Co2d}II`=}-Ja6jSW$H(UoSRnKwRu37#DXA694E- z4pC@MrYDo^c}?b2`i7T!zY8cH8+(nYSQXP^BU72=+ue2U4AEHkEobU>jfXjhw03CL zNI_1i`=7GV_F^hcc8DPDC^ zRNoZ?ziQjfGu|xTj@3gZrvb2IXtE@|go8`11BGZ{UWG8TM}!aXG5A7BoC;e+E>N6K zCu3VnJh@(idGaW+29ZDxi`GsI4gE};+QDYl3c{=@bT*seOW5!G8HP+mOP1*&KU|nc zsV*3P&n<%$!=lbU2wx^nK2y{9>m4jw0b`3 zx_3o*01Y$t_DVT23@|%_5AMS=Df21`nhWqRmj{&7jze9kD6c44xo&VmFVR57fn=FA z+Enp}%Hndh3w{gjFDo;mZ>gTDaeI!d-J$OlnH#Q2@w5#w{f1TWbkI`~cfT#!=eGTN z>}@p}kzv~B)MaiobFj-rJp)%M7ILd^racQO{B>u^d@M_DH~-O@RrPOe?z_#>g+EpD zY=#xShnEv&Zfn3fWQcK@6ku^YQ*h`4{gav+m*if*t-d56I)8Zyo?r{K+w-zZ%CkKy z8H{)DmdcqacR=c|k;F^igO!s~L6PBAP29w&Hz#BqGJ-}!anBJsPJ7+BYey8Ppy!0n zJA*hM1t;I7WUB~^*SNeNGAlAoA3vj4i~T`Dq z)kxbeg;Nl)giIHk`KybWQ>RgZ=oeiS=;FrW0^n=9!#m!9f zW3oZUF0&1tZ$Rd5H@G!n0q=KGSUT zU&Oi8FhalA24w}_tLc63b2$!@2mo!y#1lz~n(PaM1-h+47N+q9d zC2}SNcNy+^ZN90+Ax|HHI-TNRqO8UH_Y`&7B@wIqA#WDyOx{Eh_zLg*rIjVslNp(X zjBrSXCd5=N-WANvzfU$rlcAARo8WVcUQ`t|xOKSlg-o?Ao?OGhlBjSkIMe|S*Lo%8@FtzwnK`JRb5ARuMxB~$Pj6NJWZ!je zN8s}U_ZNGhbU34d4@9DXd?;sqC?FD@sD2B?Lw%QC8D)d6yVwXR9Kit?*G6iP{8w7H z`^7v<07lLrs%eyJs_s$-fX$h74h=Dq0k_jssQ^sJD(j|!P;3T33>dD(`4r+4!J8UZ+iH`0gG28MJ534H_ zEX*=ZqfX3Cx__2svZiHZfoQT`&IT z9N8d_!-(=hV+*1z1Gb?TDngWRS;pV&faKLYDU$toU*lt#3QNIPvm$K5eb+eI6C9*T zLPhX&?>BF&ct0Yxp2&litROLrw7i(Ltc+EVyb@e=pRAPH>oMQSt7e!n*w#0IdtCo4 zP7AI{CvWAL>gr`}Ub4;%{=5~Z)tjM}w`z&V?Aw5RF)!)+OMheYBRYJ7$&iKRXaae2 zK-zF>U|%a`MzfO3mW&-tRKh`;k3jlr`AMKNPzxCrMz$~sQ#ZgGeOFh%wc$NAN_$YX zy&h;pLI3U2sSwm@rvmiDEf(|pw`&xSEN%5uKbHUeaze&JjC4GY`aR*6#NfQ=AD^AB z?oFo>EuAgXW#o;WYi_veTTrbr;Ze$(+1gadT2kR+rj43~L7^IP`-EJv<1Ng0^nx6& zAQlcw`}lfv@q)b7vu`XqV3QXW!p+xudroIGm0cR=)hy zltP|71wYgJy!AS9uE3f)VV^-Ck?;S!RFW#i{>AvVLlOU%(uTg`)+q0|81L4o@~V7U zKdJ)V!KSc$95Wzk)m=7_sDUP93Bc2~WNr&vy~?O>NRS@2`wc&kp%`KjBsDaD(uAxS zKT)CU^_y_89s(3LnaBlz=+%>iX~fTW!N*A!@K##}_n=cxMCK6H5M++RQBEUW{lTcG z4M0XNOEAi!eHGyR_@aw;|KII?-e+lie+c{{7p;S!RP-@3iN7F2$<`1JME#W+3&mugOmr2ud;QSwHY zGSB>91(?Xu6Os*utVCdxyhPb3sH-Ger`Hor=@2-HY=G;zss6u#&P7HzlRP&JdW^yy z-?aZhE+2DsQS)=bg}WF%wuJYMn=p3VlN~B$Y>H%nVx}N58K9MRyf!TjT=GhQ3fX@% ze&Oktb~*uR2Qgd)wYk1kMI#Mi;I;uE&K9EY2U>aoP?pKL*`}bWkkdgYDBvI{Ao)~% z|1bOZ2sFQu8X4;h4ieF=sx?BKTukE{2cxRMVFqte^vO5uQ+KY~-7Q2$yc~NR?hO75 zu~q;Z0-X7&5X=3ShT5;h)oQ?=h`6cqG~&`OM91J7=cuF>QnG@~>B%g{CBr)o7v&=^~vL?h17F zV|s>2EKZu?YGyZ`Vr7L9u`EgKGQhXrZ24x2n|PAgQ$4A4LULOY!Ze9DHw(UM)=QWZ z&T^|SapQU(FGhG2aPoq@*#Kv_)@u;_4@1x3{&BV$JaLuA$s30d^`xLboyGH`evr;8 zUfR02aLzIAJIRi#*$Si9)k0Gm%kf%IvNRxiEeu@gjHkhw7tht=EBG%H|5L1)ZyXi#DGVv011q)!LTlm-6rq#*W^`Vcw=$-sbCk z=d+eBSwF%RBugS7Yda&SP8U=$)$4LBGH4e)_X<|#|9RCiahinaM?7)=dDVD53&(l; zEE_B4rGN)cH)g!;vQqy8s`<69OZk?uv3NWBknldfy8yqeW#{sx@T`@X;Dv~1%dbxS zNEyT^6$H&B=Zf3r)g+!}BJ<^PTWQvrc=uiiMTY3)7hOytAY@in*?}y*@lZcN;HV#5 z1~fG7clQPXIvyWal=`1a4V#H~^iQu`rP%t#gsm2bGgRu8yewa4gnuFW2mCKklR!4Z zlU4S82{Tu^ulbXVuhFr~aG6C#0T$d2oNb&C?DFH29Ff6M7yQRj6H<16)-AU}Z2^g+ z@T&5k)yCIZUj3|PW~t<0X<3G|m=d7%KVG{pFgWVQ&v3?4e>#E`$Xfr>`e(hK7jLt+GsnYh3CzKO`sTI%@+@Xs0&F#&F?IXEeB=8GofR%q@jr|j(B^nU z{wjdH8t~p{ZE(3_NNR0@*E>pQ)hkT&!pGk;yT50d*J-@#bI(N^e+4wWOrld4+9SY})H-@&{`M{f{1ZXfJDtKcr zW@A5V<8S%KLG#AphmE7rjepA<$GaOR%=Dw>z_>?*ZAQVCN${3+(4-IDlX)XbXN@_V z4zHlIw9r|5=xk$j_7yru1q~WoXZDAVl-@+CZgQDyayxDE_-^vruypZ;@MUibRBQ^i zYzp;k3Xg4yK;is+fO_y2nr{muy(O-?C1J9Kb=s0V@!gWTvn3t7C6m2%s$%PO%a&}< zmfYCZnUyX1y{)s*Z3VtyZ55~ObH3Z>?`*5aZp-a$s#R=jv}|kkY-^2e zYp-nU>}~5pcW`_=deS@ksyha;+v+AeM!q}7cXmu-cTBT)%qn)wTXrmZb}UbB8U=;2 zRc^hD1F$0g*h&Anp!&z&L1s!KW;03-1q)? zKzH$cyPnd!UaGrSOm?s60+?+>ZBzdE#_sxM@A_BlUTfL)Exs+gck6WQ|IBH?UNGNY zi1c2l>fTM0y<1Luw|)1*?(E%(-3!m&yX _o}jcw}53+q=KA_dxegz}OC9<)*dA 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 cGOJ={wCP4aZ1nmGzKX#c81yI^3 Lamp Manager*. -[TODO: Screenshot] +![Lamp Manager](lamp-manager.png) > [!note] > We use the terms *lights* and *lamps* as follows: @@ -57,17 +57,33 @@ The **Type** column defines how the signal is interpreted by the lamp. This is i - *Single On|Off* - Typically lamps from the lamp matrix. They can only be on or off. Receiving `0` will turn the lamp off, any other value will tell it on. - *Single Fading* - Individually connected lamps that can be dimmed by the gamelogic engine. Received values can be `0` to `255`, where `0` turns the lamp off, and `255` sets it to full intensity. -- *RGB Multi* - An RGB lamp that can change its color during gameplay. Lamps of this type received three inputs from red, green and blue, which can be set in the next column. -- *RGB* - An RGB lamp that receives its data from a single channel. This is the only mode where the lamp doesn't receive an integer, but an entire color. +- *RGB Multi* - An RGB lamp that can change its color during gameplay. Lamps of this type receive three connections, one from each red, green and blue. +- *RGB* - An RGB lamp that receives its data from a single connection. This is the only mode where the lamp doesn't receive an integer, but an entire color. -## Inserts +## Flashers -[TODO] +When using a gamelogic engine that behaves like real hardware like PinMAME, high-powered lamps such as flashers show usually up as connected driver board. -## Flashers +VPE allows routing coil outputs to lamps. For that, go to the [Coil Manager](coil-manager.md) and select *Lamp* as **Destination**: + +![Coil as lamp](coil-manager-lamp.png) + +This will make the coil show up in the lamp manager where you can configure it: + +![Lamp from coil](lamp-manager-coil.png) -[TODO] +You note that you cannot change the *ID* of the lamp, because it's still linked to the coil. Also, removing or changing the coil destination will remove the entry from the lamp manager. Changing the ID in the coil manager will also update it in the lamp manager. + +## GI Strips + +There is currently no special support for GI strips. In Visual Pinball, you can put GI lamps into a collection and address the whole collection at once via script. VPE doesn't have this feature yet. In order to hook up GI lamps, you can can add an entry per lamp and link all of them to the same ID. + +We want to make this easier in the future, so we're thinking of integrating this into the editor directly. ## Editor vs Runtime -While editing the table in the Unity editor, you can and probably should disable lights you're not editing. During runtime, VPE first turns all lights off, then turns on the constant lights, and then waits for the gamelogic engine for further instructions. +While editing the table in the Unity editor, you can and probably should disable lights you're not editing. During runtime, VPE first turns all lights off, then turns on the constant lights, and then waits for the gamelogic engine for further instructions. + +If you run the game in the editor, the lamp manager shows the lamp statuses in real-time: + +![Lamps runtime](lamp-manager-gameplay.gif) diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.png b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.png new file mode 100644 index 0000000000000000000000000000000000000000..84d106f39e7bc595bc33ff88e5a55183b1987afc GIT binary patch literal 86787 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 z

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ℜ9TTQ+ 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 u-RIdZ(yvG)%)HL!T$p(*sbya literal 0 HcmV?d00001 From f43faf0c61a648d71013a7bc9f67726231fcb1da Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 18 Jan 2021 23:02:22 +0100 Subject: [PATCH 20/23] doc: Add missing lamp type section. --- .../Documentation~/creators-guide/editor/lamp-manager.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md index 3084caa80..68a365c50 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md @@ -60,6 +60,10 @@ The **Type** column defines how the signal is interpreted by the lamp. This is i - *RGB Multi* - An RGB lamp that can change its color during gameplay. Lamps of this type receive three connections, one from each red, green and blue. - *RGB* - An RGB lamp that receives its data from a single connection. This is the only mode where the lamp doesn't receive an integer, but an entire color. +### R G B + +If the type of the previous column has been set to *RGB Multi*, here is where you link each wire to a color. Note that *red* is always the one shown under the *ID* column, so changing the red link will also change the ID (and vice versa). + ## Flashers When using a gamelogic engine that behaves like real hardware like PinMAME, high-powered lamps such as flashers show usually up as connected driver board. From d4e35c421367e76e13ac54198b68c9215cc81673 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 18 Jan 2021 23:06:47 +0100 Subject: [PATCH 21/23] doc: Update coil manager with new destination. --- .../Documentation~/creators-guide/editor/coil-manager.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md index 394f5129e..a53f35941 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md @@ -26,10 +26,11 @@ The **Description** column is optional. If you're setting up a re-creation, you ### Destination -The **Destination** column defines where the element in the following column is located. There are two options: +The **Destination** column defines where the element in the following column is located. There are three options: - *Playfield* lets you select a game element on the playfield that features the coil - *Device* lets you choose a *coil device*, a mechanism which may include multiple coils, such as a [trough](../manual/mechanisms/troughs.md). +- *Lamp* sets the coil to be configured in the lamp manager (see [flashers in the lamp manager](lamp-manager.html#flashers) for more details). ### Element From 408379cf7276586a9338784643779c14cb561a96 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 19 Jan 2021 21:20:05 +0100 Subject: [PATCH 22/23] doc: Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45416fd2e..8649a7fe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Built with [Unity 2020.2](https://github.com/freezy/VisualPinball.Engine/pull/255). ### Added +- [Lamp Manager](https://docs.visualpinball.org/creators-guide/editor/coil-manager.html) ([#282](https://github.com/freezy/VisualPinball.Engine/pull/282)) - The VPE core is now also available on [NuGet](https://www.nuget.org/packages/VisualPinball.Engine/). - VPE is now packaged and published on every merge! - Native trough component ([#229](https://github.com/freezy/VisualPinball.Engine/pull/229), [#248](https://github.com/freezy/VisualPinball.Engine/pull/248), [#256](https://github.com/freezy/VisualPinball.Engine/pull/256), [Documentation](https://docs.visualpinball.org/creators-guide/manual/mechanisms/troughs.html)) From 83309c2f2b173510c13f74208ef6f011632914bd Mon Sep 17 00:00:00 2001 From: Jason Millard Date: Tue, 19 Jan 2021 15:46:05 -0500 Subject: [PATCH 23/23] docs: light to lamp updates --- .../Documentation~/creators-guide/editor/lamp-manager.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md index 68a365c50..bd345b2de 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/lamp-manager.md @@ -3,9 +3,9 @@ description: The lamp manager lets you connect and configure lights, flashers an --- # Lamp Manager -There are many types of lamps a real pinball machine might use, and there are different ways a gamelogic engine might be addressing them. VPE uses the Unity game engine to accurately simulate lights on the playfield. Those lights have a standardized set of parameters, which you can tweak in the editor. However, lights in a game are dynamic, so the gamelogic engine will toggle them, fade them, or even change their color. +There are many types of lamps a real pinball machine might use, and there are different ways a gamelogic engine might address them. VPE uses the Unity game engine to accurately simulate lamps on the playfield. Lamps have a standardized set of parameters, which can be tweaked in the editor. Lamps in a game are also dynamic, so the gamelogic engine will toggle them, fade them, or even change their color. -In order to link each of the playfield lights to the gamelogic engine and configure how they react during gameplay, the *Lamp Manager* is used. You can find it under *Visual Pinball -> Lamp Manager*. +In order to link each of the playfield lamps to the gamelogic engine and configure how they react during gameplay, the *Lamp Manager* is used. You can find it under *Visual Pinball -> Lamp Manager*. ![Lamp Manager](lamp-manager.png) @@ -86,7 +86,7 @@ We want to make this easier in the future, so we're thinking of integrating this ## Editor vs Runtime -While editing the table in the Unity editor, you can and probably should disable lights you're not editing. During runtime, VPE first turns all lights off, then turns on the constant lights, and then waits for the gamelogic engine for further instructions. +While editing the table in the Unity editor, you can and probably should disable lamps you're not editing. During runtime, VPE first turns all lamps off, then turns on the constant lamps, and then waits for the gamelogic engine for further instructions. If you run the game in the editor, the lamp manager shows the lamp statuses in real-time: