diff --git a/README.md b/README.md index a63e8e57c..f33d08e7f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ other "current gen" engines, while keeping backwards-compatibility. VPE also aims to significantly improve the editor experience by extending the editor of the game engine. +For a more detailed overview, header over to the [website](https://docs.visualpinball.org/creators-guide/introduction/overview.html)! + ## How? The "core" of VPE (i.e. the `VisualPinball.Engine` project) is a pure C# port @@ -32,7 +34,7 @@ VPE is still work in progress. You can check the current features list [here](ht and the open issues [here](https://github.com/freezy/VisualPinball.Engine/issues). There are a few videos in the [VPF thread](https://www.vpforums.org/index.php?showtopic=43651), -where you can discuss. +where you can discuss. Screenshots are [here](https://github.com/freezy/VisualPinball.Engine/wiki/Unity-Screenshots! :) ## Credits diff --git a/VisualPinball.Engine.Test/Fixtures~/MappingsTest.vpx b/VisualPinball.Engine.Test/Fixtures~/MappingsTest.vpx index d9aa82095..d7e8aaa7d 100644 Binary files a/VisualPinball.Engine.Test/Fixtures~/MappingsTest.vpx and b/VisualPinball.Engine.Test/Fixtures~/MappingsTest.vpx differ diff --git a/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx b/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx new file mode 100644 index 000000000..24e3db48a Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/TroughTest.vpx differ diff --git a/VisualPinball.Engine.Test/Test/Fixtures.cs b/VisualPinball.Engine.Test/Test/Fixtures.cs index 8fad2c9b2..03e75338c 100644 --- a/VisualPinball.Engine.Test/Test/Fixtures.cs +++ b/VisualPinball.Engine.Test/Test/Fixtures.cs @@ -51,6 +51,7 @@ public static class VpxPath public static readonly string Texture = PathHelper.GetFixturePath("TextureTest.vpx"); public static readonly string Timer = PathHelper.GetFixturePath("TimerTest.vpx"); public static readonly string Trigger = PathHelper.GetFixturePath("TriggerTest.vpx"); + public static readonly string Trough = PathHelper.GetFixturePath("TroughTest.vpx"); } public static class ObjPath diff --git a/VisualPinball.Engine.Test/VPT/Mappings/MappingsDataTests.cs b/VisualPinball.Engine.Test/VPT/Mappings/MappingsDataTests.cs index 06f6e0d56..3515b7090 100644 --- a/VisualPinball.Engine.Test/VPT/Mappings/MappingsDataTests.cs +++ b/VisualPinball.Engine.Test/VPT/Mappings/MappingsDataTests.cs @@ -44,7 +44,7 @@ public void ShouldWriteMappingsData() private static void ValidateTableData(MappingsData data) { - data.Switches.Length.Should().Be(4); + data.Switches.Length.Should().Be(13); data.Switches[0].Id.Should().Be("s_create_ball"); data.Switches[0].Description.Should().Be("Create Ball"); @@ -72,7 +72,20 @@ private static void ValidateTableData(MappingsData data) data.Switches[3].Source.Should().Be(SwitchSource.Constant); data.Switches[3].Constant.Should().Be(SwitchConstant.NormallyClosed); - data.Coils.Length.Should().Be(2); + data.Switches[3].Id.Should().Be("s_right_flipper"); + data.Switches[3].Description.Should().Be("Right Flipper"); + data.Switches[3].Source.Should().Be(SwitchSource.Constant); + data.Switches[3].Constant.Should().Be(SwitchConstant.NormallyClosed); + + data.Switches[7].Id.Should().Be("s_trough1"); + data.Switches[7].Description.Should().Be("Trough 1 (eject)"); + data.Switches[7].Source.Should().Be(SwitchSource.Device); + data.Switches[7].Device.Should().Be("Trough"); + data.Switches[7].DeviceItem.Should().Be("1"); + data.Switches[7].Type.Should().Be(SwitchType.OnOff); + data.Switches[7].PulseDelay.Should().Be(250); + + data.Coils.Length.Should().Be(6); data.Coils[0].Id.Should().Be("c_auto_plunger"); data.Coils[0].Description.Should().Be("Auto Plunger"); @@ -86,6 +99,14 @@ private static void ValidateTableData(MappingsData data) data.Coils[1].PlayfieldItem.Should().Be("Flipper1"); data.Coils[1].Type.Should().Be(CoilType.DualWound); data.Coils[1].HoldCoilId.Should().Be("c_left_flipper_hold"); + + data.Coils[5].Id.Should().Be("c_trough_eject"); + data.Coils[5].Description.Should().Be("Trough Eject"); + data.Coils[5].Destination.Should().Be(CoilDestination.Device); + data.Coils[5].Device.Should().Be("Trough"); + data.Coils[5].DeviceItem.Should().Be("eject"); + data.Coils[5].Type.Should().Be(CoilType.SingleWound); + data.Coils[5].HoldCoilId.Should().Be(""); } } } diff --git a/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs new file mode 100644 index 000000000..c69fefdb0 --- /dev/null +++ b/VisualPinball.Engine.Test/VPT/Trough/TroughDataTests.cs @@ -0,0 +1,53 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using FluentAssertions; +using NUnit.Framework; +using VisualPinball.Engine.Test.Test; +using VisualPinball.Engine.VPT.Trough; + +namespace VisualPinball.Engine.Test.VPT.Trough +{ + public class TroughDataTests + { + [Test] + public void ShouldReadTroughData() + { + var table = Engine.VPT.Table.Table.Load(VpxPath.Trough); + ValidateTroughData(table.Trough("Trough1").Data); + } + + [Test] + public void ShouldWriteTroughData() + { + const string tmpFileName = "ShouldWriteTroughData.vpx"; + var table = Engine.VPT.Table.Table.Load(VpxPath.Trough); + table.Save(tmpFileName); + var writtenTable = Engine.VPT.Table.Table.Load(tmpFileName); + ValidateTroughData(writtenTable.Trough("Trough1").Data); + } + + private static void ValidateTroughData(TroughData data) + { + data.BallCount.Should().Be(3); + data.SwitchCount.Should().Be(4); + data.SettleTime.Should().Be(112); + data.EntryKicker.Should().Be("BallDrain"); + data.ExitKicker.Should().Be("BallRelease"); + data.JamSwitch.Should().Be("TroughJam"); + } + } +} diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs index 84141bf0a..ee1c4b768 100644 --- a/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs +++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineCoil.cs @@ -22,5 +22,7 @@ public struct GamelogicEngineCoil public string Description; public string PlayfieldItemHint; public string MainCoilIdOfHoldCoil; + public string DeviceHint; + public string DeviceItemHint; } } diff --git a/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs b/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs index 3a293e752..5153a197e 100644 --- a/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs +++ b/VisualPinball.Engine/Game/Engines/GamelogicEngineSwitch.cs @@ -23,5 +23,7 @@ public struct GamelogicEngineSwitch public string InputActionHint; public string InputMapHint; public string PlayfieldItemHint; + public string DeviceHint; + public string DeviceItemHint; } } diff --git a/VisualPinball.Engine/Game/ICoilable.cs b/VisualPinball.Engine/Game/ICoilable.cs index 33962c2a2..b4fd19383 100644 --- a/VisualPinball.Engine/Game/ICoilable.cs +++ b/VisualPinball.Engine/Game/ICoilable.cs @@ -19,6 +19,5 @@ namespace VisualPinball.Engine.Game public interface ICoilable { string Name { get; } - bool IsDualWound { get; set; } } } diff --git a/VisualPinball.Engine/Game/ICoilableDevice.cs b/VisualPinball.Engine/Game/ICoilableDevice.cs new file mode 100644 index 000000000..1443bf351 --- /dev/null +++ b/VisualPinball.Engine/Game/ICoilableDevice.cs @@ -0,0 +1,28 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.Collections.Generic; +using VisualPinball.Engine.Game.Engines; + +namespace VisualPinball.Engine.Game +{ + public interface ICoilableDevice + { + string Name { get; } + + IEnumerable AvailableCoils { get; } + } +} diff --git a/VisualPinball.Engine/Game/ICoilableDevice.cs.meta b/VisualPinball.Engine/Game/ICoilableDevice.cs.meta new file mode 100644 index 000000000..5dffe9fff --- /dev/null +++ b/VisualPinball.Engine/Game/ICoilableDevice.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 29418066007efd546bf0919c88d02a88 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Engine/Game/ISwitchableDevice.cs b/VisualPinball.Engine/Game/ISwitchableDevice.cs new file mode 100644 index 000000000..d159aef6f --- /dev/null +++ b/VisualPinball.Engine/Game/ISwitchableDevice.cs @@ -0,0 +1,28 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.Collections.Generic; +using VisualPinball.Engine.Game.Engines; + +namespace VisualPinball.Engine.Game +{ + public interface ISwitchableDevice + { + string Name { get; } + + IEnumerable AvailableSwitches { get; } + } +} diff --git a/VisualPinball.Engine/Game/ISwitchableDevice.cs.meta b/VisualPinball.Engine/Game/ISwitchableDevice.cs.meta new file mode 100644 index 000000000..050f95f1e --- /dev/null +++ b/VisualPinball.Engine/Game/ISwitchableDevice.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f8a55aed6ac821c44ac1699352ef97ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Engine/VPT/Item.cs b/VisualPinball.Engine/VPT/Item.cs index 34de7558e..d2f0ee8b5 100644 --- a/VisualPinball.Engine/VPT/Item.cs +++ b/VisualPinball.Engine/VPT/Item.cs @@ -15,7 +15,6 @@ // along with this program. If not, see . using System; -using VisualPinball.Engine.Math; namespace VisualPinball.Engine.VPT { diff --git a/VisualPinball.Engine/VPT/ItemType.cs b/VisualPinball.Engine/VPT/ItemType.cs index 6546da8aa..849d328b6 100644 --- a/VisualPinball.Engine/VPT/ItemType.cs +++ b/VisualPinball.Engine/VPT/ItemType.cs @@ -54,5 +54,6 @@ public enum ItemType // VPE internal Ball = 100, + Trough = 101, } } diff --git a/VisualPinball.Engine/VPT/Mappings/Mappings.cs b/VisualPinball.Engine/VPT/Mappings/Mappings.cs index e62c2950e..fece24b9d 100644 --- a/VisualPinball.Engine/VPT/Mappings/Mappings.cs +++ b/VisualPinball.Engine/VPT/Mappings/Mappings.cs @@ -18,6 +18,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using NLog; using VisualPinball.Engine.Common; using VisualPinball.Engine.Game; using VisualPinball.Engine.Game.Engines; @@ -29,6 +30,8 @@ public class Mappings : Item public override string ItemName { get; } = "Mapping"; public override string ItemGroupName { get; } = "Mappings"; + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + public Mappings() : this(new MappingsData("Mappings")) { } @@ -49,12 +52,16 @@ public bool IsEmpty() #region Switch Population - public void PopulateSwitches(GamelogicEngineSwitch[] engineSwitches, IEnumerable tableSwitches) + public void PopulateSwitches(GamelogicEngineSwitch[] engineSwitches, IEnumerable tableSwitches, IEnumerable tableSwitchDevices) { var switches = tableSwitches .GroupBy(x => x.Name.ToLower()) .ToDictionary(x => x.Key, x => x.First()); + var switchDevices = tableSwitchDevices + .GroupBy(x => x.Name.ToLower()) + .ToDictionary(x => x.Key, x => x.First()); + foreach (var engineSwitch in GetSwitchIds(engineSwitches)) { var switchMapping = Data.Switches.FirstOrDefault(mappingsSwitchData => mappingsSwitchData.Id == engineSwitch.Id); @@ -64,6 +71,8 @@ public void PopulateSwitches(GamelogicEngineSwitch[] engineSwitches, IEnumerable var description = engineSwitch.Description ?? string.Empty; var source = GuessSwitchSource(engineSwitch); var playfieldItem = source == SwitchSource.Playfield ? GuessPlayfieldSwitch(switches, engineSwitch) : null; + var device = source == SwitchSource.Device ? GuessDevice(switchDevices, engineSwitch) : null; + var deviceItem = source == SwitchSource.Device && device != null ? GuessDeviceSwitch(engineSwitch, device) : default; var inputActionMap = source == SwitchSource.InputSystem ? string.IsNullOrEmpty(engineSwitch.InputMapHint) ? InputConstants.MapCabinetSwitches : engineSwitch.InputMapHint : string.Empty; @@ -77,7 +86,9 @@ public void PopulateSwitches(GamelogicEngineSwitch[] engineSwitches, IEnumerable Source = source, PlayfieldItem = playfieldItem != null ? playfieldItem.Name : string.Empty, InputActionMap = inputActionMap, - InputAction = inputAction + InputAction = inputAction, + Device = device != null ? device.Name : string.Empty, + DeviceItem = deviceItem.Id }); } } @@ -105,7 +116,6 @@ public IEnumerable GetSwitchIds(GamelogicEngineSwitch[] e Id = mappingsSwitchData.Id }); } - } ids.Sort((s1, s2) => s1.Id.CompareTo(s2.Id)); @@ -114,6 +124,10 @@ public IEnumerable GetSwitchIds(GamelogicEngineSwitch[] e private static int GuessSwitchSource(GamelogicEngineSwitch engineSwitch) { + if (!string.IsNullOrEmpty(engineSwitch.DeviceHint)) { + return SwitchSource.Device; + } + return !string.IsNullOrEmpty(engineSwitch.InputActionHint) ? SwitchSource.InputSystem : SwitchSource.Playfield; } @@ -137,6 +151,33 @@ private static ISwitchable GuessPlayfieldSwitch(Dictionary return switches.ContainsKey(matchKey) ? switches[matchKey] : null; } + private static ISwitchableDevice GuessDevice(Dictionary switchDevices, GamelogicEngineSwitch engineSwitch) + { + // match by regex if hint provided + if (!string.IsNullOrEmpty(engineSwitch.DeviceHint)) { + foreach (var deviceName in switchDevices.Keys) { + var regex = new Regex(engineSwitch.DeviceHint.ToLower()); + if (regex.Match(deviceName).Success) { + return switchDevices[deviceName]; + } + } + } + return null; + } + + private static GamelogicEngineSwitch GuessDeviceSwitch(GamelogicEngineSwitch engineSwitch, ISwitchableDevice device) + { + if (!string.IsNullOrEmpty(engineSwitch.DeviceItemHint)) { + foreach (var deviceSwitch in device.AvailableSwitches) { + var regex = new Regex(engineSwitch.DeviceItemHint.ToLower()); + if (regex.Match(deviceSwitch.Id).Success) { + return deviceSwitch; + } + } + } + return default; + } + #endregion #region Coil Population @@ -147,12 +188,16 @@ private static ISwitchable GuessPlayfieldSwitch(Dictionary /// /// List of coils provided by the gamelogic engine /// List of coils on the playfield - public void PopulateCoils(GamelogicEngineCoil[] engineCoils, IEnumerable tableCoils) + public void PopulateCoils(GamelogicEngineCoil[] engineCoils, IEnumerable tableCoils, IEnumerable tableCoilDevices) { var coils = tableCoils .GroupBy(x => x.Name.ToLower()) .ToDictionary(x => x.Key, x => x.First()); + var coilDevices = tableCoilDevices + .GroupBy(x => x.Name.ToLower()) + .ToDictionary(x => x.Key, x => x.First()); + var holdCoils = new List(); foreach (var engineCoil in GetCoils(engineCoils)) { @@ -165,14 +210,19 @@ public void PopulateCoils(GamelogicEngineCoil[] engineCoils, IEnumerable coilDevices, GamelogicEngineCoil engineCoil) + { + // match by regex if hint provided + if (!string.IsNullOrEmpty(engineCoil.DeviceHint)) { + foreach (var deviceName in coilDevices.Keys) { + var regex = new Regex(engineCoil.DeviceHint.ToLower()); + if (regex.Match(deviceName).Success) { + return coilDevices[deviceName]; + } + } + } + return null; + } + + private static GamelogicEngineCoil GuessDeviceCoil(GamelogicEngineCoil engineCoil, ICoilableDevice device) + { + if (!string.IsNullOrEmpty(engineCoil.DeviceItemHint)) { + foreach (var deviceCoil in device.AvailableCoils) { + var regex = new Regex(engineCoil.DeviceItemHint.ToLower()); + if (regex.Match(deviceCoil.Id).Success) { + return deviceCoil; + } + } + } + return default; + } + /// /// Returns a sorted list of coil names from the gamelogic engine, /// appended with the additional names in the coil mapping. In short, diff --git a/VisualPinball.Engine/VPT/Rubber/Rubber.cs b/VisualPinball.Engine/VPT/Rubber/Rubber.cs index 6ae41f93f..85aa94ca9 100644 --- a/VisualPinball.Engine/VPT/Rubber/Rubber.cs +++ b/VisualPinball.Engine/VPT/Rubber/Rubber.cs @@ -66,7 +66,7 @@ public static Rubber GetDefault(Table.Table table) #region IRenderable - Matrix3D IRenderable.TransformationMatrix(Table.Table table, Origin origin) => Matrix3D.Identity; + Matrix3D IRenderable.TransformationMatrix(Table.Table table, Origin origin) => _meshGenerator.GetPostMatrix(table, origin); public RenderObject GetRenderObject(Table.Table table, string id = null, Origin origin = Origin.Global, bool asRightHanded = true) { diff --git a/VisualPinball.Engine/VPT/Table/Table.cs b/VisualPinball.Engine/VPT/Table/Table.cs index 81f8735e8..bbef877ed 100644 --- a/VisualPinball.Engine/VPT/Table/Table.cs +++ b/VisualPinball.Engine/VPT/Table/Table.cs @@ -111,6 +111,7 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable) private readonly Dictionary _textBoxes = new Dictionary(); private readonly Dictionary _timers = new Dictionary(); private readonly Dictionary _triggers = new Dictionary(); + private readonly Dictionary _troughs = new Dictionary(); public Bumper.Bumper Bumper(string name) => _bumpers[name]; public Decal.Decal Decal(int i) => _decals[i]; @@ -131,6 +132,7 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable) public TextBox.TextBox TextBox(string name) => _textBoxes[name]; public Timer.Timer Timer(string name) => _timers[name]; public Trigger.Trigger Trigger(string name) => _triggers[name]; + public Trough.Trough Trough(string name) => _troughs[name]; public IEnumerable Renderables => new IRenderable[] { this } .Concat(_bumpers.Values) @@ -166,9 +168,10 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable) .Concat(_surfaces.Values) .Concat(_textBoxes.Values) .Concat(_timers.Values) - .Concat(_triggers.Values); + .Concat(_triggers.Values) + .Concat(_troughs.Values); - public IEnumerable ItemDatas => new ItemData[] {} + public IEnumerable ItemDatas => new ItemData[] { } .Concat(_bumpers.Values.Select(i => i.Data)) .Concat(_decals.Select(i => i.Data)) .Concat(_dispReels.Values.Select(i => i.Data)) @@ -187,7 +190,8 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable) .Concat(_surfaces.Values.Select(i => i.Data)) .Concat(_textBoxes.Values.Select(i => i.Data)) .Concat(_timers.Values.Select(i => i.Data)) - .Concat(_triggers.Values.Select(i => i.Data)); + .Concat(_triggers.Values.Select(i => i.Data)) + .Concat(_troughs.Values.Select(i => i.Data)); public IEnumerable Hittables => new IHittable[] {this} .Concat(_bumpers.Values) @@ -204,7 +208,6 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable) .Concat(_triggers.Values) .SelectMany(ApplyColliderOverrides); - public IEnumerable Playables => new IPlayable[0] .Concat(_bumpers.Values) .Concat(_flippers.Values) @@ -228,12 +231,18 @@ private IEnumerable ApplyColliderOverrides(IHittable hittable) .Concat(_spinners.Values) .Concat(_triggers.Values); + public IEnumerable SwitchableDevices => new ISwitchableDevice[0] + .Concat(_troughs.Values); + public IEnumerable Coilables => new ICoilable[0] .Concat(_bumpers.Values) .Concat(_flippers.Values) .Concat(_kickers.Values) .Concat(_plungers.Values); + public IEnumerable CoilableDevices => new ICoilableDevice[0] + .Concat(_troughs.Values); + private void AddItem(string name, TItem item, IDictionary d, bool updateStorageIndices) where TItem : IItem { if (updateStorageIndices) { @@ -324,6 +333,10 @@ private Dictionary GetItemDictionary() where T : IItem return _triggers as Dictionary; } + if (typeof(T) == typeof(VPT.Trough.Trough)) { + return _troughs as Dictionary; + } + return null; } @@ -394,7 +407,6 @@ public void ReplaceAll(IEnumerable items) where T : IItem /// True if the game item exists, false otherwise public bool Has(string name) where T : IItem => GetItemDictionary().ContainsKey(name); - /// /// Returns all game items of a given type. /// @@ -521,6 +533,7 @@ public RenderObjectGroup GetRenderObjects(Table table, Origin origin = Origin.Gl public HitObject[] GetHitShapes() => _hitGenerator.GenerateHitObjects(this).ToArray(); public bool IsCollidable => true; + public bool HasTrough => _troughs.Count > 0; public HitPlane GeneratePlayfieldHit() => _hitGenerator.GeneratePlayfieldHit(this); public HitPlane GenerateGlassHit() => _hitGenerator.GenerateGlassHit(this); diff --git a/VisualPinball.Engine/VPT/Table/TableLoader.cs b/VisualPinball.Engine/VPT/Table/TableLoader.cs index 6b2370980..3f65d59df 100644 --- a/VisualPinball.Engine/VPT/Table/TableLoader.cs +++ b/VisualPinball.Engine/VPT/Table/TableLoader.cs @@ -112,6 +112,7 @@ public static void LoadGameItem(byte[] itemData, int storageIndex, out ItemType case ItemType.TextBox: item = new TextBox.TextBox(reader, itemName); break; case ItemType.Timer: item = new Timer.Timer(reader, itemName); break; case ItemType.Trigger: item = new Trigger.Trigger(reader, itemName); break; + case ItemType.Trough: item = new Trough.Trough(reader, itemName); break; default: Logger.Info("Unhandled item type " + itemType); itemType = ItemType.Invalid; break; @@ -238,6 +239,11 @@ private static void LoadGameItems(Table table, CFStorage storage) table.Add(item); break; } + case ItemType.Trough: { + var item = new Trough.Trough(reader, itemName); + table.Add(item); + break; + } } } } diff --git a/VisualPinball.Engine/VPT/Trough.meta b/VisualPinball.Engine/VPT/Trough.meta new file mode 100644 index 000000000..d0ffa86e7 --- /dev/null +++ b/VisualPinball.Engine/VPT/Trough.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3fd1432f7bc694b779c7e8e0d9018022 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Engine/VPT/Trough/Trough.cs b/VisualPinball.Engine/VPT/Trough/Trough.cs new file mode 100644 index 000000000..035dea9d6 --- /dev/null +++ b/VisualPinball.Engine/VPT/Trough/Trough.cs @@ -0,0 +1,68 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using VisualPinball.Engine.Game; +using VisualPinball.Engine.Game.Engines; + +namespace VisualPinball.Engine.VPT.Trough +{ + public class Trough : Item, ISwitchableDevice, ICoilableDevice + { + public override string ItemName { get; } = "Trough"; + public override string ItemGroupName { get; } = null; + + public const string JamSwitchId = "jam"; + public const string EjectCoilId = "eject"; + public const string EntryCoilId = "entry"; + + public IEnumerable AvailableSwitches => Enumerable.Repeat(0, Data.SwitchCount) + .Select((_, i) => new GamelogicEngineSwitch {Description = SwitchDescription(i), Id = $"{i + 1}"}) + .Concat( new[]{ new GamelogicEngineSwitch{Description = "Jam Switch", Id = JamSwitchId} }); + + public IEnumerable AvailableCoils => new[] { + new GamelogicEngineCoil {Description = "Entry", Id = EntryCoilId}, + new GamelogicEngineCoil {Description = "Eject", Id = EjectCoilId} + }; + + public Trough(TroughData data) : base(data) + { + } + + public Trough(BinaryReader reader, string itemName) : this(new TroughData(reader, itemName)) + { + } + + private string SwitchDescription(int i) + { + if (i == 0) { + return "Ball 1 (eject)"; + } + + return i == Data.SwitchCount - 1 + ? $"Ball {i + 1} (entry)" + : $"Ball {i + 1}"; + } + + public static Trough GetDefault(Table.Table table) + { + var primitiveData = new TroughData(table.GetNewName("Trough")); + return new Trough(primitiveData); + } + } +} diff --git a/VisualPinball.Engine/VPT/Trough/Trough.cs.meta b/VisualPinball.Engine/VPT/Trough/Trough.cs.meta new file mode 100644 index 000000000..c25340d05 --- /dev/null +++ b/VisualPinball.Engine/VPT/Trough/Trough.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 359e5a55a56254a68931446a9b6d6e9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs b/VisualPinball.Engine/VPT/Trough/TroughData.cs new file mode 100644 index 000000000..74e79b289 --- /dev/null +++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs @@ -0,0 +1,87 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#region ReSharper +// ReSharper disable UnassignedField.Global +// ReSharper disable StringLiteralTypo +// ReSharper disable FieldCanBeMadeReadOnly.Global +// ReSharper disable ConvertToConstant.Global +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using VisualPinball.Engine.IO; +using VisualPinball.Engine.VPT.Table; + +namespace VisualPinball.Engine.VPT.Trough +{ + [Serializable] + public class TroughData : ItemData + { + public override string GetName() => Name; + public override void SetName(string name) { Name = name; } + + [BiffString("NAME", IsWideString = true, Pos = 1)] + public string Name; + + [BiffString("ENTK", Pos = 2)] + public string EntryKicker = string.Empty; + + [BiffString("EXIT", Pos = 3)] + public string ExitKicker = string.Empty; + + [BiffString("JAMS", Pos = 4)] + public string JamSwitch = string.Empty; + + [BiffInt("BCNT", Pos = 5)] + public int BallCount = 6; + + [BiffInt("SCNT", Pos = 6)] + public int SwitchCount = 6; + + [BiffInt("TIME", Pos = 7)] + public int SettleTime = 100; + + public TroughData(string name) : base(StoragePrefix.GameItem) + { + Name = name; + } + + #region BIFF + + static TroughData() + { + Init(typeof(TroughData), Attributes); + } + + public TroughData(BinaryReader reader, string storageName) : base(storageName) + { + Load(this, reader, Attributes); + } + + public override void Write(BinaryWriter writer, HashWriter hashWriter) + { + writer.Write((int)ItemType.Trough); + WriteRecord(writer, Attributes, hashWriter); + WriteEnd(writer, hashWriter); + } + + private static readonly Dictionary> Attributes = new Dictionary>(); + + #endregion + } +} diff --git a/VisualPinball.Engine/VPT/Trough/TroughData.cs.meta b/VisualPinball.Engine/VPT/Trough/TroughData.cs.meta new file mode 100644 index 000000000..6de4b2d57 --- /dev/null +++ b/VisualPinball.Engine/VPT/Trough/TroughData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 23d7577ef803444a88c18204c09da04b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md index 01d407a33..1309e9816 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.md @@ -1,55 +1,58 @@ # Coil Manager -On a real pinball table, most moving parts, including the flippers, are triggered by [coils](https://en.wikipedia.org/wiki/Inductor) (also called [solenoids](https://en.wikipedia.org/wiki/Solenoid)). It's the job of the [gamelogic engine](~/creators-guide/manual/gamelogic-engine.md) to trigger them when needed. +On a real pinball table most moving parts, including the flippers, are triggered by [coils](https://en.wikipedia.org/wiki/Inductor) (also called [solenoids](https://en.wikipedia.org/wiki/Solenoid)). In VPE it's the job of the [gamelogic engine](~/creators-guide/manual/gamelogic-engine.md) to trigger them when needed. -On a typical table there are usually several coils that need to be wired up to the controller board. In VPE, you can do that with the coil manager under *Visual Pinball -> Coil Manager*. +Just as the coils are physically wired to the power driver board on a regular machine they can be virtually connected in VPE using the coil manager under *Visual Pinball -> Coil Manager*. ![Coil Manager](coil-manager.png) ## Setup -Every row in the coil manager corresponds to a wire going from the gamelogic engine output to the coil. Similar to switches, a coil can be linked to multiple outputs, and an output can be linked to multiple coils. +Every row in the coil manager corresponds to a logical wire going from a gamelogic engine output to the coil. As with switches, a single coil can be linked to multiple outputs, and an output can be linked to multiple coils. ### IDs -The first column **ID** shows the coil names that the gamelogic engine expects to be wired up. +The first column, **ID** shows the name that the gamelogic engine exports for each coil. > [!note] -> As we cannot be 100% sure that the gamelogic engine has accurate data about the coil names, you can also add coil IDs yourself, but that should be the exception. +> As we cannot be 100% sure that the gamelogic engine has accurate data about the coil names, you can also add coil IDs manually, but that should be the exception. ### Description -The **Description** column is an optional free text field. If you're setting up a re-creation, that's where you typically put what is in the game manual. It's purely for your own benefit, and you can keep this empty if you want. +The **Description** column is optional. If you're setting up a re-creation, you would typically use this for the coil name from the game manual. It's purely for your own benefit, and you can keep this empty if you want. ### Destination -The **Destination** column defines where the element in the next column is located. Currently, VPE only supports playfield items with one coil. In the future, VPE will support devices with multiple coils, which will also be listed here. +The **Destination** column defines where the element in the following column is located. There are two options: + +- *Playfield* lets you select a game element on the playfield that features the coil +- *Device* lets you choose a *coil device*, a mechanism which may include multiple coils, such as a [trough](../manual/mechanisms/troughs.md). ### Element -The **Element** column is where you choose the playfield element with the coil. VPE can receive coil events for bumpers, flippers, kickers and plungers. +The **Element** column is where you choose which specifc element in the destination column should be activated. VPE can receive coil events for bumpers, flippers, kickers and plungers and coil devices. > [!note] -> Bumpers are currently hard-wired, i.e. their switch will directly trigger the coil without going through the gamelogic engine. That means they don't need to be configured in the switch- or coil manager. VPE will make this configurable in the future. +> Bumpers are currently hard-wired, i.e. their switch will directly trigger the coil without going through the gamelogic engine. That means they don't need to be configured in the switch or coil manager. VPE will make this configurable in the future. ### Type In the **Type** column you can define whether the coil is single-wound or dual-wound. There's an excellent page about the differences in [MPF's documentation](https://docs.missionpinball.org/en/latest/mechs/coils/dual_vs_single_wound.html). In short, dual-wound coils have two circuits, one for powering the coil, and one for holding it, while single-wound coils only have one. -This changes in how the coils power off: +This changes how the coil powers off: -- For **single-wound** coils, VPE uses the same coil event for powering on and off. +- For **single-wound** coils, VPE uses the same coil's events for powering on and off. - For **dual-wound** coils, it uses the *on* event from the main coil and the *off* event from the hold coil. ### Hold Coil -If the coil type is set to *Dual-Wound*, this column defines the hold coil event, i.e. on which event the coil powers off. +When the coil type is set to *Dual-Wound*, this column defines the hold coil event, i.e. the event on which the coil powers off. -These coils are pretty common. For example, *Medieval Madness* has the following dual-wound coils: +Dual-wound coils are fairly common. For example, *Medieval Madness* has the following dual-wound coils: ![Medieval Madness dual-wound coils](dual-wound-coils.png) *From the Medieval Madness manual* -In VPE, this would map to the following configuration: +In VPE, the two flippers would map to the following configuration: ![Dual-wound example configuration](switch-manager-dual-wound.png) diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.png b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.png index 9e3c15f6a..7d45211ab 100644 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.png and b/VisualPinball.Unity/Documentation~/creators-guide/editor/coil-manager.png differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.md b/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.md index 98cba60ac..65c0f932f 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.md @@ -1,8 +1,8 @@ # Switch Manager -During gameplay, the [gamelogic engine](~/creators-guide/manual/gamelogic-engine.md) needs to know what is happening on the playfield. For that reason, real pinball tables have switches all over the playfield that signal when a ball rolls over a certain position. These switches are also built into targets, bumpers, kickers, and some other items (see *[Supported Game Mechanisms](#supported-game-mechanisms)* below). +During gameplay, the [gamelogic engine](~/creators-guide/manual/gamelogic-engine.md) needs to know what is happening on the playfield. For that, real pinball tables have switches on the playfield that signal when a ball rolls over or settles in a certain position. These switches are also built into targets, bumpers, kickers, and other mechanisms (see *[Supported Game Mechanisms](#supported-game-mechanisms)* below). -Wiring these switches up to the gamelogic engine per code can be a tedious process. That's why VPE provides a graphical interface where you can do it easily. It even can guess which switch maps to which game item, if you've named them accordingly. +Wiring these switches up to the gamelogic engine with code can be a tedious process, so VPE provides a graphical interface where you can do it easily. If you've named them appropriately it can even guess which switch maps to which game item. You can open the switch manager under *Visual Pinball -> Switch Manager*. @@ -10,68 +10,64 @@ You can open the switch manager under *Visual Pinball -> Switch Manager*. ## Setup -Imagine every row as a wire connecting the physical switch to the gamelogic engine. The relation between the two is *0..n -> 0..n*, meaning you can link multiple switches an input and a switch to multiple inputs. - -> [!note] -> We use the terms "open" and "close" for switches, since that's what they do in real life. In electrical terms these are called "NC" (normally closed) and "NO" (normally open). In summary, these terms are synonyms: -> -> | Technical | Status | Common Term | In code | -> |-----------|--------|-------------|---------| -> | NO | Open | Off | `false` | -> | NC | Closed | On | `true` | +Imagine every row as a wire connecting the physical switch to the gamelogic engine. The relation between the two is *0..n -> 0..n*, meaning you can link multiple switches to one input or a single switch to multiple inputs. ### IDs -The first column **ID** shows the switch names that the gamelogic engine expects to be wired up. +The first column **ID** shows the names of each switch that the gamelogic engine is aware of. > [!note] -> As we cannot be 100% sure that the gamelogic engine has accurate data about the switch names, you can also add switch IDs yourself, but that should be the exception. +> As we cannot be 100% sure that the gamelogic engine has accurate data about the switch names, you can also add switch IDs yourself, but those should be the exception. ### Description -The **Description** column is an optional free text field. If you're setting up a re-creation, that's where you typically put what's in the game manual. It's purely for your own benefit and you can keep this empty if you want. +The **Description** column is optional. If you're setting up a re-creation, you would typically use this for the switch name from the game manual. It's purely for your own benefit, and you can keep this empty if you want. ### Source -The **Source** column defines where the element in the next column is located. There are three options: +The **Source** column defines where the element in the following column originates. There are four options: + +- *Playfield* lets you choose a game item from the playfield +- *Input System* lets you choose an input action from a pre-defined list, e.g. cabinet switches +- *Constant* sets the switch once at the beginning of the game to the given value. +- *Device* lets you choose a switch device containing the switch. Switch devices are mechanisms that include multiple switches, for example [troughs](../manual/mechanisms/troughs.md). -- *Playfield* lets you choose a game item in the playfield -- *Input System* lets you choose an input action from a pre-defined list, i.e. cabinet switches -- *Constant* sets the switch at the beginning of the game to the given value. ### Element The **Element** column is where you choose which element triggers the switch. -For **Playfield**, you can choose a game item that triggers switch events. Currently, VPE only emits switch events for items that would do the same in real life, i.e. bumpers, flippers, gates, targets, kickers, spinners and triggers. +For **Playfield** sources, you can choose a game item that triggers switch events. Currently, VPE only emits switch events for items that would do so in real life, i.e. bumpers, flippers, gates, targets, kickers, spinners and triggers. > [!note] > We realize that you might want to use other game items like ramps and walls to emit switch events as well, and we will address this at some point, but for now we're keeping it simple. -If **Input System** is selected, you choose which input action to use. We call it "input action", because it's not an actual key binding. While actions have default key bindings, the final bindings will be defined in the host application (the VPE player). So what VPE is dealing with in terms of keyboard input is what we call *input actions*. +If **Input System** is selected, you choose which input action to use (it's an "action", because it's not an permanent key binding). Actions may have default key bindings, but the final bindings to a key or other input will be defined in the host application (the VPE player). + +If the source is a **Device**, then there are two values to select. The actual switch device, and which switch of that device should be connected to the gamelogic engine. Finally, if **Constant** is selected, you choose the value that will be permanently set at the beginning of the game. ### Pulse Delay -Internally, VPX connects switches to events. For example, a trigger on the playfield has a `Hit` event, which occurs when the ball rolls into the trigger's zone, and an `UnHit` event when the ball leaves that zone. These two events close and open the trigger's switch. +Internally, VPX connects switches to events. For example, a trigger on the playfield has a `Hit` event, which occurs when the ball rolls into the trigger's collision zone, and an `UnHit` event when the ball leaves that zone. These two events close and open the trigger's switch. -However, not all mechanisms behave like that. For example a spinner just emits one `Spin` event. So in order to not let the switch closed indefinitely, VPE automatically re-opens it after a given delay. +However, not all mechanisms behave like that. For example a spinner emits a single `Spin` event. So to prevent the switch from being closed indefinitely VPE automatically re-opens it after a given delay. -We call that the **Pulse Delay**. "Pulse", because it gets triggered by one event and opens immediately right after. +We call that the **Pulse Delay**. "Pulse", because it gets triggered by one event and reopens after a brief delay. -In most cases, you can leave the default delay of 250ms. What's important is that gamelogic engine gets notified not too long after the switch was closed. Note that when setting it to 0, the switch will stay closed. +In most cases, you can leave the default delay of 250ms. What's important is that the gamelogic engine gets notified not too long after the switch was closed. Note that if pulse delay is set to 0, the switch will stay closed. ## Supported Game Mechanisms Below a list of game mechanisms that contain built-in switches. -| | Close | Open | +| | Closes | Opens | |-------------|--------------------------------------------------------|------------------------------------------------------------------------------| -| **Bumper** | On ball collision | *Opens after pulse delay* | +| **Bumper** | On ball collision | *After pulse delay* | | **Flipper** | On EOS, i.e. when the flipper reaches its end position | When the flipper switch is opened, i.e. the flipper starts moving down again | | **Gate** | When ball is passing through the gate | When ball has passed through | -| **Target** | On collision | *Opens after pulse delay* | -| **Kicker** | When ball goes into the kicker | When ball's outside the kicker | -| **Spinner** | On each spin | *Opens after pulse delay* | +| **Target** | On collision | *After pulse delay* | +| **Kicker** | When ball enters the kicker | When ball's outside the kicker | +| **Spinner** | On each spin | *After pulse delay* | | **Trigger** | When the ball rolls over the trigger | When the ball is outside of the trigger | \ No newline at end of file diff --git a/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.png b/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.png index 76ecc4149..4e405292b 100644 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.png and b/VisualPinball.Unity/Documentation~/creators-guide/editor/switch-manager.png differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/flipper.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/flipper.md deleted file mode 100644 index df242232a..000000000 --- a/VisualPinball.Unity/Documentation~/creators-guide/manual/flipper.md +++ /dev/null @@ -1,5 +0,0 @@ -# Flipper - -Flippers. They flip. - -[You can program them too](xref:VisualPinball.Unity.FlipperApi)! \ No newline at end of file diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/items/flippers.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/flippers.md similarity index 100% rename from VisualPinball.Unity/Documentation~/creators-guide/manual/items/flippers.md rename to VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/flippers.md diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-coils.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-coils.png new file mode 100644 index 000000000..61f32fba5 Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-coils.png differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png new file mode 100644 index 000000000..b158e77b1 Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-inspector.png differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-switches.png b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-switches.png new file mode 100644 index 000000000..877485438 Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/trough-switches.png differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md new file mode 100644 index 000000000..79fefbf78 --- /dev/null +++ b/VisualPinball.Unity/Documentation~/creators-guide/manual/mechanisms/troughs.md @@ -0,0 +1,31 @@ +# Troughs / Ball Drains + +If you are unfamiliar with ball troughs, have a quick look at [MPF's documentation](https://mpf-docs.readthedocs.io/en/latest/mechs/troughs/), which does an excellent job explaining them. + +VPE comes with a trough mechanism that simulates the behaviour of a real-world ball trough. This is especially important when emulating existing games, since the [gamelogic engine](../gamelogic-engine.md) expects the trough's switches to be in a plausible state, or else it may have errors. + +## Creating a Trough + +When importing a `.vpx` file that doesn't have any troughs (which is likely, because Visual Pinball doesn't currently handle them in the same way as VPE), VPE will automatically add a main trough to the root of the table. If you're creating a trough for a new game, click on the *Trough* button in the toolbox. + +## Linking to the Playfield + + + +To interact with the game, you'll need to setup an entry kicker to drain the ball into the trough, and an exit kicker to release a new ball from the trough. This terminology may seem weird, since the ball *exits* the playfield when draining, but from the the trough's perspective, that's where the ball *enters*. + +You can setup the kickers by selecting the trough in the hierarchy panel and linking them to the desired kickers using the inspector. + +## Switch Setup + +The number of simulated switches in the trough depends on the *Switch Count* property in the inspector panel. For recreations, you can quickly determine the number of trough switches by looking at the switch matrix in the operation manual, it usually matches the number of balls installed in the game. + +Open the [switch manager](../../editor/switch-manager.md) and add the trough switches if they're not already there. As *Destination* select "Device", under *Element*, select the trough you've created and which switch to connect. For a five-ball trough, it will look something like this: + +![Switch Manager](trough-switches.png) + +## Coil Setup + +VPE's trough supports two coils, an entry coil which drains the ball from the outhole into the trough, and an eject coil which pushes a new ball into the plunger lane. Open the [coil manager](../../editor/coil-manager.md), find or add the coils, and link them to the trough like you did with the switches: + +![Coil Manager](trough-coils.png) \ No newline at end of file diff --git a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml index 88d3be812..e9a36bd3b 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml +++ b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml @@ -30,7 +30,9 @@ items: - name: Gamelogic Engine href: manual/gamelogic-engine.md - - name: Game Items + - name: Pinball Mechanisms items: + - name: Troughs / Ball Drains + href: manual/mechanisms/troughs.md - name: Flippers - href: manual/items/flippers.md \ No newline at end of file + href: manual/mechanisms/flippers.md \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs index 81f4d85d1..121716117 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs @@ -31,7 +31,7 @@ public class DragPointsHandler /// /// Authoring item /// - public IItemMainAuthoring Editable { get; private set; } + public IItemMainRenderableAuthoring Editable { get; private set; } /// /// Authoring item as IDragPointsEditable @@ -87,7 +87,7 @@ public class DragPointsHandler /// public DragPointsHandler(Object target) { - Editable = target as IItemMainAuthoring + Editable = target as IItemMainRenderableAuthoring ?? throw new ArgumentException("Target must extend `IEditableItemAuthoring`."); DragPointEditable = target as IDragPointsEditable diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsItemInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsItemInspector.cs index bb1e939a4..ce71c8a9d 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsItemInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsItemInspector.cs @@ -27,7 +27,7 @@ namespace VisualPinball.Unity.Editor public abstract class DragPointsItemInspector : ItemMainInspector, IDragPointsItemInspector where TData : ItemData where TItem : Item, IHittable, IRenderable - where TMainAuthoring : ItemMainAuthoring + where TMainAuthoring : ItemMainRenderableAuthoring { /// /// Catmull Curve Handler @@ -103,7 +103,7 @@ public void PasteDragPoint(int controlId) /// True if game item is locked, false otherwise. public bool IsItemLocked() { - return !(target is IItemMainAuthoring editable) || editable.IsLocked; + return !(target is IItemMainRenderableAuthoring editable) || editable.IsLocked; } /// @@ -143,7 +143,7 @@ public void FlipDragPoints(FlipAxis flipAxis) public void RemapControlPoints() { var rebuilt = DragPointsHandler.RemapControlPoints(); - if (rebuilt && target is IItemMainAuthoring meshAuthoring) { + if (rebuilt && target is IItemMainRenderableAuthoring meshAuthoring) { meshAuthoring.SetMeshDirty(); } } @@ -179,7 +179,7 @@ public void PrepareUndo(string message) // Set MeshDirty to true there so it'll trigger again after Undo var recordObjs = new List(); - if (target is IItemMainAuthoring meshAuthoring) { + if (target is IItemMainRenderableAuthoring meshAuthoring) { meshAuthoring.SetMeshDirty(); recordObjs.Add(this); } @@ -191,7 +191,7 @@ public override void OnInspectorGUI() { base.OnInspectorGUI(); - var editable = target as IItemMainAuthoring; + var editable = target as IItemMainRenderableAuthoring; var dragPointEditable = target as IDragPointsEditable; if (editable == null || dragPointEditable == null) { return; @@ -243,7 +243,7 @@ public override void OnInspectorGUI() private void UpdateDragPointsLock() { - if (target is IItemMainAuthoring editable && DragPointsHandler.UpdateDragPointsLock(editable.IsLocked)) { + if (target is IItemMainRenderableAuthoring editable && DragPointsHandler.UpdateDragPointsLock(editable.IsLocked)) { HandleUtility.Repaint(); } } @@ -256,14 +256,14 @@ private void OnDragPointPositionChange(Vector3 newPos) private void OnUndoRedoPerformed() { RemapControlPoints(); - if (target is IItemMainAuthoring meshAuthoring) { + if (target is IItemMainRenderableAuthoring meshAuthoring) { meshAuthoring.SetMeshDirty(); } } protected virtual void OnSceneGUI() { - var editable = target as IItemMainAuthoring; + var editable = target as IItemMainRenderableAuthoring; var dragPointEditable = target as IDragPointsEditable; var bh = target as Behaviour; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs new file mode 100644 index 000000000..919fb3ce3 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.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 . + +// ReSharper disable AssignmentInConditionalExpression + +using UnityEditor; +using UnityEngine; +using VisualPinball.Engine.VPT.Trough; +using VisualPinball.Engine.VPT.Kicker; +using VisualPinball.Engine.VPT.Trigger; + +namespace VisualPinball.Unity.Editor +{ + [CustomEditor(typeof(TroughAuthoring))] + public class TroughInspector : ItemMainInspector + { + public override void OnInspectorGUI() + { + ItemReferenceField("Entry Kicker", "entryKicker", ref Data.EntryKicker); + ItemReferenceField("Exit Kicker", "exitKicker", ref Data.ExitKicker); + ItemReferenceField("Jam Switch", "jamSwitch", ref Data.JamSwitch); + + ItemDataField("Max Balls", ref Data.BallCount, false); + ItemDataField("Switch Count", ref Data.SwitchCount, false); + ItemDataField("Settle Time", ref Data.SettleTime, false); + } + + protected override void FinishEdit(string label, bool dirtyMesh = true) + { + base.FinishEdit(label, dirtyMesh); + ItemAuthoring.UpdatePosition(); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs.meta new file mode 100644 index 000000000..5f04f1680 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Inspectors/TroughInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f921351c72e0e4f6f99bbbf5bc8be9fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListData.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListData.cs index 0a6d45378..ca5e98340 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListData.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListData.cs @@ -29,7 +29,7 @@ public class CoilListData : IManagerListData [ManagerListColumn(Order = 2, HeaderName = "Destination", Width = 150)] public int Destination; - [ManagerListColumn(Order = 3, HeaderName = "Element", Width = 200)] + [ManagerListColumn(Order = 3, HeaderName = "Element", Width = 270)] public string Element; [ManagerListColumn(Order = 4, HeaderName = "Type", Width = 110)] diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs index 77a22b58f..ff87480ff 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilListViewItemRenderer.cs @@ -27,7 +27,7 @@ namespace VisualPinball.Unity.Editor { public class CoilListViewItemRenderer { - private readonly string[] OPTIONS_COIL_DESTINATION = { "Playfield" }; + private readonly string[] OPTIONS_COIL_DESTINATION = { "Playfield", "Device" }; private readonly string[] OPTIONS_COIL_TYPE = { "Single-Wound", "Dual-Wound" }; private enum CoilListColumn @@ -42,13 +42,15 @@ private enum CoilListColumn private readonly List _gleCoils; private readonly Dictionary _coils; + private readonly Dictionary _coilDevices; private AdvancedDropdownState _itemPickDropdownState; - public CoilListViewItemRenderer(List gleCoils, Dictionary coils) + public CoilListViewItemRenderer(List gleCoils, Dictionary coils, Dictionary coilDevices) { _gleCoils = gleCoils; _coils = coils; + _coilDevices = coilDevices; } public void Render(TableAuthoring tableAuthoring, CoilListData data, Rect cellRect, int column, Action updateAction) @@ -163,6 +165,13 @@ private void RenderElement(TableAuthoring tableAuthoring, CoilListData coilListD case CoilDestination.Playfield: RenderPlayfieldElement(tableAuthoring, coilListData, cellRect, updateAction); break; + + case CoilDestination.Device: + cellRect.width = cellRect.width / 2f - 5f; + RenderDeviceElement(tableAuthoring, coilListData, cellRect, updateAction); + cellRect.x += cellRect.width + 10f; + RenderDeviceItemElement(coilListData, cellRect, updateAction); + break; } } @@ -188,6 +197,50 @@ private void RenderPlayfieldElement(TableAuthoring tableAuthoring, CoilListData } } + private void RenderDeviceElement(TableAuthoring tableAuthoring, CoilListData coilListData, Rect cellRect, Action updateAction) + { + if (GUI.Button(cellRect, coilListData.Device, EditorStyles.objectField) || GUI.Button(cellRect, "", GUI.skin.GetStyle("IN ObjectField"))) + { + if (_itemPickDropdownState == null) { + _itemPickDropdownState = new AdvancedDropdownState(); + } + + var dropdown = new ItemSearchableDropdown( + _itemPickDropdownState, + tableAuthoring, + "Coil Devices", + item => { + coilListData.Device = item.Name; + updateAction(coilListData); + } + ); + dropdown.Show(cellRect); + } + } + + private void RenderDeviceItemElement(CoilListData coilListData, Rect cellRect, Action updateAction) + { + EditorGUI.BeginDisabledGroup(string.IsNullOrEmpty(coilListData.Device)); + + var currentIndex = 0; + var coilLabels = new string[0]; + ICoilDeviceAuthoring coilDevice = null; + if (!string.IsNullOrEmpty(coilListData.Device) && _coilDevices.ContainsKey(coilListData.Device.ToLower())) { + coilDevice = _coilDevices[coilListData.Device.ToLower()]; + coilLabels = coilDevice.AvailableCoils.Select(s => s.Description).ToArray(); + currentIndex = coilDevice.AvailableCoils.TakeWhile(s => s.Id != coilListData.DeviceItem).Count(); + } + EditorGUI.BeginChangeCheck(); + var newIndex = EditorGUI.Popup(cellRect, currentIndex, coilLabels); + if (EditorGUI.EndChangeCheck() && coilDevice != null) { + if (currentIndex != newIndex) { + coilListData.DeviceItem = coilDevice.AvailableCoils.ElementAt(newIndex).Id; + updateAction(coilListData); + } + } + EditorGUI.EndDisabledGroup(); + } + private void RenderType(CoilListData coilListData, Rect cellRect, Action updateAction) { if (coilListData.Destination == CoilDestination.Playfield) @@ -209,13 +262,16 @@ private UnityEngine.Texture GetIcon(CoilListData coilListData) switch (coilListData.Destination) { case CoilDestination.Playfield: - { - if (_coils.ContainsKey(coilListData.PlayfieldItem.ToLower())) - { - icon = Icons.ByComponent(_coils[coilListData.PlayfieldItem.ToLower()], size: IconSize.Small); - } - break; + if (_coils.ContainsKey(coilListData.PlayfieldItem.ToLower())) { + icon = Icons.ByComponent(_coils[coilListData.PlayfieldItem.ToLower()], size: IconSize.Small); } + break; + + case CoilDestination.Device: + if (_coilDevices.ContainsKey(coilListData.Device.ToLower())) { + icon = Icons.ByComponent(_coilDevices[coilListData.Device.ToLower()], IconSize.Small); + } + break; } return icon; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs index d046793ca..8a50e3ac0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Coil/CoilManager.cs @@ -42,6 +42,7 @@ class CoilManager : ManagerWindow private readonly List _gleCoils = new List(); private readonly Dictionary _coils = new Dictionary(); + private readonly Dictionary _coilDevices = new Dictionary(); private CoilListViewItemRenderer _listViewItemRenderer; @@ -68,7 +69,7 @@ public override void OnEnable() protected override void OnFocus() { - _listViewItemRenderer = new CoilListViewItemRenderer(_gleCoils, _coils); + _listViewItemRenderer = new CoilListViewItemRenderer(_gleCoils, _coils, _coilDevices); base.OnFocus(); } @@ -99,7 +100,7 @@ protected override void OnButtonBarGUI() if (_tableAuthoring != null) { RecordUndo("Populate all coil mappings"); - _tableAuthoring.Table.Mappings.PopulateCoils(GetAvailableEngineCoils(), _tableAuthoring.Table.Coilables); + _tableAuthoring.Table.Mappings.PopulateCoils(GetAvailableEngineCoils(), _tableAuthoring.Table.Coilables, _tableAuthoring.Table.CoilableDevices); Reload(); } } @@ -123,9 +124,6 @@ protected override void OnListViewItemRenderer(CoilListData data, Rect cellRect, RecordUndo(DataTypeName + " Data Change"); coilListData.Update(); - - var coil = _tableAuthoring.Table.Coilables.FirstOrDefault(c => c.Name == coilListData.PlayfieldItem); - coil.IsDualWound = coilListData.Type == CoilType.DualWound; }); } @@ -193,13 +191,17 @@ private void DisplayMessage(string message) private void RefreshCoils() { _coils.Clear(); + _coilDevices.Clear(); - if (_tableAuthoring != null) - { - foreach (var item in _tableAuthoring.GetComponentsInChildren()) - { + if (_tableAuthoring != null) { + + foreach (var item in _tableAuthoring.GetComponentsInChildren()) { _coils.Add(item.Name.ToLower(), item); } + + foreach (var item in _tableAuthoring.GetComponentsInChildren()) { + _coilDevices.Add(item.Name.ToLower(), item); + } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListData.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListData.cs index 21f8e4a74..57213c1bb 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListData.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListData.cs @@ -29,7 +29,7 @@ public class SwitchListData : IManagerListData [ManagerListColumn(Order = 2, HeaderName = "Source", Width = 150)] public int Source; - [ManagerListColumn(Order = 3, HeaderName = "Element", Width = 200)] + [ManagerListColumn(Order = 3, HeaderName = "Element", Width = 270)] public string Element; [ManagerListColumn(Order = 5, HeaderName = "Pulse Delay", Width = 100)] diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs index a1092047c..5aa4104e8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchListViewItemRenderer.cs @@ -27,7 +27,7 @@ namespace VisualPinball.Unity.Editor { public class SwitchListViewItemRenderer { - private readonly string[] OPTIONS_SWITCH_SOURCE = { "Input System", "Playfield", "Constant" }; + private readonly string[] OPTIONS_SWITCH_SOURCE = { "Input System", "Playfield", "Constant", "Device" }; private readonly string[] OPTIONS_SWITCH_CONSTANT = { "NC - Normally Closed", "NO - Normally Open" }; private struct InputSystemEntry @@ -47,14 +47,16 @@ private enum SwitchListColumn private readonly List _gleSwitches; private readonly Dictionary _switches; + private readonly Dictionary _switchDevices; private readonly InputManager _inputManager; private AdvancedDropdownState _itemPickDropdownState; - public SwitchListViewItemRenderer(List gleSwitches, Dictionary switches, InputManager inputManager) + public SwitchListViewItemRenderer(List gleSwitches, Dictionary switches, Dictionary switchDevices, InputManager inputManager) { _gleSwitches = gleSwitches; _switches = switches; + _switchDevices = switchDevices; _inputManager = inputManager; } @@ -180,6 +182,13 @@ private void RenderElement(TableAuthoring tableAuthoring, SwitchListData switchL case SwitchSource.Constant: RenderConstantElement(switchListData, cellRect, updateAction); break; + + case SwitchSource.Device: + cellRect.width = cellRect.width / 2f - 5f; + RenderDeviceElement(tableAuthoring, switchListData, cellRect, updateAction); + cellRect.x += cellRect.width + 10f; + RenderDeviceItemElement(switchListData, cellRect, updateAction); + break; } } @@ -262,6 +271,50 @@ private void RenderConstantElement(SwitchListData switchListData, Rect cellRect, } } + private void RenderDeviceElement(TableAuthoring tableAuthoring, SwitchListData switchListData, Rect cellRect, Action updateAction) + { + if (GUI.Button(cellRect, switchListData.Device, EditorStyles.objectField) || GUI.Button(cellRect, "", GUI.skin.GetStyle("IN ObjectField"))) + { + if (_itemPickDropdownState == null) { + _itemPickDropdownState = new AdvancedDropdownState(); + } + + var dropdown = new ItemSearchableDropdown( + _itemPickDropdownState, + tableAuthoring, + "Switch Devices", + item => { + switchListData.Device = item.Name; + updateAction(switchListData); + } + ); + dropdown.Show(cellRect); + } + } + + private void RenderDeviceItemElement(SwitchListData switchListData, Rect cellRect, Action updateAction) + { + EditorGUI.BeginDisabledGroup(string.IsNullOrEmpty(switchListData.Device)); + + var currentIndex = 0; + var switchLabels = new string[0]; + ISwitchDeviceAuthoring switchDevice = null; + if (!string.IsNullOrEmpty(switchListData.Device) && _switchDevices.ContainsKey(switchListData.Device.ToLower())) { + switchDevice = _switchDevices[switchListData.Device.ToLower()]; + switchLabels = switchDevice.AvailableSwitches.Select(s => s.Description).ToArray(); + currentIndex = switchDevice.AvailableSwitches.TakeWhile(s => s.Id != switchListData.DeviceItem).Count(); + } + EditorGUI.BeginChangeCheck(); + var newIndex = EditorGUI.Popup(cellRect, currentIndex, switchLabels); + if (EditorGUI.EndChangeCheck() && switchDevice != null) { + if (currentIndex != newIndex) { + switchListData.DeviceItem = switchDevice.AvailableSwitches.ElementAt(newIndex).Id; + updateAction(switchListData); + } + } + EditorGUI.EndDisabledGroup(); + } + private void RenderPulseDelay(SwitchListData switchListData, Rect cellRect, Action updateAction) { if (switchListData.Source == SwitchSource.Playfield && _switches.ContainsKey(switchListData.PlayfieldItem.ToLower())) { @@ -294,17 +347,23 @@ private UnityEngine.Texture GetIcon(SwitchListData switchListData) switch (switchListData.Source) { case SwitchSource.Playfield: { if (_switches.ContainsKey(switchListData.PlayfieldItem.ToLower())) { - icon = Icons.ByComponent(_switches[switchListData.PlayfieldItem.ToLower()], size: IconSize.Small); + icon = Icons.ByComponent(_switches[switchListData.PlayfieldItem.ToLower()], IconSize.Small); } break; } case SwitchSource.Constant: - icon = Icons.Switch(switchListData.Constant == SwitchConstant.NormallyClosed, size: IconSize.Small); + icon = Icons.Switch(switchListData.Constant == SwitchConstant.NormallyClosed, IconSize.Small); break; case SwitchSource.InputSystem: icon = Icons.Key(IconSize.Small); break; + + case SwitchSource.Device: + if (_switchDevices.ContainsKey(switchListData.Device.ToLower())) { + icon = Icons.ByComponent(_switchDevices[switchListData.Device.ToLower()], IconSize.Small); + } + break; } return icon; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchManager.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchManager.cs index d2091d6ea..32bba3844 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchManager.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Managers/Switch/SwitchManager.cs @@ -41,6 +41,7 @@ internal class SwitchManager : ManagerWindow private readonly List _gleSwitches = new List(); private readonly Dictionary _switches = new Dictionary(); + private readonly Dictionary _switchDevices = new Dictionary(); private InputManager _inputManager; private SwitchListViewItemRenderer _listViewItemRenderer; @@ -72,7 +73,7 @@ public override void OnEnable() protected override void OnFocus() { _inputManager = new InputManager(RESOURCE_PATH); - _listViewItemRenderer = new SwitchListViewItemRenderer(_gleSwitches, _switches, _inputManager); + _listViewItemRenderer = new SwitchListViewItemRenderer(_gleSwitches, _switches, _switchDevices, _inputManager); _needsAssetRefresh = true; base.OnFocus(); @@ -110,7 +111,7 @@ protected override void OnButtonBarGUI() if (_tableAuthoring != null) { RecordUndo("Populate all switch mappings"); - _tableAuthoring.Table.Mappings.PopulateSwitches(GetAvailableEngineSwitches(), _tableAuthoring.Table.Switchables); + _tableAuthoring.Table.Mappings.PopulateSwitches(GetAvailableEngineSwitches(), _tableAuthoring.Table.Switchables, _tableAuthoring.Table.SwitchableDevices); Reload(); } } @@ -201,13 +202,15 @@ private void DisplayMessage(string message) private void RefreshSwitches() { _switches.Clear(); + _switchDevices.Clear(); - if (_tableAuthoring != null) - { - foreach (var item in _tableAuthoring.GetComponentsInChildren()) - { + if (_tableAuthoring != null) { + foreach (var item in _tableAuthoring.GetComponentsInChildren()) { _switches.Add(item.Name.ToLower(), item); } + foreach (var item in _tableAuthoring.GetComponentsInChildren()) { + _switchDevices.Add(item.Name.ToLower(), item); + } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/trough.png b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/trough.png index a467b8881..0b58e5938 100644 Binary files a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/trough.png and b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_blue/trough.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/trough.png b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/trough.png index 6c0ca0135..0891bdc5e 100644 Binary files a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/trough.png and b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_gray/trough.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/trough.png b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/trough.png index 13c85e864..cc719d8a6 100644 Binary files a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/trough.png and b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_green/trough.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/trough.png b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/trough.png index d76a65a93..4d500ddfa 100644 Binary files a/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/trough.png and b/VisualPinball.Unity/VisualPinball.Unity.Editor/Resources/Icons/small_orange/trough.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs index 3250ee86a..eb4eb7ec8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Toolbox/ToolboxEditor.cs @@ -32,6 +32,7 @@ using VisualPinball.Engine.VPT.Surface; using VisualPinball.Engine.VPT.Table; using VisualPinball.Engine.VPT.Trigger; +using VisualPinball.Engine.VPT.Trough; using Light = VisualPinball.Engine.VPT.Light.Light; using Texture = UnityEngine.Texture; @@ -159,6 +160,10 @@ private void OnGUI() CreateItem(Primitive.GetDefault, "New Primitive"); } + if (CreateButton("Trough", Icons.Trough(color: iconColor), iconSize, buttonStyle)) { + CreateItem(Trough.GetDefault, "New Trough"); + } + GUILayout.EndHorizontal(); } @@ -190,7 +195,9 @@ private GameObject CreateRenderable(IRenderable renderable) private static GameObject GetOrCreateParent(Component tb, IItem renderable) { - var parent = tb.gameObject.transform.Find(renderable.ItemGroupName)?.gameObject; + var parent = string.IsNullOrEmpty(renderable.ItemGroupName) + ? tb.gameObject + : tb.gameObject.transform.Find(renderable.ItemGroupName)?.gameObject; if (parent == null) { parent = new GameObject(renderable.ItemGroupName); parent.transform.parent = tb.gameObject.transform; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs index ab44c9c94..f4c901ed6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs @@ -55,13 +55,14 @@ public IconVariant(string name, IconSize size, IconColor color) private const string TableName = "table"; private const string HitTargetName = "target"; private const string TriggerName = "trigger"; + private const string TroughName = "trough"; private const string SwitchNcName = "switch_nc"; private const string SwitchNoName = "switch_no"; private static readonly string[] Names = { BumperName, CoilName, FlipperName, GateName, KeyName, KickerName, LightName, PlayfieldName, PlungerName, PrimitiveName, RampName, RubberName, SpinnerName, SurfaceName, HitTargetName, TableName, TriggerName, - SwitchNcName, SwitchNoName + TroughName, SwitchNcName, SwitchNoName }; private readonly Dictionary _icons = new Dictionary(); @@ -108,6 +109,7 @@ private Icons() public static Texture2D Table(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(TableName, size, color); public static Texture2D Target(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(HitTargetName, size, color); public static Texture2D Trigger(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(TriggerName, size, color); + public static Texture2D Trough(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(TroughName, size, color); public static Texture2D Switch(bool normallyClosed, IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(normallyClosed ? SwitchNcName : SwitchNoName, size, color); public static Texture2D Coil(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(CoilName, size, color); public static Texture2D Key(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(KeyName, size, color); @@ -130,6 +132,7 @@ public static Texture2D ByComponent(T mb, IconSize size = IconSize.Large, Ico case SurfaceAuthoring _: return Surface(size, color); case HitTargetAuthoring _: return Target(size, color); case TriggerAuthoring _: return Trigger(size, color); + case TroughAuthoring _: return Trough(size, color); default: return null; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Gate/GateInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Gate/GateInspector.cs index 039d7029e..2364c17a1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Gate/GateInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Gate/GateInspector.cs @@ -38,7 +38,7 @@ public class GateInspector : ItemMainInspector protected void OnSceneGUI() { - if (target is IItemMainAuthoring editable) { + if (target is IItemMainRenderableAuthoring editable) { var position = editable.GetEditorPosition(); var transform = (target as MonoBehaviour).transform; if (transform != null && transform.parent != null) { diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemColliderInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemColliderInspector.cs index 734a830bc..d7bac471e 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemColliderInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemColliderInspector.cs @@ -29,7 +29,7 @@ public class ItemColliderInspector where TData : ItemData where TItem : Item, IHittable, IRenderable - where TMainAuthoring : ItemMainAuthoring + where TMainAuthoring : ItemMainRenderableAuthoring { protected TColliderAuthoring ColliderAuthoring; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs index 76f87c917..6e8fe6bf7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemInspector.cs @@ -15,10 +15,13 @@ // along with this program. If not, see . using System; +using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using VisualPinball.Engine.Game; using VisualPinball.Engine.Math; +using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Surface; namespace VisualPinball.Unity.Editor @@ -27,8 +30,9 @@ public abstract class ItemInspector : UnityEditor.Editor { public abstract MonoBehaviour UndoTarget { get; } - private TableAuthoring _table; - private SurfaceAuthoring _surface; + protected TableAuthoring _table; + + private Dictionary _refItems = new Dictionary(); private string[] _allMaterials = new string[0]; private string[] _allTextures = new string[0]; @@ -65,7 +69,7 @@ protected virtual void OnDisable() public override void OnInspectorGUI() { - if (!(target is IItemMainAuthoring item)) { + if (!(target is IItemMainRenderableAuthoring item)) { return; } @@ -99,21 +103,9 @@ private void PopulateDropDownOptions() } } - private void OnHierarchyChange() - { - if (target is MonoBehaviour bh && target is IIdentifiableItemAuthoring item && bh != null) { - var go = bh.gameObject; - if (item.Name != go.name) { - var oldName = item.Name; - item.Name = go.name; - ItemRenamed?.Invoke(item, oldName, go.name); - } - } - } - protected void OnPreInspectorGUI() { - if (!(target is IItemMainAuthoring item)) { + if (!(target is IItemMainRenderableAuthoring item)) { return; } @@ -238,28 +230,31 @@ protected void ItemDataField(string label, ref Engine.Math.Color field, bool dir } } - protected void SurfaceField(string label, ref string field, bool dirtyMesh = true) + protected void ItemReferenceField(string label, string cacheKey, ref string field, bool dirtyMesh = true) + where TItemAuthoring : ItemAuthoring + where TData : ItemData where TItem : Item, IRenderable { - if (_surface?.name != field) { - _surface = null; - } - - if (_surface == null && _table != null) { + if (!_refItems.ContainsKey(cacheKey) && _table != null) { var currentFieldName = field; - if (currentFieldName != null && _table.Table.Has(currentFieldName)) { - _surface = _table.gameObject.GetComponentsInChildren(true) + if (currentFieldName != null && _table.Table.Has(currentFieldName)) { + _refItems[cacheKey] = _table.gameObject.GetComponentsInChildren(true) .FirstOrDefault(s => s.name == currentFieldName); } } EditorGUI.BeginChangeCheck(); - _surface = (SurfaceAuthoring)EditorGUILayout.ObjectField(label, _surface, typeof(SurfaceAuthoring), true); + _refItems[cacheKey] = (TItemAuthoring)EditorGUILayout.ObjectField(label, _refItems.ContainsKey(cacheKey) ? _refItems[cacheKey] : null, typeof(TItemAuthoring), true); if (EditorGUI.EndChangeCheck()) { FinishEdit(label, dirtyMesh); - field = _surface != null ? _surface.name : string.Empty; + field = _refItems[cacheKey] != null ? _refItems[cacheKey].name : string.Empty; } } + protected void SurfaceField(string label, ref string field, bool dirtyMesh = true) + { + ItemReferenceField(label, "surface", ref field, dirtyMesh); + } + protected void DropDownField(string label, ref T field, string[] optionStrings, T[] optionValues, bool dirtyMesh = true, Action onChanged = null) where T : IEquatable { if (optionStrings == null || optionValues == null || optionStrings.Length != optionValues.Length) { @@ -341,7 +336,7 @@ protected virtual void FinishEdit(string label, bool dirtyMesh = true) Undo.RecordObject(UndoTarget, undoLabel); break; - case IItemMainAuthoring mainItem: + case IItemMainRenderableAuthoring mainItem: mainItem.SetMeshDirty(); Undo.RecordObject(UndoTarget, undoLabel); break; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMainInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMainInspector.cs index 53ada5029..e2f5ba167 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMainInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMainInspector.cs @@ -19,14 +19,13 @@ using System.Linq; using UnityEditor; using UnityEngine; -using VisualPinball.Engine.Game; using VisualPinball.Engine.VPT; namespace VisualPinball.Unity.Editor { public class ItemMainInspector : ItemInspector where TData : ItemData - where TItem : Item, IRenderable + where TItem : Item where TMainAuthoring : ItemMainAuthoring { protected TMainAuthoring ItemAuthoring; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMeshInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMeshInspector.cs index fa54e799e..9393a5d3d 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMeshInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMeshInspector.cs @@ -28,7 +28,7 @@ public class ItemMeshInspector : I where TMeshAuthoring : ItemMeshAuthoring where TData : ItemData where TItem : Item, IRenderable - where TMainAuthoring : ItemMainAuthoring + where TMainAuthoring : ItemMainRenderableAuthoring { protected TMeshAuthoring MeshAuthoring; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMovementInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMovementInspector.cs index d5faa55f9..6d3a5d167 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMovementInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/ItemMovementInspector.cs @@ -27,7 +27,7 @@ public class ItemMovementInspector where TData : ItemData where TItem : Item, IHittable, IRenderable - where TMainAuthoring : ItemMainAuthoring + where TMainAuthoring : ItemMainRenderableAuthoring { private TMovementAuthoring _movementAuthoring; diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/TransformInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/TransformInspector.cs index 63ce747d0..1436ff923 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/TransformInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/TransformInspector.cs @@ -34,7 +34,7 @@ public class TransformInspector : UnityEditor.Editor /// /// The first selected item /// - private IItemMainAuthoring _primaryItem; + private IItemMainRenderableAuthoring _primaryItem; /// /// On multi-selection, these are the other selected items. @@ -64,7 +64,7 @@ protected virtual void OnEnable() foreach (var t in targets) { // must be main but not the table itself - var item = (t as Transform)?.GetComponent(); + var item = (t as Transform)?.GetComponent(); useDefault = useDefault && (t as Transform)?.GetComponent() == null; if (item != null && !(item is TableAuthoring)) { @@ -361,7 +361,7 @@ private void FinishScale(Vector3 newScale) private class SecondaryItem { public Transform Transform; - public IItemMainAuthoring Item; + public IItemMainRenderableAuthoring Item; public Vector3 Offset; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs new file mode 100644 index 000000000..224e556b8 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs @@ -0,0 +1,76 @@ +// 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 NLog; +using Unity.Entities; + +namespace VisualPinball.Unity +{ + public class DeviceSwitch : IApiSwitch + { + private readonly bool _isPulseSwitch; + private List _engineSwitchIds; + private readonly IGamelogicEngineWithSwitches _engine; + private static VisualPinballSimulationSystemGroup SimulationSystemGroup => World.DefaultGameObjectInjectionWorld.GetOrCreateSystem(); + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public DeviceSwitch(bool isPulseSwitch, IGamelogicEngineWithSwitches engine) + { + _isPulseSwitch = isPulseSwitch; + _engine = engine; + } + + public void AddSwitchId(string switchId, int pulseDelay) + { + if (_engineSwitchIds == null) { + _engineSwitchIds = new List(); + } + + _engineSwitchIds.Add(new SwitchConfig(switchId, _isPulseSwitch, pulseDelay)); + } + + public void SetSwitch(bool closed) + { + if (_engine != null && _engineSwitchIds != null) { + foreach (var switchConfig in _engineSwitchIds) { + _engine.Switch(switchConfig.SwitchId, closed); + + // Schedule switch opening if closed and pulse + if (closed && switchConfig.IsPulseSwitch) { + SimulationSystemGroup.ScheduleSwitch(switchConfig.PulseDelay, + () => _engine.Switch(switchConfig.SwitchId, false)); + } + } + } else { + Logger.Warn("Cannot trigger device switch."); + } + } + + public void ScheduleSwitch(bool closed, int delay) + { + if (_engine != null && _engineSwitchIds != null) { + foreach (var switchConfig in _engineSwitchIds) { + SimulationSystemGroup.ScheduleSwitch(delay, + () => _engine.Switch(switchConfig.SwitchId, closed)); + } + } + else { + Logger.Warn("Cannot schedule device switch."); + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs.meta new file mode 100644 index 000000000..0e920b5e4 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/DeviceSwitch.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0a8ec62997da4b4da399005468e4db0 +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 9a64353a7..e40bd1d2b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Engine/DefaultGamelogicEngine.cs @@ -37,16 +37,29 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit private const string SwLeftFlipperEos = "s_left_flipper_eos"; private const string SwRightFlipper = "s_right_flipper"; private const string SwRightFlipperEos = "s_right_flipper_eos"; + private const string SwTrough1 = "s_trough1"; + private const string SwTrough2 = "s_trough2"; + private const string SwTrough3 = "s_trough3"; + private const string SwTrough4 = "s_trough4"; + private const string SwTrough5 = "s_trough5"; + private const string SwTrough6 = "s_trough6"; + private const string SwTroughJam = "s_trough_jam"; private const string SwPlunger = "s_plunger"; private const string SwCreateBall = "s_create_ball"; - public GamelogicEngineSwitch[] AvailableSwitches { get; } = - { + public GamelogicEngineSwitch[] AvailableSwitches { get; } = { new GamelogicEngineSwitch { Id = SwLeftFlipper, Description = "Left Flipper (button)", InputActionHint = InputConstants.ActionLeftFlipper }, new GamelogicEngineSwitch { Id = SwRightFlipper, Description = "Right Flipper (button)", InputActionHint = InputConstants.ActionRightFlipper }, new GamelogicEngineSwitch { Id = SwLeftFlipperEos, Description = "Left Flipper (EOS)", PlayfieldItemHint = "^(LeftFlipper|LFlipper|FlipperLeft|FlipperL)$"}, new GamelogicEngineSwitch { Id = SwRightFlipperEos, Description = "Right Flipper (EOS)", PlayfieldItemHint = "^(RightFlipper|RFlipper|FlipperRight|FlipperR)$"}, new GamelogicEngineSwitch { Id = SwPlunger, Description = "Plunger", InputActionHint = InputConstants.ActionPlunger }, + new GamelogicEngineSwitch { Id = SwTrough1, Description = "Trough 1 (eject)", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "1"}, + new GamelogicEngineSwitch { Id = SwTrough2, Description = "Trough 2", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "2"}, + 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 = SwTrough5, Description = "Trough 5", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "5"}, + new GamelogicEngineSwitch { Id = SwTrough6, Description = "Trough 6 (entry)", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "6"}, + new GamelogicEngineSwitch { Id = SwTroughJam, Description = "Trough Jam", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "jam"}, new GamelogicEngineSwitch { Id = SwCreateBall, Description = "Create Debug Ball", InputActionHint = InputConstants.ActionCreateBall, InputMapHint = InputConstants.MapDebug } }; @@ -55,14 +68,15 @@ public class DefaultGamelogicEngine : IGamelogicEngine, IGamelogicEngineWithSwit private const string CoilRightFlipperMain = "c_flipper_right_main"; private const string CoilRightFlipperHold = "c_flipper_right_hold"; private const string CoilAutoPlunger = "c_auto_plunger"; + private const string CoilTroughEject = "c_trough_eject"; - public GamelogicEngineCoil[] AvailableCoils { get; } = - { + public GamelogicEngineCoil[] AvailableCoils { get; } = { new GamelogicEngineCoil { Id = CoilLeftFlipperMain, Description = "Left Flipper", PlayfieldItemHint = "^(LeftFlipper|LFlipper|FlipperLeft|FlipperL)$" }, new GamelogicEngineCoil { Id = CoilLeftFlipperHold, MainCoilIdOfHoldCoil = CoilLeftFlipperMain }, new GamelogicEngineCoil { Id = CoilRightFlipperMain, Description = "Right Flipper", PlayfieldItemHint = "^(RightFlipper|RFlipper|FlipperRight|FlipperR)$" }, new GamelogicEngineCoil { Id = CoilRightFlipperHold, MainCoilIdOfHoldCoil = CoilRightFlipperMain }, - new GamelogicEngineCoil { Id = CoilAutoPlunger, Description = "Plunger", PlayfieldItemHint = "Plunger" } + new GamelogicEngineCoil { Id = CoilAutoPlunger, Description = "Plunger", PlayfieldItemHint = "Plunger" }, + new GamelogicEngineCoil { Id = CoilTroughEject, Description = "Trough Eject", DeviceHint = "^Trough\\s*\\d?", DeviceItemHint = "eject"} }; private TableApi _tableApi; @@ -155,6 +169,12 @@ public void Switch(string id, bool normallyClosed) OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilAutoPlunger, normallyClosed)); break; + case SwTrough6: + if (normallyClosed) { + OnCoilChanged?.Invoke(this, new CoilEventArgs(CoilTroughEject, true)); + } + break; + case SwCreateBall: { if (normallyClosed) { _ballManager.CreateBall(new DebugBallCreator()); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs index 749418438..1e8786b6b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs @@ -37,6 +37,7 @@ using VisualPinball.Engine.VPT.Surface; using VisualPinball.Engine.VPT.Table; using VisualPinball.Engine.VPT.Trigger; +using VisualPinball.Engine.VPT.Trough; using Logger = NLog.Logger; namespace VisualPinball.Unity @@ -44,6 +45,7 @@ namespace VisualPinball.Unity public class Player : MonoBehaviour { public Table Table { get; private set; } + public TableApi TableApi { get; } = new TableApi(); // shortcuts public Matrix4x4 TableToWorld => transform.localToWorldMatrix; @@ -58,7 +60,7 @@ public class Player : MonoBehaviour [HideInInspector] [SerializeField] public string physicsEngineId; // table related - private readonly TableApi _tableApi = new TableApi(); + private readonly List _apis = new List(); private readonly List _initializables = new List(); private readonly Dictionary _hittables = new Dictionary(); private readonly Dictionary _rotatables = new Dictionary(); @@ -66,7 +68,9 @@ public class Player : MonoBehaviour 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 _coilDevices = new Dictionary(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -75,11 +79,11 @@ public class Player : MonoBehaviour [NonSerialized] private readonly Dictionary> _keyAssignments = new Dictionary>(); - private readonly Dictionary>> _coilAssignments = new Dictionary>>(); + private readonly Dictionary>> _coilAssignments = new Dictionary>>(); public Player() { - _initializables.Add(_tableApi); + _initializables.Add(TableApi); } #region Lifecycle @@ -117,26 +121,31 @@ private void OnDestroy() if (_coilAssignments.Count > 0) { (GameEngine as IGamelogicEngineWithCoils).OnCoilChanged -= HandleCoilEvent; } + + foreach (var i in _apis) { + i.OnDestroy(); + } + GameEngine?.OnDestroy(); } private void Start() { - // hook up mapping configuration - SetupSwitchMapping(); - SetupCoilMapping(); - // bootstrap table script(s) var tableScripts = GetComponents(); foreach (var tableScript in tableScripts) { - tableScript.OnAwake(_tableApi, BallManager); + tableScript.OnAwake(TableApi, BallManager); } // trigger init events now foreach (var i in _initializables) { i.OnInit(BallManager); } + + // hook up mapping configuration + SetupSwitchMapping(); + SetupCoilMapping(); } #endregion @@ -146,7 +155,8 @@ private void Start() public void RegisterBumper(Bumper bumper, Entity entity, GameObject go) { var bumperApi = new BumperApi(bumper, entity, this); - _tableApi.Bumpers[bumper.Name] = bumperApi; + TableApi.Bumpers[bumper.Name] = bumperApi; + _apis.Add(bumperApi); _initializables.Add(bumperApi); _hittables[entity] = bumperApi; _switches[bumper.Name] = bumperApi; @@ -156,7 +166,8 @@ public void RegisterBumper(Bumper bumper, Entity entity, GameObject go) public void RegisterFlipper(Flipper flipper, Entity entity, GameObject go) { var flipperApi = new FlipperApi(flipper, entity, this); - _tableApi.Flippers[flipper.Name] = flipperApi; + TableApi.Flippers[flipper.Name] = flipperApi; + _apis.Add(flipperApi); _initializables.Add(flipperApi); _hittables[entity] = flipperApi; _rotatables[entity] = flipperApi; @@ -172,7 +183,8 @@ public void RegisterFlipper(Flipper flipper, Entity entity, GameObject go) public void RegisterGate(Gate gate, Entity entity, GameObject go) { var gateApi = new GateApi(gate, entity, this); - _tableApi.Gates[gate.Name] = gateApi; + TableApi.Gates[gate.Name] = gateApi; + _apis.Add(gateApi); _initializables.Add(gateApi); _hittables[entity] = gateApi; _rotatables[entity] = gateApi; @@ -182,7 +194,8 @@ public void RegisterGate(Gate gate, Entity entity, GameObject go) public void RegisterHitTarget(HitTarget hitTarget, Entity entity, GameObject go) { var hitTargetApi = new HitTargetApi(hitTarget, entity, this); - _tableApi.HitTargets[hitTarget.Name] = hitTargetApi; + TableApi.HitTargets[hitTarget.Name] = hitTargetApi; + _apis.Add(hitTargetApi); _initializables.Add(hitTargetApi); _hittables[entity] = hitTargetApi; _switches[hitTarget.Name] = hitTargetApi; @@ -191,7 +204,8 @@ public void RegisterHitTarget(HitTarget hitTarget, Entity entity, GameObject go) public void RegisterKicker(Kicker kicker, Entity entity, GameObject go) { var kickerApi = new KickerApi(kicker, entity, this); - _tableApi.Kickers[kicker.Name] = kickerApi; + TableApi.Kickers[kicker.Name] = kickerApi; + _apis.Add(kickerApi); _initializables.Add(kickerApi); _hittables[entity] = kickerApi; _switches[kicker.Name] = kickerApi; @@ -201,7 +215,8 @@ public void RegisterKicker(Kicker kicker, Entity entity, GameObject go) public void RegisterPlunger(Plunger plunger, Entity entity, GameObject go) { var plungerApi = new PlungerApi(plunger, entity, this); - _tableApi.Plungers[plunger.Name] = plungerApi; + TableApi.Plungers[plunger.Name] = plungerApi; + _apis.Add(plungerApi); _initializables.Add(plungerApi); _rotatables[entity] = plungerApi; _coils[plunger.Name] = plungerApi; @@ -210,7 +225,8 @@ public void RegisterPlunger(Plunger plunger, Entity entity, GameObject go) public void RegisterPrimitive(Primitive primitive, Entity entity, GameObject go) { var primitiveApi = new PrimitiveApi(primitive, entity, this); - _tableApi.Primitives[primitive.Name] = primitiveApi; + TableApi.Primitives[primitive.Name] = primitiveApi; + _apis.Add(primitiveApi); _initializables.Add(primitiveApi); _hittables[entity] = primitiveApi; } @@ -218,14 +234,16 @@ public void RegisterPrimitive(Primitive primitive, Entity entity, GameObject go) public void RegisterRamp(Ramp ramp, Entity entity, GameObject go) { var rampApi = new RampApi(ramp, entity, this); - _tableApi.Ramps[ramp.Name] = rampApi; + TableApi.Ramps[ramp.Name] = rampApi; + _apis.Add(rampApi); _initializables.Add(rampApi); } public void RegisterRubber(Rubber rubber, Entity entity, GameObject go) { var rubberApi = new RubberApi(rubber, entity, this); - _tableApi.Rubbers[rubber.Name] = rubberApi; + TableApi.Rubbers[rubber.Name] = rubberApi; + _apis.Add(rubberApi); _initializables.Add(rubberApi); _hittables[entity] = rubberApi; } @@ -233,7 +251,8 @@ public void RegisterRubber(Rubber rubber, Entity entity, GameObject go) public void RegisterSurface(Surface surface, Entity entity, GameObject go) { var surfaceApi = new SurfaceApi(surface, entity, this); - _tableApi.Surfaces[surface.Name] = surfaceApi; + TableApi.Surfaces[surface.Name] = surfaceApi; + _apis.Add(surfaceApi); _initializables.Add(surfaceApi); _hittables[entity] = surfaceApi; _slingshots[entity] = surfaceApi; @@ -242,7 +261,8 @@ public void RegisterSurface(Surface surface, Entity entity, GameObject go) public void RegisterSpinner(Spinner spinner, Entity entity, GameObject go) { var spinnerApi = new SpinnerApi(spinner, entity, this); - _tableApi.Spinners[spinner.Name] = spinnerApi; + TableApi.Spinners[spinner.Name] = spinnerApi; + _apis.Add(spinnerApi); _initializables.Add(spinnerApi); _spinnables[entity] = spinnerApi; _rotatables[entity] = spinnerApi; @@ -252,12 +272,22 @@ public void RegisterSpinner(Spinner spinner, Entity entity, GameObject go) public void RegisterTrigger(Trigger trigger, Entity entity, GameObject go) { var triggerApi = new TriggerApi(trigger, entity, this); - _tableApi.Triggers[trigger.Name] = triggerApi; + TableApi.Triggers[trigger.Name] = triggerApi; + _apis.Add(triggerApi); _initializables.Add(triggerApi); _hittables[entity] = triggerApi; _switches[trigger.Name] = triggerApi; } + public void RegisterTrough(Trough trough, GameObject go) + { + var troughApi = new TroughApi(trough, this); + _apis.Add(troughApi); + _initializables.Add(troughApi); + _switchDevices[trough.Name] = troughApi; + _coilDevices[trough.Name] = troughApi; + } + #endregion #region Mapping @@ -271,20 +301,31 @@ private void SetupCoilMapping() switch (coilData.Destination) { case CoilDestination.Playfield: if (!_coilAssignments.ContainsKey(coilData.Id)) { - _coilAssignments[coilData.Id] = new List>(); + _coilAssignments[coilData.Id] = new List>(); } - _coilAssignments[coilData.Id].Add(new Tuple(coilData.PlayfieldItem, false)); + _coilAssignments[coilData.Id].Add(new Tuple(coilData.PlayfieldItem, false, null)); if (coilData.Type == CoilType.DualWound) { if (!_coilAssignments.ContainsKey(coilData.HoldCoilId)) { - _coilAssignments[coilData.HoldCoilId] = new List>(); + _coilAssignments[coilData.HoldCoilId] = new List>(); } - _coilAssignments[coilData.HoldCoilId].Add(new Tuple(coilData.PlayfieldItem, true)); - + _coilAssignments[coilData.HoldCoilId].Add(new Tuple(coilData.PlayfieldItem, true, null)); } break; case CoilDestination.Device: - // todo + if (_coilDevices.ContainsKey(coilData.Device)) { + 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)); + + } else { + Logger.Warn($"Unknown coil \"{coilData.DeviceItem}\" in coil device \"{coilData.Device}\"."); + } + } break; } } @@ -325,6 +366,29 @@ private void SetupSwitchMapping() 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(switchData.Id, 0); + Debug.Log($"Adding switch {switchData.Id} to device {switchData.Device} ({switchData.DeviceItem})"); + + } 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; @@ -338,7 +402,7 @@ private void SetupSwitchMapping() _inputManager.Enable(HandleKeyInput); } } - GameEngine.OnInit(_tableApi, BallManager); + GameEngine.OnInit(TableApi, BallManager); } private void HandleKeyInput(object obj, InputActionChange change) @@ -362,11 +426,15 @@ private void HandleKeyInput(object obj, InputActionChange change) private void HandleCoilEvent(object sender, CoilEventArgs coilEvent) { if (_coilAssignments.ContainsKey(coilEvent.Id)) { - foreach (var (itemName, isHoldCoil) in _coilAssignments[coilEvent.Id]) { - if (_coils.ContainsKey(itemName)) { + foreach (var (itemName, isHoldCoil, deviceName) in _coilAssignments[coilEvent.Id]) { + if (deviceName != null && _coilDevices.ContainsKey(deviceName)) { + _coilDevices[deviceName].Coil(itemName).OnCoil(coilEvent.IsEnabled, isHoldCoil); + + } else if (_coils.ContainsKey(itemName)) { _coils[itemName].OnCoil(coilEvent.IsEnabled, isHoldCoil); + } else { - Logger.Warn($"Should trigger unknown coil item {itemName}."); + Logger.Warn($"Cannot trigger unknown coil item {itemName}."); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchConfig.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchConfig.cs new file mode 100644 index 000000000..3bc4fef3d --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchConfig.cs @@ -0,0 +1,32 @@ +// 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 readonly struct SwitchConfig + { + public readonly string SwitchId; + public readonly int PulseDelay; + public readonly bool IsPulseSwitch; + + public SwitchConfig(string switchId, bool isPulseSwitch, int pulseDelay) + { + SwitchId = switchId; + PulseDelay = pulseDelay; + IsPulseSwitch = isPulseSwitch; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchConfig.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchConfig.cs.meta new file mode 100644 index 000000000..986284292 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/SwitchConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3b99281fc314f6f409fdf8d79d6c9624 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Import/Job/TableLoader.cs b/VisualPinball.Unity/VisualPinball.Unity/Import/Job/TableLoader.cs index 3fb60685d..506783c7c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Import/Job/TableLoader.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Import/Job/TableLoader.cs @@ -42,6 +42,7 @@ using VisualPinball.Engine.VPT.TextBox; using VisualPinball.Engine.VPT.Timer; using VisualPinball.Engine.VPT.Trigger; +using VisualPinball.Engine.VPT.Trough; namespace VisualPinball.Unity { @@ -143,6 +144,10 @@ public static Table LoadTable(string path) table.Add(objHandle.Target as Trigger); break; } + case ItemType.Trough: { + table.Add(objHandle.Target as Trough); + break; + } default: throw new ArgumentException("Unknown item type " + (ItemType)job.ItemType[i] + "."); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs index 731e608a6..fbee9a2e4 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Import/VpxConverter.cs @@ -44,6 +44,7 @@ using VisualPinball.Engine.VPT.TextBox; using VisualPinball.Engine.VPT.Timer; using VisualPinball.Engine.VPT.Trigger; +using VisualPinball.Engine.VPT.Trough; using Logger = NLog.Logger; namespace VisualPinball.Unity @@ -57,7 +58,7 @@ public class VpxConverter : MonoBehaviour private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); //private readonly Dictionary _renderObjects = new Dictionary(); - private readonly Dictionary _parents = new Dictionary(); + private readonly Dictionary _groupParents = new Dictionary(); private Table _table; private TableAuthoring _tableAuthoring; @@ -100,10 +101,15 @@ public void Convert(string fileName, Table table, bool applyPatch = true, string go.AddComponent(); var dga = go.AddComponent(); + // add trough if none available + if (!_table.HasTrough) { + CreateTrough(); + } + // populate mappings if (_table.Mappings.IsEmpty()) { - _table.Mappings.PopulateSwitches((dga.GameEngine as IGamelogicEngineWithSwitches).AvailableSwitches, table.Switchables); - _table.Mappings.PopulateCoils((dga.GameEngine as IGamelogicEngineWithCoils).AvailableCoils, table.Coilables); + _table.Mappings.PopulateSwitches((dga.GameEngine as IGamelogicEngineWithSwitches).AvailableSwitches, table.Switchables, table.SwitchableDevices); + _table.Mappings.PopulateCoils((dga.GameEngine as IGamelogicEngineWithCoils).AvailableCoils, table.Coilables, table.CoilableDevices); } // don't need that anymore. @@ -125,16 +131,21 @@ orderby renderable.SubComponent var lookupName = renderable.Name.ToLower(); renderableLookup[lookupName] = renderable; - // create group parent if not created - if (!_parents.ContainsKey(renderable.ItemGroupName)) { - var parent = new GameObject(renderable.ItemGroupName); - parent.transform.parent = gameObject.transform; - _parents[renderable.ItemGroupName] = parent; + // create group parent if not created (if null, attach it to the table directly). + if (!string.IsNullOrEmpty(renderable.ItemGroupName)) { + if (!_groupParents.ContainsKey(renderable.ItemGroupName)) { + var parent = new GameObject(renderable.ItemGroupName); + parent.transform.parent = gameObject.transform; + _groupParents[renderable.ItemGroupName] = parent; + } } + var groupParent = !string.IsNullOrEmpty(renderable.ItemGroupName) + ? _groupParents[renderable.ItemGroupName] + : gameObject; if (renderable.SubComponent == ItemSubComponent.None) { // create object(s) - convertedItems[lookupName] = CreateGameObjects(_table, renderable, _parents[renderable.ItemGroupName]); + convertedItems[lookupName] = CreateGameObjects(_table, renderable, groupParent); } else { // if the object's names was parsed to be part of another object, re-link to other object. @@ -142,7 +153,7 @@ orderby renderable.SubComponent if (convertedItems.ContainsKey(parentName)) { var parent = convertedItems[parentName]; - var convertedItem = CreateGameObjects(_table, renderable, _parents[renderable.ItemGroupName]); + var convertedItem = CreateGameObjects(_table, renderable, groupParent); if (convertedItem.IsValidChild(parent)) { if (convertedItem.MeshAuthoring.Any()) { @@ -166,7 +177,7 @@ orderby renderable.SubComponent renderable.DisableSubComponent(); // invalid parenting, re-convert the item, because it returned only the sub component. - convertedItems[lookupName] = CreateGameObjects(_table, renderable, _parents[renderable.ItemGroupName]); + convertedItems[lookupName] = CreateGameObjects(_table, renderable, groupParent); // ..and destroy the other one convertedItem.Destroy(); @@ -186,20 +197,22 @@ orderby renderable.SubComponent } } - public static ConvertedItem CreateGameObjects(Table table, IRenderable renderable, GameObject parent) + public static ConvertedItem CreateGameObjects(Table table, IItem item, GameObject parent) { - var obj = new GameObject(renderable.Name); + var obj = new GameObject(item.Name); obj.transform.parent = parent.transform; - var importedObject = SetupGameObjects(renderable, obj); + var importedObject = SetupGameObjects(item, obj); // apply transformation - obj.transform.SetFromMatrix(renderable.TransformationMatrix(table, Origin.Original).ToUnityMatrix()); + if (item is IRenderable renderable) { + obj.transform.SetFromMatrix(renderable.TransformationMatrix(table, Origin.Original).ToUnityMatrix()); + } return importedObject; } - private static ConvertedItem SetupGameObjects(IRenderable item, GameObject obj) + private static ConvertedItem SetupGameObjects(IItem item, GameObject obj) { switch (item) { case Bumper bumper: return bumper.SetupGameObject(obj); @@ -216,6 +229,7 @@ private static ConvertedItem SetupGameObjects(IRenderable item, GameObject obj) case Surface surface: return surface.SetupGameObject(obj); case Table table: return table.SetupGameObject(obj); case Trigger trigger: return trigger.SetupGameObject(obj); + case Trough trough: return trough.SetupGameObject(obj); } throw new InvalidOperationException("Unknown item " + item + " to setup!"); @@ -257,6 +271,20 @@ private void MakeSerializable(GameObject go, Table table) string.Join(", ", sidecar.collections.Select(c => c.Name)) ); } + + private void CreateTrough() + { + var troughData = new TroughData("Trough"); + if (_table.Has("BallRelease")) { + troughData.ExitKicker = "BallRelease"; + } + if (_table.Has("Drain")) { + troughData.EntryKicker = "Drain"; + } + var item = new Trough(troughData); + _table.Add(item, true); + CreateGameObjects(_tableAuthoring.Table, item, _tableAuthoring.gameObject); + } } public class ConvertedItem @@ -300,14 +328,18 @@ public void Destroy() public void DestroyMeshComponent() { - MainAuthoring.DestroyMeshComponent(); - MeshAuthoring = new IItemMeshAuthoring[0]; + if (MainAuthoring is IItemMainRenderableAuthoring renderableAuthoring) { + renderableAuthoring.DestroyMeshComponent(); + MeshAuthoring = new IItemMeshAuthoring[0]; + } } public void DestroyColliderComponent() { - MainAuthoring.DestroyColliderComponent(); - ColliderAuthoring = null; + if (MainAuthoring is IItemMainRenderableAuthoring renderableAuthoring) { + renderableAuthoring.DestroyColliderComponent(); + ColliderAuthoring = null; + } } public bool IsValidChild(ConvertedItem parent) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs index 1760e9e6d..46af86ab1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs @@ -57,7 +57,7 @@ internal struct ColliderHeader public void Init(ColliderType type, HitObject src) { if (src.ItemIndex == 0 && src.ItemVersion == 0) { - throw new InvalidOperationException("Entity of " + type + " " + ItemType + " not set!"); + throw new InvalidOperationException("Entity of " + type + " " + ItemType + " (" + src.Item.Name + ") not set!"); } Type = type; ItemType = src.ObjType; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperAuthoring.cs index fa684d1ae..d165886cb 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperAuthoring.cs @@ -32,7 +32,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Bumper")] - public class BumperAuthoring : ItemMainAuthoring, + public class BumperAuthoring : ItemMainRenderableAuthoring, ISwitchAuthoring, ICoilAuthoring, IConvertGameObjectToEntity { protected override Bumper InstantiateItem(BumperData data) => new Bumper(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperAuthoring.cs index 0e600c3df..096ed6670 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperAuthoring.cs @@ -34,7 +34,7 @@ namespace VisualPinball.Unity [ExecuteAlways] [RequiresEntityConversion] [AddComponentMenu("Visual Pinball/Game Item/Flipper")] - public class FlipperAuthoring : ItemMainAuthoring, + public class FlipperAuthoring : ItemMainRenderableAuthoring, ISwitchAuthoring, ICoilAuthoring, IConvertGameObjectToEntity { protected override Flipper InstantiateItem(FlipperData data) => new Flipper(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateAuthoring.cs index fc814ede5..5e7105cf5 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Gate/GateAuthoring.cs @@ -34,7 +34,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Gate")] - public class GateAuthoring : ItemMainAuthoring, + public class GateAuthoring : ItemMainRenderableAuthoring, ISwitchAuthoring, IConvertGameObjectToEntity { protected override Gate InstantiateItem(GateData data) => new Gate(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetAuthoring.cs index 572ea9e49..6fabf2fe4 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetAuthoring.cs @@ -32,7 +32,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Hit Target")] - public class HitTargetAuthoring : ItemMainAuthoring, + public class HitTargetAuthoring : ItemMainRenderableAuthoring, ISwitchAuthoring, IConvertGameObjectToEntity { protected override HitTarget InstantiateItem(HitTargetData data) => new HitTarget(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs index 2adaa90bb..b12d66403 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IApi.cs @@ -19,6 +19,7 @@ namespace VisualPinball.Unity public interface IApi { string Name { get; } + void OnDestroy(); } internal interface IApiInitializable @@ -56,6 +57,16 @@ internal interface IApiSwitch void AddSwitchId(string switchId, int pulseDelay); } + internal interface IApiSwitchDevice + { + IApiSwitch Switch(string switchId); + } + + internal interface IApiCoilDevice + { + IApiCoil Coil(string coilId); + } + internal interface IApiCoil { void OnCoil(bool enabled, bool isHoldCoil); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilDeviceAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilDeviceAuthoring.cs new file mode 100644 index 000000000..1007311b2 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilDeviceAuthoring.cs @@ -0,0 +1,33 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + + +using System.Collections.Generic; +using VisualPinball.Engine.Game.Engines; + +namespace VisualPinball.Unity +{ + /// + /// A coil device is an item that contains multiple coil. + /// + public interface ICoilDeviceAuthoring : IIdentifiableItemAuthoring + { + /// + /// A list of available coils supported by the coil device + /// + IEnumerable AvailableCoils { get; } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilDeviceAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilDeviceAuthoring.cs.meta new file mode 100644 index 000000000..e2aa48615 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ICoilDeviceAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 638986c636eb9f94f8f5adef00fac4ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemColliderAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemColliderAuthoring.cs index c09ace1bf..69563fa95 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemColliderAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemColliderAuthoring.cs @@ -21,7 +21,7 @@ namespace VisualPinball.Unity { public interface IItemColliderAuthoring : IItemAuthoring { - IItemMainAuthoring MainAuthoring { get; } + IItemMainRenderableAuthoring MainAuthoring { get; } IEnumerable ValidParents { get; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainAuthoring.cs index fd4d4389e..108478fa5 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainAuthoring.cs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +// ReSharper disable InconsistentNaming + using System; using System.Collections.Generic; using UnityEngine; @@ -22,45 +24,11 @@ namespace VisualPinball.Unity { public interface IItemMainAuthoring : IItemAuthoring { - bool IsLocked { get; set; } - - bool CanBeTransformed { get; } IEnumerable ValidParents { get; } GameObject gameObject { get; } - /// - /// Sets the mesh of all mesh sub components to dirty. - /// - void SetMeshDirty(); - void RebuildMeshIfDirty(); - void Destroy(); - void DestroyMeshComponent(); - void DestroyColliderComponent(); - - // the following interfaces allow each item behavior to define which axes should - // be shown on the scene view gizmo, the gizmo itself will use the associated - // get and set methods, which are expected to update item data directly - ItemDataTransformType EditorPositionType { get; } - Vector3 GetEditorPosition(); - void SetEditorPosition(Vector3 pos); - - ItemDataTransformType EditorRotationType { get; } - Vector3 GetEditorRotation(); - void SetEditorRotation(Vector3 pos); - - ItemDataTransformType EditorScaleType { get; } - Vector3 GetEditorScale(); - void SetEditorScale(Vector3 pos); - } - - public enum ItemDataTransformType - { - None, - OneD, - TwoD, - ThreeD, } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainRenderableAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainRenderableAuthoring.cs new file mode 100644 index 000000000..4b9fb0a57 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainRenderableAuthoring.cs @@ -0,0 +1,61 @@ +// 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 System.Collections.Generic; +using UnityEngine; + +namespace VisualPinball.Unity +{ + public interface IItemMainRenderableAuthoring : IItemMainAuthoring + { + bool IsLocked { get; set; } + + bool CanBeTransformed { get; } + + /// + /// Sets the mesh of all mesh sub components to dirty. + /// + void SetMeshDirty(); + void RebuildMeshIfDirty(); + + void DestroyMeshComponent(); + void DestroyColliderComponent(); + + // the following interfaces allow each item behavior to define which axes should + // be shown on the scene view gizmo, the gizmo itself will use the associated + // get and set methods, which are expected to update item data directly + ItemDataTransformType EditorPositionType { get; } + Vector3 GetEditorPosition(); + void SetEditorPosition(Vector3 pos); + + ItemDataTransformType EditorRotationType { get; } + Vector3 GetEditorRotation(); + void SetEditorRotation(Vector3 pos); + + ItemDataTransformType EditorScaleType { get; } + Vector3 GetEditorScale(); + void SetEditorScale(Vector3 pos); + } + + public enum ItemDataTransformType + { + None, + OneD, + TwoD, + ThreeD, + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainRenderableAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainRenderableAuthoring.cs.meta new file mode 100644 index 000000000..917deec32 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMainRenderableAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9001206d60c12264f9454a1da2928b9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMeshAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMeshAuthoring.cs index 0214a4392..4255a45b5 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMeshAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/IItemMeshAuthoring.cs @@ -28,7 +28,7 @@ public interface IItemMeshAuthoring : IItemAuthoring List MaterialRefs { get; } List TextureRefs { get; } - IItemMainAuthoring IMainAuthoring { get; } + IItemMainRenderableAuthoring IMainAuthoring { get; } GameObject gameObject { get; } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchAuthoring.cs index c8a4d2781..5a08f94f8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchAuthoring.cs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - using VisualPinball.Engine.Game; namespace VisualPinball.Unity diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchDeviceAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchDeviceAuthoring.cs new file mode 100644 index 000000000..a21ab87b7 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchDeviceAuthoring.cs @@ -0,0 +1,33 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + + +using System.Collections.Generic; +using VisualPinball.Engine.Game.Engines; + +namespace VisualPinball.Unity +{ + /// + /// A switch device is an item that contains multiple switches. + /// + public interface ISwitchDeviceAuthoring : IIdentifiableItemAuthoring + { + /// + /// A list of available switches supported by the switch device + /// + IEnumerable AvailableSwitches { get; } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchDeviceAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchDeviceAuthoring.cs.meta new file mode 100644 index 000000000..7d2113230 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ISwitchDeviceAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0bf08f19436bcce4fba884e973f7cb00 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs index 032aa22e8..eb7613887 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemApi.cs @@ -30,26 +30,42 @@ public abstract class ItemApi : IApi where T : Item where TData protected TData Data => Item.Data; protected Table Table => _player.Table; + protected TableApi TableApi => _player.TableApi; - protected readonly EntityManager EntityManager = World.DefaultGameObjectInjectionWorld.EntityManager; + protected EntityManager EntityManager; internal VisualPinballSimulationSystemGroup SimulationSystemGroup => World.DefaultGameObjectInjectionWorld.GetOrCreateSystem(); private readonly Player _player; + protected ItemApi(T item, Player player) + { + Item = item; + Entity = Entity.Null; + _player = player; + _gamelogicEngineWithSwitches = (IGamelogicEngineWithSwitches)player.GameEngine; + } + protected ItemApi(T item, Entity entity, Player player) { + EntityManager = World.DefaultGameObjectInjectionWorld.EntityManager; Item = item; Entity = entity; _player = player; _gamelogicEngineWithSwitches = (IGamelogicEngineWithSwitches)player.GameEngine; } + void IApi.OnDestroy() + { + } + #region IApiSwitchable private List _switchIds; private readonly IGamelogicEngineWithSwitches _gamelogicEngineWithSwitches; + protected DeviceSwitch CreateSwitch(bool isPulseSwitch) => new DeviceSwitch(isPulseSwitch, _gamelogicEngineWithSwitches); + protected void AddSwitchId(string switchId, bool isPulseSwitch, int pulseDelay) { if (_switchIds == null) { @@ -74,19 +90,5 @@ protected void OnSwitch(bool normallyClosed) } #endregion - - private readonly struct SwitchConfig - { - public readonly string SwitchId; - public readonly int PulseDelay; - public readonly bool IsPulseSwitch; - - public SwitchConfig(string switchId, bool isPulseSwitch, int pulseDelay) - { - SwitchId = switchId; - PulseDelay = pulseDelay; - IsPulseSwitch = isPulseSwitch; - } - } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemAuthoring.cs index 6d35288f3..086db8296 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemAuthoring.cs @@ -30,7 +30,7 @@ namespace VisualPinball.Unity /// public abstract class ItemAuthoring : MonoBehaviour where TData : ItemData - where TItem : Item, IRenderable + where TItem : Item { public string Name { get => Item.Name; set => Item.Name = value; } @@ -75,5 +75,16 @@ public abstract class ItemAuthoring : MonoBehaviour protected virtual void ItemDataChanged() { } + + protected static void DrawArrow(Vector3 pos, Vector3 direction, float arrowHeadLength = 0.025f, float arrowHeadAngle = 20.0f) + { + Debug.DrawRay(pos, direction); + + var right = Quaternion.LookRotation(direction) * Quaternion.Euler(0,180+arrowHeadAngle,0) * new Vector3(0,0,1); + var left = Quaternion.LookRotation(direction) * Quaternion.Euler(0,180-arrowHeadAngle,0) * new Vector3(0,0,1); + Debug.DrawRay(pos + direction, right * arrowHeadLength); + Debug.DrawRay(pos + direction, left * arrowHeadLength); + } + } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemColliderAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemColliderAuthoring.cs index 431b7239a..516f6ac09 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemColliderAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemColliderAuthoring.cs @@ -33,7 +33,7 @@ public abstract class ItemColliderAuthoring : Item IItemColliderAuthoring where TData : ItemData where TItem : Item, IHittable, IRenderable - where TMainAuthoring : ItemMainAuthoring + where TMainAuthoring : ItemMainRenderableAuthoring { [NonSerialized] public bool ShowGizmos; @@ -49,7 +49,7 @@ public abstract class ItemColliderAuthoring : Item public HitObject[] HitObjects { get; private set; } - public new IItemMainAuthoring MainAuthoring => base.MainAuthoring; + public new IItemMainRenderableAuthoring MainAuthoring => base.MainAuthoring; private void OnDrawGizmosSelected() { diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainAuthoring.cs index 45dafa425..8f330b459 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainAuthoring.cs @@ -1,18 +1,36 @@ -using System; +// 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 . + +// ReSharper disable InconsistentNaming + +using System; using System.Collections.Generic; using System.Linq; -using Unity.Entities; using UnityEngine; -using VisualPinball.Engine.Game; using VisualPinball.Engine.VPT; namespace VisualPinball.Unity { public abstract class ItemMainAuthoring : ItemAuthoring, IItemMainAuthoring, ILayerableItemAuthoring, IIdentifiableItemAuthoring - where TItem : Item, IRenderable + where TItem : Item where TData : ItemData { + #region Data + /// /// Returns the serialized data. /// @@ -24,19 +42,10 @@ public abstract class ItemMainAuthoring : ItemAuthoring public override TItem Item => _item ?? (_item = InstantiateItem(_data)); - public virtual bool CanBeTransformed => true; - /// - /// List of types for parenting. Empty list if only to own parent. - /// - public abstract IEnumerable ValidParents { get; } - - /// - /// Authoring type of the child class. + /// Applies the GameObject data to the item data. typically name and visibility. /// - protected abstract Type MeshAuthoringType { get; } - - protected abstract Type ColliderAuthoringType { get; } + public abstract void Restore(); /// /// Instantiates a new item based on the item data. @@ -45,11 +54,6 @@ public abstract class ItemMainAuthoring : ItemAuthoringNew item instance protected abstract TItem InstantiateItem(TData data); - /// - /// Applies the GameObject data to the item data. typically name and visibility. - /// - public abstract void Restore(); - /// /// The serialized data, as written to the .vpx file. /// @@ -60,36 +64,14 @@ public abstract class ItemMainAuthoring : ItemAuthoring [SerializeField] - private TData _data; + protected TData _data; /// /// The game item object. This is not serialized and gets re-instantiated /// and cached here. /// [NonSerialized] - private TItem _item; - - /// - /// Returns all child mesh components linked to this data. - /// - protected IEnumerable MeshComponents => MeshAuthoringType != null ? - GetComponentsInChildren(MeshAuthoringType, true) - .Select(c => (IItemMeshAuthoring) c) - .Where(ma => ma.ItemData == _data) : new IItemMeshAuthoring[0]; - - protected IEnumerable ColliderComponents => ColliderAuthoringType != null ? - GetComponentsInChildren(ColliderAuthoringType, true) - .Select(c => (IItemColliderAuthoring) c) - .Where(ca => ca.ItemData == _data) : new IItemColliderAuthoring[0]; - - public IItemMainAuthoring ParentAuthoring => FindParentAuthoring(); - - public bool IsCorrectlyParented { - get { - var parentAuthoring = ParentAuthoring; - return parentAuthoring == null || ValidParents.Any(validParent => parentAuthoring.GetType() == validParent); - } - } + protected TItem _item; public IItemMainAuthoring SetItem(TItem item, string gameObjectName = null) { @@ -100,102 +82,36 @@ public IItemMainAuthoring SetItem(TItem item, string gameObjectName = null) return this; } - public void SetMeshDirty() - { - foreach (var meshComponent in MeshComponents) { - meshComponent.MeshDirty = true; - } - } - - public void RebuildMeshIfDirty() - { - foreach (var meshComponent in MeshComponents) { - if (meshComponent.MeshDirty) { - meshComponent.RebuildMeshes(); - } - } - - // update transform based on item data, but not for "Table" since its the effective "root" and the user might want to move it on their own - var ta = GetComponentInParent(); - if (ta != this) { - transform.SetFromMatrix(Item.TransformationMatrix(Table, Origin.Original).ToUnityMatrix()); - } - } - public void Destroy() { DestroyImmediate(gameObject); } - public void DestroyMeshComponent() - { - foreach (var component in MeshComponents) { - var mb = component as MonoBehaviour; - - // if game object is the same, remove component - if (mb.gameObject == gameObject) { - DestroyImmediate(mb); - - } else { - // otherwise, destroy entire game object - DestroyImmediate(mb.gameObject); - } - } - } - - public void DestroyColliderComponent() - { - foreach (var component in ColliderComponents) { - var mb = component as MonoBehaviour; - - // if game object is the same, remove component - if (mb.gameObject == gameObject) { - DestroyImmediate(mb); + #endregion - } else { - // otherwise, destroy entire game object - DestroyImmediate(mb.gameObject); - } - } - } + #region Parenting - protected void Convert(Entity entity, EntityManager dstManager) - { - Item.Index = entity.Index; - Item.Version = entity.Version; + /// + /// List of types for parenting. Empty list if only to own parent. + /// + public abstract IEnumerable ValidParents { get; } - var parentAuthoring = ParentAuthoring; - if (parentAuthoring != null && !(parentAuthoring is TableAuthoring)) { - Item.ParentIndex = parentAuthoring.IItem.Index; - Item.ParentVersion = parentAuthoring.IItem.Version; - } - } + public IItemMainRenderableAuthoring ParentAuthoring => FindParentAuthoring(); - protected virtual void OnDrawGizmos() - { - // handle dirty whenever scene view draws just in case a field or dependant changed and our - // custom inspector window isn't up to process it - RebuildMeshIfDirty(); - - // Draw invisible gizmos over top of the sub meshes of this item so clicking in the scene view - // selects the item itself first, which is most likely what the user would want - var mfs = GetComponentsInChildren(); - Gizmos.color = Color.clear; - Gizmos.matrix = Matrix4x4.identity; - foreach (var mf in mfs) { - var t = mf.transform; - Gizmos.DrawMesh(mf.sharedMesh, t.position, t.rotation, t.lossyScale); + public bool IsCorrectlyParented { + get { + var parentAuthoring = ParentAuthoring; + return parentAuthoring == null || ValidParents.Any(validParent => parentAuthoring.GetType() == validParent); } } - - private IItemMainAuthoring FindParentAuthoring() + private IItemMainRenderableAuthoring FindParentAuthoring() { - IItemMainAuthoring ma = null; + IItemMainRenderableAuthoring ma = null; var go = gameObject; // search on parent if (go.transform.parent != null) { - ma = go.transform.parent.GetComponent(); + ma = go.transform.parent.GetComponent(); } if (ma is MonoBehaviour mb && mb.GetComponent() != null) { @@ -207,7 +123,7 @@ private IItemMainAuthoring FindParentAuthoring() // search on grand parent if (go.transform.parent.transform.parent != null) { - ma = go.transform.parent.transform.parent.GetComponent(); + ma = go.transform.parent.transform.parent.GetComponent(); } if (ma is MonoBehaviour mb2 && mb2.GetComponent() != null) { @@ -217,24 +133,14 @@ private IItemMainAuthoring FindParentAuthoring() return ma; } - #region Tools - - public virtual ItemDataTransformType EditorPositionType => ItemDataTransformType.None; - public virtual Vector3 GetEditorPosition() => Vector3.zero; - public virtual void SetEditorPosition(Vector3 pos) { } - - public virtual ItemDataTransformType EditorRotationType => ItemDataTransformType.None; - public virtual Vector3 GetEditorRotation() => Vector3.zero; - public virtual void SetEditorRotation(Vector3 rot) { } - - public virtual ItemDataTransformType EditorScaleType => ItemDataTransformType.None; - public virtual Vector3 GetEditorScale() => Vector3.zero; - public virtual void SetEditorScale(Vector3 rot) { } - #endregion + #region ILayerableItemAuthoring + public int EditorLayer { get => Data.EditorLayer; set => Data.EditorLayer = value; } public string EditorLayerName { get => Data.EditorLayerName; set => Data.EditorLayerName = value; } public bool EditorLayerVisibility { get => Data.EditorLayerVisibility; set => Data.EditorLayerVisibility = value; } + + #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainRenderableAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainRenderableAuthoring.cs new file mode 100644 index 000000000..7239b5d84 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainRenderableAuthoring.cs @@ -0,0 +1,153 @@ +// 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 System.Collections.Generic; +using System.Linq; +using Unity.Entities; +using UnityEngine; +using VisualPinball.Engine.Game; +using VisualPinball.Engine.VPT; + +namespace VisualPinball.Unity +{ + public abstract class ItemMainRenderableAuthoring : ItemMainAuthoring, IItemMainRenderableAuthoring + where TItem : Item, IRenderable + where TData : ItemData + { + public virtual bool CanBeTransformed => true; + + /// + /// Authoring type of the child class. + /// + protected abstract Type MeshAuthoringType { get; } + + protected abstract Type ColliderAuthoringType { get; } + + /// + /// Returns all child mesh components linked to this data. + /// + protected IEnumerable MeshComponents => MeshAuthoringType != null ? + GetComponentsInChildren(MeshAuthoringType, true) + .Select(c => (IItemMeshAuthoring) c) + .Where(ma => ma.ItemData == _data) : new IItemMeshAuthoring[0]; + + protected IEnumerable ColliderComponents => ColliderAuthoringType != null ? + GetComponentsInChildren(ColliderAuthoringType, true) + .Select(c => (IItemColliderAuthoring) c) + .Where(ca => ca.ItemData == _data) : new IItemColliderAuthoring[0]; + + public void SetMeshDirty() + { + foreach (var meshComponent in MeshComponents) { + meshComponent.MeshDirty = true; + } + } + + public void RebuildMeshIfDirty() + { + foreach (var meshComponent in MeshComponents) { + if (meshComponent.MeshDirty) { + meshComponent.RebuildMeshes(); + } + } + + // update transform based on item data, but not for "Table" since its the effective "root" and the user might want to move it on their own + var ta = GetComponentInParent(); + if (ta != this) { + transform.SetFromMatrix(Item.TransformationMatrix(Table, Origin.Original).ToUnityMatrix()); + } + } + + public void DestroyMeshComponent() + { + foreach (var component in MeshComponents) { + var mb = component as MonoBehaviour; + + // if game object is the same, remove component + if (mb.gameObject == gameObject) { + DestroyImmediate(mb); + + } else { + // otherwise, destroy entire game object + DestroyImmediate(mb.gameObject); + } + } + } + + public void DestroyColliderComponent() + { + foreach (var component in ColliderComponents) { + var mb = component as MonoBehaviour; + + // if game object is the same, remove component + if (mb.gameObject == gameObject) { + DestroyImmediate(mb); + + } else { + // otherwise, destroy entire game object + DestroyImmediate(mb.gameObject); + } + } + } + + protected void Convert(Entity entity, EntityManager dstManager) + { + Item.Index = entity.Index; + Item.Version = entity.Version; + + var parentAuthoring = ParentAuthoring; + if (parentAuthoring != null && !(parentAuthoring is TableAuthoring)) { + Item.ParentIndex = parentAuthoring.IItem.Index; + Item.ParentVersion = parentAuthoring.IItem.Version; + } + } + + protected virtual void OnDrawGizmos() + { + // handle dirty whenever scene view draws just in case a field or dependant changed and our + // custom inspector window isn't up to process it + RebuildMeshIfDirty(); + + // Draw invisible gizmos over top of the sub meshes of this item so clicking in the scene view + // selects the item itself first, which is most likely what the user would want + var mfs = GetComponentsInChildren(); + Gizmos.color = Color.clear; + Gizmos.matrix = Matrix4x4.identity; + foreach (var mf in mfs) { + var t = mf.transform; + Gizmos.DrawMesh(mf.sharedMesh, t.position, t.rotation, t.lossyScale); + } + } + + #region Tools + + public virtual ItemDataTransformType EditorPositionType => ItemDataTransformType.None; + public virtual Vector3 GetEditorPosition() => Vector3.zero; + public virtual void SetEditorPosition(Vector3 pos) { } + + public virtual ItemDataTransformType EditorRotationType => ItemDataTransformType.None; + public virtual Vector3 GetEditorRotation() => Vector3.zero; + public virtual void SetEditorRotation(Vector3 rot) { } + + public virtual ItemDataTransformType EditorScaleType => ItemDataTransformType.None; + public virtual Vector3 GetEditorScale() => Vector3.zero; + public virtual void SetEditorScale(Vector3 rot) { } + + #endregion + + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainRenderableAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainRenderableAuthoring.cs.meta new file mode 100644 index 000000000..fba920077 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMainRenderableAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dbbfe725a97483b449275bc9d63c4f86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMeshAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMeshAuthoring.cs index 4501768a2..bf1d33c20 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMeshAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMeshAuthoring.cs @@ -27,13 +27,13 @@ namespace VisualPinball.Unity public abstract class ItemMeshAuthoring : ItemSubAuthoring, IItemMeshAuthoring where TData : ItemData where TItem : Item, IRenderable - where TAuthoring : ItemMainAuthoring + where TAuthoring : ItemMainRenderableAuthoring { public bool MeshDirty { get => _meshDirty; set => _meshDirty = value; } public List MaterialRefs => _materialRefs ?? (_materialRefs = GetMembersWithAttribute()); public List TextureRefs => _textureRefs ?? (_textureRefs = GetMembersWithAttribute()); - public IItemMainAuthoring IMainAuthoring => MainAuthoring; + public IItemMainRenderableAuthoring IMainAuthoring => MainAuthoring; protected virtual string MeshId => null; protected abstract bool IsVisible { get; set; } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMovementAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMovementAuthoring.cs index 8a6750717..df7513610 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMovementAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemMovementAuthoring.cs @@ -26,7 +26,7 @@ public abstract class ItemMovementAuthoring : Item IItemMovementAuthoring where TData : ItemData where TItem : Item, IHittable, IRenderable - where TMainAuthoring : ItemMainAuthoring + where TMainAuthoring : ItemMainRenderableAuthoring { protected Entity MainEntity { get { diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemSubAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemSubAuthoring.cs index b7b396cc5..fd5981ec6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemSubAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/ItemSubAuthoring.cs @@ -25,7 +25,7 @@ namespace VisualPinball.Unity public abstract class ItemSubAuthoring : ItemAuthoring where TItem : Item, IRenderable where TData : ItemData - where TMainAuthoring : ItemMainAuthoring + where TMainAuthoring : ItemMainRenderableAuthoring { /// /// We're in a sub component here, so in order to retrieve the data, @@ -59,7 +59,7 @@ public abstract class ItemSubAuthoring : ItemAutho /// public TMainAuthoring MainAuthoring => FindMainAuthoring(); - public IItemMainAuthoring ParentAuthoring => MainAuthoring.ParentAuthoring; + public IItemMainRenderableAuthoring ParentAuthoring => MainAuthoring.ParentAuthoring; public abstract IEnumerable ValidParents { get; } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs index 8b9b22cdb..31c6b5c0c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs @@ -61,6 +61,12 @@ public void CreateSizedBall(float radius) _ballManager.CreateBall(Item, radius, 1f, Entity); } + public void Kick() + { + SimulationSystemGroup.QueueAfterBallCreation(() => KickXYZ(Table, Entity, Data.Angle, Data.Speed, 0, 0, 0, 0)); + } + + public void Kick(float angle, float speed, float inclination = 0) { SimulationSystemGroup.QueueAfterBallCreation(() => KickXYZ(Table, Entity, angle, speed, inclination, 0, 0, 0)); @@ -87,7 +93,7 @@ public void DestroyBall() void IApiCoil.OnCoil(bool enabled, bool _) { if (enabled) { - Kick(Data.Angle, Data.Speed); + Kick(); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerAuthoring.cs index b68491ecc..acf83f0b6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerAuthoring.cs @@ -34,7 +34,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Kicker")] - public class KickerAuthoring : ItemMainAuthoring, + public class KickerAuthoring : ItemMainRenderableAuthoring, ISwitchAuthoring, ICoilAuthoring, IConvertGameObjectToEntity { protected override Kicker InstantiateItem(KickerData data) => new Kicker(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs index aef49cc77..2157d0555 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightAuthoring.cs @@ -30,7 +30,7 @@ namespace VisualPinball.Unity { [AddComponentMenu("Visual Pinball/Game Item/Light")] - public class LightAuthoring : ItemMainAuthoring + public class LightAuthoring : ItemMainRenderableAuthoring { private UnityEngine.Light _unityLight; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Playfield/PlayfieldAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Playfield/PlayfieldAuthoring.cs index 342fd16e4..6608811ba 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Playfield/PlayfieldAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Playfield/PlayfieldAuthoring.cs @@ -23,7 +23,7 @@ namespace VisualPinball.Unity { - public class PlayfieldAuthoring : ItemMainAuthoring, + public class PlayfieldAuthoring : ItemMainRenderableAuthoring, IConvertGameObjectToEntity { public override bool CanBeTransformed => false; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerAuthoring.cs index aa320eb96..7ce56022a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerAuthoring.cs @@ -26,7 +26,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Plunger")] - public class PlungerAuthoring : ItemMainAuthoring, + public class PlungerAuthoring : ItemMainRenderableAuthoring, ICoilAuthoring, IConvertGameObjectToEntity { protected override Plunger InstantiateItem(PlungerData data) => new Plunger(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveAuthoring.cs index ef0e7175c..8394b311c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Primitive/PrimitiveAuthoring.cs @@ -30,7 +30,7 @@ namespace VisualPinball.Unity { [AddComponentMenu("Visual Pinball/Game Item/Primitive")] - public class PrimitiveAuthoring : ItemMainAuthoring, IConvertGameObjectToEntity + public class PrimitiveAuthoring : ItemMainRenderableAuthoring, IConvertGameObjectToEntity { protected override Primitive InstantiateItem(PrimitiveData data) => new Primitive(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampAuthoring.cs index 2bd724802..318871ab8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampAuthoring.cs @@ -33,7 +33,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Ramp")] - public class RampAuthoring : ItemMainAuthoring, IDragPointsEditable, IConvertGameObjectToEntity + public class RampAuthoring : ItemMainRenderableAuthoring, IDragPointsEditable, IConvertGameObjectToEntity { protected override Ramp InstantiateItem(RampData data) => new Ramp(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberAuthoring.cs index b272f8713..a84a27748 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberAuthoring.cs @@ -32,7 +32,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Rubber")] - public class RubberAuthoring : ItemMainAuthoring, + public class RubberAuthoring : ItemMainRenderableAuthoring, IDragPointsEditable, IConvertGameObjectToEntity { protected override Rubber InstantiateItem(RubberData data) => new Rubber(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerAuthoring.cs index ca209384d..b2f32de51 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Spinner/SpinnerAuthoring.cs @@ -34,7 +34,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Spinner")] - public class SpinnerAuthoring : ItemMainAuthoring, + public class SpinnerAuthoring : ItemMainRenderableAuthoring, ISwitchAuthoring, IConvertGameObjectToEntity { protected override Spinner InstantiateItem(SpinnerData data) => new Spinner(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceAuthoring.cs index b46cd03f9..683b923b3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceAuthoring.cs @@ -32,7 +32,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Surface")] - public class SurfaceAuthoring : ItemMainAuthoring, + public class SurfaceAuthoring : ItemMainRenderableAuthoring, IConvertGameObjectToEntity, IDragPointsEditable { protected override Surface InstantiateItem(SurfaceData data) => new Surface(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs index da36d2657..ba1cbd182 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Table/TableAuthoring.cs @@ -46,14 +46,15 @@ using VisualPinball.Engine.VPT.TextBox; using VisualPinball.Engine.VPT.Timer; using VisualPinball.Engine.VPT.Trigger; - +using VisualPinball.Engine.VPT.Trough; using Logger = NLog.Logger; using SurfaceData = VisualPinball.Engine.VPT.Surface.SurfaceData; +using Texture = VisualPinball.Engine.VPT.Texture; namespace VisualPinball.Unity { [AddComponentMenu("Visual Pinball/Table")] - public class TableAuthoring : ItemMainAuthoring + public class TableAuthoring : ItemMainRenderableAuthoring { protected override Table InstantiateItem(TableData data) => RecreateTable(data); @@ -157,11 +158,9 @@ public override void Restore() public Texture2D GetTexture(string name) { var lowerName = name.ToLower(); - bool forceRecreate = false; // check to see if the texture we're after has been flagged as dirty and thus needs to be recreated from table data - if (CheckDirty(lowerName)) { - forceRecreate = true; - } + var forceRecreate = CheckDirty(lowerName); + // don't need to recreate it, and we have the texture in cache if (!forceRecreate && _unityTextures.ContainsKey(lowerName)) { return _unityTextures[lowerName]; @@ -244,6 +243,7 @@ public Table CreateTable(TableData data) Restore(table); Restore(table); Restore(table); + Restore(table); return table; } @@ -257,7 +257,7 @@ public Table RecreateTable(TableData tableData) } private void Restore(Table table) where TData : ItemData - where TItem : Item, IRenderable + where TItem : Item where TComp : ItemMainAuthoring { foreach (var component in GetComponentsInChildren(true)) diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerAuthoring.cs index 546d81773..ce888e6a6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerAuthoring.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerAuthoring.cs @@ -33,7 +33,7 @@ namespace VisualPinball.Unity { [ExecuteAlways] [AddComponentMenu("Visual Pinball/Game Item/Trigger")] - public class TriggerAuthoring : ItemMainAuthoring, + public class TriggerAuthoring : ItemMainRenderableAuthoring, ISwitchAuthoring, IDragPointsEditable, IConvertGameObjectToEntity { protected override Trigger InstantiateItem(TriggerData data) => new Trigger(data); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough.meta new file mode 100644 index 000000000..3a5d80118 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b2be665b13677dd4b9b1fa68b762d219 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs new file mode 100644 index 000000000..0f91e65d4 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs @@ -0,0 +1,263 @@ +// 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 System.Collections.Generic; +using NLog; +using UnityEngine; +using VisualPinball.Engine.VPT.Trough; +using Logger = NLog.Logger; + +namespace VisualPinball.Unity +{ + public class TroughApi : ItemApi, IApi, IApiInitializable, IApiSwitchDevice, IApiCoilDevice + { + /// + /// The entry kicker is where the ball rolls into the trough. + /// + private KickerApi _entryKicker; + + /// + /// The exit kicker is where new balls are created when we get the eject + /// coil event from the gamelogic engine. + /// + private KickerApi _exitKicker; + + /// + /// The ball switches. These are virtual switches that don't exist on the + /// playfield, but running on them will + /// send the event to the gamelogic engine. + /// + private DeviceSwitch[] _ballSwitches; + + /// + /// Number of virtual balls currently in the trough + /// + private int _ballCount = 0; + + /// + /// Eject coil triggers the kicker that throws out the ball. + /// + private IApiCoil _ejectCoil; + + /// + /// Since TriggerApi implements IApiSwitch, we return directly this when the + /// engine asks for the jam switch. + /// + /// + /// + /// Basically we short-wire the jam trigger to the switch, so the jam trigger + /// events go directly to the gamelogic engine. + /// + /// In case we need to hook into the jam trigger logic here, uncomment the + /// blocks below (ctrl+f jam events) + /// + private TriggerApi _jamTrigger; + + /// + /// The player will ask for switches to hook up to the gamelogic engine, + /// this allows fast lookup. + /// + private readonly Dictionary _switchLookup = new Dictionary(); + + private DeviceSwitch EntrySwitch => _ballSwitches[Data.SwitchCount - 1]; + private DeviceSwitch EjectSwitch => _ballSwitches[0]; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + /// + /// Event emitted when the table is started. + /// + public event EventHandler Init; + + internal TroughApi(Trough item, Player player) : base(item, player) + { + Debug.Log("Trough API instantiated."); + } + + /// + /// This is called when the player starts. It tells the trough + /// "please give me switch XXX so I can hook it up to the gamelogic engine". + /// + /// + /// + IApiSwitch IApiSwitchDevice.Switch(string switchId) + { + // if the engine is asking for the jam switch, return the trigger directly. + if (switchId == Trough.JamSwitchId) { + return _jamTrigger; + } + return _switchLookup.ContainsKey(switchId) ? _switchLookup[switchId] : null; + } + + /// + /// Returns a coil by ID. Same principle as + /// + /// + /// + IApiCoil IApiCoilDevice.Coil(string coilId) + { + if (coilId == Trough.EjectCoilId) { + return _ejectCoil; + } + return null; + } + + /// + /// Create a ball in the trough without triggering extra events + /// + internal void AddBall() + { + if (_ballCount < Data.BallCount) { + _ballSwitches[_ballCount].SetSwitch(true); + + _ballCount++; + } + } + + /// + /// If there are any balls in the trough add one to play and + /// trigger any switches which the remaining balls would activate + /// + internal void OnEjectCoil(bool closed) + { + if (closed && (_ballCount > 0)) { + Logger.Info("Spawning new ball."); + + _exitKicker.CreateBall(); + _exitKicker.Kick(); + + for (int i = 0; i < _ballCount; i++) { + _ballSwitches[i].ScheduleSwitch(false, Data.SettleTime / 2); + } + + _ballCount--; + + for (int i = 0; i < _ballCount; i++) { + _ballSwitches[i].ScheduleSwitch(true, Data.SettleTime); + } + } + } + + /// + /// If there's room in the trough remove the ball from play + /// and trigger any switches which it would roll over + /// + private void OnEntryKickerHit(object sender, EventArgs args) + { + if (_ballCount < Data.BallCount) { + Logger.Info("Draining ball."); + + (sender as KickerApi)?.DestroyBall(); + + int openSwitches = Data.BallCount - _ballCount; + + for (int i = 1; i < openSwitches; i++) { + _ballSwitches[Data.BallCount - i].ScheduleSwitch(true, Data.SettleTime * i); + _ballSwitches[Data.BallCount - i].ScheduleSwitch(false, Data.SettleTime * i + Data.SettleTime / 2); + } + + _ballSwitches[_ballCount].ScheduleSwitch(true, Data.SettleTime * openSwitches); + + _ballCount++; + } + } + + #region Wiring + + void IApiInitializable.OnInit(BallManager ballManager) + { + // playfield elements + _entryKicker = TableApi.Kicker(Data.EntryKicker); + _exitKicker = TableApi.Kicker(Data.ExitKicker); + _jamTrigger = TableApi.Trigger(Data.JamSwitch); + + // setup entry kicker handler + if (_entryKicker != null) { + _entryKicker.Hit += OnEntryKickerHit; + } + + // in case we need also need to handle jam events here, uncomment + // if (_jamTrigger != null) { + // _jamTrigger.Hit += OnJamTriggerHit; + // _jamTrigger.UnHit += OnJamTriggerUnHit; + // } + + // create switches to hook up + _ballSwitches = new DeviceSwitch[Data.SwitchCount]; + foreach (var sw in Item.AvailableSwitches) { + if (int.TryParse(sw.Id, out var id)) { + _ballSwitches[id - 1] = CreateSwitch(false); + _switchLookup[sw.Id] = _ballSwitches[id - 1]; + + } else if (sw.Id == Trough.JamSwitchId) { + // we short-wire the jam trigger to the switch, so we don't care about it here, + // all the jam trigger does push its switch events to the gamelogic engine. in + // case we need to hook into the jam trigger logic here, uncomment those two + // lines and and relay the events manually to the engine. + //_jamSwitch = CreateSwitch(false); + //_switchLookup[sw.Id] = _jamSwitch; + + } else { + Logger.Warn($"Unknown switch ID {sw.Id}"); + } + } + + // setup eject coil + _ejectCoil = new TroughEjectCoil(this); + + // finally, emit the event for anyone else to chew on + Init?.Invoke(this, EventArgs.Empty); + } + + void IApi.OnDestroy() + { + Logger.Info("Destroying trough!"); + + if (_entryKicker != null) { + _entryKicker.Hit -= OnEntryKickerHit; + } + + // in case we need also need to handle jam events here, uncomment + // if (_jamTrigger != null) { + // _jamTrigger.Hit -= OnJamTriggerHit; + // _jamTrigger.UnHit -= OnJamTriggerUnHit; + // } + } + + // in case we need also need to handle jam events here, uncomment + // private IApiSwitch _jamSwitch; + // private void OnJamTriggerHit(object sender, EventArgs e) => _jamSwitch?.OnSwitch(true); + // private void OnJamTriggerUnHit(object sender, EventArgs e) => _jamSwitch?.OnSwitch(false); + + #endregion + } + + internal class TroughEjectCoil : IApiCoil + { + private readonly TroughApi _troughApi; + + public TroughEjectCoil(TroughApi troughApi) + { + _troughApi = troughApi; + } + + public void OnCoil(bool closed, bool isHoldCoil) + { + _troughApi.OnEjectCoil(closed); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs.meta new file mode 100644 index 000000000..e0d3099e8 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4020d37ef294341f0b853a9dadccb9a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs new file mode 100644 index 000000000..70ec006d0 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs @@ -0,0 +1,91 @@ +// 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 CompareOfFloatsByEqualityOperator +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable MemberCanBePrivate.Global +#endregion + +using System; +using System.Collections.Generic; +using Unity.Mathematics; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.Networking.PlayerConnection; +using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT.Trough; + +namespace VisualPinball.Unity +{ + [AddComponentMenu("Visual Pinball/Trough")] + public class TroughAuthoring : ItemMainAuthoring, ISwitchDeviceAuthoring, ICoilDeviceAuthoring + { + public IEnumerable AvailableSwitches => Item.AvailableSwitches; + public IEnumerable AvailableCoils => Item.AvailableCoils; + + protected override Trough InstantiateItem(TroughData data) => new Trough(data); + public override IEnumerable ValidParents { get; } = new Type[0]; + + private Vector3 EntryPickerPos(float height) => string.IsNullOrEmpty(Data.EntryKicker) + ? Vector3.zero + : Table.Kicker(Data.EntryKicker).Data.Center.ToUnityVector3(height); + + private Vector3 ExitKickerPos(float height) => string.IsNullOrEmpty(Data.ExitKicker) + ? Vector3.zero + : Table.Kicker(Data.ExitKicker).Data.Center.ToUnityVector3(height); + + private void Start() + { + GetComponentInParent().RegisterTrough(Item, gameObject); + } + + public override void Restore() + { + Item.Name = name; + } + + private void OnDrawGizmosSelected() + { + if (!string.IsNullOrEmpty(Data.EntryKicker) && !string.IsNullOrEmpty(Data.ExitKicker)) { + var ltw = GetComponentInParent().transform; + var entryPos = EntryPickerPos(0f); + var exitPos = ExitKickerPos(0f); + var entryWorldPos = ltw.TransformPoint(entryPos); + var exitWorldPos = ltw.TransformPoint(exitPos); + var localPos = transform.localPosition; + var localPos0 = new Vector3(localPos.x, localPos.y, 0f); + var pos = ltw.TransformPoint(localPos0); + DrawArrow(entryWorldPos, pos - entryWorldPos); + DrawArrow(pos, exitWorldPos - pos); + } + } + + public void UpdatePosition() + { + // place trough between entry and exit kicker + var pos = (EntryPickerPos(75f) + ExitKickerPos(75f)) / 2; + transform.localPosition = pos; + } + + private void OnDestroy() + { + if (!Application.isPlaying) { + Table?.Remove(Name); + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs.meta new file mode 100644 index 000000000..870855312 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 426f115d8fbd6914bb7c67e1cc410097 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: b40cab9a7ee497648bc7d70ea4717888, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs new file mode 100644 index 000000000..98aceafbc --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs @@ -0,0 +1,34 @@ +// 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 Unity.Entities; +using UnityEngine; +using VisualPinball.Engine.Game; + +namespace VisualPinball.Unity +{ + internal static class TroughExtensions + { + public static ConvertedItem SetupGameObject(this Engine.VPT.Trough.Trough trough, GameObject obj) + { + var mainAuthoring = obj.AddComponent(); + mainAuthoring.SetItem(trough); + mainAuthoring.UpdatePosition(); + obj.GetComponentInParent().RegisterTrough(trough, obj); + return new ConvertedItem(mainAuthoring); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs.meta new file mode 100644 index 000000000..5cc93bac9 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trough/TroughExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f92811f35ae2d479cb785222f9e19832 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: