diff --git a/data/CameraBehavior.cs b/data/CameraBehavior.cs new file mode 100644 index 0000000000000000000000000000000000000000..6e5fa8ea361b5c218746c436cf6deeb77756d917 --- /dev/null +++ b/data/CameraBehavior.cs @@ -0,0 +1,25 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using Quantum; + +public class CameraBehavior : QuantumCallbacks +{ + private bool _isInverted = false; + + public override void OnUpdateView(QuantumGame game) + { + var player = game.Session.LocalPlayerIndices[0]; + if (player != 0 && !_isInverted) + { + transform.eulerAngles = new Vector3(transform.eulerAngles.x, transform.eulerAngles.y +180, transform.eulerAngles.z); + + for (int i = 0; i < ChessViewUpdater.Instance.Pieces.Length; i++) + { + var p = ChessViewUpdater.Instance.Pieces[i]; + p.transform.eulerAngles = new Vector3(p.transform.eulerAngles.x, p.transform.eulerAngles.y, p.transform.eulerAngles.z + 180); + } + _isInverted = true; + } + } +} diff --git a/data/CameraFollow.cs b/data/CameraFollow.cs new file mode 100644 index 0000000000000000000000000000000000000000..1b5c8f66e8bf83fc34d9b939efe6abca705db046 --- /dev/null +++ b/data/CameraFollow.cs @@ -0,0 +1,161 @@ +using Photon.Deterministic; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +[RequireComponent(typeof(Camera))] +public class CameraFollow : MonoBehaviour +{ + public Transform Target; + public Quantum.LayerMask OcclusionQueryMask = -1; + [SerializeField, Range(2f, 15f)] + float distance = 5f; + [SerializeField, Range(0.1f, 5f)] + float scrollSpeed = 1f; + [SerializeField, Min(0f)] + float focusRadius = 1f; + [SerializeField, Range(0f, 1f)] + float focusCentering = 0.5f; + + [SerializeField, Range(1f, 360f)] + float rotationSpeed = 90f; + [SerializeField, Range(-30f, 89f)] + float minVerticalAngle = -30f, maxVerticalAngle = 60f; + + Vector3 focusPoint; + Vector2 orbitAngles = new Vector2(45f, 0f); + + private Camera _camera; + void Awake() + { + //focusPoint = focus.position; + transform.localRotation = Quaternion.Euler(orbitAngles); + _camera = GetComponent(); + } + void LateUpdate() + { + if (Target == null) return; + UpdateDistance(); + UpdateFocusPoint(); + Quaternion lookRotation; + if (ManualRotation()) + { + ConstrainAngles(); + lookRotation = Quaternion.Euler(orbitAngles); + } + else + { + lookRotation = transform.localRotation; + } + + Vector3 lookDirection = lookRotation * Vector3.forward; + Vector3 lookPosition = focusPoint - lookDirection * distance; + + if (Cast(focusPoint, lookRotation, -lookDirection, out var hitDistance, distance - _camera.nearClipPlane)) + { + lookPosition = focusPoint - lookDirection * (hitDistance + _camera.nearClipPlane); + } + + transform.SetPositionAndRotation(lookPosition, lookRotation); + } + + private unsafe bool Cast(Vector3 origin, Quaternion rotation, Vector3 direction, out float hitDistance, float distance) + { + hitDistance = 0; + + var frame = QuantumRunner.Default?.Game?.Frames.Verified; + if (frame != null) + { + var shape = Quantum.Shape3D.CreateBox(CameraPlaneExtends.ToFPVector3()); + var options = Quantum.QueryOptions.ComputeDetailedInfo | Quantum.QueryOptions.HitAll; + var hit = frame.Physics3D.ShapeCast(origin.ToFPVector3(), rotation.ToFPQuaternion(), &shape, (direction * distance).ToFPVector3(), OcclusionQueryMask, options); + if (hit.HasValue) + { + hitDistance = (origin - hit.Value.Point.ToUnityVector3()).magnitude; + return true; + } + } + return false; + } + + Vector3 CameraPlaneExtends + { + get + { + Vector3 halfExtends; + halfExtends.y = + _camera.nearClipPlane * + Mathf.Tan(0.5f * Mathf.Deg2Rad * _camera.fieldOfView) * 1.25f; + halfExtends.x = halfExtends.y * _camera.aspect; + halfExtends.z = 0.01f; + return halfExtends; + } + } + + void UpdateDistance() + { + distance += Input.GetAxisRaw("Mouse ScrollWheel") * scrollSpeed; + distance = Mathf.Clamp(distance, 2, 15); + } + + bool ManualRotation() + { + Vector2 input = new Vector2( + -Input.GetAxis("Mouse Y"), + Input.GetAxis("Mouse X") + ); + const float e = 0.001f; + if (input.x < e || input.x > e || input.y < e || input.y > e) + { + orbitAngles += rotationSpeed * Time.unscaledDeltaTime * input; + return true; + } + return false; + } + + void UpdateFocusPoint() + { + Vector3 targetPoint = Target.position; + if (focusRadius > 0f) + { + float distance = Vector3.Distance(targetPoint, focusPoint); + float t = 1f; + if (distance > 0.01f && focusCentering > 0f) + { + t = Mathf.Pow(1f - focusCentering, Time.unscaledDeltaTime); + } + if (distance > focusRadius) + { + t = Mathf.Min(t, focusRadius / distance); + } + focusPoint = Vector3.Lerp(targetPoint, focusPoint, t); + } + else + { + focusPoint = targetPoint; + } + } + + void ConstrainAngles() + { + orbitAngles.x = + Mathf.Clamp(orbitAngles.x, minVerticalAngle, maxVerticalAngle); + + if (orbitAngles.y < 0f) + { + orbitAngles.y += 360f; + } + else if (orbitAngles.y >= 360f) + { + orbitAngles.y -= 360f; + } + } + + void OnValidate() + { + if (maxVerticalAngle < minVerticalAngle) + { + maxVerticalAngle = minVerticalAngle; + } + } +} diff --git a/data/CardManager.cs b/data/CardManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..6ffbe9e1ee122a5596e739edfc035b55cace6917 --- /dev/null +++ b/data/CardManager.cs @@ -0,0 +1,176 @@ +namespace Quantum +{ + using Photon.Deterministic; + + unsafe partial struct CardManager + { + // CONSTANTS + + public const byte AVAILABLE_CARDS_COUNT = 4; + + // PUBLIC METHODS + + public void Initialize(Frame frame, CardInfo[] cards, GameplaySettings settings) + { + var availableCards = frame.AllocateList(AVAILABLE_CARDS_COUNT); + var cardQueue = frame.AllocateList(cards.Length); + + CardQueue = cardQueue; + AvailableCards = availableCards; + + for (int idx = 0, count = cards.Length; idx < count; idx++) + { + cardQueue.Add(cards[idx]); + } + + cardQueue.Shuffle(frame.RNG); + + for (int idx = 0; idx < AVAILABLE_CARDS_COUNT; idx++) + { + availableCards.Add(cardQueue[idx]); + } + + QueueHeadIndex = AVAILABLE_CARDS_COUNT; + + CurrentEnergy = (int)settings.StartEnergy; + MaxEnergy = (int)settings.MaxEnergy; + } + + public void SetFillRate(FP energyFillRate) + { + EnergyFillRate = energyFillRate; + } + + public void Deinitialize(Frame frame) + { + frame.FreeList(AvailableCards); + frame.FreeList(CardQueue); + + AvailableCards = default; + CardQueue = default; + } + + public void Update(Frame frame, EntityRef entity) + { + if (CardQueue.Ptr.Offset == 0) + return; + + CurrentEnergy = FPMath.Clamp(CurrentEnergy + frame.DeltaTime * EnergyFillRate, FP._0, MaxEnergy); + NextFillTime -= frame.DeltaTime; + + if (EmptySlots > 0 && NextFillTime <= FP._0) + { + FillCardSlot(frame, entity); + } + } + + public void UseCard(Frame frame, EntityRef entity, PlayerRef owner, byte cardIndex, FPVector2 position, FP rotation) + { + if (cardIndex < 0 || cardIndex >= AVAILABLE_CARDS_COUNT) + { + Log.Error($"Out of range card use request Index: {cardIndex}"); + return; + } + + var availableCards = frame.ResolveList(AvailableCards); + var card = availableCards[cardIndex]; + + if (card.CardSettings.Id.IsValid == false) + { + Log.Error($"Invalid card use request. Index: {cardIndex}"); + return; + } + + var settings = frame.FindAsset(card.CardSettings.Id); + if (settings.EnergyCost > CurrentEnergy) + { + Log.Error($"Not enough energy card request. Index: {cardIndex} Cost: {settings.EnergyCost} Current: {CurrentEnergy}"); + return; + } + + if (settings is UnitSettings) + { + var gameplay = frame.Unsafe.GetPointerSingleton(); + if (gameplay->IsValidUnitPosition(frame, owner, position) == false) + { + Log.Error($"Invalid unit spawn position. Position: {position}"); + return; + } + } + + frame.SpawnCard(settings, owner, position, rotation, card.Level); + + AddCardToQueue(frame, card); + + CurrentEnergy -= settings.EnergyCost; + availableCards[cardIndex] = default; + EmptySlots += 1; + + frame.Events.CardsChanged(entity); + } + + // PRIVATE METHODS + + private void FillCardSlot(Frame frame, EntityRef entity) + { + var availableCards = frame.ResolveList(AvailableCards); + + for (int idx = 0; idx < AVAILABLE_CARDS_COUNT; idx++) + { + if (availableCards[idx].CardSettings.Id.IsValid == true) + continue; + + var cardQueue = frame.ResolveList(CardQueue); + + availableCards[idx] = cardQueue[QueueHeadIndex]; + QueueHeadIndex = (byte)((QueueHeadIndex + 1) % cardQueue.Count); + EmptySlots -= 1; + NextFillTime = FP._2; + + frame.Events.CardsChanged(entity); + break; + } + } + + private void AddCardToQueue(Frame frame, CardInfo card) + { + var cardQueue = frame.ResolveList(CardQueue); + + cardQueue[QueueTailIndex] = card; + QueueTailIndex = (byte)((QueueTailIndex + 1) % cardQueue.Count); + } + + private FPVector2 TransformPosition(FPVector2 position, int unitCount, int unitIndex) + { + if (unitCount == 1) + return position; + + if (unitCount <= 4) + { + var rotationOffset = FP.Rad_180 * FP._2 / unitCount * unitIndex; + position += FPVector2.Rotate(FPVector2.Right * FP._0_50, rotationOffset); + } + + if (unitCount <= 12) + { + if (unitIndex < 4) + { + var rotationOffset = FP.Rad_180 * FP._2 / 4 * unitIndex; + position += FPVector2.Rotate(FPVector2.Right * FP._0_50, rotationOffset); + } + else + { + var rotationOffset = FP.Rad_180 * FP._2 / (unitCount - 4) * unitIndex; + position += FPVector2.Rotate(FPVector2.Right, rotationOffset); + } + } + + return position; + } + } + + [System.Serializable] + unsafe partial struct CardInfo + { + } +} diff --git a/data/CardManager.qtn b/data/CardManager.qtn new file mode 100644 index 0000000000000000000000000000000000000000..e3df3519213860268e691a1146fe4b7c9ccd8d0d --- /dev/null +++ b/data/CardManager.qtn @@ -0,0 +1,36 @@ +asset CardSettings; + +[ExcludeFromPrototype] +component CardManager +{ + Byte EmptySlots; + Byte QueueHeadIndex; + Byte QueueTailIndex; + FP CurrentEnergy; + FP EnergyFillRate; + FP MaxEnergy; + FP NextFillTime; + list CardQueue; + list AvailableCards; +} + +struct CardInfo +{ + AssetRefCardSettings CardSettings; + Byte Level; +} + +[PreserveInPrototype] +enum ERarity : byte +{ + Common, + Uncommon, + Rare, + Epic, + Legendary, +} + +synced event CardsChanged +{ + EntityRef Entity; +} diff --git a/data/CardManagerSystem.cs b/data/CardManagerSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..41a604f2cab1c3f06d4cc6b71e8853cdedbb2bb3 --- /dev/null +++ b/data/CardManagerSystem.cs @@ -0,0 +1,38 @@ +namespace Quantum +{ + using Photon.Deterministic; + + unsafe class CardManagerSystem : SystemMainThread + { + public override void Update(Frame frame) + { + foreach (var pair in frame.Unsafe.GetComponentBlockIterator()) + { + pair.Component->Update(frame, pair.Entity); + } + + var gameplay = frame.Unsafe.GetPointerSingleton(); + if (gameplay->IsActive == false) + return; + + foreach (var pair in frame.Unsafe.GetComponentBlockIterator()) + { + var command = frame.GetPlayerCommand(pair.Component->PlayerRef); + switch (command) + { + case UseCardCommand useCard: + ProcessCommand(frame, pair.Entity, pair.Component->PlayerRef, useCard); + break; + } + } + } + + // PRIVATE METHODS + + private void ProcessCommand(Frame frame, EntityRef entity, PlayerRef playerRef, UseCardCommand useCard) + { + var cardManager = frame.Unsafe.GetPointer(entity); + cardManager->UseCard(frame, entity, playerRef, useCard.CardIndex, useCard.Position, playerRef * FP.Rad_180); + } + } +} diff --git a/data/CardSettings.cs b/data/CardSettings.cs new file mode 100644 index 0000000000000000000000000000000000000000..a0791c9f760cdc839b1365838520e397b0d981e1 --- /dev/null +++ b/data/CardSettings.cs @@ -0,0 +1,14 @@ +namespace Quantum +{ + using Photon.Deterministic; + using Quantum.Inspector; + + public abstract partial class CardSettings + { + [Header("Card")] + public AssetRefEntityPrototype Prefab; + public ERarity Rarity; + public byte EnergyCost; + public FP ActivationDelay; + } +} diff --git a/data/Chain.User.cs b/data/Chain.User.cs new file mode 100644 index 0000000000000000000000000000000000000000..2a711b6156c9709fec8d169c209fe6f3c53c27d8 --- /dev/null +++ b/data/Chain.User.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Photon.Deterministic; + +namespace Quantum +{ + unsafe partial struct Chain + { + public void Update(FrameThreadSafe frame, ref ChainSystem.Filter filter) + { + FPVector3 direction = filter.Transform->Position - LastPosition; + Alpha = direction.Magnitude / DistanceThreshold; + + // cycle element + if (Alpha > FP._1) + { + Alpha = Alpha % FP._1; + AddBreadCrumb(direction); + } + + filter.Body->Drag = MinDrag + Count * DragPerItem; + if (filter.Drivable->Grounded == false) filter.Body->GravityScale = FP._1; + filter.Drivable->Grounded = false; +#if DEBUG + for (int i = 0; i < Positions.Length; i++) + { + Draw.Sphere(Positions[i], FP._0_10, ColorRGBA.ColliderBlue); + } +#endif + } + + public void AddBreadCrumb(FPVector3 direction) + { + FPVector3 newPosition = LastPosition + direction.Normalized * DistanceThreshold; + *Positions.GetPointer(Current) = newPosition; + Current = (Current + 1) % Positions.Length; + LastPosition = newPosition; + } + } +} diff --git a/data/ChainItem.cs b/data/ChainItem.cs new file mode 100644 index 0000000000000000000000000000000000000000..2b57ad23e8d7d457b62861dfabc0a31c8532c4e7 --- /dev/null +++ b/data/ChainItem.cs @@ -0,0 +1,41 @@ +using System; +using Photon.Deterministic; + +namespace Quantum +{ + unsafe partial struct ChainItem + { + public void Update(FrameThreadSafe frame, ref ChainItemSystem.Filter filter) + { + // READ-ONLY for this component, PLEASE + if (frame.TryGetPointer(Chain, out var chain)) + { + if (chain->Count <= Index) + { + Destroy = true; + return; + } + + int indexFrom = (chain->Current - Index + chain->Positions.Length - 2) % chain->Positions.Length; + int indexTo = (indexFrom + 1) % chain->Positions.Length; + FPVector3 from = chain->Positions[indexFrom]; + FPVector3 to = chain->Positions[indexTo]; + FPVector3 position = FPVector3.Lerp(from, to, chain->Alpha); + filter.Transform->Position = position; + + FPVector3 direction = (to - from).Normalized; + FPQuaternion desiredRotation = FPQuaternion.LookRotation(direction); + FP angle = FPQuaternion.Angle(desiredRotation, filter.Transform->Rotation); + if (angle < SnapAngle) + { + filter.Transform->Rotation = FPQuaternion.RotateTowards(filter.Transform->Rotation, desiredRotation, RotationSpeed * frame.DeltaTime); + } + else + { + filter.Transform->Rotation = desiredRotation; + } + + } + } + } +} diff --git a/data/ChainItemSystem.cs b/data/ChainItemSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..43d16b5b28a2277d862dd5a73bc75d68fcf11585 --- /dev/null +++ b/data/ChainItemSystem.cs @@ -0,0 +1,20 @@ +using System; +using Photon.Deterministic; +using Quantum.Task; + +namespace Quantum +{ + public unsafe class ChainItemSystem : SystemThreadedFilter + { + public struct Filter + { + public EntityRef Entity; + public Transform3D* Transform; + public ChainItem* Item; + } + public override void Update(FrameThreadSafe frame, ref Filter filter) + { + filter.Item->Update(frame, ref filter); + } + } +} diff --git a/data/ChainSystem.cs b/data/ChainSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..b12364c0678a788bed9fd6f31fba1aecd395254f --- /dev/null +++ b/data/ChainSystem.cs @@ -0,0 +1,72 @@ +using System; +using Photon.Deterministic; +using Quantum.Task; + +namespace Quantum +{ + public unsafe class ChainSystem : SystemThreadedFilter, ISignalOnTrigger3D, ISignalOnCollision3D, ISignalOnComponentAdded + { + public struct Filter + { + public EntityRef Entity; + public Transform3D* Transform; + public PhysicsBody3D* Body; + public Chain* Chain; + public Drivable* Drivable; + } + + public void OnAdded(Frame frame, EntityRef entity, Chain* chain) + { + Transform3D transform = frame.Get(entity); + chain->LastPosition = transform.Position; + for (int i = 0; i < 5; i++) + { + chain->AddBreadCrumb(transform.Back); + } + } + + public void OnTrigger3D(Frame frame, TriggerInfo3D info) + { + if (frame.Unsafe.TryGetPointer(info.Other, out var pickup) && frame.Unsafe.TryGetPointer(info.Entity, out var chain)) + { + EntityRef entity = frame.Create(pickup->ItemPrototype); + if (frame.Unsafe.TryGetPointer(entity, out var item)) + { + int index = chain->Count++; + item->Chain = info.Entity; + item->Index = index; + } + if (frame.Unsafe.TryGetPointerSingleton(out var spawner)) + { + spawner->Count--; + } + frame.Destroy(info.Other); + } + } + + public void OnCollision3D(Frame frame, CollisionInfo3D info) + { + if (frame.Unsafe.TryGetPointer(info.Entity, out var drivable) && frame.Unsafe.TryGetPointer(info.Entity, out var body)) + { + FP angle = FPVector3.Angle(info.ContactNormal, FPVector3.Up); + if (angle < 45) + { + drivable->Grounded = true; + drivable->SurfaceNormal = info.ContactNormal; + body->GravityScale = FP._0; + } + } + + if (frame.Unsafe.TryGetPointer(info.Other, out var item) && frame.Unsafe.TryGetPointer(item->Chain, out var chain)) + { + chain->Count = Math.Min(item->Index, chain->Count); + info.IgnoreCollision = true; + } + } + + public override void Update(FrameThreadSafe frame, ref Filter filter) + { + filter.Chain->Update(frame, ref filter); + } + } +} diff --git a/data/CheckIndicatorView.cs b/data/CheckIndicatorView.cs new file mode 100644 index 0000000000000000000000000000000000000000..01fa87f5951fdc055e120edee622ee80a13cf815 --- /dev/null +++ b/data/CheckIndicatorView.cs @@ -0,0 +1,34 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using Quantum; + +public class CheckIndicatorView : QuantumCallbacks { + + public GameObject Indicator; + public PieceView Piece; + + void Start () { + Indicator.SetActive(false); + QuantumEvent.Subscribe(this, SetCheckIndicator); + QuantumEvent.Subscribe(this, ResetCheckIndicator); + } + + private void SetCheckIndicator(EventPlayerInCheck e) + { + if (e.Color == Piece.Color) + { + Indicator.SetActive(true); + } + } + + private void ResetCheckIndicator(EventTurnEnded e) + { + Indicator.SetActive(false); + } + + protected override void OnDisable() + { + QuantumEvent.UnsubscribeListener(this); + } +} diff --git a/data/ChecksumVerification.cs b/data/ChecksumVerification.cs new file mode 100644 index 0000000000000000000000000000000000000000..69c2187887f59b237530c54993e035bd583c641d --- /dev/null +++ b/data/ChecksumVerification.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Quantum { + public class ChecksumVerification : IDisposable { + private Dictionary _checksums; + private Quantum.CallbackDispatcher _gameCallbacks; + private bool _verbose; + + public ChecksumVerification(string pathToChecksumFile, Quantum.CallbackDispatcher callbacks, bool verbose = false) { + _checksums = JsonConvert.DeserializeObject(File.ReadAllText(pathToChecksumFile), ReplayJsonSerializerSettings.GetSettings()).ToDictionary(); + _gameCallbacks = callbacks; + _gameCallbacks.Subscribe(this, (CallbackSimulateFinished callback) => OnSimulateFinished(callback.Game, callback.Frame)); + _verbose = verbose; + } + + private void OnSimulateFinished(QuantumGame game, Frame frame) { + if (frame != null) { + var f = frame.Number; + var cs = ChecksumFileHelper.UlongToLong(frame.CalculateChecksum()); + + if (_checksums != null) { + + if (_checksums.ContainsKey(f)) { + Console.Write($"{f,6} {cs,25} "); + if (cs != _checksums[f].ChecksumAsLong) { + Console.ForegroundColor = ConsoleColor.Red; + Console.Write("(failed)"); + } else { + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("(verified)"); + } + Console.Write("\n"); + } else if (_verbose) { + Console.Write($"{f,6} {cs,25} "); + Console.Write("(skipped)"); + } + Console.ForegroundColor = ConsoleColor.Gray; + } + } + } + + public void Dispose() { + if (_gameCallbacks != null) { + _gameCallbacks.UnsubscribeListener(this); + _gameCallbacks = null; + } + } + } +} diff --git a/data/ChessViewUpdater.cs b/data/ChessViewUpdater.cs new file mode 100644 index 0000000000000000000000000000000000000000..7512a75dbfadca8522600310fdad014b4ff59c9e --- /dev/null +++ b/data/ChessViewUpdater.cs @@ -0,0 +1,148 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using Quantum; + +public unsafe class ChessViewUpdater : QuantumCallbacks +{ + public static ChessViewUpdater Instance; + + public PieceView[] Pieces = new PieceView[32]; + + public Vector3 PieceOffset = new Vector3(.5f, 0, .5f); + + public GameObject LastMoveIdicatorInitial; + public GameObject LastMoveIdicatorTarget; + + public Sprite WhiteQueenSprite; + public Sprite WhiteRookSprite; + public Sprite WhiteKnightSprite; + public Sprite WhiteBishopSprite; + // + public Sprite BlackQueenSprite; + public Sprite BlackRookSprite; + public Sprite BlackKnightSprite; + public Sprite BlackBishopSprite; + + public DeadPiecesManager DeadPieces; + + private bool _initialized = false; + + + public void Start() + { + if (Instance == null) + { + Instance = this; + } + + QuantumEvent.Subscribe(this, UpdatePiece); + QuantumEvent.Subscribe(this, RemovePiece); + QuantumEvent.Subscribe(this, PiecePromotion); + } + + public override void OnUpdateView(QuantumGame game) + { + if (!_initialized) + { + _initialized = true; + var f = game.Frames.Verified; + for (int i = 0; i < f.Global->Board.Cells.Length; i++) + { + int index = GetPieceIndex(i); + if (index != -1) + { + var p = Pieces[index]; + var position = BoardHelper.GetCordinatesByIndex(p.IndexOnBoard); + p.SetTargetPosition(new Vector3((float)position.X + PieceOffset.x, PieceOffset.x, (float)position.Y + PieceOffset.z)); + } + } + } + } + + public void PiecePromotion(EventPiecePromotion e) + { + var index = GetPieceIndex(e.Index); + Pieces[index].Type = e.NewType; + switch (e.NewType) + { + case PieceType.Bishop: + if (Pieces[index].Color == PieceColor.White) + Pieces[index].GetComponent().sprite = WhiteBishopSprite; + else + Pieces[index].GetComponent().sprite = BlackBishopSprite; + break; + case PieceType.Knight: + if (Pieces[index].Color == PieceColor.White) + Pieces[index].GetComponent().sprite = WhiteKnightSprite; + else + Pieces[index].GetComponent().sprite = BlackKnightSprite; + break; + case PieceType.Queen: + if (Pieces[index].Color == PieceColor.White) + Pieces[index].GetComponent().sprite = WhiteQueenSprite; + else + Pieces[index].GetComponent().sprite = BlackQueenSprite; + break; + case PieceType.Rook: + if (Pieces[index].Color == PieceColor.White) + Pieces[index].GetComponent().sprite = WhiteRookSprite; + else + Pieces[index].GetComponent().sprite = BlackRookSprite; + break; + } + } + + public void UpdatePiece(EventChangePiecePosition e) + { + var origin = (int)e.Index.X; + var target = (int)e.Index.Y; + + SetObjectByIndex(LastMoveIdicatorInitial, origin); + SetObjectByIndex(LastMoveIdicatorTarget, target); + + int index = GetPieceIndex(origin); + if (index != -1) + { + var p = Pieces[index]; + p.IndexOnBoard = target; + var position = BoardHelper.GetCordinatesByIndex(p.IndexOnBoard); + p.SetTargetPosition(new Vector3((float)position.X + PieceOffset.x, PieceOffset.x, (float)position.Y + PieceOffset.z)); + } + } + + public void SetObjectByIndex(GameObject go, int index) + { + var position = BoardHelper.GetCordinatesByIndex(index); + go.transform.position = new Vector3((float)position.X + PieceOffset.x, PieceOffset.y, (float)position.Y + PieceOffset.z); + go.SetActive(true); + } + + public void RemovePiece(EventRemovePiece e) + { + var index = GetPieceIndex(e.Index); + DeadPieces.StoreDeadPiece(Pieces[index], e.Color); + Pieces[index].IndexOnBoard = -1; + } + + public int GetPieceIndex(int index) + { + for (int i = 0; i < Pieces.Length; i++) + { + if (Pieces[i] == null) + { + continue; + } + if (Pieces[i].IndexOnBoard == index) + { + return i; + } + } + return -1; + } + + public void OnDisable() + { + QuantumEvent.UnsubscribeListener(this); + } +} diff --git a/data/CodeGen.cs b/data/CodeGen.cs new file mode 100644 index 0000000000000000000000000000000000000000..d602b34ef25abcd45416a4c1ca2a9db5733e6e3b --- /dev/null +++ b/data/CodeGen.cs @@ -0,0 +1,1995 @@ +// +// This code was auto-generated by a tool, every time +// the tool executes this code will be reset. +// +// If you need to extend the classes generated to add +// fields or methods to them, please create partial +// declarations in another file. +// +#pragma warning disable 0649 +#pragma warning disable 1522 +#pragma warning disable 0414 +#pragma warning disable 0219 +#pragma warning disable 0109 + +namespace Quantum { + using System; + using System.Collections.Generic; + using System.Runtime.InteropServices; + using Photon.Deterministic; + using Quantum.Core; + using Quantum.Collections; + using Quantum.Inspector; + using Quantum.Physics2D; + using Quantum.Physics3D; + using Optional = Quantum.Inspector.OptionalAttribute; + using MethodImplAttribute = System.Runtime.CompilerServices.MethodImplAttribute; + using MethodImplOptions = System.Runtime.CompilerServices.MethodImplOptions; + + public enum TurnEndReason : int { + Time, + Skip, + Play, + Resolved, + } + public enum TurnStatus : int { + Inactive, + Active, + Resolving, + } + public enum TurnType : int { + Play, + Countdown, + } + [System.FlagsAttribute()] + public enum InputButtons : int { + } + public static unsafe partial class InputButtons_ext { + public static Boolean IsFlagSet(this InputButtons self, InputButtons flag) { + return (self & flag) == flag; + } + public static InputButtons SetFlag(this InputButtons self, InputButtons flag) { + return self | flag; + } + public static InputButtons ClearFlag(this InputButtons self, InputButtons flag) { + return self & ~flag; + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BitSet1024 { + public const Int32 SIZE = 128; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + private fixed UInt64 bits[16]; + public const Int32 BitsSize = 1024; + public Int32 Length { + get { + return 1024; + } + } + public static void Print(void* ptr, FramePrinter printer) { + var p = (BitSet1024*)ptr; + printer.ScopeBegin(); + UnmanagedUtils.PrintBytesBits((byte*)&p->bits, 1024, 64, printer); + printer.ScopeEnd(); + } + [System.ObsoleteAttribute("Use instance Set method instead")] + public static void Set(BitSet1024* set, Int32 bit) { + set->bits[bit/64] |= (1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance Clear method instead")] + public static void Clear(BitSet1024* set, Int32 bit) { + set->bits[bit/64] &= ~(1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance ClearAll method instead")] + public static void ClearAll(BitSet1024* set) { + Native.Utils.Clear(&set->bits[0], 128); + } + [System.ObsoleteAttribute("Use instance IsSet method instead")] + public static Boolean IsSet(BitSet1024* set, Int32 bit) { + return (set->bits[bit/64]&(1UL<<(bit%64))) != 0UL; + } + public static BitSet1024 FromArray(UInt64[] values) { + Assert.Always(16 == values.Length); + BitSet1024 result = default; + for (int i = 0; i < 16; ++i) { + result.bits[i] = values[i]; + } + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Int32 bit) { + Assert.Check(bit >= 0 && bit < 1024); + fixed (UInt64* p = bits) (p[bit/64]) |= (1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(Int32 bit) { + Assert.Check(bit >= 0 && bit < 1024); + fixed (UInt64* p = bits) (p[bit/64]) &= ~(1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAll() { + fixed (UInt64* p = bits) Native.Utils.Clear(p, 128); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Boolean IsSet(Int32 bit) { + fixed (UInt64* p = bits) return ((p[bit/64])&(1UL<<(bit%64))) != 0UL; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 37; + fixed (UInt64* p = bits) hash = hash * 31 + HashCodeUtils.GetArrayHashCode(p, 16); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BitSet1024*)ptr; + serializer.Stream.SerializeBuffer(&p->bits[0], 16); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BitSet128 { + public const Int32 SIZE = 16; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + private fixed UInt64 bits[2]; + public const Int32 BitsSize = 128; + public Int32 Length { + get { + return 128; + } + } + public static void Print(void* ptr, FramePrinter printer) { + var p = (BitSet128*)ptr; + printer.ScopeBegin(); + UnmanagedUtils.PrintBytesBits((byte*)&p->bits, 128, 64, printer); + printer.ScopeEnd(); + } + [System.ObsoleteAttribute("Use instance Set method instead")] + public static void Set(BitSet128* set, Int32 bit) { + set->bits[bit/64] |= (1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance Clear method instead")] + public static void Clear(BitSet128* set, Int32 bit) { + set->bits[bit/64] &= ~(1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance ClearAll method instead")] + public static void ClearAll(BitSet128* set) { + Native.Utils.Clear(&set->bits[0], 16); + } + [System.ObsoleteAttribute("Use instance IsSet method instead")] + public static Boolean IsSet(BitSet128* set, Int32 bit) { + return (set->bits[bit/64]&(1UL<<(bit%64))) != 0UL; + } + public static BitSet128 FromArray(UInt64[] values) { + Assert.Always(2 == values.Length); + BitSet128 result = default; + for (int i = 0; i < 2; ++i) { + result.bits[i] = values[i]; + } + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Int32 bit) { + Assert.Check(bit >= 0 && bit < 128); + fixed (UInt64* p = bits) (p[bit/64]) |= (1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(Int32 bit) { + Assert.Check(bit >= 0 && bit < 128); + fixed (UInt64* p = bits) (p[bit/64]) &= ~(1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAll() { + fixed (UInt64* p = bits) Native.Utils.Clear(p, 16); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Boolean IsSet(Int32 bit) { + fixed (UInt64* p = bits) return ((p[bit/64])&(1UL<<(bit%64))) != 0UL; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 41; + fixed (UInt64* p = bits) hash = hash * 31 + HashCodeUtils.GetArrayHashCode(p, 2); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BitSet128*)ptr; + serializer.Stream.SerializeBuffer(&p->bits[0], 2); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BitSet2 { + public const Int32 SIZE = 8; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + private fixed UInt64 bits[1]; + public const Int32 BitsSize = 2; + public Int32 Length { + get { + return 2; + } + } + public static void Print(void* ptr, FramePrinter printer) { + var p = (BitSet2*)ptr; + printer.ScopeBegin(); + UnmanagedUtils.PrintBytesBits((byte*)&p->bits, 2, 64, printer); + printer.ScopeEnd(); + } + [System.ObsoleteAttribute("Use instance Set method instead")] + public static void Set(BitSet2* set, Int32 bit) { + set->bits[bit/64] |= (1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance Clear method instead")] + public static void Clear(BitSet2* set, Int32 bit) { + set->bits[bit/64] &= ~(1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance ClearAll method instead")] + public static void ClearAll(BitSet2* set) { + Native.Utils.Clear(&set->bits[0], 8); + } + [System.ObsoleteAttribute("Use instance IsSet method instead")] + public static Boolean IsSet(BitSet2* set, Int32 bit) { + return (set->bits[bit/64]&(1UL<<(bit%64))) != 0UL; + } + public static BitSet2 FromArray(UInt64[] values) { + Assert.Always(1 == values.Length); + BitSet2 result = default; + for (int i = 0; i < 1; ++i) { + result.bits[i] = values[i]; + } + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Int32 bit) { + Assert.Check(bit >= 0 && bit < 2); + fixed (UInt64* p = bits) (p[bit/64]) |= (1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(Int32 bit) { + Assert.Check(bit >= 0 && bit < 2); + fixed (UInt64* p = bits) (p[bit/64]) &= ~(1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAll() { + fixed (UInt64* p = bits) Native.Utils.Clear(p, 8); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Boolean IsSet(Int32 bit) { + fixed (UInt64* p = bits) return ((p[bit/64])&(1UL<<(bit%64))) != 0UL; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 43; + fixed (UInt64* p = bits) hash = hash * 31 + HashCodeUtils.GetArrayHashCode(p, 1); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BitSet2*)ptr; + serializer.Stream.SerializeBuffer(&p->bits[0], 1); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BitSet2048 { + public const Int32 SIZE = 256; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + private fixed UInt64 bits[32]; + public const Int32 BitsSize = 2048; + public Int32 Length { + get { + return 2048; + } + } + public static void Print(void* ptr, FramePrinter printer) { + var p = (BitSet2048*)ptr; + printer.ScopeBegin(); + UnmanagedUtils.PrintBytesBits((byte*)&p->bits, 2048, 64, printer); + printer.ScopeEnd(); + } + [System.ObsoleteAttribute("Use instance Set method instead")] + public static void Set(BitSet2048* set, Int32 bit) { + set->bits[bit/64] |= (1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance Clear method instead")] + public static void Clear(BitSet2048* set, Int32 bit) { + set->bits[bit/64] &= ~(1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance ClearAll method instead")] + public static void ClearAll(BitSet2048* set) { + Native.Utils.Clear(&set->bits[0], 256); + } + [System.ObsoleteAttribute("Use instance IsSet method instead")] + public static Boolean IsSet(BitSet2048* set, Int32 bit) { + return (set->bits[bit/64]&(1UL<<(bit%64))) != 0UL; + } + public static BitSet2048 FromArray(UInt64[] values) { + Assert.Always(32 == values.Length); + BitSet2048 result = default; + for (int i = 0; i < 32; ++i) { + result.bits[i] = values[i]; + } + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Int32 bit) { + Assert.Check(bit >= 0 && bit < 2048); + fixed (UInt64* p = bits) (p[bit/64]) |= (1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(Int32 bit) { + Assert.Check(bit >= 0 && bit < 2048); + fixed (UInt64* p = bits) (p[bit/64]) &= ~(1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAll() { + fixed (UInt64* p = bits) Native.Utils.Clear(p, 256); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Boolean IsSet(Int32 bit) { + fixed (UInt64* p = bits) return ((p[bit/64])&(1UL<<(bit%64))) != 0UL; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 47; + fixed (UInt64* p = bits) hash = hash * 31 + HashCodeUtils.GetArrayHashCode(p, 32); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BitSet2048*)ptr; + serializer.Stream.SerializeBuffer(&p->bits[0], 32); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BitSet256 { + public const Int32 SIZE = 32; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + private fixed UInt64 bits[4]; + public const Int32 BitsSize = 256; + public Int32 Length { + get { + return 256; + } + } + public static void Print(void* ptr, FramePrinter printer) { + var p = (BitSet256*)ptr; + printer.ScopeBegin(); + UnmanagedUtils.PrintBytesBits((byte*)&p->bits, 256, 64, printer); + printer.ScopeEnd(); + } + [System.ObsoleteAttribute("Use instance Set method instead")] + public static void Set(BitSet256* set, Int32 bit) { + set->bits[bit/64] |= (1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance Clear method instead")] + public static void Clear(BitSet256* set, Int32 bit) { + set->bits[bit/64] &= ~(1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance ClearAll method instead")] + public static void ClearAll(BitSet256* set) { + Native.Utils.Clear(&set->bits[0], 32); + } + [System.ObsoleteAttribute("Use instance IsSet method instead")] + public static Boolean IsSet(BitSet256* set, Int32 bit) { + return (set->bits[bit/64]&(1UL<<(bit%64))) != 0UL; + } + public static BitSet256 FromArray(UInt64[] values) { + Assert.Always(4 == values.Length); + BitSet256 result = default; + for (int i = 0; i < 4; ++i) { + result.bits[i] = values[i]; + } + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Int32 bit) { + Assert.Check(bit >= 0 && bit < 256); + fixed (UInt64* p = bits) (p[bit/64]) |= (1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(Int32 bit) { + Assert.Check(bit >= 0 && bit < 256); + fixed (UInt64* p = bits) (p[bit/64]) &= ~(1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAll() { + fixed (UInt64* p = bits) Native.Utils.Clear(p, 32); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Boolean IsSet(Int32 bit) { + fixed (UInt64* p = bits) return ((p[bit/64])&(1UL<<(bit%64))) != 0UL; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 53; + fixed (UInt64* p = bits) hash = hash * 31 + HashCodeUtils.GetArrayHashCode(p, 4); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BitSet256*)ptr; + serializer.Stream.SerializeBuffer(&p->bits[0], 4); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BitSet4096 { + public const Int32 SIZE = 512; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + private fixed UInt64 bits[64]; + public const Int32 BitsSize = 4096; + public Int32 Length { + get { + return 4096; + } + } + public static void Print(void* ptr, FramePrinter printer) { + var p = (BitSet4096*)ptr; + printer.ScopeBegin(); + UnmanagedUtils.PrintBytesBits((byte*)&p->bits, 4096, 64, printer); + printer.ScopeEnd(); + } + [System.ObsoleteAttribute("Use instance Set method instead")] + public static void Set(BitSet4096* set, Int32 bit) { + set->bits[bit/64] |= (1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance Clear method instead")] + public static void Clear(BitSet4096* set, Int32 bit) { + set->bits[bit/64] &= ~(1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance ClearAll method instead")] + public static void ClearAll(BitSet4096* set) { + Native.Utils.Clear(&set->bits[0], 512); + } + [System.ObsoleteAttribute("Use instance IsSet method instead")] + public static Boolean IsSet(BitSet4096* set, Int32 bit) { + return (set->bits[bit/64]&(1UL<<(bit%64))) != 0UL; + } + public static BitSet4096 FromArray(UInt64[] values) { + Assert.Always(64 == values.Length); + BitSet4096 result = default; + for (int i = 0; i < 64; ++i) { + result.bits[i] = values[i]; + } + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Int32 bit) { + Assert.Check(bit >= 0 && bit < 4096); + fixed (UInt64* p = bits) (p[bit/64]) |= (1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(Int32 bit) { + Assert.Check(bit >= 0 && bit < 4096); + fixed (UInt64* p = bits) (p[bit/64]) &= ~(1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAll() { + fixed (UInt64* p = bits) Native.Utils.Clear(p, 512); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Boolean IsSet(Int32 bit) { + fixed (UInt64* p = bits) return ((p[bit/64])&(1UL<<(bit%64))) != 0UL; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 59; + fixed (UInt64* p = bits) hash = hash * 31 + HashCodeUtils.GetArrayHashCode(p, 64); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BitSet4096*)ptr; + serializer.Stream.SerializeBuffer(&p->bits[0], 64); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BitSet512 { + public const Int32 SIZE = 64; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + private fixed UInt64 bits[8]; + public const Int32 BitsSize = 512; + public Int32 Length { + get { + return 512; + } + } + public static void Print(void* ptr, FramePrinter printer) { + var p = (BitSet512*)ptr; + printer.ScopeBegin(); + UnmanagedUtils.PrintBytesBits((byte*)&p->bits, 512, 64, printer); + printer.ScopeEnd(); + } + [System.ObsoleteAttribute("Use instance Set method instead")] + public static void Set(BitSet512* set, Int32 bit) { + set->bits[bit/64] |= (1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance Clear method instead")] + public static void Clear(BitSet512* set, Int32 bit) { + set->bits[bit/64] &= ~(1UL<<(bit%64)); + } + [System.ObsoleteAttribute("Use instance ClearAll method instead")] + public static void ClearAll(BitSet512* set) { + Native.Utils.Clear(&set->bits[0], 64); + } + [System.ObsoleteAttribute("Use instance IsSet method instead")] + public static Boolean IsSet(BitSet512* set, Int32 bit) { + return (set->bits[bit/64]&(1UL<<(bit%64))) != 0UL; + } + public static BitSet512 FromArray(UInt64[] values) { + Assert.Always(8 == values.Length); + BitSet512 result = default; + for (int i = 0; i < 8; ++i) { + result.bits[i] = values[i]; + } + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Int32 bit) { + Assert.Check(bit >= 0 && bit < 512); + fixed (UInt64* p = bits) (p[bit/64]) |= (1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear(Int32 bit) { + Assert.Check(bit >= 0 && bit < 512); + fixed (UInt64* p = bits) (p[bit/64]) &= ~(1UL<<(bit%64)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAll() { + fixed (UInt64* p = bits) Native.Utils.Clear(p, 64); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Boolean IsSet(Int32 bit) { + fixed (UInt64* p = bits) return ((p[bit/64])&(1UL<<(bit%64))) != 0UL; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 61; + fixed (UInt64* p = bits) hash = hash * 31 + HashCodeUtils.GetArrayHashCode(p, 8); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BitSet512*)ptr; + serializer.Stream.SerializeBuffer(&p->bits[0], 8); + } + } + [StructLayout(LayoutKind.Explicit)] + [Quantum.AssetRefAttribute(typeof(BallPoolSpec))] + [System.SerializableAttribute()] + public unsafe partial struct AssetRefBallPoolSpec : IEquatable, IAssetRef { + public const Int32 SIZE = 8; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + public AssetGuid Id; + public override String ToString() { + return AssetRef.ToString(Id); + } + public static implicit operator AssetRefBallPoolSpec(BallPoolSpec value) { + var r = default(AssetRefBallPoolSpec); + if (value != null) { + r.Id = value.Guid; + } + return r; + } + public override Boolean Equals(Object obj) { + return obj is AssetRefBallPoolSpec other && Equals(other); + } + public Boolean Equals(AssetRefBallPoolSpec other) { + return Id.Equals(other.Id); + } + public static Boolean operator ==(AssetRefBallPoolSpec a, AssetRefBallPoolSpec b) { + return a.Id == b.Id; + } + public static Boolean operator !=(AssetRefBallPoolSpec a, AssetRefBallPoolSpec b) { + return a.Id != b.Id; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 67; + hash = hash * 31 + Id.GetHashCode(); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (AssetRefBallPoolSpec*)ptr; + AssetGuid.Serialize(&p->Id, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + [Quantum.AssetRefAttribute(typeof(ConfigAssets))] + [System.SerializableAttribute()] + public unsafe partial struct AssetRefConfigAssets : IEquatable, IAssetRef { + public const Int32 SIZE = 8; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + public AssetGuid Id; + public override String ToString() { + return AssetRef.ToString(Id); + } + public static implicit operator AssetRefConfigAssets(ConfigAssets value) { + var r = default(AssetRefConfigAssets); + if (value != null) { + r.Id = value.Guid; + } + return r; + } + public override Boolean Equals(Object obj) { + return obj is AssetRefConfigAssets other && Equals(other); + } + public Boolean Equals(AssetRefConfigAssets other) { + return Id.Equals(other.Id); + } + public static Boolean operator ==(AssetRefConfigAssets a, AssetRefConfigAssets b) { + return a.Id == b.Id; + } + public static Boolean operator !=(AssetRefConfigAssets a, AssetRefConfigAssets b) { + return a.Id != b.Id; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 71; + hash = hash * 31 + Id.GetHashCode(); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (AssetRefConfigAssets*)ptr; + AssetGuid.Serialize(&p->Id, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + [Quantum.AssetRefAttribute(typeof(GameConfig))] + [System.SerializableAttribute()] + public unsafe partial struct AssetRefGameConfig : IEquatable, IAssetRef { + public const Int32 SIZE = 8; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + public AssetGuid Id; + public override String ToString() { + return AssetRef.ToString(Id); + } + public static implicit operator AssetRefGameConfig(GameConfig value) { + var r = default(AssetRefGameConfig); + if (value != null) { + r.Id = value.Guid; + } + return r; + } + public override Boolean Equals(Object obj) { + return obj is AssetRefGameConfig other && Equals(other); + } + public Boolean Equals(AssetRefGameConfig other) { + return Id.Equals(other.Id); + } + public static Boolean operator ==(AssetRefGameConfig a, AssetRefGameConfig b) { + return a.Id == b.Id; + } + public static Boolean operator !=(AssetRefGameConfig a, AssetRefGameConfig b) { + return a.Id != b.Id; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 73; + hash = hash * 31 + Id.GetHashCode(); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (AssetRefGameConfig*)ptr; + AssetGuid.Serialize(&p->Id, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + [Quantum.AssetRefAttribute(typeof(TurnConfig))] + [System.SerializableAttribute()] + public unsafe partial struct AssetRefTurnConfig : IEquatable, IAssetRef { + public const Int32 SIZE = 8; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + public AssetGuid Id; + public override String ToString() { + return AssetRef.ToString(Id); + } + public static implicit operator AssetRefTurnConfig(TurnConfig value) { + var r = default(AssetRefTurnConfig); + if (value != null) { + r.Id = value.Guid; + } + return r; + } + public override Boolean Equals(Object obj) { + return obj is AssetRefTurnConfig other && Equals(other); + } + public Boolean Equals(AssetRefTurnConfig other) { + return Id.Equals(other.Id); + } + public static Boolean operator ==(AssetRefTurnConfig a, AssetRefTurnConfig b) { + return a.Id == b.Id; + } + public static Boolean operator !=(AssetRefTurnConfig a, AssetRefTurnConfig b) { + return a.Id != b.Id; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 79; + hash = hash * 31 + Id.GetHashCode(); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (AssetRefTurnConfig*)ptr; + AssetGuid.Serialize(&p->Id, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + [Quantum.AssetRefAttribute(typeof(UserMap))] + [System.SerializableAttribute()] + public unsafe partial struct AssetRefUserMap : IEquatable, IAssetRef { + public const Int32 SIZE = 8; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + public AssetGuid Id; + public override String ToString() { + return AssetRef.ToString(Id); + } + public static implicit operator AssetRefUserMap(UserMap value) { + var r = default(AssetRefUserMap); + if (value != null) { + r.Id = value.Guid; + } + return r; + } + public override Boolean Equals(Object obj) { + return obj is AssetRefUserMap other && Equals(other); + } + public Boolean Equals(AssetRefUserMap other) { + return Id.Equals(other.Id); + } + public static Boolean operator ==(AssetRefUserMap a, AssetRefUserMap b) { + return a.Id == b.Id; + } + public static Boolean operator !=(AssetRefUserMap a, AssetRefUserMap b) { + return a.Id != b.Id; + } + public override Int32 GetHashCode() { + unchecked { + var hash = 83; + hash = hash * 31 + Id.GetHashCode(); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (AssetRefUserMap*)ptr; + AssetGuid.Serialize(&p->Id, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BallPoolPlayer { + public const Int32 SIZE = 48; + public const Int32 ALIGNMENT = 8; + [FieldOffset(0)] + public PlayerRef Ref; + [FieldOffset(4)] + public QBoolean StripedBalls; + [FieldOffset(8)] + public TurnData TurnStats; + public override Int32 GetHashCode() { + unchecked { + var hash = 89; + hash = hash * 31 + Ref.GetHashCode(); + hash = hash * 31 + StripedBalls.GetHashCode(); + hash = hash * 31 + TurnStats.GetHashCode(); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BallPoolPlayer*)ptr; + PlayerRef.Serialize(&p->Ref, serializer); + QBoolean.Serialize(&p->StripedBalls, serializer); + Quantum.TurnData.Serialize(&p->TurnStats, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct Input { + public const Int32 SIZE = 48; + public const Int32 ALIGNMENT = 8; + [FieldOffset(8)] + public FPVector2 BallPosition; + [FieldOffset(24)] + public FPVector3 Direction; + [FieldOffset(0)] + public FP ForceBarMarkPos; + public const int MAX_COUNT = 2; + public override Int32 GetHashCode() { + unchecked { + var hash = 97; + hash = hash * 31 + BallPosition.GetHashCode(); + hash = hash * 31 + Direction.GetHashCode(); + hash = hash * 31 + ForceBarMarkPos.GetHashCode(); + return hash; + } + } + public static Input Read(FrameSerializer serializer) { + Input i = new Input(); + Serialize(&i, serializer); + return i; + } + public static void Write(FrameSerializer serializer, Input i) { + Serialize(&i, serializer); + } + public Boolean IsDown(InputButtons button) { + switch (button) { + } + return false; + } + public Boolean WasPressed(InputButtons button) { + switch (button) { + } + return false; + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (Input*)ptr; + FP.Serialize(&p->ForceBarMarkPos, serializer); + FPVector2.Serialize(&p->BallPosition, serializer); + FPVector3.Serialize(&p->Direction, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct TurnData { + public const Int32 SIZE = 40; + public const Int32 ALIGNMENT = 8; + [FieldOffset(24)] + public AssetRefTurnConfig ConfigRef; + [FieldOffset(32)] + public EntityRef Entity; + [FieldOffset(0)] + public Int32 Number; + [FieldOffset(8)] + public PlayerRef Player; + [FieldOffset(12)] + public TurnStatus Status; + [FieldOffset(4)] + public Int32 Ticks; + [FieldOffset(16)] + public TurnType Type; + public override Int32 GetHashCode() { + unchecked { + var hash = 101; + hash = hash * 31 + ConfigRef.GetHashCode(); + hash = hash * 31 + Entity.GetHashCode(); + hash = hash * 31 + Number.GetHashCode(); + hash = hash * 31 + Player.GetHashCode(); + hash = hash * 31 + (Int32)Status; + hash = hash * 31 + Ticks.GetHashCode(); + hash = hash * 31 + (Int32)Type; + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (TurnData*)ptr; + serializer.Stream.Serialize(&p->Number); + serializer.Stream.Serialize(&p->Ticks); + PlayerRef.Serialize(&p->Player, serializer); + serializer.Stream.Serialize((Int32*)&p->Status); + serializer.Stream.Serialize((Int32*)&p->Type); + Quantum.AssetRefTurnConfig.Serialize(&p->ConfigRef, serializer); + EntityRef.Serialize(&p->Entity, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct _globals_ { + public const Int32 SIZE = 776; + public const Int32 ALIGNMENT = 8; + [FieldOffset(4)] + public QBoolean CapturedEighthBall; + [FieldOffset(120)] + public TurnData CurrentTurn; + [FieldOffset(48)] + public FP DeltaTime; + [FieldOffset(80)] + public FrameMetaData FrameMetaData; + [FieldOffset(8)] + public QBoolean HasFirstCaptured; + [FieldOffset(12)] + public QBoolean IsFirstTurn; + [FieldOffset(32)] + public AssetRefMap Map; + [FieldOffset(56)] + public NavMeshRegionMask NavMeshRegions; + [FieldOffset(480)] + public PhysicsSceneSettings PhysicsSettings; + [FieldOffset(40)] + public BitSet2 PlayerLastConnectionState; + [FieldOffset(16)] + public QBoolean PlayerScoreThisTurn; + [FieldOffset(160)] + [FramePrinter.FixedArrayAttribute(typeof(BallPoolPlayer), 2)] + private fixed Byte _Players_[96]; + [FieldOffset(20)] + public QBoolean ReplaceWhiteBall; + [FieldOffset(64)] + public RNGSession RngSession; + [FieldOffset(352)] + public BitSet1024 Systems; + [FieldOffset(0)] + public Int32 TicksResolving; + [FieldOffset(24)] + public QBoolean WhiteBallFirstContact; + [FieldOffset(256)] + [FramePrinter.FixedArrayAttribute(typeof(Input), 2)] + private fixed Byte _input_[96]; + public FixedArray Players { + get { + fixed (byte* p = _Players_) { return new FixedArray(p, 48, 2); } + } + } + public FixedArray input { + get { + fixed (byte* p = _input_) { return new FixedArray(p, 48, 2); } + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 103; + hash = hash * 31 + CapturedEighthBall.GetHashCode(); + hash = hash * 31 + CurrentTurn.GetHashCode(); + hash = hash * 31 + DeltaTime.GetHashCode(); + hash = hash * 31 + FrameMetaData.GetHashCode(); + hash = hash * 31 + HasFirstCaptured.GetHashCode(); + hash = hash * 31 + IsFirstTurn.GetHashCode(); + hash = hash * 31 + Map.GetHashCode(); + hash = hash * 31 + NavMeshRegions.GetHashCode(); + hash = hash * 31 + PhysicsSettings.GetHashCode(); + hash = hash * 31 + PlayerLastConnectionState.GetHashCode(); + hash = hash * 31 + PlayerScoreThisTurn.GetHashCode(); + hash = hash * 31 + HashCodeUtils.GetArrayHashCode(Players); + hash = hash * 31 + ReplaceWhiteBall.GetHashCode(); + hash = hash * 31 + RngSession.GetHashCode(); + hash = hash * 31 + Systems.GetHashCode(); + hash = hash * 31 + TicksResolving.GetHashCode(); + hash = hash * 31 + WhiteBallFirstContact.GetHashCode(); + hash = hash * 31 + HashCodeUtils.GetArrayHashCode(input); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (_globals_*)ptr; + serializer.Stream.Serialize(&p->TicksResolving); + QBoolean.Serialize(&p->CapturedEighthBall, serializer); + QBoolean.Serialize(&p->HasFirstCaptured, serializer); + QBoolean.Serialize(&p->IsFirstTurn, serializer); + QBoolean.Serialize(&p->PlayerScoreThisTurn, serializer); + QBoolean.Serialize(&p->ReplaceWhiteBall, serializer); + QBoolean.Serialize(&p->WhiteBallFirstContact, serializer); + AssetRefMap.Serialize(&p->Map, serializer); + Quantum.BitSet2.Serialize(&p->PlayerLastConnectionState, serializer); + FP.Serialize(&p->DeltaTime, serializer); + NavMeshRegionMask.Serialize(&p->NavMeshRegions, serializer); + RNGSession.Serialize(&p->RngSession, serializer); + FrameMetaData.Serialize(&p->FrameMetaData, serializer); + Quantum.TurnData.Serialize(&p->CurrentTurn, serializer); + FixedArray.Serialize(p->Players, serializer, StaticDelegates.SerializeBallPoolPlayer); + FixedArray.Serialize(p->input, serializer, StaticDelegates.SerializeInput); + Quantum.BitSet1024.Serialize(&p->Systems, serializer); + PhysicsSceneSettings.Serialize(&p->PhysicsSettings, serializer); + } + } + [StructLayout(LayoutKind.Explicit)] + public unsafe partial struct BallFields : Quantum.IComponent { + public const Int32 SIZE = 40; + public const Int32 ALIGNMENT = 8; + [FieldOffset(4)] + public QBoolean InTable; + [FieldOffset(0)] + public Int32 Number; + [FieldOffset(16)] + public AssetRefBallPoolSpec Spec; + [FieldOffset(24)] + public FPVector2 Spin; + [FieldOffset(8)] + public QBoolean Striped; + public override Int32 GetHashCode() { + unchecked { + var hash = 107; + hash = hash * 31 + InTable.GetHashCode(); + hash = hash * 31 + Number.GetHashCode(); + hash = hash * 31 + Spec.GetHashCode(); + hash = hash * 31 + Spin.GetHashCode(); + hash = hash * 31 + Striped.GetHashCode(); + return hash; + } + } + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (BallFields*)ptr; + serializer.Stream.Serialize(&p->Number); + QBoolean.Serialize(&p->InTable, serializer); + QBoolean.Serialize(&p->Striped, serializer); + Quantum.AssetRefBallPoolSpec.Serialize(&p->Spec, serializer); + FPVector2.Serialize(&p->Spin, serializer); + } + } + public unsafe partial class Frame { + private ISignalOnBallPoolShot[] _ISignalOnBallPoolShotSystems; + private ISignalOnBallPoolHitHole[] _ISignalOnBallPoolHitHoleSystems; + private ISignalOnTurnEnded[] _ISignalOnTurnEndedSystems; + private ISignalOnPlayCommandReceived[] _ISignalOnPlayCommandReceivedSystems; + private ISignalOnSkipCommandReceived[] _ISignalOnSkipCommandReceivedSystems; + partial void AllocGen() { + _globals = (_globals_*)Context.Allocator.AllocAndClear(sizeof(_globals_)); + } + partial void FreeGen() { + Context.Allocator.Free(_globals); + } + partial void CopyFromGen(Frame frame) { + Native.Utils.Copy(_globals, frame._globals, sizeof(_globals_)); + } + static partial void InitStaticGen() { + ComponentTypeId.Setup(() => { + ComponentTypeId.Add(Quantum.BallFields.Serialize, null, null, ComponentFlags.None); + }); + } + partial void InitGen() { + Initialize(this, this.SimulationConfig.Entities); + _ISignalOnBallPoolShotSystems = BuildSignalsArray(); + _ISignalOnBallPoolHitHoleSystems = BuildSignalsArray(); + _ISignalOnTurnEndedSystems = BuildSignalsArray(); + _ISignalOnPlayCommandReceivedSystems = BuildSignalsArray(); + _ISignalOnSkipCommandReceivedSystems = BuildSignalsArray(); + _ComponentSignalsOnAdded = new ComponentReactiveCallbackInvoker[ComponentTypeId.Type.Length]; + _ComponentSignalsOnRemoved = new ComponentReactiveCallbackInvoker[ComponentTypeId.Type.Length]; + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + BuildSignalsArrayOnComponentAdded(); + BuildSignalsArrayOnComponentRemoved(); + } + public void SetPlayerInput(Int32 player, Input input) { + if ((uint)player >= (uint)_globals->input.Length) { throw new System.ArgumentOutOfRangeException("player"); } + var i = _globals->input.GetPointer(player); + i->Direction = input.Direction; + i->ForceBarMarkPos = input.ForceBarMarkPos; + i->BallPosition = input.BallPosition; + } + public Input* GetPlayerInput(Int32 player) { + if ((uint)player >= (uint)_globals->input.Length) { throw new System.ArgumentOutOfRangeException("player"); } + return _globals->input.GetPointer(player); + } + public unsafe partial struct FrameSignals { + public void OnBallPoolShot(PlayerRef player) { + var array = _f._ISignalOnBallPoolShotSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnBallPoolShot(_f, player); + } + } + } + public void OnBallPoolHitHole(EntityRef ball) { + var array = _f._ISignalOnBallPoolHitHoleSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnBallPoolHitHole(_f, ball); + } + } + } + public void OnTurnEnded(TurnData data, TurnEndReason reason) { + var array = _f._ISignalOnTurnEndedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnTurnEnded(_f, data, reason); + } + } + } + public void OnPlayCommandReceived(PlayerRef player, PlayCommandData data) { + var array = _f._ISignalOnPlayCommandReceivedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnPlayCommandReceived(_f, player, data); + } + } + } + public void OnSkipCommandReceived(PlayerRef player, SkipCommandData data) { + var array = _f._ISignalOnSkipCommandReceivedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnSkipCommandReceived(_f, player, data); + } + } + } + } + public unsafe partial struct FrameEvents { + public const Int32 EVENT_TYPE_COUNT = 18; + public static Int32 GetParentEventID(Int32 eventID) { + switch (eventID) { + case EventTurnTypeChanged.ID: return EventTurnEvent.ID; + case EventTurnStatusChanged.ID: return EventTurnEvent.ID; + case EventTurnEnded.ID: return EventTurnEvent.ID; + case EventTurnTimerReset.ID: return EventTurnEvent.ID; + case EventTurnActivated.ID: return EventTurnEvent.ID; + case EventPlayCommandReceived.ID: return EventCommandEvent.ID; + case EventSkipCommandReceived.ID: return EventCommandEvent.ID; + default: return -1; + } + } + public static System.Type GetEventType(Int32 eventID) { + switch (eventID) { + case EventMessage.ID: return typeof(EventMessage); + case EventRemoveBall.ID: return typeof(EventRemoveBall); + case EventEndGame.ID: return typeof(EventEndGame); + case EventReplaceBall.ID: return typeof(EventReplaceBall); + case EventCueHitBall.ID: return typeof(EventCueHitBall); + case EventBallsCollision.ID: return typeof(EventBallsCollision); + case EventBallsHitWall.ID: return typeof(EventBallsHitWall); + case EventBallsHitHole.ID: return typeof(EventBallsHitHole); + case EventGameplayEnded.ID: return typeof(EventGameplayEnded); + case EventTurnEvent.ID: return typeof(EventTurnEvent); + case EventTurnTypeChanged.ID: return typeof(EventTurnTypeChanged); + case EventTurnStatusChanged.ID: return typeof(EventTurnStatusChanged); + case EventTurnEnded.ID: return typeof(EventTurnEnded); + case EventTurnTimerReset.ID: return typeof(EventTurnTimerReset); + case EventTurnActivated.ID: return typeof(EventTurnActivated); + case EventCommandEvent.ID: return typeof(EventCommandEvent); + case EventPlayCommandReceived.ID: return typeof(EventPlayCommandReceived); + case EventSkipCommandReceived.ID: return typeof(EventSkipCommandReceived); + default: throw new System.ArgumentOutOfRangeException("eventID"); + } + } + public EventMessage Message(String Header, String Text) { + var ev = _f.Context.AcquireEvent(EventMessage.ID); + ev.Header = Header; + ev.Text = Text; + _f.AddEvent(ev); + return ev; + } + public EventRemoveBall RemoveBall(Int32 Num) { + var ev = _f.Context.AcquireEvent(EventRemoveBall.ID); + ev.Num = Num; + _f.AddEvent(ev); + return ev; + } + public EventEndGame EndGame(PlayerRef Winner) { + var ev = _f.Context.AcquireEvent(EventEndGame.ID); + ev.Winner = Winner; + _f.AddEvent(ev); + return ev; + } + public EventReplaceBall ReplaceBall() { + var ev = _f.Context.AcquireEvent(EventReplaceBall.ID); + _f.AddEvent(ev); + return ev; + } + public EventCueHitBall CueHitBall() { + var ev = _f.Context.AcquireEvent(EventCueHitBall.ID); + _f.AddEvent(ev); + return ev; + } + public EventBallsCollision BallsCollision() { + var ev = _f.Context.AcquireEvent(EventBallsCollision.ID); + _f.AddEvent(ev); + return ev; + } + public EventBallsHitWall BallsHitWall() { + var ev = _f.Context.AcquireEvent(EventBallsHitWall.ID); + _f.AddEvent(ev); + return ev; + } + public EventBallsHitHole BallsHitHole() { + var ev = _f.Context.AcquireEvent(EventBallsHitHole.ID); + _f.AddEvent(ev); + return ev; + } + public EventGameplayEnded GameplayEnded() { + if (_f.IsPredicted) return null; + var ev = _f.Context.AcquireEvent(EventGameplayEnded.ID); + _f.AddEvent(ev); + return ev; + } + public EventTurnTypeChanged TurnTypeChanged(TurnData Turn, TurnType PreviousType) { + if (_f.IsPredicted) return null; + var ev = _f.Context.AcquireEvent(EventTurnTypeChanged.ID); + ev.Turn = Turn; + ev.PreviousType = PreviousType; + _f.AddEvent(ev); + return ev; + } + public EventTurnStatusChanged TurnStatusChanged(TurnData Turn, TurnStatus PreviousStatus) { + if (_f.IsPredicted) return null; + var ev = _f.Context.AcquireEvent(EventTurnStatusChanged.ID); + ev.Turn = Turn; + ev.PreviousStatus = PreviousStatus; + _f.AddEvent(ev); + return ev; + } + public EventTurnEnded TurnEnded(TurnData Turn, TurnEndReason Reason) { + if (_f.IsPredicted) return null; + var ev = _f.Context.AcquireEvent(EventTurnEnded.ID); + ev.Turn = Turn; + ev.Reason = Reason; + _f.AddEvent(ev); + return ev; + } + public EventTurnTimerReset TurnTimerReset(TurnData Turn) { + if (_f.IsPredicted) return null; + var ev = _f.Context.AcquireEvent(EventTurnTimerReset.ID); + ev.Turn = Turn; + _f.AddEvent(ev); + return ev; + } + public EventTurnActivated TurnActivated(TurnData Turn) { + if (_f.IsPredicted) return null; + var ev = _f.Context.AcquireEvent(EventTurnActivated.ID); + ev.Turn = Turn; + _f.AddEvent(ev); + return ev; + } + public EventPlayCommandReceived PlayCommandReceived(PlayerRef Player, PlayCommandData Data) { + var ev = _f.Context.AcquireEvent(EventPlayCommandReceived.ID); + ev.Player = Player; + ev.Data = Data; + _f.AddEvent(ev); + return ev; + } + public EventSkipCommandReceived SkipCommandReceived(PlayerRef Player, SkipCommandData Data) { + var ev = _f.Context.AcquireEvent(EventSkipCommandReceived.ID); + ev.Player = Player; + ev.Data = Data; + _f.AddEvent(ev); + return ev; + } + } + public unsafe partial struct FrameAssets { + public BallPoolSpec BallPoolSpec(AssetRefBallPoolSpec assetRef) { + return _f.FindAsset(assetRef.Id); + } + public ConfigAssets ConfigAssets(AssetRefConfigAssets assetRef) { + return _f.FindAsset(assetRef.Id); + } + public GameConfig GameConfig(AssetRefGameConfig assetRef) { + return _f.FindAsset(assetRef.Id); + } + public UserMap UserMap(AssetRefUserMap assetRef) { + return _f.FindAsset(assetRef.Id); + } + public TurnConfig TurnConfig(AssetRefTurnConfig assetRef) { + return _f.FindAsset(assetRef.Id); + } + } + } + public unsafe interface ISignalOnBallPoolShot : ISignal { + void OnBallPoolShot(Frame f, PlayerRef player); + } + public unsafe interface ISignalOnBallPoolHitHole : ISignal { + void OnBallPoolHitHole(Frame f, EntityRef ball); + } + public unsafe interface ISignalOnTurnEnded : ISignal { + void OnTurnEnded(Frame f, TurnData data, TurnEndReason reason); + } + public unsafe interface ISignalOnPlayCommandReceived : ISignal { + void OnPlayCommandReceived(Frame f, PlayerRef player, PlayCommandData data); + } + public unsafe interface ISignalOnSkipCommandReceived : ISignal { + void OnSkipCommandReceived(Frame f, PlayerRef player, SkipCommandData data); + } + public unsafe partial class EventMessage : EventBase { + public new const Int32 ID = 0; + public String Header; + public String Text; + protected EventMessage(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventMessage() : + base(0, EventFlags.Server|EventFlags.Client) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 37; + hash = hash * 31 + Header.GetHashCode(); + hash = hash * 31 + Text.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventRemoveBall : EventBase { + public new const Int32 ID = 1; + public Int32 Num; + protected EventRemoveBall(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventRemoveBall() : + base(1, EventFlags.Server|EventFlags.Client) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 41; + hash = hash * 31 + Num.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventEndGame : EventBase { + public new const Int32 ID = 2; + public PlayerRef Winner; + protected EventEndGame(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventEndGame() : + base(2, EventFlags.Server|EventFlags.Client) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 43; + hash = hash * 31 + Winner.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventReplaceBall : EventBase { + public new const Int32 ID = 3; + protected EventReplaceBall(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventReplaceBall() : + base(3, EventFlags.Server|EventFlags.Client) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 47; + return hash; + } + } + } + public unsafe partial class EventCueHitBall : EventBase { + public new const Int32 ID = 4; + protected EventCueHitBall(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventCueHitBall() : + base(4, EventFlags.Server|EventFlags.Client) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 53; + return hash; + } + } + } + public unsafe partial class EventBallsCollision : EventBase { + public new const Int32 ID = 5; + protected EventBallsCollision(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventBallsCollision() : + base(5, EventFlags.Server|EventFlags.Client) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 59; + return hash; + } + } + } + public unsafe partial class EventBallsHitWall : EventBase { + public new const Int32 ID = 6; + protected EventBallsHitWall(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventBallsHitWall() : + base(6, EventFlags.Server|EventFlags.Client) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 61; + return hash; + } + } + } + public unsafe partial class EventBallsHitHole : EventBase { + public new const Int32 ID = 7; + protected EventBallsHitHole(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventBallsHitHole() : + base(7, EventFlags.Server|EventFlags.Client) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 67; + return hash; + } + } + } + public unsafe partial class EventGameplayEnded : EventBase { + public new const Int32 ID = 8; + protected EventGameplayEnded(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventGameplayEnded() : + base(8, EventFlags.Server|EventFlags.Client|EventFlags.Synced) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 71; + return hash; + } + } + } + public abstract unsafe partial class EventTurnEvent : EventBase { + public new const Int32 ID = 9; + public TurnData Turn; + protected EventTurnEvent(Int32 id, EventFlags flags) : + base(id, flags) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 73; + hash = hash * 31 + Turn.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventTurnTypeChanged : EventTurnEvent { + public new const Int32 ID = 10; + public TurnType PreviousType; + protected EventTurnTypeChanged(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventTurnTypeChanged() : + base(10, EventFlags.Server|EventFlags.Client|EventFlags.Synced) { + } + public override Int32 GetHashCode() { + unchecked { + var hash = 79; + hash = hash * 31 + Turn.GetHashCode(); + hash = hash * 31 + PreviousType.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventTurnStatusChanged : EventTurnEvent { + public new const Int32 ID = 11; + public TurnStatus PreviousStatus; + protected EventTurnStatusChanged(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventTurnStatusChanged() : + base(11, EventFlags.Server|EventFlags.Client|EventFlags.Synced) { + } + public override Int32 GetHashCode() { + unchecked { + var hash = 83; + hash = hash * 31 + Turn.GetHashCode(); + hash = hash * 31 + PreviousStatus.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventTurnEnded : EventTurnEvent { + public new const Int32 ID = 12; + public TurnEndReason Reason; + protected EventTurnEnded(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventTurnEnded() : + base(12, EventFlags.Server|EventFlags.Client|EventFlags.Synced) { + } + public override Int32 GetHashCode() { + unchecked { + var hash = 89; + hash = hash * 31 + Turn.GetHashCode(); + hash = hash * 31 + Reason.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventTurnTimerReset : EventTurnEvent { + public new const Int32 ID = 13; + protected EventTurnTimerReset(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventTurnTimerReset() : + base(13, EventFlags.Server|EventFlags.Client|EventFlags.Synced) { + } + public override Int32 GetHashCode() { + unchecked { + var hash = 97; + hash = hash * 31 + Turn.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventTurnActivated : EventTurnEvent { + public new const Int32 ID = 14; + protected EventTurnActivated(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventTurnActivated() : + base(14, EventFlags.Server|EventFlags.Client|EventFlags.Synced) { + } + public override Int32 GetHashCode() { + unchecked { + var hash = 101; + hash = hash * 31 + Turn.GetHashCode(); + return hash; + } + } + } + public abstract unsafe partial class EventCommandEvent : EventBase { + public new const Int32 ID = 15; + public PlayerRef Player; + protected EventCommandEvent(Int32 id, EventFlags flags) : + base(id, flags) { + } + public new QuantumGame Game { + get { + return (QuantumGame)base.Game; + } + set { + base.Game = value; + } + } + public override Int32 GetHashCode() { + unchecked { + var hash = 103; + hash = hash * 31 + Player.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventPlayCommandReceived : EventCommandEvent { + public new const Int32 ID = 16; + public PlayCommandData Data; + protected EventPlayCommandReceived(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventPlayCommandReceived() : + base(16, EventFlags.Server|EventFlags.Client) { + } + public override Int32 GetHashCode() { + unchecked { + var hash = 107; + hash = hash * 31 + Player.GetHashCode(); + hash = hash * 31 + Data.GetHashCode(); + return hash; + } + } + } + public unsafe partial class EventSkipCommandReceived : EventCommandEvent { + public new const Int32 ID = 17; + public SkipCommandData Data; + protected EventSkipCommandReceived(Int32 id, EventFlags flags) : + base(id, flags) { + } + public EventSkipCommandReceived() : + base(17, EventFlags.Server|EventFlags.Client) { + } + public override Int32 GetHashCode() { + unchecked { + var hash = 109; + hash = hash * 31 + Player.GetHashCode(); + hash = hash * 31 + Data.GetHashCode(); + return hash; + } + } + } + public static unsafe partial class BitStreamExtensions { + public static void Serialize(this IBitStream stream, ref AssetRefBallPoolSpec value) { + stream.Serialize(ref value.Id.Value); + } + public static void Serialize(this IBitStream stream, ref AssetRefConfigAssets value) { + stream.Serialize(ref value.Id.Value); + } + public static void Serialize(this IBitStream stream, ref AssetRefGameConfig value) { + stream.Serialize(ref value.Id.Value); + } + public static void Serialize(this IBitStream stream, ref AssetRefTurnConfig value) { + stream.Serialize(ref value.Id.Value); + } + public static void Serialize(this IBitStream stream, ref AssetRefUserMap value) { + stream.Serialize(ref value.Id.Value); + } + } + [System.SerializableAttribute()] + public unsafe partial class BallPoolSpec : AssetObject { + } + [System.SerializableAttribute()] + public unsafe partial class ConfigAssets : AssetObject { + } + [System.SerializableAttribute()] + public unsafe partial class GameConfig : AssetObject { + } + [System.SerializableAttribute()] + public unsafe partial class UserMap : AssetObject { + } + [System.SerializableAttribute()] + public unsafe partial class TurnConfig : AssetObject { + } + public unsafe partial class ComponentPrototypeVisitor : Prototypes.ComponentPrototypeVisitorBase { + public virtual void Visit(Prototypes.BallFields_Prototype prototype) { + VisitFallback(prototype); + } + } + public static unsafe partial class Constants { + public const Int32 MAX_PLAYERS = 2; + } + public static unsafe partial class StaticDelegates { + public static FrameSerializer.Delegate SerializeBallPoolPlayer; + public static FrameSerializer.Delegate SerializeInput; + static partial void InitGen() { + SerializeBallPoolPlayer = Quantum.BallPoolPlayer.Serialize; + SerializeInput = Quantum.Input.Serialize; + } + } + public unsafe partial class TypeRegistry { + partial void AddGenerated() { + Register(typeof(AssetGuid), AssetGuid.SIZE); + Register(typeof(Quantum.AssetRefBallPoolSpec), Quantum.AssetRefBallPoolSpec.SIZE); + Register(typeof(AssetRefCharacterController2DConfig), AssetRefCharacterController2DConfig.SIZE); + Register(typeof(AssetRefCharacterController3DConfig), AssetRefCharacterController3DConfig.SIZE); + Register(typeof(Quantum.AssetRefConfigAssets), Quantum.AssetRefConfigAssets.SIZE); + Register(typeof(AssetRefEntityPrototype), AssetRefEntityPrototype.SIZE); + Register(typeof(AssetRefEntityView), AssetRefEntityView.SIZE); + Register(typeof(Quantum.AssetRefGameConfig), Quantum.AssetRefGameConfig.SIZE); + Register(typeof(AssetRefMap), AssetRefMap.SIZE); + Register(typeof(AssetRefNavMesh), AssetRefNavMesh.SIZE); + Register(typeof(AssetRefNavMeshAgentConfig), AssetRefNavMeshAgentConfig.SIZE); + Register(typeof(AssetRefPhysicsMaterial), AssetRefPhysicsMaterial.SIZE); + Register(typeof(AssetRefPolygonCollider), AssetRefPolygonCollider.SIZE); + Register(typeof(AssetRefTerrainCollider), AssetRefTerrainCollider.SIZE); + Register(typeof(Quantum.AssetRefTurnConfig), Quantum.AssetRefTurnConfig.SIZE); + Register(typeof(Quantum.AssetRefUserMap), Quantum.AssetRefUserMap.SIZE); + Register(typeof(Quantum.BallFields), Quantum.BallFields.SIZE); + Register(typeof(Quantum.BallPoolPlayer), Quantum.BallPoolPlayer.SIZE); + Register(typeof(Quantum.BitSet1024), Quantum.BitSet1024.SIZE); + Register(typeof(Quantum.BitSet128), Quantum.BitSet128.SIZE); + Register(typeof(Quantum.BitSet2), Quantum.BitSet2.SIZE); + Register(typeof(Quantum.BitSet2048), Quantum.BitSet2048.SIZE); + Register(typeof(Quantum.BitSet256), Quantum.BitSet256.SIZE); + Register(typeof(Quantum.BitSet4096), Quantum.BitSet4096.SIZE); + Register(typeof(Quantum.BitSet512), Quantum.BitSet512.SIZE); + Register(typeof(Button), Button.SIZE); + Register(typeof(CharacterController2D), CharacterController2D.SIZE); + Register(typeof(CharacterController3D), CharacterController3D.SIZE); + Register(typeof(ColorRGBA), ColorRGBA.SIZE); + Register(typeof(ComponentPrototypeRef), ComponentPrototypeRef.SIZE); + Register(typeof(DistanceJoint), DistanceJoint.SIZE); + Register(typeof(DistanceJoint3D), DistanceJoint3D.SIZE); + Register(typeof(EntityPrototypeRef), EntityPrototypeRef.SIZE); + Register(typeof(EntityRef), EntityRef.SIZE); + Register(typeof(FP), FP.SIZE); + Register(typeof(FPBounds2), FPBounds2.SIZE); + Register(typeof(FPBounds3), FPBounds3.SIZE); + Register(typeof(FPMatrix2x2), FPMatrix2x2.SIZE); + Register(typeof(FPMatrix3x3), FPMatrix3x3.SIZE); + Register(typeof(FPMatrix4x4), FPMatrix4x4.SIZE); + Register(typeof(FPQuaternion), FPQuaternion.SIZE); + Register(typeof(FPVector2), FPVector2.SIZE); + Register(typeof(FPVector3), FPVector3.SIZE); + Register(typeof(FrameMetaData), FrameMetaData.SIZE); + Register(typeof(HingeJoint), HingeJoint.SIZE); + Register(typeof(HingeJoint3D), HingeJoint3D.SIZE); + Register(typeof(Hit), Hit.SIZE); + Register(typeof(Hit3D), Hit3D.SIZE); + Register(typeof(Quantum.Input), Quantum.Input.SIZE); + Register(typeof(Quantum.InputButtons), 4); + Register(typeof(Joint), Joint.SIZE); + Register(typeof(Joint3D), Joint3D.SIZE); + Register(typeof(LayerMask), LayerMask.SIZE); + Register(typeof(MapEntityId), MapEntityId.SIZE); + Register(typeof(MapEntityLink), MapEntityLink.SIZE); + Register(typeof(NavMeshAvoidanceAgent), NavMeshAvoidanceAgent.SIZE); + Register(typeof(NavMeshAvoidanceObstacle), NavMeshAvoidanceObstacle.SIZE); + Register(typeof(NavMeshPathfinder), NavMeshPathfinder.SIZE); + Register(typeof(NavMeshRegionMask), NavMeshRegionMask.SIZE); + Register(typeof(NavMeshSteeringAgent), NavMeshSteeringAgent.SIZE); + Register(typeof(NullableFP), NullableFP.SIZE); + Register(typeof(NullableFPVector2), NullableFPVector2.SIZE); + Register(typeof(NullableFPVector3), NullableFPVector3.SIZE); + Register(typeof(NullableNonNegativeFP), NullableNonNegativeFP.SIZE); + Register(typeof(PhysicsBody2D), PhysicsBody2D.SIZE); + Register(typeof(PhysicsBody3D), PhysicsBody3D.SIZE); + Register(typeof(PhysicsCollider2D), PhysicsCollider2D.SIZE); + Register(typeof(PhysicsCollider3D), PhysicsCollider3D.SIZE); + Register(typeof(PhysicsSceneSettings), PhysicsSceneSettings.SIZE); + Register(typeof(PlayerRef), PlayerRef.SIZE); + Register(typeof(Ptr), Ptr.SIZE); + Register(typeof(QBoolean), QBoolean.SIZE); + Register(typeof(Quantum.Ptr), Quantum.Ptr.SIZE); + Register(typeof(RNGSession), RNGSession.SIZE); + Register(typeof(Shape2D), Shape2D.SIZE); + Register(typeof(Shape3D), Shape3D.SIZE); + Register(typeof(SpringJoint), SpringJoint.SIZE); + Register(typeof(SpringJoint3D), SpringJoint3D.SIZE); + Register(typeof(Transform2D), Transform2D.SIZE); + Register(typeof(Transform2DVertical), Transform2DVertical.SIZE); + Register(typeof(Transform3D), Transform3D.SIZE); + Register(typeof(Quantum.TurnData), Quantum.TurnData.SIZE); + Register(typeof(Quantum.TurnEndReason), 4); + Register(typeof(Quantum.TurnStatus), 4); + Register(typeof(Quantum.TurnType), 4); + Register(typeof(View), View.SIZE); + Register(typeof(Quantum._globals_), Quantum._globals_.SIZE); + } + } + public unsafe partial class FramePrinterGen { + public static void EnsureNotStripped() { + FramePrinter.EnsurePrimitiveNotStripped(); + FramePrinter.EnsurePrimitiveNotStripped(); + FramePrinter.EnsurePrimitiveNotStripped(); + FramePrinter.EnsurePrimitiveNotStripped(); + FramePrinter.EnsurePrimitiveNotStripped(); + FramePrinter.EnsurePrimitiveNotStripped(); + FramePrinter.EnsurePrimitiveNotStripped(); + FramePrinter.EnsurePrimitiveNotStripped(); + FramePrinter.EnsurePrimitiveNotStripped(); + } + } +} +namespace Quantum.Prototypes { + using System; + using System.Collections.Generic; + using System.Runtime.InteropServices; + using Photon.Deterministic; + using Quantum.Core; + using Quantum.Collections; + using Quantum.Inspector; + using Quantum.Physics2D; + using Quantum.Physics3D; + using Optional = Quantum.Inspector.OptionalAttribute; + using MethodImplAttribute = System.Runtime.CompilerServices.MethodImplAttribute; + using MethodImplOptions = System.Runtime.CompilerServices.MethodImplOptions; + + [System.SerializableAttribute()] + [Prototype(typeof(TurnEndReason))] + public unsafe partial struct TurnEndReason_Prototype { + public Int32 Value; + public static implicit operator TurnEndReason(TurnEndReason_Prototype value) { + return (TurnEndReason)value.Value; + } + public static implicit operator TurnEndReason_Prototype(TurnEndReason value) { + return new TurnEndReason_Prototype() { Value = (Int32)value }; + } + } + [System.SerializableAttribute()] + [Prototype(typeof(TurnStatus))] + public unsafe partial struct TurnStatus_Prototype { + public Int32 Value; + public static implicit operator TurnStatus(TurnStatus_Prototype value) { + return (TurnStatus)value.Value; + } + public static implicit operator TurnStatus_Prototype(TurnStatus value) { + return new TurnStatus_Prototype() { Value = (Int32)value }; + } + } + [System.SerializableAttribute()] + [Prototype(typeof(TurnType))] + public unsafe partial struct TurnType_Prototype { + public Int32 Value; + public static implicit operator TurnType(TurnType_Prototype value) { + return (TurnType)value.Value; + } + public static implicit operator TurnType_Prototype(TurnType value) { + return new TurnType_Prototype() { Value = (Int32)value }; + } + } + [System.SerializableAttribute()] + [Prototype(typeof(InputButtons))] + public unsafe partial struct InputButtons_Prototype { + public Int32 Value; + public static implicit operator InputButtons(InputButtons_Prototype value) { + return (InputButtons)value.Value; + } + public static implicit operator InputButtons_Prototype(InputButtons value) { + return new InputButtons_Prototype() { Value = (Int32)value }; + } + } + [System.SerializableAttribute()] + [Prototype(typeof(BallFields))] + public sealed unsafe partial class BallFields_Prototype : ComponentPrototype { + public FPVector2 Spin; + public Int32 Number; + public QBoolean Striped; + public AssetRefBallPoolSpec Spec; + public QBoolean InTable; + partial void MaterializeUser(Frame frame, ref BallFields result, in PrototypeMaterializationContext context); + public override Boolean AddToEntity(FrameBase f, EntityRef entity, in PrototypeMaterializationContext context) { + BallFields component = default; + Materialize((Frame)f, ref component, in context); + return f.Set(entity, component) == SetResult.ComponentAdded; + } + public void Materialize(Frame frame, ref BallFields result, in PrototypeMaterializationContext context) { + result.InTable = this.InTable; + result.Number = this.Number; + result.Spec = this.Spec; + result.Spin = this.Spin; + result.Striped = this.Striped; + MaterializeUser(frame, ref result, in context); + } + public override void Dispatch(ComponentPrototypeVisitorBase visitor) { + ((ComponentPrototypeVisitor)visitor).Visit(this); + } + } + [System.SerializableAttribute()] + [Prototype(typeof(BallPoolPlayer))] + public sealed unsafe partial class BallPoolPlayer_Prototype : StructPrototype { + public PlayerRef Ref; + public TurnData_Prototype TurnStats; + public QBoolean StripedBalls; + partial void MaterializeUser(Frame frame, ref BallPoolPlayer result, in PrototypeMaterializationContext context); + public void Materialize(Frame frame, ref BallPoolPlayer result, in PrototypeMaterializationContext context) { + result.Ref = this.Ref; + result.StripedBalls = this.StripedBalls; + this.TurnStats.Materialize(frame, ref result.TurnStats, in context); + MaterializeUser(frame, ref result, in context); + } + } + [System.SerializableAttribute()] + [Prototype(typeof(Input))] + public sealed unsafe partial class Input_Prototype : StructPrototype { + public FPVector3 Direction; + public FP ForceBarMarkPos; + public FPVector2 BallPosition; + partial void MaterializeUser(Frame frame, ref Input result, in PrototypeMaterializationContext context); + public void Materialize(Frame frame, ref Input result, in PrototypeMaterializationContext context) { + result.BallPosition = this.BallPosition; + result.Direction = this.Direction; + result.ForceBarMarkPos = this.ForceBarMarkPos; + MaterializeUser(frame, ref result, in context); + } + } + [System.SerializableAttribute()] + [Prototype(typeof(TurnData))] + public sealed unsafe partial class TurnData_Prototype : StructPrototype { + public PlayerRef Player; + public MapEntityId Entity; + public AssetRefTurnConfig ConfigRef; + public TurnType_Prototype Type; + public TurnStatus_Prototype Status; + public Int32 Number; + public Int32 Ticks; + partial void MaterializeUser(Frame frame, ref TurnData result, in PrototypeMaterializationContext context); + public void Materialize(Frame frame, ref TurnData result, in PrototypeMaterializationContext context) { + result.ConfigRef = this.ConfigRef; + PrototypeValidator.FindMapEntity(this.Entity, in context, out result.Entity); + result.Number = this.Number; + result.Player = this.Player; + result.Status = this.Status; + result.Ticks = this.Ticks; + result.Type = this.Type; + MaterializeUser(frame, ref result, in context); + } + } + public unsafe partial class FlatEntityPrototypeContainer { + [ArrayLength(0, 1)] + public List BallFields; + partial void CollectGen(List target) { + Collect(BallFields, target); + } + public unsafe partial class StoreVisitor { + public override void Visit(Prototypes.BallFields_Prototype prototype) { + Storage.Store(prototype, ref Storage.BallFields); + } + } + } +} +#pragma warning restore 0649 +#pragma warning restore 1522 +#pragma warning restore 0414 +#pragma warning restore 0219 +#pragma warning restore 0109 diff --git a/data/CommandSetup.Legacy.cs b/data/CommandSetup.Legacy.cs new file mode 100644 index 0000000000000000000000000000000000000000..f9ecc886d6943496693bfe20f2a0184b92a2fd52 --- /dev/null +++ b/data/CommandSetup.Legacy.cs @@ -0,0 +1,12 @@ +using Photon.Deterministic; + +namespace Quantum +{ + public static class CommandSetup + { + public static DeterministicCommand[] CreateCommands(RuntimeConfig gameConfig, SimulationConfig simulationConfig) + { + return null; + } + } +} diff --git a/data/CommandSetup.User.cs b/data/CommandSetup.User.cs new file mode 100644 index 0000000000000000000000000000000000000000..ba9a4648ac481500f7ee551960be9400a86564e1 --- /dev/null +++ b/data/CommandSetup.User.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Photon.Deterministic; + +namespace Quantum +{ + public static partial class DeterministicCommandSetup + { + static partial void AddCommandFactoriesUser(ICollection factories, RuntimeConfig gameConfig, SimulationConfig simulationConfig) + { + factories.Add(new GameMaster_SetCharacterPosition()); + factories.Add(new GameMaster_SpawnPrototype()); + } + } +} diff --git a/data/CommandSetup.cs b/data/CommandSetup.cs new file mode 100644 index 0000000000000000000000000000000000000000..faddd53b97083c836d4b50840025d1c9bcf57fdb --- /dev/null +++ b/data/CommandSetup.cs @@ -0,0 +1,31 @@ +using Photon.Deterministic; +using System; +using System.Collections.Generic; + +namespace Quantum +{ + public static class CommandSetup + { + public static DeterministicCommand[] CreateCommands(RuntimeConfig gameConfig, SimulationConfig simulationConfig) + { + Type baseType = typeof(DeterministicCommand); + Type[] allTypes = typeof(CommandSetup).Assembly.GetTypes(); + + List commands = new List(16); + + foreach (Type type in allTypes) + { + if (type.IsSubclassOf(baseType) == true && type.IsAbstract == false) + { + DeterministicCommand command = Activator.CreateInstance(type) as DeterministicCommand; + if (command != null) + { + commands.Add(command); + } + } + } + + return commands.ToArray(); + } + } +} diff --git a/data/CommandSystem.cs b/data/CommandSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..6153f8101a2d51eddba049a71752165a4104cdbb --- /dev/null +++ b/data/CommandSystem.cs @@ -0,0 +1,41 @@ +using System; + +namespace Quantum +{ + public unsafe class CommandSystem : SystemMainThread + { + public override void Update(Frame f) + { + var currentTurn = f.Global->CurrentTurn; + if (currentTurn.Status != TurnStatus.Active) + { + return; + } + + + var currentPlayer = f.Global->CurrentTurn.Player; + + switch (f.GetPlayerCommand(currentPlayer)) + { + case PlayCommand playCommand: + if (currentTurn.Type != TurnType.Play) + { + return; + } + f.Signals.OnPlayCommandReceived(currentPlayer, playCommand.Data); + f.Events.PlayCommandReceived(currentPlayer, playCommand.Data); + break; + + case SkipCommand skipCommand: + var config = f.FindAsset(currentTurn.ConfigRef.Id); + if (!config.IsSkippable) + { + return; + } + f.Signals.OnSkipCommandReceived(currentPlayer, skipCommand.Data); + f.Events.SkipCommandReceived(currentPlayer, skipCommand.Data); + break; + } + } + } +} \ No newline at end of file diff --git a/data/ConfigAssets.cs b/data/ConfigAssets.cs new file mode 100644 index 0000000000000000000000000000000000000000..0242686dc5eb85f7b3c3b65a8df3464e99fe58af --- /dev/null +++ b/data/ConfigAssets.cs @@ -0,0 +1,8 @@ +namespace Quantum { + public partial class ConfigAssets { + public AssetRefGameConfig GameConfig; + public AssetRefTurnConfig StartCountdownConfig; + public AssetRefTurnConfig PlayTurnConfig; + public AssetRefTurnConfig CountdownTurnConfig; + } +} diff --git a/data/ConfigAssetsHelper.cs b/data/ConfigAssetsHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..3a1178e27cd22bd8633eeee60ec511878f310956 --- /dev/null +++ b/data/ConfigAssetsHelper.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Quantum +{ + unsafe public class ConfigAssetsHelper + { + public static TurnConfig GetTurnConfig(Frame f, TurnType type) + { + var configAssets = f.FindAsset(f.RuntimeConfig.ConfigAssets.Id); + TurnConfig config = null; + switch (type) + { + case TurnType.Countdown: + config = f.FindAsset(configAssets.CountdownTurnConfig.Id); + break; + case TurnType.Play: + config = f.FindAsset(configAssets.PlayTurnConfig.Id); + break; + default: + break; + } + return config; + } + + public static GameConfig GetGameConfig(Frame f) { + var configAssets = f.FindAsset(f.RuntimeConfig.ConfigAssets.Id); + return f.FindAsset(configAssets.GameConfig.Id); + } + } +} diff --git a/data/Configuration Files.txt b/data/Configuration Files.txt new file mode 100644 index 0000000000000000000000000000000000000000..d57c5522ccb072fb8177502a27b9bcf0fe3eee17 --- /dev/null +++ b/data/Configuration Files.txt @@ -0,0 +1,191 @@ +Configuration Files +Introduction +Quantum Start Sequence +Config Files +PhotonServerSettings +DeterministicConfig +SimulationConfig +Delta Time Type +RuntimeConfig +RuntimePlayer +Using DSL Generated Code With RuntimePlayer And RuntimeConfig Serialization + +Introduction +There are a few Quantum config files that have specific roles and purposes. + +These config files are placed in different folders in the Unity project. Finding them quickly is made easy with the shortcuts (unity) editor window found in "Menu/Quantum/Show Shortcuts". + +Most of default config instances reside as Scriptable Objects inside the "Resources" folder at the root level of the Unity project Assets, and will end up in your app build from there (see DeterministicSessionConfigAsset.Instance for example) while others (RuntimeConfig, RuntimePlayer) can be assembled during run-time. + +Back To Top + + +Quantum Start Sequence +Which config is used by whom and send when is shown in the diagram below. + +Config Sequence Diagram +Config Sequence Diagram +Back To Top + + +Config Files + +PhotonServerSettings +Assets/Resources/PhotonServerSettings.asset +Quantum, from version 2.0, uses Photon Realtime to connect and communicate to the Photon Cloud. This config describes where the client connects to (cloud + region, local ip, ..). + +photon realtime introduction + +Also a valid AppId (referring to an active Quantum plugin) is set here. + +Only one instance of this config file is allowed. The loading is tightly integrated into the PhotonNetwork class. See PhotonNetwork.PhotonServerSettings. + +Photon Server Settings +Photon Server Settings +Back To Top + + +DeterministicConfig +Assets/Resouces/DeterministicConfig.asset +Via the DeterministicConfig developers can parametrize internals of the deterministic simulation and plugin (the Quantum server component). Toggle Show Help Info in the inspector of this config for details of each parameter. + +The default way only allows one instance of this asset but as long as it is passed into QuantumRunner.StartParameters it does not matter how the file is retrieved. + +This config file will be synchronized between all clients of one session. Although each player starts their own simulation locally with their own version of the DeterministicConfig, the server will distribute the config file instance of the first player who joined the plugin. + +The data on this config is included in the checksum generation. + +Deterministic Config +Deterministic Config +Back To Top + + +SimulationConfig +Assets/Resources/DB/Configs/SimulationConfig.asset +This config file holds parameters used in the ECS layer and inside core systems like physics and navigation. See the related system sections in the manual for more details of each value. + +The SimulationConfig is part of the Quantum DB and multiple instances of this config are supported. Add the config asset GUID to the RuntimeConfig to select which SimualtionConfig should be used. + +Developers can "extend" (create a partial class) the quantum_code/quantum.state/Core/SimulationConfig.cs class and add more data to it. + +Simulation Config +Simulation Config +Back To Top + + +Delta Time Type +You can customize how the QuantumRunner will accumulate elapsed time to update the Quantum simulation (see the QuantumRunner.DeltaTime property). + +The Default setting will use an internal stopwatch and is the recommended setting for production. +EngineDeltaTime will use, for example Unity.deltaTime, to track when to trigger simulation updates. This is very handy when debugging the project using break points, because upon resuming the simulation with not fast-forward but continue from the exact time the simulation was paused. Alas, this setting can cause issues with time synchronization when initializing online matches: the time tracking can be inaccurate under load (e.g. level loading) and result in a lot of large extra time syncs request and cancelled inputs for a client when starting an online game. +Back To Top + + +RuntimeConfig +In contrast to the SimulationConfig, which has only static configuration data, the RuntimeConfig holds information that can be different from game to game. By default is defines for example what map to load and the random start seed. It is assembled from scratch each time starting a game. + +Developers can add custom data to quantum_code/quantum.state/RuntimeConfig.User.cs (don't forget to fill out the serialization methods). + +Like the DeterministicConfig this "file" is distributed to every other client after the first player connected and joined the Quantum plugin. + +A convenient way of using this config is by creating a MonoBehaviour that stores an instance of RuntimeConfig (and RuntimePlayer) with default values and asset links (GUIDs) for example pointing to other asset files containing specific balancing data. When the player is inside a game lobby parts of the Runtime configs can be overwritten with his custom load-out before connecting and starting the game. See QuantumRunnerLocalDebug.cs or the sample below: + +Runtime Setup +Runtime Setup +using Quantum; +using UnityEngine; + +public sealed class RuntimeSetup : MonoBehaviour +{ + public static RuntimeSetup Instance { get; private set; } + + public RuntimeConfig GameConfig { get { return _gameConfig; } } + public RuntimePlayer PlayerConfig { get { return _playerConfig; } } + + [SerializeField] private RuntimeConfig _gameConfig; + [SerializeField] private RuntimePlayer _playerConfig; + + private void Awake() { + Instance = this; + } +} +Back To Top + + +RuntimePlayer +Similar to the RuntimeConfig the RuntimePlayer describes dynamic properties for one player (quantum_code/quantum.state/RuntimePlayer.User.cs). + +The data for a player behaves differently to the other configs, because it is send by each player individually after the actual game has been started. See the Player document in the manual for more information. + +Back To Top + + +Using DSL Generated Code With RuntimePlayer And RuntimeConfig Serialization +RuntimeConfig and RuntimePlayer require to write manual serialization code. When using DSL generated structs of component prototypes the serialization code can be simplified. + +Caveat: Never use objects that are actually pointers that require a frame to be resolved (e.g. Quantum collections). + +The following struct Foo43 and components prototype Component43 will be used in the RuntimePlayer. + +struct Foo43 { + int Integer; + array[8] Bytes; + asset_ref MapAssetReference; + Bar43 Bar43; +} + +struct Bar43 { + FPVector3 Vector3; +} + +component Component43 { + int Integer; + OtherComponent43 OtherComponent; +} + +component OtherComponent43 { + int Integer; + FP FP; +} +The partial RuntimePlayer.User implementation looks like this. + +partial class RuntimePlayer { + // A) Use a DSL generated struct on RuntimePlayer + public Foo43 Foo; + + // B) Piggyback on a component prototype to set data + public Component43_Prototype Component43 = new Component43_Prototype { OtherComponent = new OtherComponent43_Prototype() }; + + partial void SerializeUserData(BitStream stream) { + // A) Because the struct is memory alined we can pin the memory and serialize it as a byte array which will work platform indenpentently. + unsafe { + fixed (Foo43* p = &Foo) { + stream.SerializeBuffer((byte*)p, sizeof(Foo43)); + } + } + + // B) Initialized the references in the field declaration with new and serialize all fields here. + stream.Serialize(ref Component43.Integer); + stream.Serialize(ref Component43.OtherComponent.Integer); + stream.Serialize(ref Component43.OtherComponent.FP); + } +} +Send the RuntimePlayer from the client: + +var runtimePlayer = new Quantum.RuntimePlayer { + Component43 = new Quantum.Prototypes.Component43_Prototype { + Integer = 1, + OtherComponent = new Quantum.Prototypes.OtherComponent43_Prototype { FP = 2, Integer = 3 } }, + Foo = new Foo43 { + Bar43 = new Bar43 { Vector3 = FPVector3.One }, + Integer = 4, + MapAssetReference = new AssetRefMap() { Id = 66 } + } +}; + +unsafe { + runtimePlayer.Foo.Bytes[0] = 7; + runtimePlayer.Foo.Bytes[1] = 6; +} + +game.SendPlayerData(lp, runtimePlayer); \ No newline at end of file diff --git a/data/Consideration.cs b/data/Consideration.cs new file mode 100644 index 0000000000000000000000000000000000000000..fe9b7508ccaa1019a9fdb39b8964c2fdb5ffdbef --- /dev/null +++ b/data/Consideration.cs @@ -0,0 +1,164 @@ +using Photon.Deterministic; +using System; +using Quantum.Prototypes; + +namespace Quantum +{ + [Serializable] + public struct ResponseCurvePack + { + public FP MultiplyFactor; + public AssetRefAIFunctionFP ResponseCurveRef; + [NonSerialized] public ResponseCurve ResponseCurve; + } + + public unsafe partial class Consideration + { + public string Label; + + public AssetRefAIFunctionInt RankRef; + public AssetRefAIFunctionBool CommitmentRef; + public AssetRefConsideration[] NextConsiderationsRefs; + public AssetRefAIAction[] OnEnterActionsRefs; + public AssetRefAIAction[] OnUpdateActionsRefs; + public AssetRefAIAction[] OnExitActionsRefs; + + [NonSerialized] public AIFunctionInt Rank; + [NonSerialized] public AIFunctionBool Commitment; + [NonSerialized] public Consideration[] NextConsiderations; + [NonSerialized] public AIAction[] OnEnterActions; + [NonSerialized] public AIAction[] OnUpdateActions; + [NonSerialized] public AIAction[] OnExitActions; + + public ResponseCurvePack[] ResponseCurvePacks; + + public FP BaseScore; + + public UTMomentumData MomentumData; + public FP Cooldown; + + public byte Depth; + + public int GetRank(Frame frame, EntityRef entity = default) + { + if (Rank == null) + return 0; + + return Rank.Execute(frame, entity); + } + + public FP Score(Frame frame, EntityRef entity = default) + { + if (ResponseCurvePacks.Length == 0) + return 0; + + FP score = 1; + for (int i = 0; i < ResponseCurvePacks.Length; i++) + { + score *= ResponseCurvePacks[i].ResponseCurve.Execute(frame, entity) * ResponseCurvePacks[i].MultiplyFactor; + + // If we find a negative veto, the final score would be zero anyways, so we stop here + if (score == 0) + { + break; + } + } + + score += BaseScore; + + FP modificationFactor = 1 - (1 / ResponseCurvePacks.Length); + FP makeUpValue = (1 - score) * modificationFactor; + FP finalScore = score + (makeUpValue * score); + + return finalScore; + } + + public void OnEnter(Frame frame, UtilityReasoner* reasoner, EntityRef entity = default) + { + for (int i = 0; i < OnEnterActions.Length; i++) + { + OnEnterActions[i].Update(frame, entity); + } + } + + public void OnExit(Frame frame, UtilityReasoner* reasoner, EntityRef entity = default) + { + for (int i = 0; i < OnExitActions.Length; i++) + { + OnExitActions[i].Update(frame, entity); + } + } + + public void OnUpdate(Frame frame, UtilityReasoner* reasoner, EntityRef entity = default) + { + for (int i = 0; i < OnUpdateActions.Length; i++) + { + OnUpdateActions[i].Update(frame, entity); + } + + if (NextConsiderationsRefs != null && NextConsiderationsRefs.Length > 0) + { + Consideration chosenConsideration = reasoner->SelectBestConsideration(frame, NextConsiderations, (byte)(Depth + 1), reasoner, entity); + if (chosenConsideration != default) + { + chosenConsideration.OnUpdate(frame, reasoner, entity); + UTManager.ConsiderationChosen?.Invoke(entity, chosenConsideration.Identifier.Guid.Value); + } + } + } + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + Rank = (AIFunctionInt)resourceManager.GetAsset(RankRef.Id); + + if (ResponseCurvePacks != null) + { + for (Int32 i = 0; i < ResponseCurvePacks.Length; i++) + { + ResponseCurvePacks[i].ResponseCurve = (ResponseCurve)resourceManager.GetAsset(ResponseCurvePacks[i].ResponseCurveRef.Id); + ResponseCurvePacks[i].MultiplyFactor = 1; + } + } + + OnEnterActions = new AIAction[OnEnterActionsRefs == null ? 0 : OnEnterActionsRefs.Length]; + if (OnEnterActionsRefs != null) + { + for (Int32 i = 0; i < OnEnterActionsRefs.Length; i++) + { + OnEnterActions[i] = (AIAction)resourceManager.GetAsset(OnEnterActionsRefs[i].Id); + } + } + + OnUpdateActions = new AIAction[OnUpdateActionsRefs == null ? 0 : OnUpdateActionsRefs.Length]; + if (OnEnterActionsRefs != null) + { + for (Int32 i = 0; i < OnUpdateActionsRefs.Length; i++) + { + OnUpdateActions[i] = (AIAction)resourceManager.GetAsset(OnUpdateActionsRefs[i].Id); + } + } + + OnExitActions = new AIAction[OnExitActionsRefs == null ? 0 : OnExitActionsRefs.Length]; + if (OnEnterActionsRefs != null) + { + for (Int32 i = 0; i < OnExitActionsRefs.Length; i++) + { + OnExitActions[i] = (AIAction)resourceManager.GetAsset(OnExitActionsRefs[i].Id); + } + } + + Commitment = (AIFunctionBool)resourceManager.GetAsset(CommitmentRef.Id); + + NextConsiderations = new Consideration[NextConsiderationsRefs == null ? 0 : NextConsiderationsRefs.Length]; + if (NextConsiderationsRefs != null) + { + for (Int32 i = 0; i < NextConsiderationsRefs.Length; i++) + { + NextConsiderations[i] = (Consideration)resourceManager.GetAsset(NextConsiderationsRefs[i].Id); + } + } + } + } +} diff --git a/data/Core.cs b/data/Core.cs new file mode 100644 index 0000000000000000000000000000000000000000..d577cbd0fdb8910ec99626adab8156dc096313da --- /dev/null +++ b/data/Core.cs @@ -0,0 +1,6033 @@ +using System.Collections.Generic; +using Photon.Deterministic; +using System; +using Quantum.Core; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Runtime.InteropServices; +using Quantum.Inspector; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Quantum; +using Quantum.Profiling; +using Quantum.Allocator; +using System.Collections.Specialized; +using System.Threading; +using Quantum.Prototypes; +using System.Reflection; +using Quantum.Task; +using System.Runtime.CompilerServices; + +// Core/CommandSetup.cs + +namespace Quantum { + public static partial class DeterministicCommandSetup { + public static IDeterministicCommandFactory[] GetCommandFactories(RuntimeConfig gameConfig, SimulationConfig simulationConfig) { + var factories = new List() { + // pre-defined core commands + Core.DebugCommand.CreateCommand(), + new DeterministicCommandPool(), + }; + + AddCommandFactoriesUser(factories, gameConfig, simulationConfig); + +#pragma warning disable 618 // Use of obsolete members + var obsoleteCommandsCreated = CommandSetup.CreateCommands(gameConfig, simulationConfig); + if (obsoleteCommandsCreated != null && obsoleteCommandsCreated.Length > 0) { + Log.Warn("'CommandSetup.CreateCommands' is now deprecated. " + + "Implement a partial declaration of '" + nameof(DeterministicCommandSetup) + "." + nameof(AddCommandFactoriesUser) + "' instead, as shown on 'CommandSetup.User.cs' available on the SDK package." + + "Command instances can be used as factories of their own type."); + factories.AddRange(obsoleteCommandsCreated); + } +#pragma warning restore 618 + + return factories.ToArray(); + } + + static partial void AddCommandFactoriesUser(ICollection factories, RuntimeConfig gameConfig, SimulationConfig simulationConfig); + } +} + +// Core/Collision.cs + +namespace Quantum { + + /// + /// Interface for receiving callbacks once per frame while two non-trigger 2D colliders are touching. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics2dApi + public interface ISignalOnCollision2D : ISignal { + /// + /// Called once per frame while two non-trigger 2D colliders are touching. + /// + /// The frame in which the collision happened. + /// The with data about the collision. + /// \ingroup Physics2dApi + void OnCollision2D(Frame f, CollisionInfo2D info); + } + + /// + /// Interface for receiving callbacks once two non-trigger 2D colliders start touching. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics2dApi + public interface ISignalOnCollisionEnter2D : ISignal { + /// + /// Called once two non-trigger 2D colliders start touching. + /// + /// The frame in which the collision happened. + /// The with data about the collision. + /// \ingroup Physics2dApi + void OnCollisionEnter2D(Frame f, CollisionInfo2D info); + } + + /// + /// Interface for receiving callbacks once two non-trigger 2D colliders stop touching. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics2dApi + public interface ISignalOnCollisionExit2D : ISignal { + /// + /// Called once two non-trigger 2D colliders stop touching. + /// + /// The frame in which the entities stopped touching. + /// The with the entities that were touching. + /// \ingroup Physics2dApi + void OnCollisionExit2D(Frame f, ExitInfo2D info); + } + + /// + /// Interface for receiving callbacks once per frame while a non-trigger and a trigger 2D colliders are touching. + /// No collision is checked between two kinematic colliders that are both trigger or both non-trigger. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics2dApi + public interface ISignalOnTrigger2D : ISignal { + /// + /// Called once per frame while a non-trigger and a trigger 2D colliders are touching. + /// + /// The frame in which the collision happened. + /// The with data about the trigger collision. + /// \ingroup Physics2dApi + void OnTrigger2D(Frame f, TriggerInfo2D info); + } + + /// + /// Interface for receiving callbacks once a non-trigger and a trigger 2D colliders start touching. + /// No collision is checked between two kinematic colliders that are both trigger or both non-trigger. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics2dApi + public interface ISignalOnTriggerEnter2D : ISignal { + /// + /// Called once a non-trigger and a trigger 2D colliders start touching. + /// + /// The frame in which the collision happened. + /// The with data about the trigger collision. + /// \ingroup Physics2dApi + void OnTriggerEnter2D(Frame f, TriggerInfo2D info); + } + + /// + /// Interface for receiving callbacks once a non-trigger and a trigger 2D colliders stop touching. + /// No collision is checked between two kinematic colliders that are both trigger or both non-trigger. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics2dApi + public interface ISignalOnTriggerExit2D : ISignal { + /// + /// Called once a non-trigger and a trigger 2D colliders stop touching. + /// + /// The frame in which the entities stopped touching. + /// The with the entities that were touching. + /// \ingroup Physics2dApi + void OnTriggerExit2D(Frame f, ExitInfo2D info); + } + + /// + /// Interface for receiving callbacks once per frame while two non-trigger 3D colliders are touching. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics3dApi + public interface ISignalOnCollision3D : ISignal { + /// + /// Called once per frame while two non-trigger 3D colliders are touching. + /// + /// The frame in which the collision happened. + /// The with data about the collision. + /// \ingroup Physics3dApi + void OnCollision3D(Frame f, CollisionInfo3D info); + } + + /// + /// Interface for receiving callbacks once two non-trigger 3D colliders start touching. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics3dApi + public interface ISignalOnCollisionEnter3D : ISignal { + /// + /// Called once two non-trigger 3D colliders start touching. + /// + /// The frame in which the collision happened. + /// The with data about the collision. + /// \ingroup Physics3dApi + void OnCollisionEnter3D(Frame f, CollisionInfo3D info); + } + + /// + /// Interface for receiving callbacks once two non-trigger 3D colliders stop touching. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics3dApi + public interface ISignalOnCollisionExit3D : ISignal { + /// + /// Called once two non-trigger 3D colliders stop touching. + /// + /// The frame in which the entities stopped touching. + /// The with the entities that were touching. + /// \ingroup Physics3dApi + void OnCollisionExit3D(Frame f, ExitInfo3D info); + } + + /// + /// Interface for receiving callbacks once per frame while a non-trigger and a trigger 3D colliders are touching. + /// No collision is checked between two kinematic colliders that are both trigger or both non-trigger. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics3dApi + public interface ISignalOnTrigger3D : ISignal { + /// + /// Called once per frame while a non-trigger and a trigger 3D colliders are touching. + /// + /// The frame in which the collision happened. + /// The with data about the trigger collision. + /// \ingroup Physics3dApi + void OnTrigger3D(Frame f, TriggerInfo3D info); + } + + /// + /// Interface for receiving callbacks once a non-trigger and a trigger 3D colliders start touching. + /// No collision is checked between two kinematic colliders that are both trigger or both non-trigger. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics3dApi + public interface ISignalOnTriggerEnter3D : ISignal { + /// + /// Called once a non-trigger and a trigger 3D colliders start touching. + /// + /// The frame in which the collision happened. + /// The with data about the trigger collision. + /// \ingroup Physics3dApi + void OnTriggerEnter3D(Frame f, TriggerInfo3D info); + } + + /// + /// Interface for receiving callbacks once a non-trigger and a trigger 3D colliders stop touching. + /// No collision is checked between two kinematic colliders that are both trigger or both non-trigger. + /// At least one of the entities involved in a collision must have the respective set for the callback to be called. + /// See for setting the callbacks flags to an entity. + /// + /// \ingroup Physics3dApi + public interface ISignalOnTriggerExit3D : ISignal { + /// + /// Called once a non-trigger and a trigger 3D colliders stop touching. + /// + /// The frame in which the entities stopped touching. + /// The with the entities that were touching. + /// \ingroup Physics3dApi + void OnTriggerExit3D(Frame f, ExitInfo3D info); + } +} + +// Core/Frame.cs + +namespace Quantum { + /// + /// The user implementation of that resides in the project quantum_state and has access to all user relevant classes. + /// + /// \ingroup FrameClass + public unsafe partial class Frame : Core.FrameBase { + + public const int DumpFlag_NoSimulationConfig = 1 << 1; + public const int DumpFlag_NoRuntimeConfig = 1 << 3; + public const int DumpFlag_NoDeterministicSessionConfig = 1 << 4; + public const int DumpFlag_NoRuntimePlayers = 1 << 5; + public const int DumpFlag_NoDynamicDB = 1 << 6; + public const int DumpFlag_ReadableDynamicDB = 1 << 7; + public const int DumpFlag_PrintRawValues = 1 << 8; + public const int DumpFlag_ComponentChecksums = 1 << 9; + public const int DumpFlag_AssetDBCheckums = 1 << 10; + public const int DumpFlag_NoIsVerified = 1 << 11; + + [Obsolete("Use DumpFlag_ComponentChecksums")] + public const int DumpFlag_PrintComponentChecksums = DumpFlag_ComponentChecksums; + [Obsolete("Use DumpFlag_ReadableDynamicDB")] + public const int DumpFlag_PrintReadableDynamicDB = DumpFlag_ReadableDynamicDB; + + struct RuntimePlayerData { + public Int32 ActorId; + public Byte[] Data; + public RuntimePlayer Player; + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + _globals_* _globals; + + // configs + RuntimeConfig _runtimeConfig; + SimulationConfig _simulationConfig; + DeterministicSessionConfig _sessionConfig; + + // systems + SystemBase[] _systemsAll; + SystemBase[] _systemsRoots; + Dictionary _systemIndexByType; + + // player data + PersistentMap _playerData; + + ISignalOnPlayerDataSet[] _ISignalOnPlayerDataSet; + + // 2D Physics collision signals + ISignalOnCollision2D[] _ISignalOnCollision2DSystems; + ISignalOnCollisionEnter2D[] _ISignalOnCollisionEnter2DSystems; + ISignalOnCollisionExit2D[] _ISignalOnCollisionExit2DSystems; + + // 2D Physics trigger signals + ISignalOnTrigger2D[] _ISignalOnTrigger2DSystems; + ISignalOnTriggerEnter2D[] _ISignalOnTriggerEnter2DSystems; + ISignalOnTriggerExit2D[] _ISignalOnTriggerExit2DSystems; + + // 3D Physics collision signals + ISignalOnCollision3D[] _ISignalOnCollision3DSystems; + ISignalOnCollisionEnter3D[] _ISignalOnCollisionEnter3DSystems; + ISignalOnCollisionExit3D[] _ISignalOnCollisionExit3DSystems; + + // 3D Physics trigger signals + ISignalOnTrigger3D[] _ISignalOnTrigger3DSystems; + ISignalOnTriggerEnter3D[] _ISignalOnTriggerEnter3DSystems; + ISignalOnTriggerExit3D[] _ISignalOnTriggerExit3DSystems; + + ISignalOnNavMeshWaypointReached[] _ISignalOnNavMeshWaypointReachedSystems; + ISignalOnNavMeshSearchFailed[] _ISignalOnNavMeshSearchFailedSystems; + ISignalOnNavMeshMoveAgent[] _ISignalOnNavMeshMoveAgentSystems; + + ISignalOnMapChanged[] _ISignalOnMapChangedSystems; + ISignalOnEntityPrototypeMaterialized[] _ISignalOnEntityPrototypeMaterializedSystems; + + ISignalOnPlayerConnected[] _ISignalOnPlayerConnectedSystems; + ISignalOnPlayerDisconnected[] _ISignalOnPlayerDisconnectedSystems; + + /// + /// Access the global struct with generated values from the DSL. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public _globals_* Global { get { return _globals; } } + + /// + /// The randomization session started with the seed from the used to start the simulation with. + /// + /// Supports determinism under roll-backs. + /// If random is used in conjunction with the prediction area feature the session needs to be stored on the entities themselves. + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public RNGSession* RNG { get { return &_globals->RngSession; } } + + /// + /// Defines the amount of player in this Quantum session. + /// + /// The value is takes from the Deterministic session config. + public Int32 PlayerCount { get { return _sessionConfig.PlayerCount; } } + + public override NavMeshRegionMask* NavMeshRegionMask => &_globals->NavMeshRegions; + + public override FrameMetaData* FrameMetaData => &_globals->FrameMetaData; + + public override CommitCommandsModes CommitCommandsMode => SimulationConfig.Entities.CommitCommandsMode; + + /// + /// Access the signal API.\n + /// Signals are function signatures used as a decoupled inter-system communication API (a bit like a publisher/subscriber API or observer pattern). + /// + /// Custom signals are defined in the DSL. + public FrameSignals Signals; + + /// + /// Access the event API.\n + /// Events are a fine-grained solution to communicate things that happen inside the simulation to the rendering engine (they should never be used to modify/update part of the game state). + /// + /// Custom events are defined in the DSL. + public FrameEvents Events; + + /// + /// Access to the assets API + /// + public FrameAssets Assets; + + /// + /// The frame user context + /// + public new FrameContextUser Context { + get { return (FrameContextUser)base.Context; } + } + + /// + /// The used for this session. + /// + public RuntimeConfig RuntimeConfig { get { return _runtimeConfig; } internal set { _runtimeConfig = value; } } + + /// + /// The used for this session. + /// + public SimulationConfig SimulationConfig { get { return _simulationConfig; } internal set { _simulationConfig = value; } } + + /// + /// The used for this session. + /// + public DeterministicSessionConfig SessionConfig { get { return _sessionConfig; } internal set { _sessionConfig = value; } } + + /// + /// All systems running in the session. + /// + public SystemBase[] SystemsAll { get { return _systemsAll; } } + + /// + /// See . This getter acquires the value from the though. + /// + public override int UpdateRate { get { return _sessionConfig.UpdateFPS; } } + + /// + /// Globally access the physics settings which are taken from the during the Frame constructor. + /// + public sealed override PhysicsSceneSettings* PhysicsSceneSettings { get { return &_globals->PhysicsSettings; } } + + /// + /// Delta time in seconds. Can be set during run-time. + /// + public override FP DeltaTime { + get { return _globals->DeltaTime; } + set { _globals->DeltaTime = value; } + } + + /// + /// Retrieves the Quantum map asset. Can be set during run-time. + /// + /// If assigned value is different than the current one, signal is raised. + public sealed override Map Map { + get { return FindAsset(_globals->Map.Id); } + set { + AssetRefMap newValue = value; + var previousValue = _globals->Map; + if (previousValue.Id != newValue.Id) { + _globals->Map = newValue; + Signals.OnMapChanged(previousValue); + } + } + } + + public Frame(FrameContext context, SystemBase[] systemsAll, SystemBase[] systemsRoots, DeterministicSessionConfig sessionConfig, RuntimeConfig runtimeConfig, SimulationConfig simulationConfig, FP deltaTime) : base(context) { + Assert.Check(context != null); + + _systemsAll = systemsAll; + _systemsRoots = systemsRoots; + + _runtimeConfig = runtimeConfig; + _simulationConfig = simulationConfig; + _sessionConfig = sessionConfig; + + _playerData = new PersistentMap(); + + AllocGen(); + InitStatic(); + InitGen(); + + Assets = new FrameAssets(this); + Events = new FrameEvents(this); + Signals = new FrameSignals(this); + Unsafe = new FrameBaseUnsafe(this); + + Physics2D = new Physics2D.PhysicsEngine2D.Api(this, context.TaskContext.ThreadCount); + Physics3D = new Physics3D.PhysicsEngine3D.Api(this, context.TaskContext.ThreadCount); + + // player data set signal + _ISignalOnPlayerDataSet = BuildSignalsArray(); + + // 2D Physics collision signals + _ISignalOnCollision2DSystems = BuildSignalsArray(); + _ISignalOnCollisionEnter2DSystems = BuildSignalsArray(); + _ISignalOnCollisionExit2DSystems = BuildSignalsArray(); + + // 2D Physics trigger signals + _ISignalOnTrigger2DSystems = BuildSignalsArray(); + _ISignalOnTriggerEnter2DSystems = BuildSignalsArray(); + _ISignalOnTriggerExit2DSystems = BuildSignalsArray(); + + // 3D Physics collision signals + _ISignalOnCollision3DSystems = BuildSignalsArray(); + _ISignalOnCollisionEnter3DSystems = BuildSignalsArray(); + _ISignalOnCollisionExit3DSystems = BuildSignalsArray(); + + // 3D Physics trigger signals + _ISignalOnTrigger3DSystems = BuildSignalsArray(); + _ISignalOnTriggerEnter3DSystems = BuildSignalsArray(); + _ISignalOnTriggerExit3DSystems = BuildSignalsArray(); + + _ISignalOnNavMeshWaypointReachedSystems = BuildSignalsArray(); + _ISignalOnNavMeshSearchFailedSystems = BuildSignalsArray(); + _ISignalOnNavMeshMoveAgentSystems = BuildSignalsArray(); + + // map changed signal + _ISignalOnMapChangedSystems = BuildSignalsArray(); + + // prototype materialized signal + _ISignalOnEntityPrototypeMaterializedSystems = BuildSignalsArray(); + if ( _ISignalOnEntityPrototypeMaterializedSystems.Length > 0 ) { + base._SignalOnEntityPrototypeMaterialized = (entity, prototype) => Signals.OnEntityPrototypeMaterialized(entity, prototype); + } + + _ISignalOnPlayerConnectedSystems = BuildSignalsArray(); + _ISignalOnPlayerDisconnectedSystems = BuildSignalsArray(); + + // assign map, rng session, etc. + _globals->Map = FindAsset(runtimeConfig.Map.Id); + _globals->RngSession = new RNGSession(runtimeConfig.Seed); + _globals->DeltaTime = deltaTime; + + _systemIndexByType = new Dictionary(_systemsAll.Length); + + for (Int32 i = 0; i < _systemsAll.Length; ++i) { + var systemType = _systemsAll[i].GetType(); + + if (_systemIndexByType.ContainsKey(systemType) == false) { + _systemIndexByType.Add(systemType, i); + } + + // set default enabled systems + if (_systemsAll[i].StartEnabled) { + _globals->Systems.Set(_systemsAll[i].RuntimeIndex); + } + } + + // init physics settings + Quantum.PhysicsSceneSettings.Init(&_globals->PhysicsSettings, simulationConfig.Physics); + + // Init navmesh regions to all bit fields to be set + ClearAllNavMeshRegions(); + + // user callbacks + AllocUser(); + InitUser(); + } + + /// + /// Set the prediction area. + /// + /// Center of the prediction area + /// Radius of the prediction area + /// The Prediction Culling feature must be explicitly enabled in . + /// This can be safely called from the main-thread. + /// Prediction Culling allows developers to save CPU time in games where the player has only a partial view of the game scene. + /// Quantum prediction and rollbacks, which are time consuming, will only run for important entities that are visible to the local player(s). Leaving anything outside that area to be simulated only once per tick with no rollbacks as soon as the inputs are confirmed from server. + /// It is safe and simple to activate and, depending on the game, the performance difference can be quite large.Imagine a 30Hz game to constantly rollback ten ticks for every confirmed input (with more players, the predictor eventually misses at least for one of them). This requires the game simulation to be lightweight to be able to run at almost 300Hz(because of the rollbacks). With Prediction Culling enabled the full frames will be simulated at the expected 30Hz all the time while the much smaller prediction area is the only one running within the prediction buffer. + public void SetPredictionArea(FPVector3 position, FP radius) { + Context.SetPredictionArea(position, radius); + } + + /// + /// See . + /// + /// + /// + public void SetPredictionArea(FPVector2 position, FP radius) { + Context.SetPredictionArea(position.XOY, radius); + } + + /// + /// Test is a position is inside the prediction area. + /// + /// Position + /// True if the position is inside the prediction area. + public Boolean InPredictionArea(FPVector3 position) { + return Context.InPredictionArea(this, position); + } + + /// + /// See . + /// + /// + /// + public Boolean InPredictionArea(FPVector2 position) { + return Context.InPredictionArea(this, position); + } + + + /// + /// Serializes the frame using a temporary buffer (20MB). + /// + /// + /// + public override Byte[] Serialize(DeterministicFrameSerializeMode mode) { + return Serialize(mode, new byte[1024 * 1024 * 20], allocOutput: true).Array; + } + + /// + /// Serializes the frame using as a buffer for temporary data. + /// + /// If is set to false, then is also used for the final data - use offset and count from the result to access + /// the part of where serialized frame is stored. + /// + /// If is set to true then a new array is allocated for the result. + /// + /// Despite accepting a buffer, this method still allocates a few small temporary objects. + /// is also going + /// to allocate when serializing DynamicAssetDB, but how much depends on the serializer itself and the number of dynamic assets. + /// + /// + /// + /// + /// + /// Segment of where the serialized frame is stored + /// Do not serialize during GameStart callback because systems have not been initialized, yet. Rather use CallbackSimulateFinished to wait for the first update. + public ArraySegment Serialize(DeterministicFrameSerializeMode mode, byte[] buffer, int offset = 0, bool allocOutput = false) { + offset = ByteUtils.AddValueBlock((int)mode, buffer, offset); + offset = ByteUtils.AddValueBlock(Number, buffer, offset); + offset = ByteUtils.AddValueBlock(CalculateChecksum(false), buffer, offset); + + BitStream stream; + + { + offset = ByteUtils.BeginByteBlockHeader(buffer, offset, out var blockOffset); + + stream = new BitStream(buffer, buffer.Length - offset, offset) { + Writing = true + }; + + SerializeRuntimePlayers(stream); + + offset = ByteUtils.EndByteBlockHeader(buffer, blockOffset, stream.BytesRequired); + } + + { + offset = ByteUtils.BeginByteBlockHeader(buffer, offset, out var blockOffset); + + stream.SetBuffer(buffer, buffer.Length - offset, offset); + var serializer = new FrameSerializer(mode, this, stream) { + Writing = true + }; + + SerializeState(serializer); + + offset = ByteUtils.EndByteBlockHeader(buffer, blockOffset, stream.BytesRequired); + } + + _dynamicAssetDB.Serialize(Context.AssetSerializer, out var assetDBHeader, out var assetDBData); + + { + // write the header for the byte block but don't actually copy into the buffer + // - we'll do that later during the compression stage + offset = ByteUtils.BeginByteBlockHeader(buffer, offset, out var blockOffset); + ByteUtils.EndByteBlockHeader(buffer, blockOffset, assetDBHeader.Length + assetDBData.Length); + } + + using (var outputStream = allocOutput ? new MemoryStream() : new MemoryStream(buffer, offset, buffer.Length - offset)) { + using (var compressedOutput = ByteUtils.CreateGZipCompressStream(outputStream)) { + compressedOutput.Write(buffer, 0, offset); + compressedOutput.Write(assetDBHeader, 0, assetDBHeader.Length); + compressedOutput.Write(assetDBData, 0, assetDBData.Length); + } + + if (allocOutput) { + return new ArraySegment(outputStream.ToArray()); + } else { + return new ArraySegment(buffer, offset, (int)outputStream.Position); + } + + } + } + + public override void Deserialize(Byte[] data) { + var blocks = ByteUtils.ReadByteBlocks(ByteUtils.GZipDecompressBytes(data)).ToArray(); + + var mode = (DeterministicFrameSerializeMode)BitConverter.ToInt32(blocks[0], 0); + + Number = BitConverter.ToInt32(blocks[1], 0); + + var checksum = BitConverter.ToUInt64(blocks[2], 0); + + DeserializeRuntimePlayers(blocks[3]); + DeserializeDynamicAssetDB(blocks[5]); + + FrameSerializer serializer; + serializer = new FrameSerializer(mode, this, blocks[4]); + serializer.Reading = true; + + SerializeState(serializer); + + serializer.VerifyNoUnresolvedPointers(); + + if (CalculateChecksum(false) != checksum) { + throw new Exception($"Checksum of deserialized frame does not match checksum in the source data"); + } + } + + void SerializeState(FrameSerializer serializer) { + FrameBase.Serialize(this, serializer); + _globals_.Serialize(_globals, serializer); + SerializeEntitiesGen(serializer); + SerializeUser(serializer); + } + + Byte[] SerializeRuntimePlayers() { + BitStream stream; + stream = new BitStream(1024 * 10); + stream.Writing = true; + SerializeRuntimePlayers(stream); + return stream.ToArray(); + } + + void SerializeRuntimePlayers(BitStream stream) { + stream.WriteInt(_playerData.Count); + + foreach (var player in _playerData.Iterator()) { + stream.WriteInt(player.Key); + stream.WriteInt(player.Value.ActorId); + stream.WriteByteArrayLengthPrefixed(player.Value.Data); + player.Value.Player.Serialize(stream); + } + } + + void DeserializeRuntimePlayers(Byte[] bytes) { + BitStream stream; + stream = new BitStream(bytes); + stream.Reading = true; + + var count = stream.ReadInt(); + _playerData = new PersistentMap(); + for (Int32 i = 0; i < count; ++i) { + var player = stream.ReadInt(); + + RuntimePlayerData data; + data.ActorId = stream.ReadInt(); + data.Data = stream.ReadByteArrayLengthPrefixed(); + data.Player = new RuntimePlayer(); + data.Player.Serialize(stream); + + _playerData = _playerData.Add(player, data); + } + } + + void DeserializeDynamicAssetDB(Byte[] bytes) { + var assets = _dynamicAssetDB.Deserialize(bytes, Context.AssetSerializer); + + foreach (var asset in assets) { + asset.Loaded(_dynamicAssetDB, Context.Allocator); + } + } + + /// + /// Dump the frame in human readable form into a string. + /// + /// Frame representation + public sealed override String DumpFrame(int dumpFlags = 0) { + var printer = new FramePrinter(); + printer.Reset(this); + + printer.IsRawPrintEnabled = ((dumpFlags & DumpFlag_PrintRawValues) == DumpFlag_PrintRawValues); + + // frame info + if ((dumpFlags & DumpFlag_NoIsVerified) == DumpFlag_NoIsVerified) { + printer.AddLine($"#### FRAME DUMP FOR {Number} ####"); + } else { + printer.AddLine($"#### FRAME DUMP FOR {Number} IsVerified={IsVerified} ####"); + } + + if ((dumpFlags & DumpFlag_NoSimulationConfig) != DumpFlag_NoSimulationConfig) { + printer.AddLine(); + printer.AddObject("# " + nameof(SimulationConfig), SimulationConfig); + } + + if ((dumpFlags & DumpFlag_NoRuntimeConfig) != DumpFlag_NoRuntimeConfig) { + printer.AddLine(); + printer.AddObject("# " + nameof(RuntimeConfig), RuntimeConfig); + } + + if ((dumpFlags & DumpFlag_NoDeterministicSessionConfig) != DumpFlag_NoDeterministicSessionConfig) { + printer.AddLine(); + printer.AddObject("# " + nameof(SessionConfig), SessionConfig); + } + + if ((dumpFlags & DumpFlag_NoRuntimePlayers) != DumpFlag_NoRuntimePlayers) { + printer.AddLine(); + printer.AddLine("# PLAYERS"); + { + printer.ScopeBegin(); + foreach (var kv in _playerData.Iterator()) { + printer.AddObject($"[{kv.Key}]", kv.Value); + } + printer.ScopeEnd(); + } + } + + // globals state + printer.AddLine(); + printer.AddPointer("# GLOBALS", _globals); + + // print entities + printer.AddLine(); + printer.AddLine("# ENTITIES"); + Print(this, printer, (dumpFlags & DumpFlag_ComponentChecksums) == DumpFlag_ComponentChecksums); + + if ((dumpFlags & DumpFlag_AssetDBCheckums) == DumpFlag_AssetDBCheckums) { + printer.AddLine(); + printer.AddLine("# ASSETDB CHECKSUMS"); + { + printer.ScopeBegin(); + AssetObject[] assets = new AssetObject[1]; + + var orderedAssets = this.Context.AssetDB.FindAllAssets(true).OrderBy(x => x.Guid); + + foreach (var asset in orderedAssets) { + assets[0] = asset; + var bytes = this.Context.AssetSerializer.SerializeAssets(assets); + fixed (byte* p = bytes) { + var hash = CRC64.Calculate(0, p, bytes.Length); + printer.AddLine($"{asset.Identifier}: {hash}"); + } + } + printer.ScopeEnd(); + } + } + + if ((dumpFlags & DumpFlag_NoDynamicDB) != DumpFlag_NoDynamicDB) { + printer.AddLine(); + printer.AddLine("# DYNAMICDB"); + { + printer.ScopeBegin(); + + var assetSerializer = Context.AssetSerializer; + if ((dumpFlags & DumpFlag_ReadableDynamicDB) == DumpFlag_ReadableDynamicDB) { + printer.AddLine($"NextGuid: {_dynamicAssetDB.NextGuid}"); + foreach (var asset in _dynamicAssetDB.Assets) { + printer.AddLine($"{asset.GetType().FullName}:"); + printer.ScopeBegin(); + printer.AddLine($"{assetSerializer.PrintAsset(asset)}"); + printer.ScopeEnd(); + } + } else { + printer.AddLine("Dump: "); + printer.ScopeBegin(); + var data = _dynamicAssetDB.Serialize(assetSerializer); + fixed (byte* p = data) { + UnmanagedUtils.PrintBytesHex(p, data.Length, 32, printer); + } + printer.ScopeEnd(); + } + + printer.ScopeEnd(); + } + } + + // physics states + if (Physics2D != null) { + printer.AddLine(); + Physics2D.Print(printer); + } + + if (Physics3D != null) { + printer.AddLine(); + Physics3D.Print(printer); + } + + // heap state + if ((dumpFlags & DumpFlag_NoHeap) != DumpFlag_NoHeap) { + printer.AddLine(); + printer.AddLine("# HEAP"); + Allocator.Heap.Print(_heap, printer); + } + + // dump user data + var dump = printer.ToString(); + DumpFrameUser(ref dump); + + return dump; + } + + + /// + /// Calculates a checksum for the current game state. If the game is not started with + /// flag, this method is not thread-safe, i.e. calling it from multiple threads for frames from the same simulation is going to break. + /// + public sealed override UInt64 CalculateChecksum() { + return CalculateChecksum(Context.UseSharedChecksumSerializer); + } + + /// + /// Calculates a checksum for the current game state. + /// + /// True - use shared checksum serializer to avoid allocs (not thread-safe). + /// + public UInt64 CalculateChecksum(bool useSharedSerializer) { + FrameSerializer frameSerializer; + if (useSharedSerializer) { + frameSerializer = Context.SharedChecksumSerializer; + Assert.Check(frameSerializer != null); + } else { + frameSerializer = new FrameSerializer(DeterministicFrameSerializeMode.Serialize, this, new FrameChecksumerBitStream()); + } + return CalculateChecksumInternal(frameSerializer); + } + + internal UInt64 CalculateChecksumInternal(FrameSerializer serializer) { + + if (serializer == null) { + throw new ArgumentNullException(nameof(serializer)); + } + + if (serializer.Mode != DeterministicFrameSerializeMode.Serialize) { + throw new ArgumentException($"Serializer needs to be in {nameof(DeterministicFrameSerializeMode.Serialize)} mode", nameof(serializer)); + } + + if (serializer.Stream is FrameChecksumerBitStream checksumStream) { + + Profiling.HostProfiler.Start("CalculateChecksumInternal"); + + try { + serializer.Reset(); + serializer.Writing = true; + serializer.Frame = this; + + checksumStream.Checksum = (ulong)Number; + + // checksum globals + _globals_.Serialize(_globals, serializer); + + // checksum entity registry + FrameBase.Serialize(this, serializer); + + // checksum heap + return checksumStream.Checksum; + } finally { + serializer.Frame = null; + Profiling.HostProfiler.End(); + } + } else { + throw new InvalidOperationException($"Serializer's stream needs to be of {nameof(FrameChecksumerBitStream)} type (is: {serializer.Stream?.GetType().FullName}"); + } + } + + /// + /// Copies the complete frame memory. + /// + /// Input frame object + protected sealed override void Copy(DeterministicFrame frame) { + var f = (Frame)frame; + + // copy player data + _playerData = f._playerData; + + // copy heap from frame + Allocator.Heap.Copy(Context.Allocator, _heap, f._heap); + + // copy entity registry + FrameBase.Copy(this, f); + + // dynamic DB + _dynamicAssetDB.CopyFrom(f._dynamicAssetDB); + + // perform native copy + CopyFromGen(f); + CopyFromUser(f); + } + + public sealed override void Free() { + FreeUser(); + FreeGen(); + base.Free(); + } + + [Obsolete("Use SystemIsEnabledSelf instead.")] + public Boolean SystemIsEnabled() where T : SystemBase { + return SystemIsEnabledSelf(); + } + + [Obsolete("Use SystemIsEnabledSelf instead.")] + public Boolean SystemIsEnabled(Type t) { + return SystemIsEnabledSelf(t); + } + + /// + /// Test if a system is enabled. + /// + /// System type + /// True if the system is enabled + /// Logs an error if the system type is not found. + public Boolean SystemIsEnabledSelf() where T : SystemBase { + var system = FindSystem(); + if (system.Item0 == null) { + return false; + } + + return _globals->Systems.IsSet(system.Item1); + } + + public Boolean SystemIsEnabledSelf(Type t) { + var system = FindSystem(t); + if (system.Item0 == null) { + return false; + } + + return _globals->Systems.IsSet(system.Item1); + } + + public Boolean SystemIsEnabledSelf(SystemBase s) { + if (s == null) { + return false; + } + + return _globals->Systems.IsSet(s.RuntimeIndex); + } + + public Boolean SystemIsEnabledInHierarchy() where T : SystemBase { + var system = FindSystem(); + return SystemIsEnabledInHierarchy(system.Item0); + } + + public Boolean SystemIsEnabledInHierarchy(Type t) { + var system = FindSystem(t); + return SystemIsEnabledInHierarchy(system.Item0); + } + + public Boolean SystemIsEnabledInHierarchy(SystemBase system) { + if (system == null) + return false; + + if (_globals->Systems.IsSet(system.RuntimeIndex) == false) + return false; + if (system.ParentSystem == null) + return true; + + return SystemIsEnabledInHierarchy(system.ParentSystem); + } + + /// + /// Enable a system. + /// + /// System type + /// Logs an error if the system type is not found. + public void SystemEnable() where T : SystemBase { + SystemEnable(typeof(T)); + } + + public void SystemEnable(Type t) { + var system = FindSystem(t); + if (system.Item0 == null) { + return; + } + + if (_globals->Systems.IsSet(system.Item1) == false) { + // set flag + _globals->Systems.Set(system.Item1); + + // Fire callback only if it becomes enabled in hierarchy + if (system.Item0.ParentSystem == null || SystemIsEnabledInHierarchy(system.Item0.ParentSystem)) { + try { + system.Item0.OnEnabled(this); + } catch (Exception exn) { + Log.Exception(exn); + } + } + } + } + + /// + /// Disables a system. + /// + /// System type + /// Logs an error if the system type is not found. + /// + /// // test for a certain asset and disable the system during its OnInit method + /// public override void OnInit(Frame f) { + /// var testSettings = f.FindAsset(f.Map.UserAsset.Id); + /// if (testSettings == null) { + /// f.SystemDisable(); + /// return; + /// } + /// //.. + /// } + /// + public void SystemDisable() where T : SystemBase { + SystemDisable(typeof(T)); + } + + public void SystemDisable(T system) where T : SystemBase { + SystemDisable(system.GetType()); + } + + public void SystemDisable(Type t) { + var system = FindSystem(t); + if (system.Item0 == null) { + return; + } + + if (_globals->Systems.IsSet(system.Item1)) { + // clear flag + _globals->Systems.Clear(system.Item1); + + // Fire callback only if it was previously enabled in hierarchy + if (system.Item0.ParentSystem == null || SystemIsEnabledInHierarchy(system.Item0.ParentSystem)) { + try { + system.Item0.OnDisabled(this); + } catch (Exception exn) { + Log.Exception(exn); + } + } + } + } + + QTuple FindSystem() { + return FindSystem(typeof(T)); + } + + QTuple FindSystem(Type t) { + if (_systemIndexByType.TryGetValue(t, out var i)) { + return QTuple.Create(_systemsAll[i], i); + } + + Log.Error("System '{0}' not found, did you forget to add it to SystemSetup.CreateSystems ?", t.Name); + return new QTuple(null, -1); + } + + + T[] BuildSignalsArray() { + return _systemsAll.Where(x => x is T).Cast().ToArray(); + } + + void BuildSignalsArrayOnComponentAdded() where T : unmanaged, IComponent { + Assert.Check(ComponentTypeId.Id > 0); + + var array = _systemsAll.Where(x => x is ISignalOnComponentAdded).Cast>().ToArray(); + if (array.Length > 0) { + _ComponentSignalsOnAdded[ComponentTypeId.Id] = (entity, componentData) => { + var component = (T*)componentData; + var systems = &(_globals->Systems); + for (Int32 i = 0; i < array.Length; ++i) { + if (SystemIsEnabledInHierarchy((SystemBase)array[i])) { + array[i].OnAdded(this, entity, component); + } + } + }; + } else { + _ComponentSignalsOnAdded[ComponentTypeId.Id] = null; + } + } + + void BuildSignalsArrayOnComponentRemoved() where T : unmanaged, IComponent { + Assert.Check(ComponentTypeId.Id > 0); + + var array = _systemsAll.Where(x => x is ISignalOnComponentRemoved).Cast>().ToArray(); + if (array.Length > 0) { + _ComponentSignalsOnRemoved[ComponentTypeId.Id] = (entity, componentData) => { + var component = (T*)componentData; + var systems = &(_globals->Systems); + for (Int32 i = 0; i < array.Length; ++i) { + if (SystemIsEnabledInHierarchy((SystemBase)array[i])) { + array[i].OnRemoved(this, entity, component); + } + } + }; + } else { + _ComponentSignalsOnRemoved[ComponentTypeId.Id] = null; + } + } + + void AddEvent(EventBase evnt) { + // set evnt.Tick + evnt.Tick = Number; + + // add ast last + Context.Events.AddLast(evnt); + } + + public static void InitStatic() { + StaticDelegates.Init(); + InitStaticGen(); + } + + // partial declarations populated from code generator + static partial void InitStaticGen(); + partial void InitGen(); + partial void FreeGen(); + partial void AllocGen(); + partial void CopyFromGen(Frame frame); + partial void SerializeEntitiesGen(FrameSerializer serializer); + + partial void InitUser(); + partial void FreeUser(); + partial void AllocUser(); + partial void CopyFromUser(Frame frame); + + partial void SerializeUser(FrameSerializer serializer); + partial void DumpFrameUser(ref String dump); + + + /// + /// Gets the runtime player configuration data for a certain player. + /// + /// Player ref + /// Player config or null if player was not found + public RuntimePlayer GetPlayerData(PlayerRef player) { + RuntimePlayerData data; + + if (_playerData.TryFind(player, out data)) { + return data.Player; + } + + return null; + } + + /// + /// Converts a Quantum PlayerRef to an ActorId (Photon client id). + /// + /// Player reference + /// ActorId or null if payer was not found + public Int32? PlayerToActorId(PlayerRef player) { + RuntimePlayerData data; + + if (_playerData.TryFind(player, out data)) { + return data.ActorId; + } + + return null; + } + + /// + /// Returns the first player that is using a certain ActorId (Photon client id). + /// + /// Actor id + /// Player reference or null if actor id was not found + /// The first player because multiple players from the same Photon client can join. + public PlayerRef? ActorIdToFirstPlayer(Int32 actorId) { + foreach (var kvp in _playerData.Iterator()) { + if (kvp.Value.ActorId == actorId) { + return kvp.Key; + } + } + + return null; + } + + /// + /// Returns all players with a certain ActorId (Photon client id). + /// + /// Actor id + /// Array of player references + public PlayerRef[] ActorIdToAllPlayers(Int32 actorId) { + return _playerData.Iterator().Where(x => x.Value.ActorId == actorId).Select(x => (PlayerRef)x.Key).ToArray(); + } + + public void UpdatePlayerData() { + UInt64 set = 0; + + for (Int32 i = 0; i < PlayerCount; ++i) { + var rpc = GetRawRpc(i); + if (rpc != null && rpc.Length > 0) { + var flags = GetPlayerInputFlags(i); + if ((flags & DeterministicInputFlags.Command) != DeterministicInputFlags.Command) { + var playerDataOriginal = _playerData; + + try { + // create player data + RuntimePlayerData data; + data.Data = rpc; + data.ActorId = BitConverter.ToInt32(rpc, rpc.Length - 4); + data.Player = Quantum.RuntimePlayer.FromByteArray(rpc); + + // set data + _playerData = _playerData.AddOrSet(i, data); + + // set mask + set |= 1UL << FPMath.Clamp(i, 0, 63); +#if DEBUG + } catch (Exception e) { + Log.Error("## RuntimePlayer Deserialization Threw Exception ##"); + Log.Exception(e); +#else + } catch { +#endif + _playerData = playerDataOriginal; + } + } + } + } + + if (set != 0UL) { + for (Int32 i = 0; i < PlayerCount; ++i) { + var b = 1UL << i; + if ((set & b) == b) { + try { + Signals.OnPlayerDataSet(i); + } catch (Exception exn) { + Log.Exception(exn); + } + } + } + } + } + } +} + + +// Core/FrameAssets.cs +namespace Quantum { + partial class Frame { + public partial struct FrameAssets { + Frame _f; + + public FrameAssets(Frame f) { + _f = f; + } + + public EntityView View(string view, DatabaseType dbType = DatabaseType.Default) { + return _f.FindAsset(view, dbType); + } + + public EntityPrototype Prototype(string prototype, DatabaseType dbType = DatabaseType.Default) { + return _f.FindAsset(prototype, dbType); + } + + public EntityView View(AssetRefEntityView view) { + return _f.FindAsset(view.Id); + } + + public EntityPrototype Prototype(AssetRefEntityPrototype prototype) { + return _f.FindAsset(prototype.Id); + } + + public Map Map(AssetRefMap assetRef) { + return _f.FindAsset(assetRef.Id); + } + + public PhysicsMaterial PhysicsMaterial(AssetRefPhysicsMaterial assetRef) { + return _f.FindAsset(assetRef.Id); + } + + public PolygonCollider PolygonCollider(AssetRefPolygonCollider assetRef) { + return _f.FindAsset(assetRef.Id); + } + + public CharacterController3DConfig CharacterController3DConfig(AssetRefCharacterController3DConfig assetRef) { + return _f.FindAsset(assetRef.Id); + } + + public CharacterController2DConfig CharacterController2DConfig(AssetRefCharacterController2DConfig assetRef) { + return _f.FindAsset(assetRef.Id); + } + + public NavMesh NavMesh(AssetRefNavMesh assetRef) { + return _f.FindAsset(assetRef.Id); + } + + public NavMeshAgentConfig NavMeshAgentConfig(AssetRefNavMeshAgentConfig assetRef) { + return _f.FindAsset(assetRef.Id); + } + + public SimulationConfig SimulationConfig(AssetRefSimulationConfig assetRef) { + return _f.FindAsset(assetRef.Id); + } + + public TerrainCollider TerrainCollider(AssetRefTerrainCollider assetRef) { + return _f.FindAsset(assetRef.Id); + } + } + } +} + +// Core/FrameContextUser.cs +namespace Quantum { + public partial class FrameContextUser : Core.FrameContext { + public FrameContextUser(Args args) + : base(args) { + ConstructUser(args); + } + + public override sealed void Dispose() { + DisposeUser(); + base.Dispose(); + } + + partial void ConstructUser(Args args); + partial void DisposeUser(); + } +} + +// Core/FrameEvents.cs + +namespace Quantum { + partial class Frame { + public partial struct FrameEvents { + Frame _f; + + public FrameEvents(Frame f) { + _f = f; + } + } + } +} + + +// Core/FrameSignals.cs + +namespace Quantum { + public unsafe interface ISignalOnComponentAdded : ISignal where T : unmanaged, IComponent { + void OnAdded(Frame f, EntityRef entity, T* component); + } + + public unsafe interface ISignalOnComponentRemoved : ISignal where T : unmanaged, IComponent { + void OnRemoved(Frame f, EntityRef entity, T* component); + } + + public unsafe interface ISignalOnMapChanged : ISignal { + void OnMapChanged(Frame f, AssetRefMap previousMap); + } + + public unsafe interface ISignalOnEntityPrototypeMaterialized : ISignal { + void OnEntityPrototypeMaterialized(Frame f, EntityRef entity, EntityPrototypeRef prototypeRef); + } + + public unsafe interface ISignalOnPlayerConnected : ISignal { + void OnPlayerConnected(Frame f, PlayerRef player); + } + + public unsafe interface ISignalOnPlayerDisconnected : ISignal { + void OnPlayerDisconnected(Frame f, PlayerRef player); + } + + partial class Frame { + public unsafe partial struct FrameSignals { + Frame _f; + + public FrameSignals(Frame f) { + _f = f; + } + + public void OnPlayerDataSet(PlayerRef player) { + var array = _f._ISignalOnPlayerDataSet; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnPlayerDataSet(_f, player); + } + } + } + + public void OnMapChanged(AssetRefMap previousMap) { + var array = _f._ISignalOnMapChangedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnMapChanged(_f, previousMap); + } + } + } + + public void OnEntityPrototypeMaterialized(EntityRef entity, EntityPrototypeRef prototypeRef) { + var array = _f._ISignalOnEntityPrototypeMaterializedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnEntityPrototypeMaterialized(_f, entity, prototypeRef); + } + } + } + + public void OnPlayerConnected(PlayerRef player) { + var array = _f._ISignalOnPlayerConnectedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnPlayerConnected(_f, player); + } + } + } + + public void OnPlayerDisconnected(PlayerRef player) { + var array = _f._ISignalOnPlayerDisconnectedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnPlayerDisconnected(_f, player); + } + } + } + + public void OnNavMeshWaypointReached(EntityRef entity, FPVector3 waypoint, Navigation.WaypointFlag waypointFlags, ref bool resetAgent) { + var array = _f._ISignalOnNavMeshWaypointReachedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnNavMeshWaypointReached(_f, entity, waypoint, waypointFlags, ref resetAgent); + } + } + } + + public void OnNavMeshSearchFailed(EntityRef entity, ref bool resetAgent) { + var array = _f._ISignalOnNavMeshSearchFailedSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnNavMeshSearchFailed(_f, entity, ref resetAgent); + } + } + } + + public void OnNavMeshMoveAgent(EntityRef entity, FPVector2 desiredDirection) { + var array = _f._ISignalOnNavMeshMoveAgentSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnNavMeshMoveAgent(_f, entity, desiredDirection); + } + } + } + + public void OnCollision2D(CollisionInfo2D info) { + var array = _f._ISignalOnCollision2DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnCollision2D(_f, info); + } + } + } + + public void OnCollisionEnter2D(CollisionInfo2D info) { + var array = _f._ISignalOnCollisionEnter2DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnCollisionEnter2D(_f, info); + } + } + } + + public void OnCollisionExit2D(ExitInfo2D info) { + var array = _f._ISignalOnCollisionExit2DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnCollisionExit2D(_f, info); + } + } + } + + public void OnTrigger2D(TriggerInfo2D info) { + var array = _f._ISignalOnTrigger2DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnTrigger2D(_f, info); + } + } + } + + public void OnTriggerEnter2D(TriggerInfo2D info) { + var array = _f._ISignalOnTriggerEnter2DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnTriggerEnter2D(_f, info); + } + } + } + + public void OnTriggerExit2D(ExitInfo2D info) { + var array = _f._ISignalOnTriggerExit2DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnTriggerExit2D(_f, info); + } + } + } + + public void OnCollision3D(CollisionInfo3D info) { + var array = _f._ISignalOnCollision3DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnCollision3D(_f, info); + } + } + } + + public void OnCollisionEnter3D(CollisionInfo3D info) { + var array = _f._ISignalOnCollisionEnter3DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnCollisionEnter3D(_f, info); + } + } + } + + public void OnCollisionExit3D(ExitInfo3D info) { + var array = _f._ISignalOnCollisionExit3DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnCollisionExit3D(_f, info); + } + } + } + + public void OnTrigger3D(TriggerInfo3D info) { + var array = _f._ISignalOnTrigger3DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnTrigger3D(_f, info); + } + } + } + + public void OnTriggerEnter3D(TriggerInfo3D info) { + var array = _f._ISignalOnTriggerEnter3DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnTriggerEnter3D(_f, info); + } + } + } + + public void OnTriggerExit3D(ExitInfo3D info) { + var array = _f._ISignalOnTriggerExit3DSystems; + for (Int32 i = 0; i < array.Length; ++i) { + var s = array[i]; + if (_f.SystemIsEnabledInHierarchy((SystemBase)s)) { + s.OnTriggerExit3D(_f, info); + } + } + } + } + } +} + + +// Core/NavMeshSignals.cs + +namespace Quantum +{ + /// + /// Signal is fired when an agent reaches a waypoint. + /// + /// Requires enabled in . + /// \ingroup NavigationApi + public unsafe interface ISignalOnNavMeshWaypointReached : ISignal { + /// Current frame object + /// The entity the navmesh agent component belongs to + /// The current waypoint position + /// The current waypoint flags + /// If set to true the NavMeshPathfinder component will be cleared and stopped. Set to false if NavMeshPathfinder.SetTarget() was called inside the callback. + void OnNavMeshWaypointReached(Frame f, EntityRef entity, FPVector3 waypoint, Navigation.WaypointFlag waypointFlags, ref bool resetAgent); + } + + /// + /// Signal is fired when the agent could not find a path in the agent update after using + /// + /// Requires enabled in . + /// \ingroup NavigationApi + public unsafe interface ISignalOnNavMeshSearchFailed: ISignal { + /// Current frame object + /// The entity the navmesh agent component belongs to + /// Set this to true if the agent should reset its internal state (default is true). + void OnNavMeshSearchFailed(Frame f, EntityRef entity, ref bool resetAgent); + } + + /// + /// Signal is called when the agent should move. The desired direction is influence by avoidance. + /// + /// The agent velocity should be set in the callback. + /// \ingroup NavigationApi + public unsafe interface ISignalOnNavMeshMoveAgent: ISignal { + void OnNavMeshMoveAgent(Frame f, EntityRef entity, FPVector2 desiredDirection); + } +} + + +// Core/RecordingFlags.cs + +namespace Quantum { + [Flags] + public enum RecordingFlags { + None = 0, + Input = 1 << 0, + Checksums = 1 << 1, + Default = Input | Checksums, + All = 0xFF + } +} + +// Core/RuntimeConfig.cs + +namespace Quantum { + /// + /// In contrast to the , which has only static configuration data, the RuntimeConfig holds information that can be different from game to game. + /// + /// By default is defines for example what map to load and the random start seed. It is assembled from scratch each time starting a game. + /// Developers can add custom data to quantum_code/quantum.state/RuntimeConfig.User.cs (don't forget to fill out the serialization methods). + /// Like the this config is distributed to every other client after the first player connected and joined the Quantum plugin. + [Serializable] + public partial class RuntimeConfig { + /// Seed to initialize the randomization session under . + public Int32 Seed; + /// Asset reference of the Quantum map used with the upcoming game session. + public AssetRefMap Map; + /// Asset reference to the SimulationConfig used with the upcoming game session. + public AssetRefSimulationConfig SimulationConfig; + + /// + /// Serializing the members to be send to the server plugin and other players. + /// + /// Input output stream + public void Serialize(BitStream stream) { + stream.Serialize(ref Seed); + stream.Serialize(ref Map.Id.Value); + stream.Serialize(ref SimulationConfig.Id.Value); + SerializeUserData(stream); + } + + /// + /// Dump the content into a human readable form. + /// + /// String representation + public String Dump() { + String dump = ""; + DumpUserData(ref dump); + + StringBuilder sb = new StringBuilder(); + sb.Append(dump); + sb.Append("\n"); + sb.AppendLine("Seed: " + Seed); + sb.AppendLine($"Map.Guid: {Map.ToString()}"); + sb.AppendLine($"SimulationConfig.Guid: {SimulationConfig.ToString()} "); + + return sb.ToString(); + } + + partial void DumpUserData(ref String dump); + partial void SerializeUserData(BitStream stream); + + /// + /// Serialize the class into a byte array. + /// + /// Config to serialized + /// Byte array + public static Byte[] ToByteArray(RuntimeConfig config) { + BitStream stream; + + stream = new BitStream(new Byte[8192]); + stream.Writing = true; + + config.Serialize(stream); + + return stream.ToArray(); + } + + /// + /// Deserialize the class from a byte array. + /// + /// Config class in byte array form + /// New instance of the deserialized class + public static RuntimeConfig FromByteArray(Byte[] data) { + BitStream stream; + stream = new BitStream(data); + stream.Reading = true; + + RuntimeConfig config; + config = new RuntimeConfig(); + config.Serialize(stream); + + return config; + } + } +} + + +// Core/RuntimePlayer.cs + +namespace Quantum { + + public interface ISignalOnPlayerDataSet : ISignal { + void OnPlayerDataSet(Frame f, PlayerRef player); + } + + [Serializable] + public partial class RuntimePlayer { + public void Serialize(BitStream stream) { + SerializeUserData(stream); + } + + public String Dump() { + String dump = ""; + DumpUserData(ref dump); + return dump ?? ""; + } + + partial void DumpUserData(ref String dump); + partial void SerializeUserData(BitStream stream); + + public static Byte[] ToByteArray(RuntimePlayer player) { + BitStream stream; + + stream = new BitStream(new Byte[8192]); + stream.Writing = true; + + player.Serialize(stream); + + return stream.ToArray(); + } + + public static RuntimePlayer FromByteArray(Byte[] data) { + BitStream stream; + stream = new BitStream(data); + stream.Reading = true; + + RuntimePlayer player; + player = new RuntimePlayer(); + player.Serialize(stream); + + return player; + } + } +} + + +// Core/SimulationConfig.cs + +namespace Quantum { + /// + /// The SimulationConfig holds parameters used in the ECS layer and inside core systems like physics and navigation. + /// + [Serializable, AssetObjectConfig(GenerateLinkingScripts = true, GenerateAssetCreateMenu = false, GenerateAssetResetMethod = false)] + public partial class SimulationConfig : AssetObject { + public const long DEFAULT_ID = (long)DefaultAssetGuids.SimulationConfig; + + public enum AutoLoadSceneFromMapMode { + Disabled, + Legacy, + UnloadPreviousSceneThenLoad, + LoadThenUnloadPreviousScene + } + + /// + /// Global navmesh configurations. + /// + [Space] + public Navigation.Config Navigation; + /// + /// Global physics configurations. + /// + [Space] + public PhysicsCommon.Config Physics; + /// + /// Global entities configuration + /// + [Space] + public FrameBase.EntitiesConfig Entities; + /// + /// This option will trigger a Unity scene load during the Quantum start sequence.\n + /// This might be convenient to start with but once the starting sequence is customized disable it and implement the scene loading by yourself. + /// "Previous Scene" refers to a scene name in Quantum Map. + /// + [Tooltip("This option will trigger a Unity scene load during the Quantum start sequence.\nThis might be convenient to start with but once the starting sequence is customized disable it and implement the scene loading by yourself.\n\"Previous Scene\" refers to a scene name in Quantum Map.")] + public AutoLoadSceneFromMapMode AutoLoadSceneFromMap = AutoLoadSceneFromMapMode.UnloadPreviousSceneThenLoad; + /// + /// Configure how the client tracks the time to progress the Quantum simulation from the QuantumRunner class. + /// + [Tooltip("Configure how the client tracks the time to progress the Quantum simulation from the QuantumRunner class.")] + public SimulationUpdateTime DeltaTimeType = SimulationUpdateTime.Default; + /// + /// Define the max heap size for one page of memory the frame class uses for custom allocations like QList<> for example. + /// + /// 2^15 = 32.768 bytes + /// TotalHeapSizeInBytes = (1 << HeapPageShift) * HeapPageCount + [Tooltip("Define the max heap size for one page of memory the frame class uses for custom allocations like QList<> for example.\n\n2^15 = 32.768 bytes\nTotalHeapSizeInBytes = (1 << HeapPageShift) * HeapPageCount\n\nDefault is 15.")] + public int HeapPageShift = 15; + /// + /// Define the max heap page count for memory the frame class uses for custom allocations like QList<> for example. + /// + /// TotalHeapSizeInBytes = (1 << HeapPageShift) * HeapPageCount + [Tooltip("Define the max heap page count for memory the frame class uses for custom allocations like QList<> for example.\n\nTotalHeapSizeInBytes = (1 << HeapPageShift) * HeapPageCount\n\nDefault is 256.")] + public int HeapPageCount = 256; + /// + /// Sets extra heaps to allocate for a session in case you need to + /// create 'auxiliary' frames than actually required for the simulation itself + /// + [Tooltip("Sets extra heaps to allocate for a session in case you need to create 'auxiliary' frames than actually required for the simulation itself.\nDefault is 0.")] + public int HeapExtraCount = 0; + /// + /// Override the number of threads used internally. + /// + [Tooltip("Override the number of threads used internally.\nDefault is 2.")] + public int ThreadCount = 2; + /// + /// How long to store checksumed verified frames. The are used to generate a frame dump in case of a checksum error happening. Not used in Replay and Local mode. + /// + [Tooltip("How long to store checksumed verified frames.\nThe are used to generate a frame dump in case of a checksum error happening. Not used in Replay and Local mode.\nDefault is 3.")] + public FP ChecksumSnapshotHistoryLengthSeconds = 3; + /// + /// Additional options for checksum dumps, if the default settings don't provide a clear picture. + /// + [EnumFlags] + [Tooltip("Additional options for checksum dumps, if the default settings don't provide a clear picture. ")] + public SimulationConfigChecksumErrorDumpOptions ChecksumErrorDumpOptions; + } + + [Serializable, StructLayout(LayoutKind.Explicit)] + public unsafe struct AssetRefSimulationConfig : IEquatable { + public const int SIZE = sizeof(ulong); + + [FieldOffset(0)] + public AssetGuid Id; + + public static implicit operator AssetRefSimulationConfig(Map value) { + var r = default(AssetRefSimulationConfig); + if (value != null) { + r.Id = value.Guid; + } + return r; + } + + public static void Serialize(void* ptr, FrameSerializer serializer) { + var p = (AssetRefSimulationConfig*)ptr; + AssetGuid.Serialize(&p->Id, serializer); + } + + public override string ToString() { + return AssetRef.ToString(Id); + } + + public bool Equals(AssetRefSimulationConfig other) { + return Id.Equals(other.Id); + } + + public override bool Equals(object obj) { + return obj is AssetRefSimulationConfig other && Equals(other); + } + + public override int GetHashCode() { + return Id.GetHashCode(); + } + + public static bool operator ==(AssetRefSimulationConfig a, AssetRefSimulationConfig b) { + return a.Id == b.Id; + } + + public static bool operator !=(AssetRefSimulationConfig a, AssetRefSimulationConfig b) { + return a.Id != b.Id; + } + } + + public static class AssetRefSimulationConfigExt { + public static SimulationConfig FindAsset(this Core.FrameBase f, AssetRefSimulationConfig assetRef) { + return f.FindAsset(assetRef.Id); + } + } + + [Flags] + public enum SimulationConfigChecksumErrorDumpOptions { + SendAssetDBChecksums = 1 << 0, + ReadableDynamicDB = 1 << 1, + RawFPValues = 1 << 2, + ComponentChecksums = 1 << 3, + } + +} + + +// Core/SimulationUpdateTime.cs +namespace Quantum { + /// + /// The type of measuring time progressions to update the local simulation. + /// + /// Caveat: Changing it will make every client use the setting which might be undesirable when only used for debugging. + public enum SimulationUpdateTime { + /// + /// Internal stopwatch. Recommended for releasing games. + /// + Default, + /// + /// Engine (Unity) delta time. Extremely useful when pausing the Unity simulation during debugging for example. + /// + /// Caveat: the setting can cause issues with time synchronization when initializing online matches: the time tracking can be inaccurate under load (e.g.level loading) and result in a lot of large extra time syncs request and canceled inputs for a client when starting an online game. + EngineDeltaTime, + /// + /// Engine unscaled delta time. + /// + EngineUnscaledDeltaTime + } +} + +// Core/StaticDelegates.cs + +namespace Quantum { + public static unsafe partial class StaticDelegates { + internal struct Tag {} + + public static void Init() { + CallOnce.Invoke(() => InitGen()); + } + + static partial void InitGen(); + } +} + +// Core/TypeRegistry.cs + +namespace Quantum { + public partial class TypeRegistry { + readonly Dictionary _types = new Dictionary(); + + public ReadOnlyDictionary Types { + get { + return new ReadOnlyDictionary(_types); + } + } + + public TypeRegistry() { + AddBuiltIns(); + AddGenerated(); + } + + void Register(Type type, int size) { + if (_types.ContainsKey(type)) { + return; + } + + _types.Add(type, size); + } + + void AddBuiltIns() { + Register(typeof(EntityRef), EntityRef.SIZE); + Register(typeof(ComponentReference), ComponentReference.SIZE); + Register(typeof(AssetRef), AssetRef.SIZE); + Register(typeof(Shape2DType), 1); + Register(typeof(Shape3DType), 1); + Register(typeof(Shape2D), Shape2D.SIZE); + Register(typeof(Shape2D.BoxShape), Shape2D.BoxShape.SIZE); + Register(typeof(Shape2D.CircleShape), Shape2D.CircleShape.SIZE); + Register(typeof(Shape2D.PolygonShape), Shape2D.PolygonShape.SIZE); + Register(typeof(Shape2D.EdgeShape), Shape2D.EdgeShape.SIZE); + Register(typeof(Shape2D.CompoundShape2D), Shape2D.CompoundShape2D.SIZE); + Register(typeof(Shape3D), Shape3D.SIZE); + Register(typeof(Shape3D.BoxShape), Shape3D.BoxShape.SIZE); + Register(typeof(Shape3D.SphereShape), Shape3D.SphereShape.SIZE); + Register(typeof(Shape3D.MeshShape), Shape3D.MeshShape.SIZE); + Register(typeof(Shape3D.TerrainShape), Shape3D.TerrainShape.SIZE); + Register(typeof(Shape3D.CompoundShape3D), Shape3D.CompoundShape3D.SIZE); + Register(typeof(Physics2D.Joint), Physics2D.Joint.SIZE); + Register(typeof(Physics2D.JointType), sizeof(short)); + Register(typeof(Physics2D.SpringJoint), Physics2D.SpringJoint.SIZE); + Register(typeof(Physics2D.DistanceJoint), Physics2D.DistanceJoint.SIZE); + Register(typeof(Physics2D.HingeJoint), Physics2D.HingeJoint.SIZE); + Register(typeof(Physics3D.Joint3D), Physics3D.Joint3D.SIZE); + Register(typeof(Physics3D.JointType3D), sizeof(short)); + Register(typeof(Physics3D.SpringJoint3D), Physics3D.SpringJoint3D.SIZE); + Register(typeof(Physics3D.DistanceJoint3D), Physics3D.DistanceJoint3D.SIZE); + Register(typeof(Physics3D.HingeJoint3D), Physics3D.HingeJoint3D.SIZE); + Register(typeof(QBoolean), QBoolean.SIZE); + + // register internal heap types - heap struct itself does not need to be registered + Allocator.Heap.RegisterInternalTypes(Register); + + // register internal physics types + PhysicsCommon.RegisterInternalTypes(Register); + Physics2D.PhysicsEngine2D.RegisterInternalTypes(Register); + Physics3D.PhysicsEngine3D.RegisterInternalTypes(Register); + + // register collection memory integrity checks + Collections.QCollectionsUtils.RegisterTypes(Register); + } + + partial void AddGenerated(); + } +} + +// Game/CallbackDispatcher.cs + +namespace Quantum { + public class CallbackDispatcher : DispatcherBase, Quantum.ICallbackDispatcher { + + protected static Dictionary GetBuiltInTypes() { + return new Dictionary() { + { typeof(CallbackChecksumComputed), CallbackChecksumComputed.ID }, + { typeof(CallbackChecksumError), CallbackChecksumError.ID }, + { typeof(CallbackChecksumErrorFrameDump), CallbackChecksumErrorFrameDump.ID }, + { typeof(CallbackEventCanceled), CallbackEventCanceled.ID }, + { typeof(CallbackEventConfirmed), CallbackEventConfirmed.ID }, + { typeof(CallbackGameDestroyed), CallbackGameDestroyed.ID }, + { typeof(CallbackGameStarted), CallbackGameStarted.ID }, + { typeof(CallbackGameResynced), CallbackGameResynced.ID }, + { typeof(CallbackInputConfirmed), CallbackInputConfirmed.ID }, + { typeof(CallbackPollInput), CallbackPollInput.ID }, + { typeof(CallbackSimulateFinished), CallbackSimulateFinished.ID }, + { typeof(CallbackUpdateView), CallbackUpdateView.ID }, + { typeof(CallbackPluginDisconnect), CallbackPluginDisconnect.ID }, + }; + } + + public CallbackDispatcher() : base(GetBuiltInTypes()) { } + protected CallbackDispatcher(Dictionary callbackTypes) : base(callbackTypes) { } + + public bool Publish(CallbackBase e) { + return base.InvokeMeta(e.ID, e); + } + } +} + + +// Game/ChecksumErrorFrameDumpContext.cs + +namespace Quantum { + public unsafe partial class ChecksumErrorFrameDumpContext { + public SimulationConfig SimulationConfig; + public QTuple[] AssetDBChecksums; + + private ChecksumErrorFrameDumpContext() {} + + public ChecksumErrorFrameDumpContext(QuantumGame game, Frame frame) { + var options = game.Configurations.Simulation.ChecksumErrorDumpOptions; + + SimulationConfig = game.Configurations.Simulation; + + // write checksums + if ((options & SimulationConfigChecksumErrorDumpOptions.SendAssetDBChecksums) == SimulationConfigChecksumErrorDumpOptions.SendAssetDBChecksums) { + + var assets = frame.Context.AssetDB.FindAllAssets(true).ToList(); + AssetDBChecksums = new QTuple[assets.Count]; + assets.Sort((a, b) => a.Guid.CompareTo(b.Guid)); + + AssetObject[] temp = new AssetObject[1]; + for (int i = 0; i < assets.Count; ++i) { + temp[0] = assets[i]; + var bytes = frame.Context.AssetSerializer.SerializeAssets(temp); + fixed (byte* p = bytes) { + var crc = CRC64.Calculate(0, p, bytes.Length); + AssetDBChecksums[i] = QTuple.Create(assets[i].Guid, crc); + } + } + } + + ConstructUser(game, frame); + } + + partial void ConstructUser(QuantumGame game, Frame frame); + partial void SerializeUser(QuantumGame game, BinaryWriter writer); + partial void DeserializeUser(QuantumGame game, BinaryReader reader); + + public void Serialize(QuantumGame game, BinaryWriter writer) { + writer.Write(AssetDBChecksums?.Length ?? 0); + if (AssetDBChecksums != null) { + foreach (var asset in AssetDBChecksums) { + writer.Write(asset.Item0.Value); + writer.Write(asset.Item1); + } + } + + // write simulation config + if (SimulationConfig != null) { + var simConfigBytes = game.AssetSerializer.SerializeAssets(new[] { SimulationConfig }); + writer.Write(simConfigBytes.Length); + writer.Write(simConfigBytes); + } else { + writer.Write(0); + } + + SerializeUser(game, writer); + } + + public static ChecksumErrorFrameDumpContext Deserialize(QuantumGame game, BinaryReader reader) { + var result = new ChecksumErrorFrameDumpContext(); + + // read checksums + { + int count = reader.ReadInt32(); + if (count > 0) { + result.AssetDBChecksums = new QTuple[count]; + + for (int i = 0; i < count; ++i) { + var guidRaw = reader.ReadInt64(); + var crc64 = reader.ReadUInt64(); + result.AssetDBChecksums[i] = QTuple.Create(new AssetGuid(guidRaw), crc64); + } + } + } + + // read sim config + { + int count = reader.ReadInt32(); + if (count > 0) { + var configBytes = reader.ReadBytes(count); + result.SimulationConfig = (SimulationConfig)game.AssetSerializer.DeserializeAssets(configBytes).Single(); + } + } + + result.DeserializeUser(game, reader); + + return result; + } + } +} + + +// Game/EventDispatcher.cs + +namespace Quantum { + public class EventDispatcher : DispatcherBase, IEventDispatcher { + + private static Dictionary GetEventTypes() { + var result = new Dictionary { + { typeof(EventBase), 0 } + }; + + for (int eventID = 0; eventID < Frame.FrameEvents.EVENT_TYPE_COUNT; ++eventID) { + result.Add(Frame.FrameEvents.GetEventType(eventID), eventID + 1); + } + + return result; + } + + public EventDispatcher() : base(GetEventTypes()) { } + + public unsafe bool Publish(EventBase e) { + + int eventDepth = 0; + for (int id = e.Id; id >= 0; id = Frame.FrameEvents.GetParentEventID(id)) { + ++eventDepth; + } + + int* eventIdStack = stackalloc int[eventDepth]; + for (int id = e.Id, i = 0; id >= 0; id = Frame.FrameEvents.GetParentEventID(id), i++) { + eventIdStack[i] = id; + } + + bool hadActiveHandlers = false; + + // start with the EventBase + int metaIndex = 0; + + for (; ; ) { + hadActiveHandlers |= base.InvokeMeta(metaIndex, e); + + if (--eventDepth >= 0) { + // choose next event + metaIndex = eventIdStack[eventDepth] + 1; + } else { + break; + } + } + + return hadActiveHandlers; + } + } +} + + +// Game/InstantReplaySettings.cs + +namespace Quantum { + [Serializable] + public struct InstantReplaySettings { + public int SnapshotsPerSecond; + public FP LenghtSeconds; + + public static InstantReplaySettings Default => new InstantReplaySettings() { + LenghtSeconds = 3, + SnapshotsPerSecond = 1, + }; + + public static InstantReplaySettings FromLength(FP length, int snapshotsPerSecond) { + return new InstantReplaySettings() { + LenghtSeconds = length, + SnapshotsPerSecond = snapshotsPerSecond + }; + } + + public override string ToString() { + return $"({nameof(SnapshotsPerSecond)}: {SnapshotsPerSecond}, {nameof(LenghtSeconds)}: {LenghtSeconds})"; + } + } +} + + +// Game/QuantumGame.Snapshots.cs + +namespace Quantum { + public partial class QuantumGame { + + DeterministicFrameRingBuffer _checksumSnapshotBuffer; + DeterministicFrameRingBuffer _instantReplaySnapshotBuffer; + + bool _instantReplaySnapshotsRecording; + Int32 _commonSnapshotInterval; + Int32 _instantReplaySnapshotInterval; + + void SnapshotsOnDestroy() { + _checksumSnapshotBuffer?.Clear(); + _checksumSnapshotBuffer = null; + _instantReplaySnapshotBuffer?.Clear(); + _instantReplaySnapshotBuffer = null; + } + + void SnapshotsOnSimulateFinished(DeterministicFrame state) { + + if (!state.IsVerified) { + return; + } + + HostProfiler.Start("QuantumGame.RecordingSnapshots"); + + if (_checksumSnapshotBuffer != null) { + // in case replay interval is less than checksum interval and replay is not being recorded, + // there's no need to sample at a common rate + Int32 interval; + if (_commonSnapshotInterval > 0 && _instantReplaySnapshotsRecording) { + Assert.Check(_instantReplaySnapshotBuffer == null); + interval = _commonSnapshotInterval; + } else { + interval = Session.SessionConfig.ChecksumInterval; + } + + if ((state.Number % interval) == 0) { + _checksumSnapshotBuffer.PushBack(state, this, _context); + } + } + + if (_instantReplaySnapshotsRecording && _instantReplaySnapshotBuffer != null) { + Assert.Check(_commonSnapshotInterval <= 0); + if (_instantReplaySnapshotBuffer.Count == 0 || (state.Number % _instantReplaySnapshotInterval) == 0) { + _instantReplaySnapshotBuffer.PushBack(state, this, _context); + } + } + + HostProfiler.End(); + } + + Int32 SnapshotsCreateBuffers(Int32 simulationRate, Int32 checksumInterval, FP checksumTimeWindow, Int32 replayInterval, FP replayTimeWindow) { + + var checksumFrameWindow = FPMath.CeilToInt(simulationRate * checksumTimeWindow); + var replayFrameWindow = FPMath.CeilToInt(simulationRate * replayTimeWindow); + + var checksumBufferSize = DeterministicFrameRingBuffer.GetSize(FPMath.CeilToInt(simulationRate * checksumTimeWindow), checksumInterval); + var replayBufferSize = DeterministicFrameRingBuffer.GetSize(FPMath.CeilToInt(simulationRate * replayTimeWindow), replayInterval); + + if (checksumInterval > 0 && checksumBufferSize > 0 && replayBufferSize > 0 && replayInterval > 0) { + if (DeterministicFrameRingBuffer.TryGetCommonSamplingPattern(checksumFrameWindow, checksumInterval, replayFrameWindow, replayInterval, out var commonWindow, out var commonInterval)) { + _commonSnapshotInterval = commonInterval; + _checksumSnapshotBuffer = new DeterministicFrameRingBuffer(DeterministicFrameRingBuffer.GetSize(commonWindow, commonInterval)); + Log.Trace($"Snapshots: common buffer created with interval: {_commonSnapshotInterval}, window: {commonWindow}, capacity: {_checksumSnapshotBuffer.Capacity}"); + return _checksumSnapshotBuffer.Capacity; + } else { + // shared buffer not possible + Log.Warn($"Unable to create a shared buffer for checksumed frames and replay snapshots. This is not optimal. Check the documentation for details."); + } + } + + if (checksumBufferSize > 0) { + _checksumSnapshotBuffer = new DeterministicFrameRingBuffer(checksumBufferSize); + Log.Trace($"Snapshots: checksum buffer created with interval {checksumInterval}, capacity: {checksumBufferSize}"); + } + + if (replayBufferSize > 0) { + _instantReplaySnapshotInterval = replayInterval; + _instantReplaySnapshotBuffer = new DeterministicFrameRingBuffer(replayBufferSize); + Log.Trace($"Snapshots: replay buffer created with interval {replayInterval}, capacity: {replayBufferSize}"); + } + + return checksumBufferSize + replayBufferSize; + } + + static Int32 SnapshotsGetMinBufferSize(Int32 window, Int32 samplingRate) { + return samplingRate <= 0 ? 0 : (1 + window / samplingRate); + } + } +} + +// Game/QuantumGame.cs + +namespace Quantum { + /// + /// This class contains values for flags that will be accessible with . + /// Built-in flags control some aspects of QuantumGame inner workings, without affecting the simulation + /// outcome. + /// + public partial class QuantumGameFlags { + /// + /// Starts the game in the server mode. + /// When this flag is not set, all the events marked with "server" get culled immediatelly. + /// If this flag is set, all the events marked with "client" get culled immediatelly. + /// + public const int Server = 1 << 0; + /// + /// By default, QuantumGame uses a single shared checksum serializer to reduce allocations. + /// The serializer is *not* static - it is only shared between frames comming from the same QuantumGame. + /// Set this flag if you want to disable this behaviour, for example if you calculate + /// checksums for multiple frames using multiple threads. + /// + public const int DisableSharedChecksumSerializer = 1 << 1; + /// + /// Custom user flags start from this value. Flags are accessible with . + /// + public const int CustomFlagsStart = 1 << 16; + } + + /// + /// QuantumGame acts as an interface to the simulation from the client code's perspective. + /// + /// Access and method to this class is always safe from the clients point of view. + public unsafe partial class QuantumGame : IDeterministicGame { + public event Action ProfilerSampleGenerated; + + public struct StartParameters { + public IResourceManager ResourceManager; + public IAssetSerializer AssetSerializer; + public ICallbackDispatcher CallbackDispatcher; + public IEventDispatcher EventDispatcher; + public InstantReplaySettings InstantReplaySettings; + public int HeapExtraCount; + public DynamicAssetDB InitialDynamicAssets; + public int GameFlags; + } + + + /// + /// Stores the different frames the simulation uses during one tick. + /// + public class FramesContainer { + public Frame Verified; + public Frame Predicted; + public Frame PredictedPrevious; + public Frame PreviousUpdatePredicted; + } + + // Caveat: Only set after the first CreateFrame() call + public class ConfigurationsContainer { + public RuntimeConfig Runtime; + public SimulationConfig Simulation; + } + + /// Access the frames of various times available during one tick. + public FramesContainer Frames { get; } + + /// Access the configurations that the simulation is running with. + public ConfigurationsContainer Configurations { get; } + + /// Access the Deterministic session object to query more internals. + public DeterministicSession Session { get; private set; } + + /// Used for position interpolation on the client for smoother interpolation results. + public Single InterpolationFactor { get; private set; } + + /// + public InstantReplaySettings InstantReplayConfig { get; private set; } + + /// + public IAssetSerializer AssetSerializer { get; } + + /// Extra heaps to allocate for a session in case you need to create 'auxiliary' frames than actually required for the simulation itself. + public int HeapExtraCount { get; } + + + Byte[] _inputStreamReadZeroArray; + IResourceManager _resourceManager; + ICallbackDispatcher _callbackDispatcher; + IEventDispatcher _eventDispatcher; + + FrameSerializer _inputSerializerRead; + FrameSerializer _inputSerializerWrite; + + SystemBase[] _systemsRoot; + SystemBase[] _systemsAll; + + FrameContext _context; + TypeRegistry _typeRegistry; + bool _polledInputInThisSimulation; + DynamicAssetDB _initialDynamicAssets; + int _flags; + + public QuantumGame(in StartParameters startParams) { + _typeRegistry = new TypeRegistry(); + Frames = new FramesContainer(); + Configurations = new ConfigurationsContainer(); + + _resourceManager = startParams.ResourceManager; + AssetSerializer = startParams.AssetSerializer; + _callbackDispatcher = startParams.CallbackDispatcher; + _eventDispatcher = startParams.EventDispatcher; + InstantReplayConfig = startParams.InstantReplaySettings; + HeapExtraCount = startParams.HeapExtraCount; + _flags = startParams.GameFlags; + + if (startParams.InitialDynamicAssets != null) { + _initialDynamicAssets = new DynamicAssetDB(); + _initialDynamicAssets.CopyFrom(startParams.InitialDynamicAssets); + } + + InitCallbacks(); + } + + [Obsolete] + public QuantumGame(IResourceManager manager, IAssetSerializer assetSerializer, ICallbackDispatcher callbackDispatcher, IEventDispatcher eventDispatcher) + : this(new StartParameters() { + ResourceManager = manager, + AssetSerializer = assetSerializer, + CallbackDispatcher = callbackDispatcher, + EventDispatcher = eventDispatcher, + }) { } + + /// + /// Returns an array that is unique on every client and represents the indexes for players that your local machine controls in the Quantum simulation. + /// + /// Array of player indices + public Int32[] GetLocalPlayers() { + return Session.LocalPlayerIndices; + } + + /// + /// Helps to decide if a PlayerRef is associated with the local player. + /// + /// Player reference + /// True if the player is the local player + public Boolean PlayerIsLocal(PlayerRef playerRef) { + if (playerRef == PlayerRef.None) { + return false; + } + + for (Int32 i = 0; i < Session.LocalPlayerIndices.Length; i++) { + if (Session.LocalPlayerIndices[i] == playerRef) { + return true; + } + } + + return false; + } + + /// + /// Sends a command to the server. + /// + /// Command to send + /// Commands are similar to input, they drive the simulation, but do not have to be sent regularly. + /// + /// RemoveUnitCommand command = new RemoveUnitCommand(); + /// command.CellIndex = 42; + /// QuantumRunner.Default.Game.SendCommand(command); + /// + public void SendCommand(DeterministicCommand command) { + var players = GetLocalPlayers(); + if (players.Length > 0) { + Session.SendCommand(players[0], command); + } else { + Log.Error("No local player found to send command for"); + } + } + + /// + /// Sends a command to the server. + /// + /// Specify the player index (PlayerRef) when you have multiple players controlled from the same machine. + /// Command to send + /// See + /// Games that only have one local player can ignore the player index field. + public void SendCommand(Int32 player, DeterministicCommand command) { + Session.SendCommand(player, command); + } + + /// + /// Send data for the local player to join the online match. + /// If the client has multiple local players, the data will be sent for the first of them (smallest player index). + /// + /// Player data + /// After starting, joining the Quantum Game and after the OnGameStart signal has been fired each player needs to call the SendPlayerData method to be added as a player in every ones simulation.\n + /// The reason this needs to be called explicitly is that it greatly simplifies late-joining players. + public void SendPlayerData(RuntimePlayer data) { + var players = GetLocalPlayers(); + if (players.Length > 0) { + Session.SetPlayerData(players[0], RuntimePlayer.ToByteArray(data)); + } else { + Log.Error("No local player found to send player data for."); + } + } + + /// + /// Send data for one local player to join the online match. + /// + /// Local player index + /// Player data + /// After starting, joining the Quantum Game and after the OnGameStart signal has been fired each player needs to call the SendPlayerData method to be added as a player in every ones simulation.\n + /// The reason this needs to be called explicitly is that it greatly simplifies late-joining players. + public void SendPlayerData(Int32 player, RuntimePlayer data) { + Session.SetPlayerData(player, RuntimePlayer.ToByteArray(data)); + } + + /// + /// + /// + public int GameFlags => _flags; + + public void OnDestroy() { + SnapshotsOnDestroy(); + InvokeOnDestroy(); + } + + public Frame CreateFrame() { + return (Frame)((IDeterministicGame)this).CreateFrame(_context); + } + + DeterministicFrame IDeterministicGame.CreateFrame(IDisposable context) { + return new Frame((FrameContextUser)context, _systemsAll, _systemsRoot, Session.SessionConfig, Configurations.Runtime, Configurations.Simulation, Session.DeltaTime); + } + + DeterministicFrame IDeterministicGame.CreateFrame(IDisposable context, Byte[] data) { + Frame f = CreateFrame(); + f.Deserialize(data); + return f; + } + + public DeterministicFrame GetVerifiedFrame(int tick) { + if (_checksumSnapshotBuffer != null) { + var result = _checksumSnapshotBuffer.Find(tick, DeterministicFrameSnapshotBufferFindMode.Equal); + if (result == null) { + Log.Warn($"Unable to find verified frame for tick {tick}, increase {nameof(DeterministicSessionConfig.ChecksumInterval)} or increase {nameof(SimulationConfig.ChecksumSnapshotHistoryLengthSeconds)}."); + } + return result; + } + return null; + } + + public IDisposable CreateFrameContext() { + if (_context == null) { + Assert.Check(_systemsAll == null); + Assert.Check(_systemsRoot == null); + + // create asset database + var assetDB = _resourceManager.CreateAssetDatabase(); + + // de-serialize runtime config, session is the one from the server + Configurations.Runtime = RuntimeConfig.FromByteArray(Session.RuntimeConfig); + Configurations.Simulation = assetDB.FindAsset(Configurations.Runtime.SimulationConfig.Id, true); + + // register commands + Session.CommandSerializer.RegisterFactories(DeterministicCommandSetup.GetCommandFactories(Configurations.Runtime, Configurations.Simulation)); + + // initialize systems + _systemsRoot = SystemSetup.CreateSystems(Configurations.Runtime, Configurations.Simulation).Where(x => x != null).ToArray(); + _systemsAll = _systemsRoot.SelectMany(x => x.Hierarchy).ToArray(); + + Int32 heapCount = 4; + heapCount += Math.Max(0, Configurations.Simulation.HeapExtraCount); + heapCount += Math.Max(0, HeapExtraCount); + heapCount += SnapshotsCreateBuffers(Session.SessionConfig.UpdateFPS, + Session.IsOnline ? Session.SessionConfig.ChecksumInterval : 0, Configurations.Simulation.ChecksumSnapshotHistoryLengthSeconds, + InstantReplayConfig.SnapshotsPerSecond == 0 ? 0 : Session.SessionConfig.UpdateFPS / InstantReplayConfig.SnapshotsPerSecond, InstantReplayConfig.LenghtSeconds); + + // set system runtime indices + for (Int32 i = 0; i < _systemsAll.Length; ++i) { + _systemsAll[i].RuntimeIndex = i; + } + + // set core count override + Session.PlatformInfo.CoreCount = Configurations.Simulation.ThreadCount; + + FrameContext.Args args; + args.AssetDatabase = assetDB; + args.PlatformInfo = Session.PlatformInfo; + args.IsServer = (_flags & QuantumGameFlags.Server) == QuantumGameFlags.Server; + args.IsLocalPlayer = Session.IsLocalPlayer; + args.HeapConfig = new Heap.Config(Configurations.Simulation.HeapPageShift, Configurations.Simulation.HeapPageCount, heapCount); + args.PhysicsConfig = Configurations.Simulation.Physics; + args.NavigationConfig = Configurations.Simulation.Navigation; + args.CommandSerializer = Session.CommandSerializer; + args.AssetSerializer = AssetSerializer; + args.InitialDynamicAssets = _initialDynamicAssets; + args.UseSharedChecksumSerialized = (_flags & QuantumGameFlags.DisableSharedChecksumSerializer) != QuantumGameFlags.DisableSharedChecksumSerializer; + + // toggle various parts of the context code + args.UsePhysics2D = _systemsAll.FirstOrDefault(x => x is PhysicsSystem2D) != null; + args.UsePhysics3D = _systemsAll.FirstOrDefault(x => x is PhysicsSystem3D) != null; + args.UseNavigation = _systemsAll.FirstOrDefault(x => x is NavigationSystem) != null; + args.UseCullingArea = _systemsAll.FirstOrDefault(x => x is CullingSystem2D) != null || _systemsAll.FirstOrDefault(x => x is CullingSystem3D) != null; + + // create frame context + _context = new FrameContextUser(args); + } + + return _context; + } + + /// + /// Set the prediction area. + /// + /// Center of the prediction area + /// Radius of the prediction area + /// The Prediction Culling feature must be explicitly enabled in . + /// This can be safely called from the main-thread. + /// Prediction Culling allows developers to save CPU time in games where the player has only a partial view of the game scene. + /// Quantum prediction and rollbacks, which are time consuming, will only run for important entities that are visible to the local player(s). Leaving anything outside that area to be simulated only once per tick with no rollbacks as soon as the inputs are confirmed from server. + /// It is safe and simple to activate and, depending on the game, the performance difference can be quite large.Imagine a 30Hz game to constantly rollback ten ticks for every confirmed input (with more players, the predictor eventually misses at least for one of them). This requires the game simulation to be lightweight to be able to run at almost 300Hz(because of the rollbacks). With Prediction Culling enabled the full frames will be simulated at the expected 30Hz all the time while the much smaller prediction area is the only one running within the prediction buffer. + public void SetPredictionArea(FPVector3 position, FP radius) { + _context.SetPredictionArea(position, radius); + } + + /// + /// See . + /// + /// + /// + public void SetPredictionArea(FPVector2 position, FP radius) { + _context.SetPredictionArea(position.XOY, radius); + } + + public void OnGameEnded() { + InvokeOnGameEnded(); + } + + public void OnGameStart(DeterministicFrame f) { + // init event invoker + InitEventInvoker(Session.RollbackWindow); + + Frames.Predicted = (Frame)f; + Frames.PredictedPrevious = (Frame)f; + Frames.Verified = (Frame)f; + Frames.PreviousUpdatePredicted = (Frame)f; + + InvokeOnGameStart(); + + // init systems on latest frame + InitSystems(f); + + Log.Debug("Local Players: " + string.Join(" ", Session.LocalPlayerIndices)); + } + + public void OnGameResync() { + _checksumSnapshotBuffer?.Clear(); + ReplayToolsOnGameResync(); + + // reset physics engines statics + Frames.Verified.Physics2D.Init(); + Frames.Verified.Physics3D.Init(); + + // events won't get confirmed + CancelPendingEvents(); + + InvokeOnGameResync(); + } + + public DeterministicFrameInputTemp OnLocalInput(Int32 frame, Int32 player) { + var input = default(QTuple); + + // poll input + try { + bool isFirst = _polledInputInThisSimulation == false; + _polledInputInThisSimulation = true; + input = InvokeOnPollInput(frame, player, isFirst); + } catch (Exception exn) { + Log.Error("## Input Code Threw Exception ##"); + Log.Exception(exn); + } + + if (_inputSerializerWrite == null) { + _inputSerializerWrite = new FrameSerializer(DeterministicFrameSerializeMode.Serialize, null, new Byte[1024]); + } + + // clear old data + _inputSerializerWrite.Reset(); + _inputSerializerWrite.Writing = true; + _inputSerializerWrite.InputMode = true; + + // pack into stream + Input.Write(_inputSerializerWrite, input.Item0); + + // return temp input + return DeterministicFrameInputTemp.Predicted(frame, player, _inputSerializerWrite.Stream.Data, _inputSerializerWrite.Stream.BytesRequired, input.Item1); + } + + public void OnSimulate(DeterministicFrame state) { + HostProfiler.Start("QuantumGame.OnSimulate"); + + var f = (Frame)state; + + try { + // reset profiling + HostProfiler.Start("Init Profiler"); + f.Context.ProfilerContext.Reset(); + var profiler = f.Context.ProfilerContext.GetProfilerForTaskThread(0); + HostProfiler.End(); + + HostProfiler.Start("ApplyInputs"); + ApplyInputs(f); + HostProfiler.End(); + + HostProfiler.Start("OnSimulateBegin"); + f.Context.OnFrameSimulationBegin(f); + f.OnFrameSimulateBegin(); + f.Context.TaskContext.BeginFrame(f); + HostProfiler.End(); + + var handle = f.Context.TaskContext.AddRootTask(); + + HostProfiler.Start("UpdatePlayerData"); + f.UpdatePlayerData(); + HostProfiler.End(); + + profiler.Start("Scheduling Tasks #ff9900"); + HostProfiler.Start("Scheduling Tasks"); + + var systems = &f.Global->Systems; + + for (Int32 i = 0; i < _systemsRoot.Length; ++i) { + if (f.SystemIsEnabledSelf(_systemsRoot[i])) { + try { + handle = _systemsRoot[i].OnSchedule(f, handle); + } catch (Exception exn) { + LogSimulationException(exn); + } + } + } + + HostProfiler.End(); + profiler.End(); + + try { + f.Context.TaskContext.EndFrame(); + f.OnFrameSimulateEnd(); + f.Context.OnFrameSimulationEnd(); + } catch (Exception exn) { + Log.Exception(exn); + } + + if (ProfilerSampleGenerated != null) { + var data = f.Context.ProfilerContext.CreateReport(f.Number, f.IsVerified); + ProfilerSampleGenerated(data); + } + +#if PROFILER_FRAME_AVERAGE + f.Context.ProfilerContext.StoreFrameTime(); + Log.Info("Frame Average: " + f.Context.ProfilerContext.GetFrameTimeAverage()); +#endif + } catch (Exception exn) { + LogSimulationException(exn); + } + + HostProfiler.End(); + } + + public void OnSimulateFinished(DeterministicFrame state) { + SnapshotsOnSimulateFinished(state); + InvokeOnSimulateFinished(state); + } + + public void OnUpdateDone() { + Frames.Predicted = (Frame)Session.FramePredicted; + Frames.PredictedPrevious = (Frame)Session.FramePredictedPrevious; + Frames.Verified = (Frame)Session.FrameVerified; + Frames.PreviousUpdatePredicted = (Frame)Session.PreviousUpdateFramePredicted; + + if (Session.IsStalling == false) { + var f = (float)(Session.AccumulatedTime / Frames.Predicted.DeltaTime.AsFloat); + InterpolationFactor = f < 0.0f ? 0.0f : f > 1.0f ? 1.0f : f; // Clamp01 + } + + InvokeOnUpdateView(); + InvokeEvents(); + } + + public void AssignSession(DeterministicSession session) { + Session = session; + + DeterministicSessionConfig sessionConfig; + Session.GetLocalConfigs(out sessionConfig, out _); + + // verify player count is in correct range + if (sessionConfig.PlayerCount < 1 || sessionConfig.PlayerCount > Quantum.Input.MAX_COUNT) { + throw new Exception(String.Format("Invalid player count {0} (needs to be in 1-{1} range)", sessionConfig.PlayerCount, Quantum.Input.MAX_COUNT)); + } + + // verify all types + var verifier = new MemoryLayoutVerifier(MemoryLayoutVerifier.Platform ?? new MemoryLayoutVerifier.DefaultPlatform()); + var result = verifier.Verify(_typeRegistry.Types); + if (result.Count > 0) { + throw new Exception("MemoryIntegrity Check Failed: " + System.Environment.NewLine + String.Join(System.Environment.NewLine, result.ToArray())); + } else { + Log.Info("Memory Integrity Verified"); + } + } + + public void OnChecksumError(DeterministicTickChecksumError error, DeterministicFrame[] frames) { + InvokeOnChecksumError(error, frames); + } + + public void OnChecksumComputed(Int32 frame, ulong checksum) { + InvokeOnChecksumComputed(frame, checksum); + ReplayToolsOnChecksumComputed(frame, checksum); + } + + public void OnSimulationEnd() { + _context.OnSimulationEnd(); + } + + public void OnSimulationBegin() { + _polledInputInThisSimulation = false; + _context.OnSimulationBegin(); + } + + public void OnInputConfirmed(DeterministicFrameInputTemp input) { + InvokeOnInputConfirmed(input); + ReplayToolsOnInputConfirmed(input); + } + + public void OnChecksumErrorFrameDump(int actorId, int frameNumber, DeterministicSessionConfig sessionConfig, byte[] runtimeConfig, byte[] frameData, byte[] extraData) { + InvokeOnChecksumErrorFrameDump(actorId, frameNumber, sessionConfig, runtimeConfig, frameData, extraData); + } + + public void OnPluginDisconnect(string reason) { + Log.Error("DISCONNECTED: " + reason); + InvokeOnPluginDisconnect(reason); + } + + public int GetInputInMemorySize() { + return sizeof(Input); + } + + public Int32 GetInputSerializedFixedSize() { + var stream = new FrameSerializer(DeterministicFrameSerializeMode.Serialize, null, 1024); + stream.Writing = true; + stream.InputMode = true; + Input.Write(stream, new Input()); + return stream.ToArray().Length; + } + + void InitSystems(DeterministicFrame df) { + var f = (Frame)df; + + try { + f.Context.OnFrameSimulationBegin(f); + + // call init on ALL systems + for (Int32 i = 0; i < _systemsAll.Length; ++i) { + try { + _systemsAll[i].OnInit(f); + + if (f.CommitCommandsMode == CommitCommandsModes.InBetweenSystems) { + f.Unsafe.CommitAllCommands(); + } + } catch (Exception exn) { + LogSimulationException(exn); + } + } + + // TODO: this seems like a good place to fire OnMapChanged, + // if we want to do that for the initial map + + // call OnEnabled on all systems which start enabled + for (Int32 i = 0; i < _systemsAll.Length; ++i) { + if (_systemsAll[i].StartEnabled) { + try { + _systemsAll[i].OnEnabled(f); + + if (f.CommitCommandsMode == CommitCommandsModes.InBetweenSystems) { + f.Unsafe.CommitAllCommands(); + } + } catch (Exception exn) { + LogSimulationException(exn); + } + } + } + + f.Context.OnFrameSimulationEnd(); + } catch (Exception e) { + LogSimulationException(e); + } + + // invoke events from OnInit/OnEnabled + InvokeEvents(); + } + + public void DeserializeInputInto(int player, byte[] data, byte* buffer) { + if (_inputSerializerRead == null) { + _inputStreamReadZeroArray = new Byte[1024]; + _inputSerializerRead = new FrameSerializer(DeterministicFrameSerializeMode.Serialize, null, new Byte[1024]); + } + + _inputSerializerRead.Reset(); + _inputSerializerRead.Frame = null; + _inputSerializerRead.Reading = true; + _inputSerializerRead.InputMode = true; + + if (data == null || data.Length == 0) { + _inputSerializerRead.CopyFromArray(_inputStreamReadZeroArray); + } else { + _inputSerializerRead.CopyFromArray(data); + } + + try { + *(Input*)buffer = Input.Read(_inputSerializerRead); + } catch (Exception exn) { + *(Input*)buffer = default; + + // log exception + Log.Error("Received invalid input data from player {0}, could not deserialize.", player); + Log.Exception(exn); + } + } + + void ApplyInputs(Frame f) { + for (Int32 i = 0; i < Session.PlayerCount; i++) { + var raw = f.GetRawInput(i); + if (raw == null) { + Log.Error($"Got null input for player {i}"); + } else { + f.SetPlayerInput(i, *(Input*)raw); + } + } + } + + Boolean ReadInputFromStream(out Input input) { + try { + input = Input.Read(_inputSerializerRead); + return true; + } catch { + input = default(Input); + return false; + } + } + + void LogSimulationException(Exception exn) { + Log.Error("## Simulation Code Threw Exception ##"); + Log.Exception(exn); + } + + public byte[] GetExtraErrorFrameDumpData(DeterministicFrame frame) { + using (var stream = new MemoryStream()) { + using (var writer = new BinaryWriter(stream)) { + var data = new ChecksumErrorFrameDumpContext(this, (Frame)frame); + data.Serialize(this, writer); + } + return stream.ToArray(); + } + } + } +} + +// Game/QuantumGame.ReplayTools.cs + +namespace Quantum { + public partial class QuantumGame { + + public InputProvider RecordedInputs { get; private set; } + public ChecksumFile RecordedChecksums { get; private set; } + + ChecksumFile _checksumsToVerify; + + [Obsolete("No longer needed. Just use File.WriteAllBytes(path, serializer.SerializeAssets(assets))")] + public static void ExportDatabase(IEnumerable assets, IAssetSerializer serializer, string folderPath, int serializationBufferSize, string dbExtension = ".json") { + var filePath = Path.Combine(folderPath, "db" + dbExtension); + File.WriteAllBytes(filePath, serializer.SerializeAssets(assets)); + } + + [Obsolete("Use GetInstantReplaySnapshot(int)")] + public Frame GetRecordedSnapshot(int frame) { + return GetInstantReplaySnapshot(frame); + } + + public Frame GetInstantReplaySnapshot(int frame) { + if (!_instantReplaySnapshotsRecording) { + Log.Error("Can't find any recorded snapshots. Use StartRecordingSnapshots to start recording."); + return null; + } + + var buffer = (_commonSnapshotInterval > 0 ? _checksumSnapshotBuffer : _instantReplaySnapshotBuffer); + Assert.Check(buffer != null); + + var result = buffer.Find(frame, DeterministicFrameSnapshotBufferFindMode.ClosestLessThanOrEqual); + if (result == null) { + result = buffer.Find(frame, DeterministicFrameSnapshotBufferFindMode.Closest); + if (result == null) { + Log.Warn("Unable to find a replay snapshot for frame {0}. No snapshots were saved.", frame); + } else { + Log.Warn("Unable to find a replay snapshot for frame {0} or earlier. The closest match is {1}." + + "Increase the max replay length.", frame, result.Number); + } + } + + return (Frame)result; + } + + public void GetInstantReplaySnapshots(int startFrame, int endFrame, List frames) { + if (!_instantReplaySnapshotsRecording) { + Log.Error("Can't find any recorded snapshots. Use StartRecordingSnapshots to start recording."); + return; + } + + var buffer = (_commonSnapshotInterval > 0 ? _checksumSnapshotBuffer : _instantReplaySnapshotBuffer); + Assert.Check(buffer != null); + + var firstFrame = buffer.Find(startFrame, DeterministicFrameSnapshotBufferFindMode.ClosestLessThanOrEqual); + var minFrameNumber = firstFrame?.Number ?? startFrame; + + foreach (Frame frame in buffer.Data) { + if (frame == null) { + continue; + } + + if (frame.Number >= minFrameNumber && frame.Number <= endFrame) + frames.Add(frame); + } + } + + public ReplayFile CreateSavegame() { + if (Frames.Verified == null) { + Log.Error("Cannot create a savegame. Frames verified not found."); + return null; + } + + return new ReplayFile { + DeterministicConfig = Frames.Verified.SessionConfig, + RuntimeConfig = Frames.Verified.RuntimeConfig, + InputHistory = null, + Length = Frames.Verified.Number, + Frame = Frames.Verified.Serialize(DeterministicFrameSerializeMode.Serialize) + }; + } + + public ReplayFile GetRecordedReplay() { + if (Frames.Verified == null) { + Log.Error("Cannot create a replay. Frames current or verified are not valid, yet."); + return null; + } + + if (RecordedInputs == null) { + Log.Error("Cannot create a replay, because no recorded input was found. Use StartRecordingInput to start recording or setup RecordingFlags."); + return null; + } + + var verifiedFrame = Frames.Verified.Number; + + return new ReplayFile { + DeterministicConfig = Frames.Verified.SessionConfig, + RuntimeConfig = Frames.Verified.RuntimeConfig, + InputHistory = RecordedInputs.ExportToList(verifiedFrame), + Length = verifiedFrame, + InitialFrame = Session.InitialTick, + InitialFrameData = Session.IntitialFrameData, + }; + } + + + private void ReplayToolsOnInputConfirmed(DeterministicFrameInputTemp input) { + if (RecordedInputs == null) + return; + RecordedInputs.OnInputConfirmed(this, input); + } + + private void ReplayToolsOnGameResync() { + _instantReplaySnapshotBuffer?.Clear(); + RecordedInputs?.Clear(Frames.Verified.Number); + RecordedChecksums?.Clear(); + } + + private void ReplayToolsOnChecksumComputed(Int32 frame, ulong checksum) { + if (RecordedChecksums != null) { + RecordedChecksums.RecordChecksum(this, frame, checksum); + } + if (_checksumsToVerify != null) { + _checksumsToVerify.VerifyChecksum(this, frame, checksum); + } + } + + public void StartRecordingInput(Int32? startFrame = null) { + if (Session == null) { + Log.Error("Can't start input recording, because the session is invalid. Wait for the OnGameStart callback."); + return; + } + if (RecordedInputs == null) { + if (startFrame.HasValue) { + RecordedInputs = new InputProvider(Session.SessionConfig.PlayerCount, startFrame.Value, 60 * 60, 0); + } else { + // start frame is the session RollbackWindow + RecordedInputs = new InputProvider(Session.SessionConfig); + } + Log.Info("QuantumGame.ReplayTools: Input recording started"); + } + } + + public void StartRecordingChecksums() { + if (RecordedChecksums == null) { + RecordedChecksums = new ChecksumFile(); + Log.Info("QuantumGame.ReplayTools: Checksum recording started"); + } + } + + public void StartVerifyingChecksums(ChecksumFile checksums) { + if (_checksumsToVerify == null) { + _checksumsToVerify = checksums; + Log.Info("QuantumGame.ReplayTools: Checksum verification started"); + } + } + + public void StartRecordingInstantReplaySnapshots() { + if (_instantReplaySnapshotsRecording) { + return; + } + + if (InstantReplayConfig.LenghtSeconds <= 0 || InstantReplayConfig.SnapshotsPerSecond <= 0) { + Assert.Check(_instantReplaySnapshotBuffer == null); + Assert.Check(_commonSnapshotInterval <= 0); + Log.Error($"Can't start recording replay snapshots with these settings: {InstantReplayConfig}"); + return; + } + + _instantReplaySnapshotsRecording = true; + } + + [Obsolete("Use StartRecordingInstantReplaySnapshots() instead and StartParameters properties instead.")] + public void StartRecordingSnapshots(float bufferSizeSec, int snapshotFrequencyPerSec) { + } + } +} + +// Game/QuantumGame.EventDispatcher.cs + +namespace Quantum { + public partial class QuantumGame { + + Dictionary _eventsTriggered; + Queue _eventsConfirmationQueue; + + + public int EventWaitingForConfirmationCount => _eventsConfirmationQueue.Count; + + void InitEventInvoker(Int32 size) { + // how many events per frame without a resize + const int EventsPerTickHeuristic = 50; + _eventsTriggered = new Dictionary(size * EventsPerTickHeuristic); + _eventsConfirmationQueue = new Queue(size * EventsPerTickHeuristic); + } + + void RaiseEvent(EventBase evnt) { + try { + evnt.Game = this; + _eventDispatcher?.Publish(evnt); + } catch (Exception exn) { + Log.Error("## Event Callback Threw Exception ##"); + Log.Exception(exn); + } + } + + void CancelPendingEvents() { + while (_eventsConfirmationQueue.Count > 0) { + var key = _eventsConfirmationQueue.Dequeue(); + _eventsTriggered.Remove(key); + InvokeOnEvent(key, false); + } + _eventsTriggered.Clear(); + } + + + void InvokeEvents() { + while (_context.Events.Count > 0) { + var head = _context.Events.PopHead(); + try { + if (head.Synced) { + if (Session.IsFrameVerified(head.Tick)) { + RaiseEvent(head); + } + } else { + // calculate hash code + var key = new EventKey(head.Tick, head.Id, head.GetHashCode()); + + // if frame is verified, CONFIRM the event in the temp collection of hashes + bool confirmed = Session.IsFrameVerified(head.Tick); + + // if this was already raised, do nothing + if (!_eventsTriggered.TryGetValue(key, out var alreadyConfirmed)) { + // dont trigger this again + _eventsTriggered.Add(key, confirmed); + // trigger event + RaiseEvent(head); + // enqueue confirmation + _eventsConfirmationQueue.Enqueue(key); + } else if (confirmed && !alreadyConfirmed) { + // confirm this event is definitive... + _eventsTriggered[key] = confirmed; + } + } + } finally { + _context.ReleaseEvent(head); + } + } + + // invoke confirmed/canceled event callbacks + while (_eventsConfirmationQueue.Count > 0) { + + var key = _eventsConfirmationQueue.Peek(); + + // need to wait; this will block confirmations from resimulations to maintain order + if (!Session.IsFrameVerified(key.Tick)) { + Assert.Check(key.Tick <= Session.FrameVerified.Number + Session.RollbackWindow); + break; + } + + var confirmed = _eventsTriggered[key]; + _eventsTriggered.Remove(key); + _eventsConfirmationQueue.Dequeue(); + + InvokeOnEvent(key, confirmed); + } + } + } +} + +// Game/QuantumGameCallbacks.cs + +namespace Quantum { + + public enum CallbackId { + PollInput, + GameStarted, + GameResynced, + GameDestroyed, + UpdateView, + SimulateFinished, + EventCanceled, + EventConfirmed, + ChecksumError, + ChecksumErrorFrameDump, + InputConfirmed, + ChecksumComputed, + PluginDisconnect, + UserCallbackIdStart, + } + + /// + /// Callback called when the simulation queries local input. + /// + public sealed class CallbackPollInput : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.PollInput; + internal CallbackPollInput(QuantumGame game) : base(ID, game) { } + + public Int32 Frame; + public Int32 Player; + + public void SetInput(Input input, DeterministicInputFlags flags) { + IsInputSet = true; + Input = input; + Flags = flags; + } + + public void SetInput(QTuple input) { + SetInput(input.Item0, input.Item1); + } + + public bool IsFirstInThisUpdate { get; internal set; } + public bool IsInputSet { get; internal set; } + public Input Input { get; private set; } + public DeterministicInputFlags Flags { get; private set; } + } + + /// + /// Callback called when the game has been started. + /// + public sealed class CallbackGameStarted : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.GameStarted; + internal CallbackGameStarted(QuantumGame game) : base(ID, game) { } + } + + /// + /// Callback called when the game has been re-synchronized from a snapshot. + /// + public sealed class CallbackGameResynced : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.GameResynced; + internal CallbackGameResynced(QuantumGame game) : base(ID, game) { } + } + + /// + /// Callback called when the game was destroyed. + /// + public sealed class CallbackGameDestroyed : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.GameDestroyed; + internal CallbackGameDestroyed(QuantumGame game) : base(ID, game) { } + } + + /// + /// Callback guaranteed to be called every rendered frame. + /// + public sealed class CallbackUpdateView : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.UpdateView; + internal CallbackUpdateView(QuantumGame game) : base(ID, game) { } + } + + /// + /// Callback called when frame simulation has completed. + /// + public sealed class CallbackSimulateFinished : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.SimulateFinished; + internal CallbackSimulateFinished(QuantumGame game) : base(ID, game) { } + + public Frame Frame; + } + + /// + /// Callback called when an event raised in a predicted frame was canceled in a verified frame due to a roll-back / missed prediction. + /// Synchronised events are only raised on verified frames and thus will never be canceled; this is useful to graciously discard non-sync'ed events in the view. + /// + public sealed class CallbackEventCanceled : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.EventCanceled; + internal CallbackEventCanceled(QuantumGame game) : base(ID, game) { } + + public EventKey EventKey; + } + + /// + /// Callback called when an event was confirmed by a verified frame. + /// + public sealed class CallbackEventConfirmed : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.EventConfirmed; + internal CallbackEventConfirmed(QuantumGame game) : base(ID, game) { } + + public EventKey EventKey; + } + + /// + /// Callback called on a checksum error. + /// + public sealed class CallbackChecksumError : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.ChecksumError; + internal CallbackChecksumError(QuantumGame game) : base(ID, game) { } + + public DeterministicTickChecksumError Error; + internal DeterministicFrame[] _rawFrames; + internal Frame[] _convertedFrame; + + public int FrameCount => Frames.Length; + public Frame GetFrame(int index) => (Frame)Frames[index]; + + public Frame[] Frames { + get { + if (_convertedFrame == null) { + _convertedFrame = new Frame[_rawFrames.Length]; + for (int i = 0; i < _rawFrames.Length; ++i) { + _convertedFrame[i] = (Frame)_rawFrames[i]; + } + } + return _convertedFrame; + } + } + } + + /// + /// Callback called when due to a checksum error a frame is dumped. + /// + public sealed class CallbackChecksumErrorFrameDump : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.ChecksumErrorFrameDump; + internal CallbackChecksumErrorFrameDump(QuantumGame game) : base(ID, game) { } + + public Int32 ActorId; + public Int32 FrameNumber; + public Byte[] FrameData; + public Byte[] RuntimeConfigBytes; + public Byte[] ExtraBytes; + public DeterministicSessionConfig SessionConfig; + + private Frame _frameToOverride; + + private Byte[] _overridenFrameData; + private SimulationConfig _overridenSimulationConfig; + private DeterministicSessionConfig _overridenSessionConfig; + private RuntimeConfig _overridenRuntimeConfig; + + private QTuple _frameDump; + private QTuple _frame; + private QTuple _runtimeConfig; + private QTuple _context; + + internal void Clear() { + + try { + if (_overridenRuntimeConfig != null) { + _frameToOverride.RuntimeConfig = _overridenRuntimeConfig; + } + if (_overridenSessionConfig != null) { + _frameToOverride.SessionConfig = _overridenSessionConfig; + } + + if (_overridenSimulationConfig != null) { + _frameToOverride.SimulationConfig = _overridenSimulationConfig; + } + + if (_overridenFrameData != null) { + _frameToOverride.Deserialize(_overridenFrameData); + } + } finally { + _frameToOverride = null; + _overridenFrameData = null; + _overridenSimulationConfig = null; + _overridenSessionConfig = null; + _overridenRuntimeConfig = null; + + _runtimeConfig = default; + _context = default; + _frame = default; + _frameDump = default; + SessionConfig = null; + } + } + + public Frame Frame { + get { + if (!_frame.Item0) { + _frame = QTuple.Create(true, (Frame)null); + if (_frameToOverride != null) { + var originalFrameData = _frameToOverride.Serialize(DeterministicFrameSerializeMode.Serialize); + try { + _frameToOverride.Deserialize(FrameData); + _frame = QTuple.Create(true, _frameToOverride); + _overridenFrameData = originalFrameData; + } catch (System.Exception ex) { + // revert to the old data + Log.Warn($"Failed to deserilize dump frame. The snapshot will appear as raw data.\n{ex}"); + _frameToOverride.Deserialize(originalFrameData); + } + + _overridenRuntimeConfig = _frameToOverride.RuntimeConfig; + _overridenSessionConfig = _frameToOverride.SessionConfig; + _overridenSimulationConfig = _frameToOverride.SimulationConfig; + _frameToOverride.SessionConfig = SessionConfig; + + if (RuntimeConfig != null) { + _frameToOverride.RuntimeConfig = RuntimeConfig; + } + + if (SimulationConfig != null) { + _frameToOverride.SimulationConfig = SimulationConfig; + } + } + } + return _frame.Item1; + } + } + + public string FrameDump { + get { + if (!_frameDump.Item0) { + if (Frame != null) { + int dumpFlags = Frame.DumpFlag_NoHeap | Frame.DumpFlag_NoIsVerified; + if (RuntimeConfig == null) { + dumpFlags |= Frame.DumpFlag_NoRuntimeConfig; + } + if (SimulationConfig == null) { + dumpFlags |= Frame.DumpFlag_NoSimulationConfig; + } + + var options = Game.Configurations.Simulation.ChecksumErrorDumpOptions; + if (options.HasFlag(SimulationConfigChecksumErrorDumpOptions.ReadableDynamicDB)) { + dumpFlags |= Frame.DumpFlag_ReadableDynamicDB; + } + if (options.HasFlag(SimulationConfigChecksumErrorDumpOptions.RawFPValues)) { + dumpFlags |= Frame.DumpFlag_PrintRawValues; + } + if (options.HasFlag(SimulationConfigChecksumErrorDumpOptions.ComponentChecksums)) { + dumpFlags |= Frame.DumpFlag_ComponentChecksums; + } + + _frameDump = QTuple.Create(true, Frame.DumpFrame(dumpFlags)); + + if (Context?.AssetDBChecksums != null) { + var sb = new StringBuilder(); + sb.Append(_frameDump.Item1); + sb.AppendLine(); + sb.AppendLine("# RECEIVED ASSETDB CHECKSUMS"); + foreach (var entry in Context.AssetDBChecksums) { + sb.Append(entry.Item0).Append(": ").Append(entry.Item1).AppendLine(); + } + + _frameDump = QTuple.Create(true, sb.ToString()); + } + } else { + unsafe { + byte[] actualData = FrameData; + bool wasCompressed = false; + try { + actualData = ByteUtils.GZipDecompressBytes(FrameData); + wasCompressed = true; + } catch { } + + fixed (byte* p = actualData) { + var printer = new FramePrinter(); + printer.AddLine($"#### RAW FRAME DUMP (was compressed: {wasCompressed}) ####"); + printer.ScopeBegin(); + UnmanagedUtils.PrintBytesHex(p, FrameData.Length, 32, printer); + printer.ScopeEnd(); + _frameDump = QTuple.Create(true, printer.ToString()); + } + } + } + } + + return _frameDump.Item1; + } + } + + + public RuntimeConfig RuntimeConfig { + get { + if (!_runtimeConfig.Item0) { + try { + _runtimeConfig = QTuple.Create(true, RuntimeConfig.FromByteArray(RuntimeConfigBytes)); + } catch (Exception ex) { + Log.Exception(ex); + _runtimeConfig = QTuple.Create(true, (RuntimeConfig)null); + } + } + return _runtimeConfig.Item1; + } + } + + public SimulationConfig SimulationConfig => Context?.SimulationConfig; + + internal void Init(Frame frame) { + _frameToOverride = frame; + } + + public ChecksumErrorFrameDumpContext Context { + get { + if (!_context.Item0) { + try { + using (var reader = new BinaryReader(new MemoryStream(ExtraBytes))) { + _context = QTuple.Create(true, ChecksumErrorFrameDumpContext.Deserialize(Game, reader)); + } + } catch (Exception ex) { + Log.Exception(ex); + _context = QTuple.Create(true, (ChecksumErrorFrameDumpContext)null); + } + } + return _context.Item1; + } + } + } + + /// + /// Callback when local input was confirmed. + /// + public sealed class CallbackInputConfirmed : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.InputConfirmed; + internal CallbackInputConfirmed(QuantumGame game) : base(ID, game) { } + public DeterministicFrameInputTemp Input; + } + + /// + /// Callback called when a checksum has been computed. + /// + public sealed class CallbackChecksumComputed : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.ChecksumComputed; + internal CallbackChecksumComputed(QuantumGame game) : base(ID, game) { } + + public Int32 Frame; + public UInt64 Checksum; + } + + /// + /// Callback called when the local client is disconnected by the plugin. + /// + public sealed class CallbackPluginDisconnect : QuantumGame.CallbackBase { + public new const Int32 ID = (int)CallbackId.PluginDisconnect; + internal CallbackPluginDisconnect(QuantumGame game) : base(ID, game) { } + + public string Reason; + } + + partial class QuantumGame { + + public class CallbackBase : Quantum.CallbackBase { + public new QuantumGame Game { + get => (QuantumGame)base.Game; + set => base.Game = value; + } + + public CallbackBase(int id, QuantumGame game) : base(id, game) { + } + + + public static Type GetCallbackType(CallbackId id) { + switch (id) { + case CallbackId.ChecksumComputed: return typeof(CallbackChecksumComputed); + case CallbackId.ChecksumError: return typeof(CallbackChecksumError); + case CallbackId.ChecksumErrorFrameDump: return typeof(CallbackChecksumErrorFrameDump); + case CallbackId.EventCanceled: return typeof(CallbackEventCanceled); + case CallbackId.EventConfirmed: return typeof(CallbackEventConfirmed); + case CallbackId.GameDestroyed: return typeof(CallbackGameDestroyed); + case CallbackId.GameStarted: return typeof(CallbackGameStarted); + case CallbackId.InputConfirmed: return typeof(CallbackInputConfirmed); + case CallbackId.PollInput: return typeof(CallbackPollInput); + case CallbackId.SimulateFinished: return typeof(CallbackSimulateFinished); + case CallbackId.UpdateView: return typeof(CallbackUpdateView); + case CallbackId.PluginDisconnect: return typeof(CallbackPluginDisconnect); + default: throw new ArgumentOutOfRangeException(nameof(id)); + } + } + } + + // callback objects + private CallbackChecksumComputed _callbackChecksumComputed; + private CallbackChecksumError _callbackChecksumError; + private CallbackChecksumErrorFrameDump _callbackChecksumErrorFrameDump; + private CallbackEventCanceled _callbackEventCanceled; + private CallbackEventConfirmed _callbackEventConfirmed; + private CallbackGameDestroyed _callbackGameDestroyed; + private CallbackGameStarted _callbackGameStarted; + private CallbackGameResynced _callbackGameResynced; + private CallbackInputConfirmed _callbackInputConfirmed; + private CallbackPollInput _callbackPollInput; + private CallbackSimulateFinished _callbackSimulateFinished; + private CallbackUpdateView _callbackUpdateView; + private CallbackPluginDisconnect _callbackPluginDisconnect; + + public void InitCallbacks() { + _callbackChecksumComputed = new CallbackChecksumComputed(this); + _callbackChecksumError = new CallbackChecksumError(this); + _callbackChecksumErrorFrameDump = new CallbackChecksumErrorFrameDump(this); + _callbackEventCanceled = new CallbackEventCanceled(this); + _callbackEventConfirmed = new CallbackEventConfirmed(this); + _callbackGameDestroyed = new CallbackGameDestroyed(this); + _callbackGameStarted = new CallbackGameStarted(this); + _callbackGameResynced = new CallbackGameResynced(this); + _callbackInputConfirmed = new CallbackInputConfirmed(this); + _callbackPollInput = new CallbackPollInput(this); + _callbackSimulateFinished = new CallbackSimulateFinished(this); + _callbackUpdateView = new CallbackUpdateView(this); + _callbackPluginDisconnect = new CallbackPluginDisconnect(this); + } + + public void InvokeOnGameEnded() { + // not implemented + } + + public void InvokeOnDestroy() { + try { + _callbackDispatcher?.Publish(_callbackGameDestroyed); + } catch (Exception ex) { + Log.Exception(ex); + + } + } + + void InvokeOnGameStart() { + try { + _callbackDispatcher?.Publish(_callbackGameStarted); + } catch (Exception ex) { + Log.Exception(ex); + } + } + + void InvokeOnGameResync() { + try { + _callbackDispatcher?.Publish(_callbackGameResynced); + } catch (Exception ex) { + Log.Exception(ex); + } + } + + + QTuple InvokeOnPollInput(int frame, int player, bool isFirstInThisUpdate) { + + try { + _callbackPollInput.IsInputSet = false; + _callbackPollInput.Frame = frame; + _callbackPollInput.Player = player; + _callbackPollInput.IsFirstInThisUpdate = isFirstInThisUpdate; + _callbackDispatcher?.Publish(_callbackPollInput); + if (_callbackPollInput.IsInputSet) { + return QTuple.Create(_callbackPollInput.Input, _callbackPollInput.Flags); + } + return default; + } catch (Exception ex) { + Log.Exception(ex); + return default; + } + } + + void InvokeOnUpdateView() { + HostProfiler.Start("QuantumGame.InvokeOnUpdateView"); + try { + _callbackDispatcher?.Publish(_callbackUpdateView); + } catch (Exception ex) { + Log.Exception(ex); + } + HostProfiler.End(); + } + + public void InvokeOnSimulateFinished(DeterministicFrame state) { + HostProfiler.Start("QuantumGame.InvokeOnSimulateFinished"); + try { + _callbackSimulateFinished.Frame = (Frame)state; + _callbackDispatcher?.Publish(_callbackSimulateFinished); + } catch (Exception ex) { + Log.Exception(ex); + } + + _callbackSimulateFinished.Frame = null; + HostProfiler.End(); + } + + public void InvokeOnChecksumError(DeterministicTickChecksumError error, DeterministicFrame[] frames) { + try { + _callbackChecksumError.Error = error; + _callbackChecksumError._rawFrames = frames; + _callbackChecksumError._convertedFrame = null; + try { + _callbackDispatcher?.Publish(_callbackChecksumError); + } finally { + _callbackChecksumError._rawFrames = null; + _callbackChecksumError._convertedFrame = null; + } + } catch (Exception ex) { + Log.Exception(ex); + } + } + + public void InvokeOnChecksumComputed(Int32 frame, ulong checksum) { + try { + _callbackChecksumComputed.Frame = frame; + _callbackChecksumComputed.Checksum = checksum; + _callbackDispatcher?.Publish(_callbackChecksumComputed); + } catch (Exception ex) { + Log.Exception(ex); + } + } + + public void InvokeOnInputConfirmed(DeterministicFrameInputTemp input) { + try { + _callbackInputConfirmed.Input = input; + try { + _callbackDispatcher?.Publish(_callbackInputConfirmed); + } finally { + _callbackInputConfirmed.Input = default; + } + } catch (Exception ex) { + Log.Exception(ex); + } + } + + public void InvokeOnChecksumErrorFrameDump(Int32 actorId, Int32 frameNumber, DeterministicSessionConfig sessionConfig, byte[] runtimeConfig, byte[] frameData, byte[] extraData) { + HostProfiler.Start("QuantumGame.InvokeOnChecksumErrorFrameDump"); + try { + + // find the frame that's going to be overwritten: + Frame frameToOverwrite = null; + + if (_checksumSnapshotBuffer?.Capacity > 0) { + if (_checksumSnapshotBuffer.Count == 0) { + _checksumSnapshotBuffer.PushBack(Frames.Verified, this, _context); + } + frameToOverwrite = (Frame)_checksumSnapshotBuffer.PeekBack(); + } else { + // TODO: use replay buffer maybe? or one of predicted? + Log.Warn("Unable to acquire a frame to decode the snapshot. The snapshot will appear as raw binary data. Increase ChecksumFrameBufferSize."); + } + + try { + _callbackChecksumErrorFrameDump.Init(frameToOverwrite); + _callbackChecksumErrorFrameDump.ActorId = actorId; + _callbackChecksumErrorFrameDump.FrameNumber = frameNumber; + _callbackChecksumErrorFrameDump.FrameData = frameData; + _callbackChecksumErrorFrameDump.SessionConfig = sessionConfig; + _callbackChecksumErrorFrameDump.RuntimeConfigBytes = runtimeConfig; + _callbackChecksumErrorFrameDump.ExtraBytes = extraData; + + _callbackDispatcher?.Publish(_callbackChecksumErrorFrameDump); + + } finally { + _callbackChecksumErrorFrameDump.Clear(); + } + } catch (Exception ex) { + HostProfiler.End(); + Log.Exception(ex); + } + } + + private void InvokeOnEvent(EventKey key, bool confirmed) { + try { + if (confirmed) { + _callbackEventConfirmed.EventKey = key; + _callbackDispatcher?.Publish(_callbackEventConfirmed); + } else { + // call event cancelation, passing: game (this), frame (f), event hash... + // also pass the index from eventCollection (trhis is the event type ID); + _callbackEventCanceled.EventKey = key; + _callbackDispatcher?.Publish(_callbackEventCanceled); + } + } catch (Exception ex) { + Log.Exception(ex); + } + } + + public void InvokeOnPluginDisconnect(string reason) { + try { + _callbackPluginDisconnect.Reason = reason; + _callbackDispatcher?.Publish(_callbackPluginDisconnect); + } catch (Exception ex) { + Log.Exception(ex); + } + } + } +} + +// Replay/DotNetTaskRunner.cs + +namespace Quantum { + public class DotNetTaskRunner : IDeterministicPlatformTaskRunner { + int _length; + bool[] _done = new bool[128]; + public void Schedule(Action[] delegates) { + // store how many we're executing + _length = delegates.Length; + // clear current state + Array.Clear(_done, 0, _done.Length); + // barrier this + Thread.MemoryBarrier(); + // queue work + for (int i = 0; i < delegates.Length; ++i) { + ThreadPool.QueueUserWorkItem(Wrap(i, delegates[i])); + } + } + public void WaitForComplete() { + throw new NotImplementedException(); + } + public bool PollForComplete() { + for (int i = 0; i < _length; ++i) { + if (Volatile.Read(ref _done[i]) == false) { + return false; + } + } + return true; + } + WaitCallback Wrap(int index, Action callback) { + return _ => { + try { + Photon.Deterministic.Assert.Check(Volatile.Read(ref _done[index]) == false); + callback(); + } catch (Exception exn) { + Log.Exception(exn); + } finally { + Volatile.Write(ref _done[index], true); + } + }; + } + } +} + +// Replay/FlatEntityPrototypeContainer.cs + +namespace Quantum.Prototypes { + [Serializable] + public partial class FlatEntityPrototypeContainer { + [ArrayLength(0, 1)] public List CharacterController2D; + [ArrayLength(0, 1)] public List CharacterController3D; + [ArrayLength(0, 1)] public List NavMeshAvoidanceAgent; + [ArrayLength(0, 1)] public List NavMeshAvoidanceObstacle; + [ArrayLength(0, 1)] public List NavMeshPathfinder; + [ArrayLength(0, 1)] public List NavMeshSteeringAgent; + [ArrayLength(0, 1)] public List PhysicsBody2D; + [ArrayLength(0, 1)] public List PhysicsBody3D; + [ArrayLength(0, 1)] public List PhysicsCollider2D; + [ArrayLength(0, 1)] public List PhysicsCollider3D; + [ArrayLength(0, 1)] public List PhysicsCallbacks2D; + [ArrayLength(0, 1)] public List PhysicsCallbacks3D; + [ArrayLength(0, 1)] public List Transform2D; + [ArrayLength(0, 1)] public List Transform2DVertical; + [ArrayLength(0, 1)] public List Transform3D; + [ArrayLength(0, 1)] public List View; + [ArrayLength(0, 1)] public List PhysicsJoints2D; + [ArrayLength(0, 1)] public List PhysicsJoints3D; + + public void Collect(List target) { + Collect(CharacterController2D, target); + Collect(CharacterController3D, target); + Collect(NavMeshAvoidanceAgent, target); + Collect(NavMeshAvoidanceObstacle, target); + Collect(NavMeshPathfinder, target); + Collect(NavMeshSteeringAgent, target); + Collect(PhysicsBody2D, target); + Collect(PhysicsBody3D, target); + Collect(PhysicsCollider2D, target); + Collect(PhysicsCollider3D, target); + Collect(PhysicsCallbacks2D, target); + Collect(PhysicsCallbacks3D, target); + Collect(Transform2D, target); + Collect(Transform2DVertical, target); + Collect(Transform3D, target); + Collect(View, target); + Collect(PhysicsJoints2D, target); + Collect(PhysicsJoints3D, target); + CollectGen(target); + } + + public void Store(IList prototypes) { + var visitor = new FlatEntityPrototypeContainer.StoreVisitor() { + Storage = this + }; + + foreach (var prototype in prototypes) { + prototype.Dispatch(visitor); + } + } + + public unsafe partial class StoreVisitor : ComponentPrototypeVisitor { + public FlatEntityPrototypeContainer Storage; + public override void Visit(CharacterController2D_Prototype prototype) { + Storage.Store(prototype, ref Storage.CharacterController2D); + } + public override void Visit(CharacterController3D_Prototype prototype) { + Storage.Store(prototype, ref Storage.CharacterController3D); + } + public override void Visit(NavMeshAvoidanceAgent_Prototype prototype) { + Storage.Store(prototype, ref Storage.NavMeshAvoidanceAgent); + } + public override void Visit(NavMeshAvoidanceObstacle_Prototype prototype) { + Storage.Store(prototype, ref Storage.NavMeshAvoidanceObstacle); + } + public override void Visit(NavMeshPathfinder_Prototype prototype) { + Storage.Store(prototype, ref Storage.NavMeshPathfinder); + } + public override void Visit(NavMeshSteeringAgent_Prototype prototype) { + Storage.Store(prototype, ref Storage.NavMeshSteeringAgent); + } + public override void Visit(PhysicsBody2D_Prototype prototype) { + Storage.Store(prototype, ref Storage.PhysicsBody2D); + } + public override void Visit(PhysicsBody3D_Prototype prototype) { + Storage.Store(prototype, ref Storage.PhysicsBody3D); + } + public override void Visit(PhysicsCollider2D_Prototype prototype) { + Storage.Store(prototype, ref Storage.PhysicsCollider2D); + } + public override void Visit(PhysicsCollider3D_Prototype prototype) { + Storage.Store(prototype, ref Storage.PhysicsCollider3D); + } + public override void Visit(PhysicsCallbacks2D_Prototype prototype) { + Storage.Store(prototype, ref Storage.PhysicsCallbacks2D); + } + public override void Visit(PhysicsCallbacks3D_Prototype prototype) { + Storage.Store(prototype, ref Storage.PhysicsCallbacks3D); + } + public override void Visit(Transform2D_Prototype prototype) { + Storage.Store(prototype, ref Storage.Transform2D); + } + public override void Visit(Transform2DVertical_Prototype prototype) { + Storage.Store(prototype, ref Storage.Transform2DVertical); + } + public override void Visit(Transform3D_Prototype prototype) { + Storage.Store(prototype, ref Storage.Transform3D); + } + public override void Visit(View_Prototype prototype) { + Storage.Store(prototype, ref Storage.View); + } + public override void Visit(PhysicsJoints2D_Prototype prototype) { + Storage.Store(prototype, ref Storage.PhysicsJoints2D); + } + + public override void Visit(PhysicsJoints3D_Prototype prototype) { + Storage.Store(prototype, ref Storage.PhysicsJoints3D); + } + } + + partial void CollectGen(List target); + + private void Collect(List source, List destination) where TPrototype : ComponentPrototype { + if (source == null) + return; + for (int i = 0; i < source.Count; ++i) + destination.Add(source[i]); + } + + private void Store(T value, ref List destination) { + Assert.Check(value.GetType() == typeof(T)); + if (destination == null) + destination = new List(1); + destination.Add(value); + } + } +} + + +// Replay/InactiveTaskRunner.cs + +namespace Quantum { + public class InactiveTaskRunner : IDeterministicPlatformTaskRunner { + public void Schedule(Action[] delegates) { } + + public void WaitForComplete() { } + + public bool PollForComplete() { + return true; + } + } +} + +// Replay/JsonAssetSerializerBase.cs + +namespace Quantum { + public abstract class JsonAssetSerializerBase : IAssetSerializer { + private List _prototypeBuffer = new List(); + + public bool IsPrettyPrintEnabled { get; set; } = false; + + /// + /// If set to a positive value, all uncompressed BinaryData assets with size over the value will be compressed + /// during serialization. + /// + public int CompressBinaryDataOnSerializationThreshold { get; set; } = 1024; + + /// + /// If true, all compressed BinaryData assets will be decompressed during deserialization. + /// + public bool DecompressBinaryDataOnDeserialization { get; set; } + + public Encoding Encoding => Encoding.UTF8; + + public byte[] SerializeReplay(ReplayFile replay) { + var json = ToJson(replay); + return Encoding.GetBytes(json); + } + + public ReplayFile DeserializeReplay(byte[] data) { + var json = Encoding.GetString(data); + return (ReplayFile)FromJson(json, typeof(ReplayFile)); + } + + public byte[] SerializeChecksum(ChecksumFile checksums) { + var json = ToJson(checksums); + return Encoding.GetBytes(json); + } + + public ChecksumFile DeserializeChecksum(byte[] data) { + var json = Encoding.GetString(data); + return (ChecksumFile)FromJson(json, typeof(ChecksumFile)); + } + + public byte[] SerializeAssets(IEnumerable assets) { + FlatDatabaseFile db = new FlatDatabaseFile(); + List userAssets = new List(); + + var visitor = new AssetVisitor() { + Storage = db, + Serializer = this + }; + + Assembly executingAssembly = typeof(JsonAssetSerializerBase).Assembly; + + foreach (var asset in assets) { + if (asset is IBuiltInAssetObject builtInAsset) { + builtInAsset.Dispatch(visitor); + } else { + var assetType = asset.GetType(); + var surrogate = new UserAssetSurrogate() { + Type = (assetType.Assembly == executingAssembly) ? assetType.FullName : assetType.AssemblyQualifiedName, + Json = ToJson(asset), + }; + userAssets.Add(surrogate); + } + } + + db.UserAssets = userAssets; + + var json = ToJson(db); + return Encoding.GetBytes(json); + } + + + public string PrintAsset(AssetObject asset) { + + object objectToSerialize = asset; + if ( asset is EntityPrototype ep ) { + objectToSerialize = CreateSurrogate(ep); + } else if ( asset is Map map) { + objectToSerialize = CreateSurrogate(map); + } + + return ToJson(objectToSerialize); + } + + public IEnumerable DeserializeAssets(byte[] data) => IAssetSerializerExtensions.DeserializeAssets(this, data); + + public IEnumerable DeserializeAssets(byte[] data, int index, int count) { + string json = Encoding.UTF8.GetString(data, index, count); + var db = (FlatDatabaseFile)FromJson(json, typeof(FlatDatabaseFile)); + + List result = new List(); + Collect(db.CharacterController2DConfig, result); + Collect(db.CharacterController3DConfig, result); + Collect(db.EntityView, result); + Collect(db.NavMesh, result); + Collect(db.NavMeshAgentConfig, result); + Collect(db.PhysicsMaterial, result); + Collect(db.PolygonCollider, result); + Collect(db.TerrainCollider, result); + + if (db.EntityPrototype != null) { + foreach (var surrogate in db.EntityPrototype) { + result.Add(CreateFromSurrogate(surrogate)); + } + } + + if (db.Map != null) { + foreach (var surrogate in db.Map) { + result.Add(CreateFromSurrogate(surrogate)); + } + } + + if (db.BinaryData != null) { + foreach (var surrogate in db.BinaryData) { + result.Add(CreateFromSurrogate(surrogate)); + } + } + + if (db.UserAssets != null) { + foreach (var surrogate in db.UserAssets) { + var type = Type.GetType(surrogate.Type, true); + var asset = (AssetObject)FromJson(surrogate.Json, type); + result.Add(asset); + } + } + + return result; + } + + protected void Collect(List source, List destination) where AssetType : AssetObject { + if (source == null) + return; + for (int i = 0; i < source.Count; ++i) + destination.Add(source[i]); + } + + protected abstract object FromJson(string json, Type type); + + protected abstract string ToJson(object obj); + + private static EntityPrototypeSurrogate CreateSurrogate(EntityPrototype asset) { + var visitor = new FlatEntityPrototypeContainer.StoreVisitor() { + Storage = new FlatEntityPrototypeContainer() + }; + + foreach (var prototype in asset.Container.Components) { + prototype.Dispatch(visitor); + } + + return new EntityPrototypeSurrogate() { + Identifier = asset.Identifier, + Container = visitor.Storage + }; + } + + private static MapSurrogate CreateSurrogate(Map asset) { + var mapEntities = new FlatEntityPrototypeContainer[asset.MapEntities.Length]; + + var visitor = new FlatEntityPrototypeContainer.StoreVisitor(); + + for (int i = 0; i < mapEntities.Length; ++i) { + visitor.Storage = mapEntities[i] = new FlatEntityPrototypeContainer(); + foreach (var prototype in asset.MapEntities[i].Components) { + prototype.Dispatch(visitor); + } + } + + return new MapSurrogate() { + Map = asset, + MapEntities = mapEntities, + }; + } + + + + private EntityPrototype CreateFromSurrogate(EntityPrototypeSurrogate surrogate) { + try { + Assert.Check(_prototypeBuffer.Count == 0); + surrogate.Container.Collect(_prototypeBuffer); + return new EntityPrototype() { + Identifier = surrogate.Identifier, + Container = new EntityPrototypeContainer() { + Components = _prototypeBuffer.ToArray() + } + }; + } finally { + _prototypeBuffer.Clear(); + } + } + + private Map CreateFromSurrogate(MapSurrogate surrogate) { + Assert.Check(_prototypeBuffer.Count == 0); + + var entityCount = surrogate.MapEntities.Length; + var mapEntities = new EntityPrototypeContainer[entityCount]; + + for (int i = 0; i < entityCount; ++i) { + try { + surrogate.MapEntities[i].Collect(_prototypeBuffer); + mapEntities[i] = new EntityPrototypeContainer() { + Components = _prototypeBuffer.ToArray() + }; + } finally { + _prototypeBuffer.Clear(); + } + } + + var map = surrogate.Map; + map.MapEntities = mapEntities; + return map; + } + + private BinaryDataSurrogate CreateSurrogate(BinaryData asset) { + + byte[] data = asset.Data ?? Array.Empty(); + bool isCompressed = asset.IsCompressed; + + if (!asset.IsCompressed && CompressBinaryDataOnSerializationThreshold > 0 && data.Length >= CompressBinaryDataOnSerializationThreshold) { + data = ByteUtils.GZipCompressBytes(data); + isCompressed = true; + } + + return new BinaryDataSurrogate() { + Identifier = asset.Identifier, + Base64Data = ByteUtils.Base64Encode(data), + IsCompressed = isCompressed, + }; + } + + private BinaryData CreateFromSurrogate(BinaryDataSurrogate surrogate) { + + var result = new BinaryData() { + Identifier = surrogate.Identifier, + Data = ByteUtils.Base64Decode(surrogate.Base64Data), + IsCompressed = surrogate.IsCompressed, + }; + + if (surrogate.IsCompressed && DecompressBinaryDataOnDeserialization) { + result.IsCompressed = false; + result.Data = ByteUtils.GZipDecompressBytes(result.Data); + } + + return result; + } + + [Serializable] + public class EntityPrototypeSurrogate { + public FlatEntityPrototypeContainer Container; + public AssetObjectIdentifier Identifier; + } + + [Serializable] + public class MapSurrogate { + public Map Map; + public FlatEntityPrototypeContainer[] MapEntities; + } + + [Serializable] + public class UserAssetSurrogate { + public string Json; + public string Type; + } + + [Serializable] + public class BinaryDataSurrogate { + public AssetObjectIdentifier Identifier; + public bool IsCompressed; + public string Base64Data; + } + + private class AssetVisitor : IAssetObjectVisitor { + public FlatDatabaseFile Storage; + public JsonAssetSerializerBase Serializer; + + void IAssetObjectVisitor.Visit(BinaryData asset) { + Storage.BinaryData.Add(Serializer.CreateSurrogate(asset)); + } + + void IAssetObjectVisitor.Visit(CharacterController2DConfig asset) { + Storage.CharacterController2DConfig.Add(asset); + } + + void IAssetObjectVisitor.Visit(CharacterController3DConfig asset) { + Storage.CharacterController3DConfig.Add(asset); + } + + void IAssetObjectVisitor.Visit(EntityPrototype asset) { + Storage.EntityPrototype.Add(CreateSurrogate(asset)); + } + + void IAssetObjectVisitor.Visit(EntityView asset) { + Storage.EntityView.Add(asset); + } + + void IAssetObjectVisitor.Visit(Map asset) { + Storage.Map.Add(CreateSurrogate(asset)); + } + + void IAssetObjectVisitor.Visit(NavMesh asset) { + Storage.NavMesh.Add(asset); + } + + void IAssetObjectVisitor.Visit(NavMeshAgentConfig asset) { + Storage.NavMeshAgentConfig.Add(asset); + } + + void IAssetObjectVisitor.Visit(PhysicsMaterial asset) { + Storage.PhysicsMaterial.Add(asset); + } + + void IAssetObjectVisitor.Visit(PolygonCollider asset) { + Storage.PolygonCollider.Add(asset); + } + + void IAssetObjectVisitor.Visit(TerrainCollider asset) { + Storage.TerrainCollider.Add(asset); + } + } + + [Serializable] + private sealed class FlatDatabaseFile { + public List CharacterController2DConfig = new List(); + public List CharacterController3DConfig = new List(); + public List EntityPrototype = new List(); + public List EntityView = new List(); + public List Map = new List(); + public List NavMesh = new List(); + public List NavMeshAgentConfig = new List(); + public List PhysicsMaterial = new List(); + public List PolygonCollider = new List(); + public List TerrainCollider = new List(); + public List UserAssets = new List(); + public List BinaryData = new List(); + } + } +} + +// Replay/ChecksumFile.cs + +namespace Quantum { + + [Serializable] + public class ChecksumFile { + public const int GrowSize = 60 * 60; // one minute of recording at 60 FPS + + [Serializable] + public struct ChecksumEntry { + public int Frame; + // This is super annoying: Unity JSON cannot read the unsigned long data type. + // We can convert on this level, keeping the ULong CalculateChecksum() signature and encode the + // checksum as a long for serialization. Any other ideas? + public long ChecksumAsLong; + } + + public ChecksumEntry[] Checksums; + + private Int32 writeIndex; + + public Dictionary ToDictionary() { + return Checksums.Where(item => item.Frame != 0).ToDictionary(item => item.Frame, item => item); + } + + internal void RecordChecksum(QuantumGame game, Int32 frame, ulong checksum) { + if (Checksums == null) { + Checksums = new ChecksumEntry[GrowSize]; + } + + if (writeIndex + 1 > Checksums.Length) { + Array.Resize(ref Checksums, Checksums.Length + GrowSize); + } + + Checksums[writeIndex].Frame = frame; + Checksums[writeIndex].ChecksumAsLong = ChecksumFileHelper.UlongToLong(checksum); + writeIndex++; + } + + internal void VerifyChecksum(QuantumGame game, Int32 frame, ulong checksum) { + if (Checksums.Length > 0) { + var readIndex = (frame - Checksums[0].Frame) / game.Session.SessionConfig.ChecksumInterval; + Assert.Check(Checksums[readIndex].Frame == frame); + if (Checksums[readIndex].ChecksumAsLong != ChecksumFileHelper.UlongToLong(checksum)) { + Log.Error($"Checksum mismatch in frame {frame}: {Checksums[readIndex].ChecksumAsLong} != {ChecksumFileHelper.UlongToLong(checksum)}"); + } + } + } + + internal void Clear() { + writeIndex = 0; + if ( Checksums != null ) { + for (int i = 0; i < Checksums.Length; ++i) { + Checksums[i] = default; + } + } + } + } + + public static class ChecksumFileHelper { + public static unsafe long UlongToLong(ulong value) { + return *((long*)&value); + } + + public static unsafe ulong LongToULong(long value) { + return *((ulong*)&value); + } + } +} + + +// Replay/InputProvider.cs + +namespace Quantum { + public class InputProvider : IDeterministicReplayProvider { + private int _playerCount; + private int _growSize; + private int _startFrame; + private DeterministicTickInputSet[] _inputs; + + private int MaxFrame => _inputs.Length + _startFrame; + + public InputProvider(DeterministicSessionConfig config, int capacity = 60 * 60, int growSize = 0) : this(config.PlayerCount, config.RollbackWindow, capacity, growSize) { + } + + [Obsolete("Use 'InputProvider(DeterministicTickInputSet[])' instead.")] + public InputProvider(DeterministicSessionConfig config, DeterministicTickInputSet[] inputList) : this(inputList) {} + + public InputProvider(DeterministicTickInputSet[] inputList) { + ImportFromList(inputList); + } + + public InputProvider(int playerCount, int startFrame, int capacity, int growSize) { + _playerCount = playerCount; + _startFrame = startFrame; + _growSize = growSize; + + if (capacity > 0) { + Allocate(capacity); + } + } + + public void Clear(int startFrame) { + _startFrame = startFrame; + for (int i = 0; i < _inputs.Length; i++) { + _inputs[i].Tick = i + _startFrame; + for (int j = 0; j < _playerCount; j++) { + _inputs[i].Inputs[j].Clear(); + } + } + } + + [Obsolete("Use 'ImportFromList(DeterministicTickInputSet[])' instead.")] + public void ImportFromList(DeterministicTickInputSet[] inputList, int startFrame) { + // all InputProvider features expect the first input on the list to be the starting frame, + // setting it to a different number will result in misbehavior + ImportFromList(inputList); + } + + public void ImportFromList(DeterministicTickInputSet[] inputList) { + _startFrame = inputList.Length == 0 ? 0 : inputList[0].Tick; + + // Use external list as our own + _inputs = inputList; + for (int i = 0; i < _inputs.Length; i++) { + for (int j = 0; j < inputList[i].Inputs.Length; j++) { + inputList[i].Inputs[j].Sent = true; + } + } + } + + public DeterministicTickInputSet[] ExportToList(int verifiedFrame) { + var size = _inputs.Length; + while (size > 0 && _inputs[size - 1].Inputs.Any(x => x.Tick == 0 || x.Tick > verifiedFrame)) { + // Truncate non-verified and incomplete input from the end + size--; + } + + if (size <= 0) { + return new DeterministicTickInputSet[0]; + } + + var result = new DeterministicTickInputSet[size]; + Array.Copy(_inputs, result, size); + + return result; + } + + public void OnInputConfirmed(QuantumGame game, DeterministicFrameInputTemp input) { + if (input.Frame < _startFrame) { + // if starting to record from a frame following a snapshot, + // confirmed inputs from previous frames can still arrive + return; + } + + if (input.Frame >= MaxFrame) { + var minSize = Math.Max(input.Frame - _startFrame, _inputs.Length); + var growSize = _growSize > 0 ? minSize + _growSize : minSize * 2; + Allocate(growSize); + } + + _inputs[ToIndex(input.Frame)].Inputs[input.Player].Set(input); + } + + public void InjectInput(DeterministicTickInput input, bool localReplay) { + if (input.Tick >= MaxFrame) { + var minSize = Math.Max(input.Tick - _startFrame, _inputs.Length); + var growSize = _growSize > 0 ? minSize + _growSize : minSize * 2; + Allocate(growSize); + } + + _inputs[ToIndex(input.Tick)].Inputs[input.PlayerIndex].CopyFrom(input); + + if (localReplay) { + _inputs[ToIndex(input.Tick)].Inputs[input.PlayerIndex].Sent = true; + } + } + + public void AddRpc(int player, byte[] data, bool command) { + } + + public bool CanSimulate(int frame) { + var index = ToIndex(frame); + + if (index >= 0 && index < _inputs.Length) { + return _inputs[index].IsComplete(); + } + + return false; + } + + public QTuple GetRpc(int frame, int player) { + if (frame < MaxFrame) { + return QTuple.Create( + _inputs[ToIndex(frame)].Inputs[player].Rpc, + (_inputs[ToIndex(frame)].Inputs[player].Flags & DeterministicInputFlags.Command) == DeterministicInputFlags.Command); + } + + return default; + } + + public DeterministicFrameInputTemp GetInput(int frame, int player) { + if (frame < MaxFrame) { + var input = _inputs[ToIndex(frame)].Inputs[player]; + return DeterministicFrameInputTemp.Verified(frame, player, null, input.DataArray, input.DataLength, input.Flags); + } + + return default; + } + + private int ToIndex(int frame) { + return frame - _startFrame; + } + + private void Allocate(int size) { + var oldSize = 0; + if (_inputs == null) { + _inputs = new DeterministicTickInputSet[size]; + } else { + oldSize = _inputs.Length; + Array.Resize(ref _inputs, size); + } + + for (int i = oldSize; i < _inputs.Length; i++) { + _inputs[i].Tick = i + _startFrame; + _inputs[i].Inputs = new DeterministicTickInput[_playerCount]; + for (int j = 0; j < _playerCount; j++) { + _inputs[i].Inputs[j] = new DeterministicTickInput(); + } + } + } + } + + public static class InputProviderExtensions { + public static void CopyFrom(this DeterministicTickInput input, DeterministicTickInput otherInput) { + input.Sent = otherInput.Sent; + input.Tick = otherInput.Tick; + input.PlayerIndex = otherInput.PlayerIndex; + input.DataLength = otherInput.DataLength; + input.Flags = otherInput.Flags; + + if (otherInput.DataArray != null) { + input.DataArray = new byte[otherInput.DataArray.Length]; + Array.Copy(otherInput.DataArray, input.DataArray, otherInput.DataArray.Length); + } + + if (otherInput.Rpc != null) { + input.Rpc = new byte[otherInput.Rpc.Length]; + Array.Copy(otherInput.Rpc, input.Rpc, otherInput.Rpc.Length); + } + } + + public static void Clear(this DeterministicTickInput input) { + input.Tick = default; + input.PlayerIndex = default; + input.DataArray = default; + input.DataLength = default; + input.Flags = default; + input.Rpc = default; + } + + + + public static void Set(this DeterministicTickInput input, DeterministicFrameInputTemp temp) { + input.Tick = temp.Frame; + input.PlayerIndex = temp.Player; + input.DataArray = temp.CloneData(); + input.DataLength = temp.DataLength; + input.Flags = temp.Flags; + input.Rpc = temp.Rpc; + } + + public static bool IsComplete(this DeterministicTickInputSet set) { + for (int i = 0; i < set.Inputs.Length; i++) { + if (set.Inputs[i].Tick == 0) { + return false; + } + } + + return true; + } + + public static bool IsFinished(this DeterministicTickInputSet set) { + for (int i = 0; i < set.Inputs.Length; i++) { + if (set.Inputs[i].Tick == 0 || + set.Inputs[i].Sent == false) { + return false; + } + } + + return true; + } + } +} + +// Replay/ReplayFile.cs + +namespace Quantum { + + [Serializable] + public class ReplayFile { + public RuntimeConfig RuntimeConfig; + public DeterministicSessionConfig DeterministicConfig; + public DeterministicTickInputSet[] InputHistory; + public Int32 Length; + public byte[] Frame; + public Int32 InitialFrame; + public byte[] InitialFrameData; + } +} + + +// Replay/SessionContainer.cs + +namespace Quantum { + public class SessionContainer { + public static Boolean _loadedAllStatics = false; + public static readonly Object _lock = new Object(); + + DeterministicSessionConfig _sessionConfig; + RuntimeConfig _runtimeConfig; + QuantumGame _game; + DeterministicSession _session; + long _startGameTimeoutInMiliseconds = -1; + DateTime _startGameTimestamp; + + public QuantumGame QuantumGame => _game; + public IDeterministicGame Game => _game; + public DeterministicSession Session => _session; + public RuntimeConfig RuntimeConfig => _runtimeConfig; + public DeterministicSessionConfig DeterministicConfig => _sessionConfig; + + /// + /// Check this when the container reconnects into a running game and handle accordingly. + /// + public bool HasGameStartTimedOut => _startGameTimeoutInMiliseconds > 0 && Session != null && Session.IsPaused && DateTime.Now > _startGameTimestamp + TimeSpan.FromMilliseconds(_startGameTimeoutInMiliseconds); + /// + /// Default is infinity (-1). Set this when the you expect to connect to a running game and wait for a snapshot. + /// + public long GameStartTimeoutInMiliseconds { + get { + return _startGameTimeoutInMiliseconds; + } + set { + _startGameTimeoutInMiliseconds = value; + } + } + + #region Obsolete Members + + [Obsolete("Renamed to Session")] + public DeterministicSession session; + [Obsolete("Renamed to SessionConfig")] + public DeterministicSessionConfig config => _sessionConfig; + [Obsolete("Renamed to RuntimeConfig")] + public RuntimeConfig runtimeConfig => _runtimeConfig; + [Obsolete("Removed allocator access because it being disposed internally and unsafe to use outside")] + public Native.Allocator allocator => null; + [Obsolete("Removed property, use StartReplay(.., IDeterministicReplayProvider provider, ..)")] + public IDeterministicReplayProvider provider; + [Obsolete("Renamed to Game, changed to getter only, set by Start(QuantumGame.StartParameters startParams)")] + public IDeterministicGame game => _game; + [Obsolete("Removed property, set by Start(QuantumGame.StartParameters startParams)")] + public IResourceManager resourceManager; + [Obsolete("Removed property, set by using constructor SessionContainer(ReplayFile)")] + public ReplayFile replayFile; + [Obsolete("Removed property, set by Start(QuantumGame.StartParameters startParams)")] + public IAssetSerializer assetSerializer; + [Obsolete("Removed property, set by Start(QuantumGame.StartParameters startParams)")] + public IEventDispatcher eventDispatcher; + [Obsolete("Removed property, set by Start(QuantumGame.StartParameters startParams)")] + public ICallbackDispatcher callbackDispatcher; + [Obsolete("Removed property, set by Start(QuantumGame.StartParameters startParams)")] + public int gameFlags; + + #endregion + + public static Native.Allocator CreateNativeAllocator() { + switch (Environment.OSVersion.Platform) { + case PlatformID.Unix: + case PlatformID.MacOSX: + return new Native.LIBCAllocator(); + default: + return new Native.MSVCRTAllocator(); + } + } + + public static Native.Utility CreateNativeUtils() { + switch (Environment.OSVersion.Platform) { + case PlatformID.Unix: + case PlatformID.MacOSX: + return new Native.LIBCUtility(); + + default: + return new Native.MSVCRTUtility(); + } + } + + [Obsolete("Use Start(QuantumGame.StartParameters startParams, IDeterministicReplayProvider provider) instead of setting properties")] + public void Start(bool logInitForConsole = true) { + if (provider == null) { + provider = new InputProvider(_sessionConfig); + } + + StartReplay(new QuantumGame.StartParameters() { + ResourceManager = resourceManager, + AssetSerializer = assetSerializer, + CallbackDispatcher = callbackDispatcher, + EventDispatcher = eventDispatcher, + GameFlags = gameFlags, + }, provider, "server", logInitForConsole); + } + + /// + /// Start the simulation as a replay by providing an input provider. + /// + /// Game start parameters + /// Input provider + /// Optional client id + /// Optionally disable setting up the console as log output (required on the Quantum plugin) + public void StartReplay(QuantumGame.StartParameters startParams, IDeterministicReplayProvider provider, string clientId = "server", bool logInitForConsole = true, IDeterministicPlatformTaskRunner taskRunner = null) { + DeterministicSessionArgs sessionArgs; + sessionArgs.Mode = DeterministicGameMode.Replay; + sessionArgs.Game = null; + sessionArgs.Replay = provider; + sessionArgs.Communicator = null; + sessionArgs.PlatformInfo = null; + sessionArgs.InitialTick = 0; + sessionArgs.FrameData = null; + sessionArgs.SessionConfig = null; + sessionArgs.RuntimeConfig = null; + Start(startParams, sessionArgs, _sessionConfig.PlayerCount, clientId, logInitForConsole, taskRunner); + } + + /// + /// Start the simulation as a spectator. + /// + /// Game start parameters + /// Quantum network comunicator (has to have a peer that is connected to a room + /// Optionally the frame to start from + /// The tick that the frame data is based on + /// Optional client id + /// Optionally disable setting up the console as log output (required on the Quantum plugin) + public void StartSpectator(QuantumGame.StartParameters startParams, ICommunicator networkCommunicator, byte[] frameData = null, int initialTick = 0, string clientId = "observer", bool logInitForConsole = true, IDeterministicPlatformTaskRunner taskRunner = null) { + DeterministicSessionArgs sessionArgs; + sessionArgs.Mode = DeterministicGameMode.Spectating; + sessionArgs.Game = null; + sessionArgs.Replay = null; + sessionArgs.Communicator = networkCommunicator; + sessionArgs.PlatformInfo = null; + sessionArgs.InitialTick = initialTick; + sessionArgs.FrameData = frameData; + sessionArgs.SessionConfig = null; + sessionArgs.RuntimeConfig = null; + Start(startParams, sessionArgs, 0, clientId, logInitForConsole, taskRunner); + } + + /// + /// Start the simulation in a custom way. + /// + /// Game start parameters + /// Game session args + /// Number of player slots + /// Optional client id + /// Optionally disable setting up the console as log output (required on the Quantum plugin) + public void Start(QuantumGame.StartParameters startParams, DeterministicSessionArgs sessionArgs, int playerSlots, string clientId = "server", bool logInitForConsole = true, IDeterministicPlatformTaskRunner taskRunner = null) { + if (!_loadedAllStatics) { + lock (_lock) { + if (!_loadedAllStatics) { + // console first + if (logInitForConsole) { + Log.InitForConsole(); + } + + // try to figure out platform if not set + if (Native.Utils == null) { + Native.Utils = CreateNativeUtils(); + } + + if (MemoryLayoutVerifier.Platform == null) { + MemoryLayoutVerifier.Platform = new MemoryLayoutVerifier.DefaultPlatform(); + } + } + + _loadedAllStatics = true; + } + } + + _game = new QuantumGame(startParams); + + DeterministicPlatformInfo info; + info = new DeterministicPlatformInfo(); + info.Allocator = CreateNativeAllocator(); + info.Architecture = DeterministicPlatformInfo.Architectures.x86; + info.RuntimeHost = DeterministicPlatformInfo.RuntimeHosts.PhotonServer; + info.Runtime = DeterministicPlatformInfo.Runtimes.NetFramework; + info.TaskRunner = taskRunner ?? new DotNetTaskRunner(); + + switch (Environment.OSVersion.Platform) { + case PlatformID.Unix: + info.Platform = DeterministicPlatformInfo.Platforms.Linux; + break; + + case PlatformID.MacOSX: + info.Platform = DeterministicPlatformInfo.Platforms.OSX; + break; + + default: + info.Platform = DeterministicPlatformInfo.Platforms.Windows; + break; + } + + sessionArgs.Game = _game; + sessionArgs.PlatformInfo = info; + sessionArgs.SessionConfig = _sessionConfig; + sessionArgs.RuntimeConfig = RuntimeConfig.ToByteArray(_runtimeConfig); + + _session = new DeterministicSession(sessionArgs); + _session.Join(clientId, playerSlots); + + _startGameTimestamp = DateTime.Now; + } + + /// + /// Update the session. + /// + /// Optionally provide a custom delta time + public void Service(double? dt = null) { + _session.Update(dt); + } + + /// + /// Destroy the session. + /// + public void Destroy() { + _session?.Destroy(); + _session = null; + } + + /// + /// Use other constructors that provide the session and runtime config. + /// + public SessionContainer() { + _sessionConfig = null; + _runtimeConfig = null; + } + + public SessionContainer(ReplayFile replayFile) { + _sessionConfig = replayFile.DeterministicConfig; + _runtimeConfig = replayFile.RuntimeConfig; + } + + public SessionContainer(DeterministicSessionConfig sessionConfig, RuntimeConfig runtimeConfig) { + _sessionConfig = sessionConfig; + _runtimeConfig = runtimeConfig; + } + } +} + + +// Replay/SessionContainer.Legacy.cs + +namespace Quantum.Legacy { + [Obsolete("Use Quantum.SessionContainer")] + public class SessionContainer { + DeterministicSessionConfig _sessionConfig; + RuntimeConfig _runtimeConfig; + + public DeterministicSession session; + public DeterministicSessionConfig config => _sessionConfig ?? replayFile.DeterministicConfig; + public RuntimeConfig runtimeConfig => _runtimeConfig ?? replayFile.RuntimeConfig; + public Native.Allocator allocator => _allocator.Value; + + + public IDeterministicReplayProvider provider; + public IDeterministicGame game; + public IResourceManager resourceManager; + public ReplayFile replayFile; + public IAssetSerializer assetSerializer; + public IEventDispatcher eventDispatcher; + public ICallbackDispatcher callbackDispatcher; + public int gameFlags; + + public static Boolean _loadedAllStatics = false; + public static Object _lock = new Object(); + + private static Lazy _allocator = new Lazy(() => CreateNativeAllocator()); + + public static Native.Allocator CreateNativeAllocator() { + switch (System.Environment.OSVersion.Platform) { + case PlatformID.Unix: + case PlatformID.MacOSX: + return new Native.LIBCAllocator(); + default: + return new Native.MSVCRTAllocator(); + } + } + + public static Native.Utility CreateNativeUtils() { + switch (System.Environment.OSVersion.Platform) { + case PlatformID.Unix: + case PlatformID.MacOSX: + return new Native.LIBCUtility(); + + default: + return new Native.MSVCRTUtility(); + } + } + + public void Start(bool logInitForConsole = true) { + + if (!_loadedAllStatics) { + lock (_lock) { + if (!_loadedAllStatics) { + // console first + if (logInitForConsole) { + Log.InitForConsole(); + } + + // try to figure out platform if not set + if (Native.Utils == null) { + Native.Utils = CreateNativeUtils(); + } + + if (MemoryLayoutVerifier.Platform == null) { + MemoryLayoutVerifier.Platform = new MemoryLayoutVerifier.DefaultPlatform(); + } + } + _loadedAllStatics = true; + } + } + + game = new QuantumGame(new QuantumGame.StartParameters() { + ResourceManager = resourceManager, + AssetSerializer = assetSerializer, + CallbackDispatcher = callbackDispatcher, + EventDispatcher = eventDispatcher, + GameFlags = gameFlags, + }); + + if (provider == null) { + provider = new InputProvider(config); + } + + DeterministicPlatformInfo info; + info = new DeterministicPlatformInfo(); + info.Allocator = allocator; + info.Architecture = DeterministicPlatformInfo.Architectures.x86; + info.RuntimeHost = DeterministicPlatformInfo.RuntimeHosts.PhotonServer; + info.Runtime = DeterministicPlatformInfo.Runtimes.NetFramework; + info.TaskRunner = new DotNetTaskRunner(); + + switch (System.Environment.OSVersion.Platform) { + case PlatformID.Unix: + info.Platform = DeterministicPlatformInfo.Platforms.Linux; + break; + + case PlatformID.MacOSX: + info.Platform = DeterministicPlatformInfo.Platforms.OSX; + break; + + default: + info.Platform = DeterministicPlatformInfo.Platforms.Windows; + break; + } + + DeterministicSessionArgs args; + args.Game = game; + args.Mode = DeterministicGameMode.Replay; + args.Replay = provider; + args.FrameData = null; + args.Communicator = null; + args.InitialTick = 0; + args.SessionConfig = config; + args.PlatformInfo = info; + args.RuntimeConfig = RuntimeConfig.ToByteArray(runtimeConfig); + + session = new DeterministicSession(args); + session.Join("server", config.PlayerCount); + } + + public void Service(double? dt = null) { + session.Update(dt); + } + + public void Destroy() { + if (session != null) + session.Destroy(); + session = null; + + //DB.Dispose(); + } + + public SessionContainer() { + _sessionConfig = null; + _runtimeConfig = null; + } + + public SessionContainer(DeterministicSessionConfig sessionConfig, RuntimeConfig runtimeConfig) { + _sessionConfig = sessionConfig; + _runtimeConfig = runtimeConfig; + } + } +} + + +// Systems/Base/SystemArrayComponent.cs + +namespace Quantum.Task { + public abstract unsafe class SystemArrayComponent : SystemBase where T : unmanaged, IComponent { + private TaskDelegateHandle _arrayTaskDelegateHandle; + + // internal max slices + private const int MAX_SLICES_COUNT = 32; + + public virtual int SlicesCount => MAX_SLICES_COUNT / 2; + + public sealed override void OnInit(Frame f) { + f.Context.TaskContext.RegisterDelegate(TaskArrayComponent, GetType().Name + ".Update", ref _arrayTaskDelegateHandle); + OnInitUser(f); + } + + protected virtual void OnInitUser(Frame f) { + + } + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + var slicesCount = Math.Max(1, Math.Min(SlicesCount, MAX_SLICES_COUNT)); + return f.Context.TaskContext.AddArrayTask(_arrayTaskDelegateHandle, null, f.ComponentCount(includePendingRemoval: true), taskHandle, slicesCount); + } + + private void TaskArrayComponent(FrameThreadSafe f, int start, int count, void* arg) { + var iterator = f.GetComponentBlockIterator(start, count).GetEnumerator(); + while (iterator.MoveNext()) { + var (entity, component) = iterator.Current; + Update(f, entity, component); + } + } + + public abstract void Update(FrameThreadSafe f, EntityRef entity, T* component); + } +} + +// Systems/Base/SystemArrayFilter.cs + +namespace Quantum.Task { + public abstract unsafe class SystemArrayFilter : SystemBase where T : unmanaged { + private TaskDelegateHandle _arrayTaskDelegateHandle; + private ComponentFilterStructMeta _filterMeta; + + // internal max slices + private const int MAX_SLICES_COUNT = 32; + + public virtual int SlicesCount => MAX_SLICES_COUNT / 2; + + public virtual bool UseCulling => true; + + public virtual ComponentSet Without => default; + + public virtual ComponentSet Any => default; + + public sealed override void OnInit(Frame f) { + _filterMeta = ComponentFilterStructMeta.Create(); + Assert.Check(_filterMeta.ComponentCount > 0, "Filter Struct '{0}' must have at least one component pointer.", typeof(T)); + + f.Context.TaskContext.RegisterDelegate(TaskArrayFilter, GetType().Name + ".Update", ref _arrayTaskDelegateHandle); + OnInitUser(f); + } + + protected virtual void OnInitUser(Frame f) { + + } + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + // figure out smallest block iterator + var taskSize = f.ComponentCount(_filterMeta.ComponentTypes[0], includePendingRemoval: true); + + for (var i = 1; i < _filterMeta.ComponentCount; ++i) { + var otherCount = f.ComponentCount(_filterMeta.ComponentTypes[i], includePendingRemoval: true); + if (otherCount < taskSize) { + taskSize = otherCount; + } + } + + var slicesCount = Math.Max(1, Math.Min(SlicesCount, MAX_SLICES_COUNT)); + return f.Context.TaskContext.AddArrayTask(_arrayTaskDelegateHandle, null, taskSize, taskHandle, slicesCount); + } + + private void TaskArrayFilter(FrameThreadSafe f, int start, int count, void* userData) { + // grab iterator + var iterator = f.FilterStruct(Without, Any, start, count); + + // set culling flag + iterator.UseCulling = UseCulling; + + var filter = default(T); + + // execute filter loop + while (iterator.Next(&filter)) { + Update(f, ref filter); + } + } + + public abstract void Update(FrameThreadSafe f, ref T filter); + } +} + +// Systems/Base/SystemBase.cs + +namespace Quantum { + public abstract partial class SystemBase { + Int32? _runtimeIndex; + String _scheduleSample; + SystemBase _parentSystem; + + public Int32 RuntimeIndex { + get { + return (Int32)_runtimeIndex; + } + set { + if (_runtimeIndex.HasValue) { + Log.Error("Can't change systems runtime index after game has started"); + } else { + _runtimeIndex = value; + } + } + } + + public SystemBase ParentSystem { + get { + return _parentSystem; + } + internal set { + _parentSystem = value; + } + } + + public virtual IEnumerable ChildSystems { + get { + return new SystemBase[0]; + } + } + + public IEnumerable Hierarchy { + get { + yield return this; + + foreach (var child in ChildSystems) { + foreach (var childHierarchy in child.Hierarchy) { + if (childHierarchy != null) { + yield return childHierarchy; + } + } + } + } + } + + public virtual Boolean StartEnabled { + get { return true; } + } + + public SystemBase() { + _scheduleSample = GetType().Name + ".Schedule"; + } + + public SystemBase(string scheduleSample) { + _scheduleSample = scheduleSample; + } + + public virtual void OnInit(Frame f) { + + } + + public virtual void OnEnabled(Frame f) { + + } + + public virtual void OnDisabled(Frame f) { + + } + + public TaskHandle OnSchedule(Frame f, TaskHandle taskHandle) { +#if DEBUG + var profiler = f.Context.ProfilerContext.GetProfilerForTaskThread(0); + try { + profiler.Start(_scheduleSample); +#endif + + return Schedule(f, taskHandle); + +#if DEBUG + } finally { + profiler.End(); + } +#endif + } + + protected abstract TaskHandle Schedule(Frame f, TaskHandle taskHandle); + } +} + +// Systems/Base/SystemGroup.cs + +namespace Quantum { + public unsafe class SystemGroup : SystemBase { + SystemBase[] _children; + + public sealed override IEnumerable ChildSystems { + get { return _children; } + } + + public SystemGroup(String name, params SystemBase[] children) : base(name + ".Schedule") { + _children = children; + + for (int i = 0; i < _children.Length; i++) { + _children[i].ParentSystem = this; + } + } + + protected sealed override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + if (_children != null) { + for (var i = 0; i < _children.Length; ++i) { + if (f.SystemIsEnabledSelf(_children[i])) { + try { + taskHandle = _children[i].OnSchedule(f, taskHandle); + } catch (Exception exn) { + Log.Exception(exn); + } + } + } + } + + return taskHandle; + } + + public override void OnEnabled(Frame f) { + base.OnEnabled(f); + + for (int i = 0; i < _children.Length; ++i) { + if (f.SystemIsEnabledSelf(_children[i])) { + _children[i].OnEnabled(f); + } + } + } + + public override void OnDisabled(Frame f) { + base.OnDisabled(f); + + for (int i = 0; i < _children.Length; ++i) { + if (f.SystemIsEnabledSelf(_children[i])) { + _children[i].OnDisabled(f); + } + } + } + } +} + +// Systems/Base/SystemMainThread.cs + +namespace Quantum { + public abstract unsafe class SystemMainThread : SystemBase { + TaskDelegateHandle _updateHandle; + + String _update; + + public SystemMainThread(string name) { + _update = name + ".Update"; + } + + public SystemMainThread() { + _update = GetType().Name + ".Update"; + } + + protected TaskHandle ScheduleUpdate(Frame f, TaskHandle taskHandle) { + if (_updateHandle.IsValid == false) { + f.Context.TaskContext.RegisterDelegate(TaskCallback, _update, ref _updateHandle); + } + + return f.Context.TaskContext.AddMainThreadTask(_updateHandle, null, taskHandle); + } + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + return ScheduleUpdate(f, taskHandle); + } + + void TaskCallback(FrameThreadSafe frame, int start, int count, void* arg) { + Update((Frame)frame); + + if (((FrameBase)frame).CommitCommandsMode == CommitCommandsModes.InBetweenSystems) { + ((FrameBase)frame).Unsafe.CommitAllCommands(); + } + } + + public abstract void Update(Frame f); + } +} + +// Systems/Base/SystemMainThreadFilter.cs + +namespace Quantum { + public abstract unsafe class SystemMainThreadFilter : SystemMainThread where T : unmanaged { + public virtual bool UseCulling { + get { return true; } + } + + public virtual ComponentSet Without { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => default; + } + + public virtual ComponentSet Any { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => default; + } + + public sealed override void Update(Frame f) { + // grab iterator + var it = f.Unsafe.FilterStruct(Without, Any); + + // set culling flag + it.UseCulling = UseCulling; + + // execute filter loop + var filter = default(T); + + while (it.Next(&filter)) { + Update(f, ref filter); + } + } + + public abstract void Update(Frame f, ref T filter); + } +} + +// Systems/Base/SystemMainThreadGroup.cs + +namespace Quantum { + public unsafe class SystemMainThreadGroup : SystemMainThread { + SystemMainThread[] _children; + + public SystemMainThreadGroup(string name, params SystemMainThread[] children) + : base(name + ".Schedule") { + Assert.Check(name != null); + Assert.Check(children != null); + + _children = children; + + for (int i = 0; i < _children.Length; i++) { + _children[i].ParentSystem = this; + } + } + + public sealed override IEnumerable ChildSystems { + get { return _children; } + } + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + if (_children != null) { + for (var i = 0; i < _children.Length; ++i) { + if (f.SystemIsEnabledSelf(_children[i])) { + try { + taskHandle = _children[i].OnSchedule(f, taskHandle); + } catch (Exception exn) { + Log.Exception(exn); + } + } + } + } + + return taskHandle; + } + + public override void OnEnabled(Frame f) { + base.OnEnabled(f); + + for (int i = 0; i < _children.Length; ++i) { + if (f.SystemIsEnabledSelf(_children[i])) { + _children[i].OnEnabled(f); + } + } + } + + public override void OnDisabled(Frame f) { + base.OnDisabled(f); + + for (int i = 0; i < _children.Length; ++i) { + if (f.SystemIsEnabledSelf(_children[i])) { + _children[i].OnDisabled(f); + } + } + } + + public sealed override void Update(Frame f) { + } + } +} + +// Systems/Base/SystemSignalsOnly.cs + +namespace Quantum { + public class SystemSignalsOnly : SystemBase { + protected sealed override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + return taskHandle; + } + } +} + +// Systems/Base/SystemThreadedComponent.cs + +namespace Quantum.Task { + public abstract unsafe class SystemThreadedComponent : SystemBase where T : unmanaged, IComponent { + private TaskDelegateHandle _threadedTaskDelegateHandle; + private int _sliceIndexer; + private int _sliceSize; + + public const int DEFAULT_SLICE_SIZE = 16; + + public virtual int SliceSize => DEFAULT_SLICE_SIZE; + + public sealed override void OnInit(Frame f) { + f.Context.TaskContext.RegisterDelegate(TaskThreadedComponent, GetType().Name + ".Update", ref _threadedTaskDelegateHandle); + OnInitUser(f); + } + + protected virtual void OnInitUser(Frame f) { + + } + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + // reset indexer + _sliceIndexer = -1; + + // cache slice size safely in main-thread + _sliceSize = Math.Max(1, SliceSize); + + return f.Context.TaskContext.AddThreadedTask(_threadedTaskDelegateHandle, null, taskHandle); + } + + private void TaskThreadedComponent(FrameThreadSafe f, int start, int count, void* userData) { + while (true) { + var sliceIndex = Interlocked.Increment(ref _sliceIndexer); + var iterator = f.GetComponentBlockIterator(sliceIndex * _sliceSize, _sliceSize).GetEnumerator(); + + if (iterator.MoveNext() == false) { + // chunk is out of buffer range, we're done + return; + } + + do { + var (entity, component) = iterator.Current; + Update(f, entity, component); + } while (iterator.MoveNext()); + } + } + + public abstract void Update(FrameThreadSafe f, EntityRef entity, T* component); + } +} + +// Systems/Base/SystemThreadedFilter.cs + +namespace Quantum.Task { + public abstract unsafe class SystemThreadedFilter : SystemBase where T : unmanaged { + private TaskDelegateHandle _threadedTaskDelegateHandle; + private int _sliceIndexer; + private int _sliceSize; + + public const int DEFAULT_SLICE_SIZE = 16; + + public virtual int SliceSize => DEFAULT_SLICE_SIZE; + + public virtual bool UseCulling => true; + + public virtual ComponentSet Without => default; + + public virtual ComponentSet Any => default; + + public sealed override void OnInit(Frame f) { + f.Context.TaskContext.RegisterDelegate(TaskThreadedFilter, GetType().Name + ".Update", ref _threadedTaskDelegateHandle); + OnInitUser(f); + } + + protected virtual void OnInitUser(Frame f) { + + } + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + // reset indexer + _sliceIndexer = -1; + + // cache slice size safely in main-thread + _sliceSize = Math.Max(1, SliceSize); + + return f.Context.TaskContext.AddThreadedTask(_threadedTaskDelegateHandle, null, taskHandle); + } + + private void TaskThreadedFilter(FrameThreadSafe f, int start, int count, void* userData) { + var iterator = f.FilterStruct(Without, Any); + + // set culling flag + iterator.UseCulling = UseCulling; + + var filter = default(T); + + while (true) { + var sliceIndex = Interlocked.Increment(ref _sliceIndexer); + + // reset iterator + iterator.Reset(sliceIndex * _sliceSize, _sliceSize); + + // execute filter loop + if (iterator.Next(&filter) == false) { + // chunk is out of buffer range, we're done + return; + } + + do { + Update(f, ref filter); + } while (iterator.Next(&filter)); + } + } + + public abstract void Update(FrameThreadSafe f, ref T filter); + } +} + +// Systems/Core/DebugSystem.cs + +namespace Quantum.Core { + + public static partial class DebugCommandType { + public const int Create = 0; + public const int Destroy = 1; + public const int UserCommandTypeStart = 1000; + } + + public static partial class DebugCommand { + + public static event Action CommandExecuted +#if DEBUG && !QUANTUM_DEBUG_COMMAND_DISABLED + { + add => _commandExecuted += value; + remove => _commandExecuted -= value; + } + private static Action _commandExecuted; +#else + { add { } remove { } } +#endif + + public static bool IsEnabled => +#if DEBUG && !QUANTUM_DEBUG_COMMAND_DISABLED + true; +#else + false; +#endif + + public static void Send(QuantumGame game, params Payload[] payload) { +#if DEBUG && !QUANTUM_DEBUG_COMMAND_DISABLED + game.SendCommand(new InternalCommand() { + Data = payload + }); +#else + Log.Warn("DebugCommand works only in DEBUG builds without QUANTUM_DEBUG_COMMAND_DISABLED define."); +#endif + } + + public static void Reset() { +#if DEBUG && !QUANTUM_DEBUG_COMMAND_DISABLED + _commandExecuted = null; +#endif + } + + + public partial struct Payload { + public long Id; + public int Type; + public EntityRef Entity; + public ComponentSet Components; + public byte[] Data; + } + + + public static Payload CreateDestroyPayload(EntityRef entityRef) { + return new Payload() { + Type = DebugCommandType.Destroy, + Entity = entityRef + }; + } + + public static Payload CreateMaterializePayload(EntityRef entityRef, EntityPrototype prototype, IAssetSerializer serializer) { + ComponentSet componentSet = default; + foreach (var component in prototype.Container.Components) { + componentSet.Add(ComponentTypeId.GetComponentIndex(component.ComponentType)); + } + return new Payload() { + Type = DebugCommandType.Create, + Entity = entityRef, + Data = serializer.SerializeAssets(new[] { prototype }), + Components = componentSet + }; + } + + public static Payload CreateRemoveComponentPayload(EntityRef entityRef, Type componentType) { + var components = new ComponentSet(); + components.Add(ComponentTypeId.GetComponentIndex(componentType)); + + return new Payload() { + Type = DebugCommandType.Destroy, + Entity = entityRef, + Components = components + }; + } + +#if QUANTUM_DEBUG_COMMAND_DISABLED + internal static DeterministicCommand CreateCommand() => null; + internal static SystemBase CreateSystem() => null; +#else + internal static DeterministicCommand CreateCommand() => new InternalCommand(); + internal static SystemBase CreateSystem() => new InternalSystem(); + +#if DEBUG + private static void Execute(Frame f, ref Payload payload) { + Exception error = null; + try { + switch (payload.Type) { + case DebugCommandType.Create: + payload.Entity = ExecuteCreate(f, payload.Entity, payload.Data); + break; + case DebugCommandType.Destroy: + ExecuteDestroy(f, payload.Entity, payload.Components); + break; + default: + if (payload.Type >= DebugCommandType.UserCommandTypeStart) { + ExecuteUser(f, ref payload); + } else { + throw new InvalidOperationException($"Unknown command type: {payload.Type}"); + } + break; + } + } catch (Exception ex) { + error = ex; + } + _commandExecuted?.Invoke(payload, error); + } + + private static void ExecuteDestroy(Frame f, EntityRef entity, ComponentSet components) { + if (!f.Exists(entity)) { + Log.Error("Entity does not exist: {0}", entity); + } else if (components.IsEmpty) { + if (!f.Destroy(entity)) { + Log.Error("Failed to destroy entity {0}", entity); + } + } else { + for (int i = 1; i < ComponentTypeId.Type.Length; ++i) { + if (!components.IsSet(i)) { + continue; + } + var type = ComponentTypeId.Type[i]; + if (!f.Remove(entity, type)) { + Log.Error("Failed to destroy component {0} of entity {1}", type, entity); + } + } + + } + } + + private static EntityRef ExecuteCreate(Frame f, EntityRef entity, byte[] data) { + EntityPrototype prototype = null; + if (data?.Length > 0) { + prototype = f.Context.AssetSerializer.DeserializeAssets(data).OfType().FirstOrDefault(); + if (prototype == null) { + Log.Error("No prototype found"); + } + } + + if (!entity.IsValid) { + if (prototype != null) { + entity = f.Create(prototype); + } else { + entity = f.Create(); + } + } else if (prototype != null) { + f.Set(entity, prototype, out _); + } + + return entity; + } + + static partial void ExecuteUser(Frame f, ref Payload payload); + static partial void SerializeUser(BitStream stream, ref Payload payload); + + private class InternalCommand : DeterministicCommand { + public Payload[] Data = { }; + + public override void Serialize(BitStream stream) { + stream.SerializeArrayLength(ref Data); + + for (int i = 0; i < Data.Length; ++i) { + stream.Serialize(ref Data[i].Id); + stream.Serialize(ref Data[i].Type); + stream.Serialize(ref Data[i].Entity); + stream.Serialize(ref Data[i].Data); + unsafe { + var set = Data[i].Components; + for (int block = 0; block < ComponentSet.BLOCK_COUNT; ++block) { + stream.Serialize((&set)->_set + block); + } + Data[i].Components = set; + } + SerializeUser(stream, ref Data[i]); + } + } + } + + private class InternalSystem : SystemMainThread { + public override void Update(Frame f) { + for (int p = 0; p < f.PlayerCount; ++p) { + if (f.GetPlayerCommand(p) is InternalCommand cmd) { + for (int i = 0; i < cmd.Data.Length; ++i) { + Execute(f, ref cmd.Data[i]); + } + } + } + } + } +#else + private class InternalCommand : DeterministicCommand { + public override void Serialize(BitStream stream) { + throw new NotSupportedException("DebugCommands only work in DEBUG mode"); + } + } + + private class InternalSystem : SystemBase { + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + return taskHandle; + } + } +#endif +#endif + } +} + + +// Systems/Core/NavigationSystem.cs + +namespace Quantum.Core { + public unsafe class NavigationSystem : SystemBase, INavigationCallbacks { + Frame _f; + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + _f = f; + return f.Navigation.Update(f, f.DeltaTime, this, taskHandle); + } + + public void OnWaypointReached(EntityRef entity, FPVector3 waypoint, Navigation.WaypointFlag waypointFlags, ref bool resetAgent) { + _f.Signals.OnNavMeshWaypointReached(entity, waypoint, waypointFlags, ref resetAgent); + } + + public void OnSearchFailed(EntityRef entity, ref bool resetAgent) { + _f.Signals.OnNavMeshSearchFailed(entity, ref resetAgent); + } + + public void OnMoveAgent(EntityRef entity, FPVector2 desiredDirection) { + _f.Signals.OnNavMeshMoveAgent(entity, desiredDirection); + } + } +} + +// Systems/Core/EntityPrototypeSystem.cs +namespace Quantum.Core { + public unsafe sealed partial class EntityPrototypeSystem : SystemSignalsOnly, ISignalOnMapChanged { + public override void OnInit(Frame f) { + OnMapChanged(f, default); + } + + public void OnMapChanged(Frame f, AssetRefMap previousMap) { + if (previousMap.Id.IsValid) { + foreach (var (entity, _) in f.GetComponentIterator()) { + f.Destroy(entity); + } + } + + if (f.Map != null) { + f.Create(f.Map.MapEntities, f.Map); + } + } + } +} + +// Systems/Core/CullingSystem.cs + +namespace Quantum.Core { + public unsafe class CullingSystem2D : SystemBase { + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + return f.Context.Culling.Schedule2D(f, taskHandle); + } + } + + public unsafe class CullingSystem3D : SystemBase { + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + return f.Context.Culling.Schedule3D(f, taskHandle); + } + } +} + +// Systems/Core/PlayerConnectedSystem.cs +namespace Quantum.Core { + unsafe class PlayerConnectedSystem : SystemMainThread { + public override void Update(Frame f) { + if (f.IsVerified == false) { + return; + } + + for (int p = 0; p < f.PlayerCount; p++) { + var isPlayerConnected = (f.GetPlayerInputFlags(p) & Photon.Deterministic.DeterministicInputFlags.PlayerNotPresent) == 0; + if (isPlayerConnected != f.Global->PlayerLastConnectionState.IsSet(p)) { + if (isPlayerConnected) { + f.Signals.OnPlayerConnected(p); + } else { + f.Signals.OnPlayerDisconnected(p); + } + + if (isPlayerConnected) { + f.Global->PlayerLastConnectionState.Set(p); + } + else { + f.Global->PlayerLastConnectionState.Clear(p); + } + } + } + } + } +} + + + +// Systems/Core/PhysicsSystem.cs + +namespace Quantum.Core { + public unsafe partial class PhysicsSystem2D : SystemBase, ICollisionCallbacks2D { + public override void OnInit(Frame f) { + f.Physics2D.Init(); + } + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + return f.Physics2D.Update(this, f.DeltaTime, taskHandle); + } + + public void OnCollision2D(FrameBase f, CollisionInfo2D info) { + ((Frame)f).Signals.OnCollision2D(info); + } + + public void OnCollisionEnter2D(FrameBase f, CollisionInfo2D info) { + ((Frame)f).Signals.OnCollisionEnter2D(info); + } + + public void OnCollisionExit2D(FrameBase f, ExitInfo2D info) { + ((Frame)f).Signals.OnCollisionExit2D(info); + } + + public void OnTrigger2D(FrameBase f, TriggerInfo2D info) { + ((Frame)f).Signals.OnTrigger2D(info); + } + + public void OnTriggerEnter2D(FrameBase f, TriggerInfo2D info) { + ((Frame)f).Signals.OnTriggerEnter2D(info); + } + + public void OnTriggerExit2D(FrameBase f, ExitInfo2D info) { + ((Frame)f).Signals.OnTriggerExit2D(info); + } + } + + public unsafe partial class PhysicsSystem3D : SystemBase, ICollisionCallbacks3D { + public override void OnInit(Frame f) { + f.Physics3D.Init(); + } + + protected override TaskHandle Schedule(Frame f, TaskHandle taskHandle) { + return f.Physics3D.Update(this, f.DeltaTime, taskHandle); + } + + public void OnCollision3D(FrameBase f, CollisionInfo3D info) { + ((Frame)f).Signals.OnCollision3D(info); + } + + public void OnCollisionEnter3D(FrameBase f, CollisionInfo3D info) { + ((Frame)f).Signals.OnCollisionEnter3D(info); + } + + public void OnCollisionExit3D(FrameBase f, ExitInfo3D info) { + ((Frame)f).Signals.OnCollisionExit3D(info); + } + + public void OnTrigger3D(FrameBase f, TriggerInfo3D info) { + ((Frame)f).Signals.OnTrigger3D(info); + } + + public void OnTriggerEnter3D(FrameBase f, TriggerInfo3D info) { + ((Frame)f).Signals.OnTriggerEnter3D(info); + } + + public void OnTriggerExit3D(FrameBase f, ExitInfo3D info) { + ((Frame)f).Signals.OnTriggerExit3D(info); + } + } +} diff --git a/data/DSL.txt b/data/DSL.txt new file mode 100644 index 0000000000000000000000000000000000000000..eade94df496a3953cf1cbf86c3b6982e987e2c10 --- /dev/null +++ b/data/DSL.txt @@ -0,0 +1,502 @@ +Introduction +Quantum requires components and other runtime game state data types to be declared with its own DSL (domain-specific-language). + +These definitions are written into text files with the .qtn extension. The Quantum compiler will parse them into an AST, and generate partial C# struct definitions for each type (definitions can be split across as many files if needed, the compiler will merge them accordingly). + +The goal of the DSL is to abstract away from the developer the complex memory alignment requirements imposed by Quantum's ECS sparse set memory model, required to support the deterministic predict/rollback approach to simulation. + +This code generation approach also eliminates the need to write "boiler-plate" code for type serialization (used for snapshots, game saves, killcam replays), checksumming and other functions, like printing/dumping frame data for debugging purposes. + +The quantum dsl integration section shows how to add qtn files to the workflow. + +Components +Components are special structs that can be attached to entities, and used for filtering them (iterating only a subset of the active entities based on its attached components). This is a basic example definition of a component: + +component Action +{ + FP Cooldown; + FP Power; +} +These will be turned into regular C# structs. Labelling them as components (like above) will generate the appropriate code structure (marker interface, id property, etc). + +Aside from custom components, Quantum comes with several pre-built ones: + +Transform2D/Transform3D: position and rotation using Fixed Point (FP) values; +PhysicsCollider, PhysicsBody, PhysicsCallbacks, PhysicsJoints (2D/3D): used by Quantum's stateless physics engines; +PathFinderAgent, SteeringAgent, AvoidanceAgent, AvoidanceObstacle: navmesh-based path finding and movement. +Back To Top + + +Structs +Structs can be defined in both the DSL and C#. + + +DSL Defined +The Quantum DSL also allows the definition of regular structs (just like components, memory alignment, and helper functions will be taken care of): + +struct ResourceItem +{ + FP Value; + FP MaxValue; + FP RegenRate; +} +The fields will be ordered in alphabetical order when the struct is generated. If you need / want to have them appear in a specific order, you will have to define the structs in C# (see section below). + +This would let you use the "Resources" struct as a type in all other parts of the DSL, for example using it inside a component definition: + +component Resources +{ + ResourceItem Health; + ResourceItem Strength; + ResourceItem Mana; +} +The generated struct is partial and can be extended in C# if so desired. + +Back To Top + + +CSharp Defined +You can define structs in C# as well; however, in this case you will have to manually define the memory aligned of the struct. + +[StructLayout(LayoutKind.Explicit)] +public struct Foo { + public const int SIZE = 12; // the size in bytes of all members in bytes. + + [FieldOffset(0)] + public int A; + + [FieldOffset(4)] + public int B; + + [FieldOffset(8)] + public int C; +} +When using C# defined structs in the DSL (e.g. inside components), you will have to manually import the struct definition. + +import struct Foo(12); +N.B.: The import does not support constants in the size; you will have to specify the exact numerical value each time. + +Back To Top + + +Components Vs. Structs +An important question is why and when should components be used instead of regular structs (components, in the end, are also structs). + +Components contain generated meta-data that turns them into a special type with the following features: + +Can be attached directly to entities; +Used to filter entities when traversing the game state (next chapter will dive into the filter API); +Components can accessed, used or passed as parameters as either pointers or as value types, just like any other struct. + +Back To Top + + +Dynamic Collections +Quantum's custom allocator exposes blittable collections as part of the rollback-able game state. Collections only support support blittable types (i.e. primitive and DSL-defined types). + +To manage collection, the Frame API offers 3 methods for each: + +Frame.AllocateXXX: To allocate space for the collection on the heap. +Frame.FreeXXX: To free/deallocate the collection's memory. +Frame.ResolveXXX: To access the collection by resolving the pointer it. +Note: After freeing a collection, it HAS TO be nullified by setting it to default. This is required for serialization of the game state to work properly. Omitting the nullification will result in indeterministic behavior and desynchronisation. As alternative to freeing a collection and nullifying its Ptrs manually, it possible to use the FreeOnComponentRemoved attribute on the field in question. + +Back To Top + + +Important Notes +Several components can reference the same collection instance. +Dynamic collections are stored as references inside components and structs. They therefore have to to be allocated when initializing them, and more importantly, freed when they are not needed any more. If the collection is part of a component, two options are available: +implement the reactive callbacks ISignalOnAdd and ISignalOnRemove and allocate/free the collections there. (For more information on these specific signals, see the Components page in the ECS section of the Manual); or, +use the [AllocateOnComponentAdded] and [FreeOnComponentRemoved] attributes to let Quantum handle the allocation and deallocation when the component is added and removed respectively. +Quantum do NOT pre-allocate collections from prototypes, unless there is at least value. If the collection is empty, the memory has to be manually allocated. +Attempting to free a collection more than once will throw an error and puts the heap in an invalid state internally. +Back To Top + + +Lists +Dynamic lists can be defined in the DSL using list MyList. + +component Targets { + list Enemies; +} +The basic API methods for dealing with these Lists are: + +Frame.AllocateList() +Frame.FreeList(QListPtr ptr) +Frame.ResolveList(QListPtr ptr) +Once resolved, a list can be iterated over or manipulated with all the expected API methods of a list such as Add, Remove, Contains, IndexOf, RemoveAt, [], etc... . + +To use the list in the component of type Targets defined in the code snippet above, you could create the following system: + +namespace Quantum +{ + public unsafe class HandleTargets : SystemMainThread, ISignalOnComponentAdded, ISignalOnComponentRemoved + { + public override void Update(Frame f) + { + var targets = f.GetComponentIterator(); + + for (int i = 0; i < targets.Count; i++) + { + var target = targets[i]; + + // To use a list, you must first resolve its pointer via the frame + var list = frame.ResolveList(target.Enemies); + + // Do stuff + } + } + + public void OnAdded(Frame f, EntityRef entity, Targets* component) + { + // allocating a new List (returns the blittable reference type - QListPtr) + component->Enemies = f.AllocateList(); + } + + public void OnRemoved(Frame f, EntityRef entity, Targets* component) + { + // A component HAS TO de-allocate all collection it owns from the frame data, otherwise it will lead to a memory leak. + // receives the list QListPtr reference. + f.FreeList(component->Enemies); + + // All dynamic collections a component points to HAVE TO be nullified in a component's OnRemoved + // EVEN IF is only referencing an external one! + // This is to prevent serialization issues that otherwise lead to a desynchronisation. + component->Enemies = default; + } + } +} +Back To Top + + +Dictionaries +Dictionaries can be declared in the DSL like so dictionary MyDictionary. + +component Hazard{ + dictionary DamageDealt; +} +The basic API methods for dealing with these dictionaries are: + +Frame.AllocateDictionary() +Frame.FreeDictionary(QDictionaryPtr ptr) +Frame.ResolveDictionary(QDictionaryPtr ptr) +Just like with any other dynamic collection it is mandatory to allocate it before using it, as well as de-allocate it from the frame data and nullified it once the dictionary is no longer used. See the example provided in the section about lists here above. + +Back To Top + + +HashSet +HashSets can be declared in the DSL like so hash_set MyHashSet. + +component Nodes{ + hash_set ProcessedNodes; +} +The basic API methods for dealing with these dictionaries are: + +Frame.AllocateHashSet(QHashSetPtr ptr, int capacity = 8) +Frame.FreeHashSet(QHashSetPtr ptr) +Frame.ResolveHashSet(QHashSetPtr ptr) +Just like with any other dynamic collection it is mandatory to allocate it before using it, as well as de-allocate it from the frame data and nullified it once the hash set is no longer used. See the example provided in the section about lists here above. + +Back To Top + + +Unions, Enums And Bitsets +C-like unions and enums can be generated as well. The example below demonstrates how to save data memory by overlapping some mutually exclusive data types/values into a union: + +struct DataA +{ + FPVector2 Something; + FP Anything; +} + +struct DataB +{ + FPVector3 SomethingElse; + Int32 AnythingElse; +} + +union Data +{ + DataA A; + DataB B; +} +The generated type Data will also include a differentiator property (to tell which union-type has been populated). "Touching" any of the union sub-types will set this property to the appropriate value. + +Bitsets can be used to declared fixed-size memory blocks for any desired purpose (for example fog-of-war, grid-like structures for pixel perfect game mechanics, etc.): + +struct FOWData +{ + bitset[256] Map; +} +Back To Top + + +Input +In Quantum, the runtime input exchanged between clients is also declared in the DSL. This example defines a simple movement vector and a Fire button as input for a game: + +input +{ + FPVector2 Movement; + button Fire; +} +The input struct is polled every tick and sent to the server (when playing online). + +For more information about input, such as best practices and recommended approaches to optimization, refer to this page: input + +Back To Top + + +Signals +Signals are function signatures used as a decoupled inter-system communication API (a form of publisher/subscriber API). This would define a simple signal (notice the special type entity_ref - these will be listed at the end of this chapter): + +signal OnDamage(FP damage, entity_ref entity); +This would generate the following interface (that can be implemented by any System): + +public interface ISignalOnDamage +{ + public void OnDamage(Frame f, FP damage, EntityRef entity); +} +Signals are the only concept which allows the direct declaration of a pointer in Quantum's DSL, so passing data by reference can be used to modify the original data directly in their concrete implementations: + +signal OnBeforeDamage(FP damage, Resources* resources); +Notice this allows the passing of a component pointer (instead of the entity reference type). + +Back To Top + + +Events +Events are a fine-grained solution to communicate what happens inside the simulation to the rendering engine / view (they should never be used to modify/update part of the game state). Use the "event" keyword to define its name and data: + +Find detailed information about events in the game events manual. + +Define an event using the Quantum DSL + +event MyEvent{ + int Foo; +} +Trigger the event from the simulation + +f.Events.MyEvent(2022); +And subscribe and consume the event in Unity + +QuantumEvent.Subscribe(listener: this, handler: (MyEvent e) => Debug.Log($"MyEvent {e.Foo}")); +Back To Top + + +Globals +It is possible to define globally accessible variables in the DSL. Globals can be declared in any .qtn file by using the global scope. + +global { + // Any type that is valid in the DSL can also be used. + FP MyGlobalValue; +} +Like all things DSL-defined, global variables are part of the state and are fully compatible with the predict-rollback system. + +Variables declared in the global scope are made available through the Frame API. They can be accessed (read/write) from any place that has access to the frame - see the Systems document in the ECS section. + +N.B.: An alternative to global variables are the Singleton Components; for more information please refer to the Components page in the ECS section of the manual. + +Back To Top + + +Special Types +Quantum has a few special types that are used to either abstract complex concepts (entity reference, player indexes, etc.), or to protect against common mistakes with unmanaged code, or both. The following special types are available to be used inside other data types (including in components, also in events, signals, etc.): + +player_ref: represents a runtime player index (also cast to and from Int32). When defined in a component, can be used to store which player controls the associated entity (combined with Quantum's player-index-based input). +entity_ref: because each frame/tick data in quantum resides on a separate memory region/block (Quantum keeps a a few copies to support rollbacks), pointers cannot be cached in-between frames (nor in the game state neither in Unity scripts). An entity ref abstracts an entity's index and version properties (protecting the developer from accidentally accessing deprecated data over destroyed or reused entity slots with old refs). +asset_ref: rollback-able reference to a data asset instance from the Quantum asset database (please refer to the data assets chapter). +list, dictionary: dynamic collection references (stored in Quantum's frame heap). Only supports blittable types (primitives + DSL-defined types). +array[size]: fixed sized "arrays" to represent data collections. A normal C# array would be a heap-allocated object reference (it has properties, etc.), which violates Quantum's memory requirements, so the special array type generates a pointer based simple API to keep rollback-able data collections inside the game state; +Back To Top + + +A Note On Assets +Assets are a special feature of Quantum that let the developer define data-driven containers (normal classes, with inheritance, polymorphic methods, etc.) that end up as immutable instances inside an indexed database. The "asset" keyword is used to assign an (existing) class as a data asset that can have references assigned inside the game state (please refer to the Data Assets chapter to learn more about features and restrictions): + +asset CharacterData; // the CharacterData class is partially defined in a normal C# file by the developer +The following struct show some valid examples of the types above (sometimes referencing previously defined types): + +struct SpecialData +{ + player_ref Player; + entity_ref Character; + entity_ref AnotherEntity; + asset_ref CharacterData; + array[10] TenNumbers; +} +Back To Top + + +Available Types +When working in the DSL, you can use a variety of types. Some are pre-imported by the parsers, while others need to be manually imported. + + +By Default +Quantum's DSL parser has a list of pre-imported cross-platform deterministic types that can be used in the game state definition: + +Boolean / bool - internally gets wrapped in QBoolean which works identically (get/set, compare, etc...) +Byte +SByte +UInt16 / Int16 +UInt32 / Int32 +UInt64 / Int64 +FP +FPVector2 +FPVector3 +FPMatrix +FPQuaternion +PlayerRef / player_ref in the DSL +EntityRef / entity_ref in the DSL +LayerMask +NullableFP / FP? in the DSL +NullableFPVector2 / FPVector2? in the DSL +NullableFPVector3 / FPVector3? in the DSL +QString is for UTF-16 (aka Unicode in .NET) +QStringUtf8 is always UTF-8 +Hit +Hit3D +Shape2D +Shape3D +Joint, DistanceJoint, SpringJoint and HingeJoint +Note on QStrings: N represents the total size of the string in bytes minus 2 bytes used for bookkeeping. In other words QString<64> will use 64 bytes for a string with a max byte length of 62 bytes, i.e. up to 31 UTF-16 characters. + +Back To Top + + +Manual Import +If you need a type that is not listed in the previous section, you will have to import it manually when using it in QTN files. + + +Namespaces / Types Outside Of Quantum +To import types defined in other namespaces, you can use the following syntax: + +import MyInterface; +or +import MyNameSpace.Utils; +For an enum the syntax is as follows: + +import enum MyEnum(underlying_type); + +// This syntax is identical for Quantum specific enums +import enum Shape3DType(byte); +Back To Top + + +Built-In Quantum Type And Custom Type +When importing a Quantum built-in type or a custom type, the struct size is predefined in their C# declaration. It is therefore important to add some safety measures. + +namespace Quantum { + [StructLayout(LayoutKind.Explicit)] + public struct Foo { + public const int SIZE = sizeof(Int32) * 2; + [FieldOffset(0)] + public Int32 A; + [FieldOffset(sizeof(Int32))] + public Int32 B; + } +} +#define FOO_SIZE 8 // Define a constant value with the known size of the struct +import struct Foo(8); +To ensure the expected size of the struct is equal to the actual size, it is recommended to add an Assert as shown below in one of your systems. + +public unsafe class MyStructSizeCheckingSystem : SystemMainThread{ + public override void OnInit(Frame f) + { + Assert.Check(Constants.FOO_SIZE == Foo.SIZE); + } +} +If the size of the built-in struct changes during an upgrade, this Assert will throw and allow you to update the values in the DSL. + +Back To Top + + +Attributes +Quantum supports several attributes to present parameters in the Inspector. + +The attributes are contained within the Quantum.Inspector namespace. + +Attribute Parameters Description +DrawIf string fieldName +long value +CompareOperator compare +HideType hide Displays the property only if the condition evaluates to true. + +fieldName = the name of the property to evaluate. +value = the value used for comparison. +compare = the comparison operation to be performed Equal, NotEqual, Less, LessOrEqual, GreaterOrEqual or Greater. +hide = the field's behavior when the expression evaluates to False:Hide or ReadOnly. + +For more information on compare and hide, see below. +Header string header Adds a header above the property. + +header = the header text to display. +HideInInspector - Serializes the field and hides the following property in the Unity inspector. +Layer - Can only be applied to type int. +Will call EditorGUI.LayerField on the field. +Optional string enabledPropertyPath Allows to turn the display of a property on/off. + +enabledPropertyPath = the path to the bool used to evaluate the toggle. +Space - Adds a space above the property +Tooltip string tooltip Displays a tool tip when hovering over the property. + +tooltip = the tip to display. +ArrayLength (since 2.1) +FixedArray (in 2.0) +ONLY FOR CSharp int length +-------- +int minLength +int maxLength Using length allows to define the size of a an array. +------ +Using minLength and maxLength allows to define a range for the size in the Inspector. +The final size can then be set in the Inspector. +(minLength and maxLength are inclusive) +ExcludeFromPrototype - Can be applied to both a component and component fields. +------ +- Field: Excludes field from a the prototype generated for the component. +- Component: No prototype will be generated for this component. +PreserveInPrototype - Added to a type marks it as usable in prototypes and prevents prototype class from being emit. +Added to a field only affects a specific field. Useful for simple [Serializable] structs as it avoids having to use _Prototype types on Unity side. +AllocateOnComponentAdded - Can be applied to dynamic collections. +This will allocate memory for the collection if it has not already been allocated when the component holding the collection is added to an entity. +FreeOnComponentRemoved - Can be applied to dynamic collections and Ptrs. +This will deallocate the associated memory and nullify the Ptr held in the field when the component is removed. +------ +IMPORTANT: Do NOT use this attribute in combination with cross-referenced collections as it only nullifies the Ptr held in that particular field and the others will be pointing to invalid memory. +The *Attributes* can be used in both C# and qtn files unless otherwise specified; however, there are some syntactic differences. +Back To Top + + +Use In CSharp +In C# files, attributes can be used and concatenated like any other attribute. + +// Multiple single attributes +[Header("Example Array")][Tooltip("min = 1\nmax = 20")] public FP[] TestArray = new FP[20]; + +// Multiple concatenated attributes +[Header("Example Array"), Tooltip("min = 1\nmax = 20")] public FP[] TestArray = new FP[20]; +Back To Top + + +Use In Qtn +In qtn files, the usage of single attributes remains the same as in C#. + +[Header("Example Array")] array[20] TestArray; +When combining multiple attributes, they have to be concatenated. + +[Header("Example Array"), Tooltip("min = 1\nmax = 20")] array[20] TestArray; +Back To Top + + +Compiler Options +The following compiler options are currently available to be used inside Quantum's DSL files (more will be added in the future): + +// pre defining max number of players (default is 6, absolute max is 64) +#pragma max_players 16 + +// numeric constants (useable inside the DSL by MY_NUMBER and useable in code by Constants.MY_NUMBER) +#define MY_NUMBER 10 + +// overriding the base class name for the generated constants (default is "Constants") +#pragma constants_class_name MyFancyConstants \ No newline at end of file diff --git a/data/DeadPieceSlot.cs b/data/DeadPieceSlot.cs new file mode 100644 index 0000000000000000000000000000000000000000..0bc0fa3a8747e118cf44472cb59dac24b4823d8b --- /dev/null +++ b/data/DeadPieceSlot.cs @@ -0,0 +1,8 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class DeadPieceSlot : MonoBehaviour { + + public PieceView Piece; +} diff --git a/data/DebugAction.cs b/data/DebugAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..6f29e63a2e0075fb96874110637454c5525f27a0 --- /dev/null +++ b/data/DebugAction.cs @@ -0,0 +1,13 @@ +namespace Quantum +{ + [System.Serializable] + public unsafe class DebugAction : AIAction + { + public string Message; + + public override void Update(Frame frame, EntityRef entity) + { + Log.Info(Message); + } + } +} diff --git a/data/DebugLeaf.cs b/data/DebugLeaf.cs new file mode 100644 index 0000000000000000000000000000000000000000..f81dfae67085ed29e3d9294fd941b8824fefdcfc --- /dev/null +++ b/data/DebugLeaf.cs @@ -0,0 +1,23 @@ +using System; + +namespace Quantum +{ + [Serializable] + public unsafe partial class DebugLeaf : BTLeaf + { + + public string Message; + + /// + /// When Update is called, we just write a message on the console. + /// This Leaf never fails, nor takes more than one frame to finish, + /// so we always return Success. + /// + protected override BTStatus OnUpdate(BTParams btParams) + { + Log.Info(Message + " | Frame: " + btParams.Frame.Number); + + return BTStatus.Success; + } + } +} diff --git a/data/DebugService.cs b/data/DebugService.cs new file mode 100644 index 0000000000000000000000000000000000000000..1174d795f138031852913bb3c19967bae32678dd --- /dev/null +++ b/data/DebugService.cs @@ -0,0 +1,14 @@ +using System; + +namespace Quantum +{ + [Serializable] + public unsafe partial class DebugService : BTService + { + public string Message; + protected unsafe override void OnUpdate(BTParams btParams) + { + Log.Info($"[BT SERVICE] { Message } | Frame: {btParams.Frame.Number}"); + } + } +} diff --git a/data/EffectArea.cs b/data/EffectArea.cs new file mode 100644 index 0000000000000000000000000000000000000000..a03b69d940c0978f076680ee4d4a732ea1ebabb0 --- /dev/null +++ b/data/EffectArea.cs @@ -0,0 +1,81 @@ +namespace Quantum +{ + using Photon.Deterministic; + + public unsafe partial struct EffectArea + { + public void Update(Frame frame, EntityRef entity) + { + StateTime -= frame.DeltaTime; + + if (State == EEffectAreaState.Init) + { + if (StateTime <= FP._0) + { + State = EEffectAreaState.Active; + TickCount = System.Math.Max((byte)1, TickCount); + + var behaviors = frame.ResolveList(Behaviors); + + for (int idx = 0, count = behaviors.Count; idx < count; idx++) + { + behaviors.GetPointer(idx)->Initialize(Level); + } + } + } + + if (State == EEffectAreaState.Active) + { + if (StateTime <= FP._0) + { + TickCount -= 1; + ProcessEffect(frame, entity); + + StateTime = TickTime; + } + + if (TickCount == 0) + { + State = EEffectAreaState.Finished; + StateTime = FP._0_50; + } + } + + if (State == EEffectAreaState.Finished) + { + if (StateTime <= FP._0) + { + frame.Destroy(entity); + } + } + } + + private void ProcessEffect(Frame frame, EntityRef entity) + { + var position = frame.Unsafe.GetPointer(entity)->Position; + var behaviors = frame.ResolveList(Behaviors); + + foreach (var targetPair in frame.Unsafe.GetComponentBlockIterator()) + { + if (TargetType == EEffectAreaTarget.Enemy && targetPair.Component->OwnerPlayerRef == Owner) + continue; + if (TargetType == EEffectAreaTarget.Friendly && targetPair.Component->OwnerPlayerRef != Owner) + continue; + + var targetPosition = frame.Unsafe.GetPointer(targetPair.Entity)->Position; + var distance = (targetPosition - position).SqrMagnitude; + + var radiusSqr = Radius + targetPair.Component->Size; + radiusSqr *= radiusSqr; + + if (distance > radiusSqr) + continue; + + for (int idx = 0, count = behaviors.Count; idx < count; idx++) + { + behaviors.GetPointer(idx)->ProcessEffect(frame, entity, targetPair.Entity, Level); + } + } + } + } +} diff --git a/data/EffectArea.qtn b/data/EffectArea.qtn new file mode 100644 index 0000000000000000000000000000000000000000..afc97d7b46a7f04634b27104ad8c3556b8c2974f --- /dev/null +++ b/data/EffectArea.qtn @@ -0,0 +1,42 @@ +component EffectArea +{ + [ExcludeFromPrototype] list Behaviors; + [ExcludeFromPrototype] PlayerRef Owner; + [ExcludeFromPrototype] FP Radius; + [ExcludeFromPrototype] FP StateTime; + [ExcludeFromPrototype] EEffectAreaState State; + [ExcludeFromPrototype] FP TickTime; + [ExcludeFromPrototype] byte TickCount; + [ExcludeFromPrototype] byte Level; + [ExcludeFromPrototype] EEffectAreaTarget TargetType; +} + +enum EEffectAreaState : Byte +{ + Init, + Active, + Finished, +} + +enum EEffectAreaTarget : Byte +{ + Enemy, + Friendly, +} + +union EffectAreaBehavior +{ + EffectAreaBehavior_Damage Damage; + EffectAreaBehavior_Buff Buff; +} + +struct EffectAreaBehavior_Damage +{ + FP Damage; + FP DamagePerLevelPercent; +} + +struct EffectAreaBehavior_Buff +{ + AssetRefEntityPrototype Buff; +} diff --git a/data/EffectAreaBehavior.cs b/data/EffectAreaBehavior.cs new file mode 100644 index 0000000000000000000000000000000000000000..e77a04880afea8589de9a2f3c6fbf1a5e21885d8 --- /dev/null +++ b/data/EffectAreaBehavior.cs @@ -0,0 +1,30 @@ +namespace Quantum +{ + public unsafe partial struct EffectAreaBehavior + { + public void Initialize(byte level) + { + switch (Field) + { + case DAMAGE: _Damage.Initialize(level); break; + + case BUFF: break; + + default: + throw new System.NotImplementedException(); + } + } + + public void ProcessEffect(Frame frame, EntityRef entity, EntityRef target, byte level) + { + switch (Field) + { + case DAMAGE: _Damage.ProcessEffect(frame, entity, target); break; + case BUFF: _Buff.ProcessEffect(frame, entity, target, level); break; + + default: + throw new System.NotImplementedException(); + } + } + } +} diff --git a/data/EffectAreaBehavior_Buff.cs b/data/EffectAreaBehavior_Buff.cs new file mode 100644 index 0000000000000000000000000000000000000000..8027d25a34de7cffb70bff433da62e36b4f1d3c2 --- /dev/null +++ b/data/EffectAreaBehavior_Buff.cs @@ -0,0 +1,16 @@ +namespace Quantum +{ + public unsafe partial struct EffectAreaBehavior_Buff + { + // PUBLIC METHODS + + public void ProcessEffect(Frame frame, EntityRef entity, EntityRef target, byte level) + { + if (frame.Unsafe.TryGetPointer(target, out var buffs) == false) + return; + + + buffs->AddBuff(frame, entity, target, Buff, level); + } + } +} diff --git a/data/EffectAreaBehavior_Damage.cs b/data/EffectAreaBehavior_Damage.cs new file mode 100644 index 0000000000000000000000000000000000000000..cbc491277fa6aaca104331585d8fb869c6dc304d --- /dev/null +++ b/data/EffectAreaBehavior_Damage.cs @@ -0,0 +1,31 @@ +namespace Quantum +{ + using Photon.Deterministic; + + public unsafe partial struct EffectAreaBehavior_Damage + { + public void Initialize(byte level) + { + var perLevel = FP._1 + DamagePerLevelPercent * FP._0_01; + + for (int idx = 1; idx < level; idx++) + { + Damage *= perLevel; + } + } + + public void ProcessEffect(Frame frame, EntityRef entity, EntityRef target) + { + var position = frame.Unsafe.GetPointer(entity)->Position; + var healthData = new HealthData() + { + Action = EHealthAction.Remove, + Value = Damage, + Target = target, + }; + + var targetHealth = frame.Unsafe.GetPointer(target); + targetHealth->ApplyHealthData(frame, healthData); + } + } +} diff --git a/data/EffectAreaSettings.cs b/data/EffectAreaSettings.cs new file mode 100644 index 0000000000000000000000000000000000000000..4a3e352701af07e1a2b3eb22a4a196ac596f4e23 --- /dev/null +++ b/data/EffectAreaSettings.cs @@ -0,0 +1,18 @@ +namespace Quantum +{ + using Quantum.Prototypes; + using Quantum.Inspector; + using Photon.Deterministic; + + [System.Serializable] + public class EffectAreaSettings : CardSettings + { + [Header("Effect Area")] + public FP TickTime; + public byte TickCount; + public FP Radius; + public EEffectAreaTarget Target; + [Space] + public EffectAreaBehavior_Prototype[] Behaviors; + } +} diff --git a/data/EffectAreaSystem.cs b/data/EffectAreaSystem.cs new file mode 100644 index 0000000000000000000000000000000000000000..0b4b22f582bd4e139c923dbeb9c82fcce27dabfc --- /dev/null +++ b/data/EffectAreaSystem.cs @@ -0,0 +1,13 @@ +namespace Quantum +{ + unsafe class EffectAreaSystem : SystemMainThread + { + public override void Update(Frame frame) + { + foreach (var pair in frame.Unsafe.GetComponentBlockIterator()) + { + pair.Component->Update(frame, pair.Entity); + } + } + } +} diff --git a/data/Fixed Point.txt b/data/Fixed Point.txt new file mode 100644 index 0000000000000000000000000000000000000000..3d107969b57306407d96400302a86735fe3ef1ec --- /dev/null +++ b/data/Fixed Point.txt @@ -0,0 +1,256 @@ +Introduction +In Quantum the FP struct (Fixed Point) completely replaces all usages of floats and doubles to ensure cross-platform determinism. It offers versions of common math data structures like FPVector2, FPVector3, FPMatrix, FPQuaternion, RNGSession, FPBounds2, etc. All systems in Quantum, including physics and navigation, exclusively use FP values in their computations. + +The fixed-point type implemented in Quantum is Q48.16. It has proved to a good balance between precision and performance with a bias towards the latter. + +Internally FP uses one long to represent the combined fixed-point number (whole number + decimal part); the long value can be accessed and set via FP.RawValue. + +Quantum's FP Math uses carefully tuned look up tables for fast trigonometric and square root functions (see QuantumSDK\quantum_unity\Assets\Photon\Quantum\Resources\LUT). + +Back To Top + + +Parsing FPs +The representable FP fraction is limited and never as accurate as a double. Parsing is an approximation and will round to the nearest possible FP precision. This is reflected in: + +parsing an FP as the float 1.1f and then converting back to float possibly resulting in 1.09999999f; and, +different parsing methods yielding different results on the same machine. +FP.FromFloat(1.1f).RawValue != FP.FromString("1.1").RawValue +Back To Top + + +TLDR +Use from float only during edit time, never inside the simulation or at runtime. +It is best to convert from raw whenever possible. +Back To Top + + +FP.FromFloat_UNSAFE() +Converting from float is not deterministic due to rounding errors and should never be done inside the simulation. Doing such a conversion in the simulation will cause desyncs 100% of the time. + +However, it can be used during edit or build time, when the converted (FP) data is first created and then shared with everyone. IMPORTANT: Data generated this way on different machines may not be compatible. + +var v = FP.FromFloat_UNSAFE(1.1f); +Back To Top + + +FP.FromString_UNSAFE() +This will internally parse the string as a float and then convert to FP. All caveats from FromFloat_UNSAFE() apply here as well. + +var v = FP.FromFloat_UNSAFE("1.1"); +Back To Top + + +FP.FromString() +This is deterministic and therefore safe to use anywhere but may not be the most performant option. A typical use case is balancing information (patch) that clients load from a server and then use to update data in Quantum assets. + +Be aware of the string locale! It only parses English number formatting for decimals and requires a dot (e.g. 1000.01f). + +var v = FP.FromFloat("1.1"); +Back To Top + + +FP.FromRaw() +This is secure and fast as it mimics the internal representation. + +var v = FP.FromRaw(72089); +This snippet can be used to create a FP converter window in Unity for convient conversion. + +using System; +using UnityEditor; +using Photon.Deterministic; + +public class FPConverter : EditorWindow { + private float _f; + private FP _fp; + + [MenuItem("Quantum/FP Converter")] + public static void ShowWindow() { + GetWindow(typeof(FPConverter), false, "FP Converter"); + } + + public virtual void OnGUI() { + _f = EditorGUILayout.FloatField("Input", _f); + try { + _fp = FP.FromFloat_UNSAFE(_f); + var f = FPPropertyDrawer.GetRawAsFloat(_fp.RawValue); + var rect = EditorGUILayout.GetControlRect(true); + EditorGUI.FloatField(rect, "Output FP", f); + QuantumEditorGUI.Overlay(rect, "(FP)"); + EditorGUILayout.LongField("Output Raw", _fp.RawValue); + } + catch (OverflowException e) { + EditorGUILayout.LabelField("Out of range"); + } + } +} +Back To Top + + +Const Variables +The `FP._1_10` syntax can not be extended or generated. +FP is a struct and can therefore not be used as a constant. It is, however, possible to hard-code and use "FP" values in const variables: + +Combine pre-defined FP._1 static getters or FP.Raw._1 const variables. +FP foo = FP._1 + FP._0_10; +// or +foo.RawValue = FP.Raw._1 + FP.Raw._0_10; +const long MagicNumber = FP.Raw._1 + FP.Raw._0_10; + +FP foo = default; +foo.RawValue = MagicNumber; +// or +foo = FP.FromRaw(MagicNumber); +Convert the specific float once to FP and save the raw value as a constant +const long MagicNumber = 72089; // 1.1 + +var foo = FP.FromRaw(MagicNumber); +// or +foo.RawValue = MagicNumber; +Create the constant inside the Quantum DSL +#define FPConst 1.1 +Then use like this: + +var foo = default(FP); +foo += Constants.FPConst; +// or +foo.RawValue += Constants.Raw.FPConst; +It will generate code to represent the constant in the following way: + +public static unsafe partial class Constants { + public const Int32 PLAYER_COUNT = 8; + /// 1.100006 + + public static FP FPConst { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { + FP result; + result.RawValue = 72090; + return result; + } + } + public static unsafe partial class Raw { + /// 1.100006 + public const Int64 FPConst = 72090; + } +} +Define readonly static variables in the class. +private readonly static FP MagicNumber = FP._1 + FP._0_10; +There is a performance penalty compared to const variables and don't forget to mark readonly because randomly changing the value during runtime could lead to desyncs. + +Back To Top + + +Casting +Implicit casting to FP from int, uint, short, ushort, byte, sbyte is allowed and safe. + +FP v = (FP)1; +FP v = 1; +Explicit casting from FP to float or double is possible, but obviously should not be used inside the simulation. + +var v = (double)FP._1; +var v = (float)FP._1; +Casting to integer and back is safe though. + +FP v = (FP)(int)FP._1; +Unsafe casts are marked as [obsolete] and will cause a InvalidOperationException. + +FP v = 1.1f; // ERROR +FP v = 1.1d; // ERROR +Back To Top + + +Inlining +All low-level Quantum systems use manual inlined FP arithmetic to extract every ounce of performance possible. Fixed point math uses integer division and multiplication. To achieve this, the result or dividend are bit-shifted by the FP-Precision (16) before or after the calculation. + +var v = parameter * FP._0_01; + +// inlined integer math +FP v = default; +v.RawValue = (parameter.RawValue * FP._0_01.RawValue) >> FPLut.PRECISION; +var v = parameter / FP._0_01; + +// inlined integer math +FP v = default; +v.RawValue = (parameter.RawValue << FPLut.PRECISION) / FP._0_01.RawValue; +Back To Top + + +Overflow +FP.UseableMax represents the highest FP number that can be multiplied with itself and not cause an overflow (exceeding long range). + +FP.UseableMax + Decimal: 32767.9999847412 + Raw: 2147483647 + Binary: 1111111111111111111111111111111 = 31 bit +FP.UseableMin + Decimal: -32768 + Raw: -2147483648 + Binary: 10000000000000000000000000000000 = 32 bit +Back To Top + + +Precision +The general FP precision is decent when the numbers are kept within a certain range (0.01..1000). FP-math related precision problems usually produce inaccurate results and tend to make systems (based on math) unstable. A very common case is to multiply very high or small numbers and than returning to the original range by division for example. The resulting numbers lose precision. + +Another example is this method excerpt from ClosestDistanceToTriangle. Where t0 is calculated from multiplying two dot products with each other, where a dot-product is already also a result of multiplications. This is a problem when very accurate results are expected. A way to mitigate this issue is shifting the values artificially before the calculation then shift the result back. This will work when the ranges of the input are somewhat known. + +var diff = p - v0; +var edge0 = v1 - v0; +var edge1 = v2 - v0; +var a00 = Dot(edge0, edge0); +var a01 = Dot(edge0, edge1); +var a11 = Dot(edge1, edge1); +var b0 = -Dot(diff, edge0); +var b1 = -Dot(diff, edge1); +var t0 = a01 * b1 - a11 * b0; +var t1 = a01 * b0 - a00 * b1; +// ... +closestPoint = v0 + t0 * edge0 + t1 * edge1; +Back To Top + + +FPAnimationCurves +Unity comes with a set of tools which are useful to express some values in the form of curves. It comes with a custom editor for such curves, which are then serialised and can be used in runtime in order to evaluate the value of the curve in some specific point. + +There are many situations where curves can be used, such as expressing steering information when implementing vehicles, the utility value during the decision making for an AI agent (as done in Bot SDK's Utility Theory), getting the multiplier value for some attack's damage, and so on. + +The Quantum SDK already comes with its own implementation of an animation curve, named FPAnimationCurve, evaluated as FPs. This custom type, when inspected directly on Data Assets and Components in Unity, are drawn with Unity's default Animation Curve editors, whose data are then internally baked into the deterministic type. + +Back To Top + + +Polling Data From An FPAnimationCurve +The code needed to poll some data from a curve, with Quantum code, is very similar to the Unity's version: + +// This returns the pre-baked value, interpolated accordingly to the curve's configuration such as it's key points, curve's resolution, tangets modes, etc +FP myValue = myCurve.Evaluate(FP._0_50); +Back To Top + + +Creating FPAnimationCurves Directly On The Simulation +Here are the snippets to create a deterministic animation curve from scratch, directly on the simulation: + +// Creating a simple, linear curve with five key points +// Change the parameter as prefered +public static class FPAnimationCurveUtils +{ + public static FPAnimationCurve CreateLinearCurve(FPAnimationCurve.WrapMode preWrapMode, FPAnimationCurve.WrapMode postWrapMode) + { + return new FPAnimationCurve + { + Samples = new FP[5] { FP._0, FP._0_25, FP._0_50, FP._0_75, FP._1 }, + PostWrapMode = (int)postWrapMode, + PreWrapMode = (int)preWrapMode, + StartTime = 0, + EndTime = 1, + Resolution = 32 + }; + } +} +// Storing a curve into a local variable +var curve = FPAnimationCurveUtils.CreateLinearCurve(FPAnimationCurve.WrapMode.Clamp, FPAnimationCurve.WrapMode.Clamp); + +// It can also be used directly to pre-initialise a curve in an asset +public unsafe partial class CollectibleData +{ + public FPAnimationCurve myCurve = FPAnimationCurveUtils.CreateLinearCurv \ No newline at end of file diff --git a/data/Frame.User.cs b/data/Frame.User.cs new file mode 100644 index 0000000000000000000000000000000000000000..58c94b3117434a064fb35156ed0cab4aef767db2 --- /dev/null +++ b/data/Frame.User.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Quantum +{ + unsafe partial class Frame + { + } +} diff --git a/data/FrameContext.User.cs b/data/FrameContext.User.cs new file mode 100644 index 0000000000000000000000000000000000000000..5a3b8b0d45f0b31a0f4233451086c1d86655004e --- /dev/null +++ b/data/FrameContext.User.cs @@ -0,0 +1,7 @@ +namespace Quantum +{ + public partial class FrameContextUser + { + + } +} \ No newline at end of file diff --git a/data/GOAP.User.cs b/data/GOAP.User.cs new file mode 100644 index 0000000000000000000000000000000000000000..c1e35b2bfaaea73659018806d8e72e014d937c89 --- /dev/null +++ b/data/GOAP.User.cs @@ -0,0 +1,10 @@ +namespace Quantum +{ + public unsafe partial struct GOAPAgent + { + public AIConfig GetConfig(Frame frame) + { + return frame.FindAsset(Config.Id); + } + } +} diff --git a/data/GOAP.qtn b/data/GOAP.qtn new file mode 100644 index 0000000000000000000000000000000000000000..a2c3ad40ef5fdefab640ffb944c08c7a8cfe9941 --- /dev/null +++ b/data/GOAP.qtn @@ -0,0 +1,39 @@ +#define MAX_PLAN_SIZE 6 + +asset GOAPRoot; +asset GOAPGoal; +asset GOAPAction; +asset GOAPBackValidation; + +component GOAPAgent +{ + asset_ref Root; + AssetRefAIConfig Config; + + [ExcludeFromPrototype] + GOAPState CurrentState; + + [ExcludeFromPrototype] + asset_ref CurrentGoal; + [ExcludeFromPrototype] + GOAPState GoalState; + + [ExcludeFromPrototype] + sbyte CurrentActionIndex; + [ExcludeFromPrototype] + sbyte LastProcessedActionIndex; + [ExcludeFromPrototype] + byte CurrentPlanSize; + [ExcludeFromPrototype] + array>[MAX_PLAN_SIZE] Plan; + + [ExcludeFromPrototype] + FP CurrentActionTime; + [ExcludeFromPrototype] + FP CurrentGoalTime; + [ExcludeFromPrototype] + FP InterruptionCheckCooldown; + + [ExcludeFromPrototype] + list GoalDisableTimes; +} \ No newline at end of file diff --git a/data/GOAPAStar.cs b/data/GOAPAStar.cs new file mode 100644 index 0000000000000000000000000000000000000000..8127ae3b80267dbdce50a29d56e098aa992a7f2b --- /dev/null +++ b/data/GOAPAStar.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using Photon.Deterministic; + +namespace Quantum +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Auto)] + public struct GOAPNode + { + public int Hash; + public byte Depth; + public int Parent; + public sbyte ActionIndex; + public GOAPState State; + public short F; + public short G; + + public string ToString(GOAPAction[] actions) + { + string action = ActionIndex >= 0 && ActionIndex < actions.Length ? actions[ActionIndex].Path : "NoAction"; + return $"{action}, real cost (G): {G / 100f}, total heuristic cost (F): {F / 100f}, hash: {Hash}, parent: {Parent}"; + } + } + + public struct StateBackValidation + { + public GOAPState ValidatedState; + public FP CostToNextState; + + public StateBackValidation(GOAPState validatedState, FP costToNextState) + { + ValidatedState = validatedState; + CostToNextState = costToNextState; + } + } + + public unsafe class GOAPAStar + { + public delegate int HeuristicCost(GOAPState fromState, GOAPState toState); + + // PUBLIC MEMBERS + + public StatisticsData Statistics; + + // PRIVATE MEMBERS + + private readonly Dictionary _active = new Dictionary(); + private readonly Dictionary _closed = new Dictionary(); + + private readonly GOAPHeap _open = new GOAPHeap(); + + private ActionData[] _actionData; + private List _plan = new List(16); + + private List _planStateValidations = new List(8); + + // PUBLIC METHODS + + public List Run(Frame frame, GOAPEntityContext context, GOAPState start, GOAPState end, GOAPGoal goal, + GOAPAction[] availableActions, HeuristicCost heuristic, int maxPlanSize) + { + Statistics = default; + + int foundPathEndHash = BackwardAStar(frame, context, start, end, goal, availableActions, heuristic, maxPlanSize); + + Statistics.Success = foundPathEndHash != 0; + Statistics.ClosedNodes = _closed.Count; + Statistics.OpenNodes = _open.Size; + Statistics.ActiveNodes = _active.Count; + + if (foundPathEndHash == 0) + return null; + + _plan.Clear(); + + int nodeHash = foundPathEndHash; + while (nodeHash != 0) + { + if (_closed.TryGetValue(nodeHash, out GOAPNode node)) + { + if (node.ActionIndex >= 0) + { + _plan.Add(availableActions[node.ActionIndex]); + } + + nodeHash = node.Parent; + } + } + + return _plan; + } + + // PRIVATE METHODS + + private int BackwardAStar(Frame frame, GOAPEntityContext context, GOAPState start, GOAPState end, GOAPGoal goal, + GOAPAction[] availableActions, HeuristicCost heuristic, int maxPlanSize) + { + _open.Clear(); + _closed.Clear(); + _active.Clear(); + + PrepareActionData(ref _actionData, availableActions.Length); + + GOAPNode startNode = new GOAPNode + { + Hash = end.GetHashCode(), + State = end, + F = 0, + G = 0, + Depth = 0, + ActionIndex = (sbyte)-1, + }; + + _open.Push(startNode); + _active.Add(startNode.Hash, startNode); + + while (_open.Size > 0) + { + GOAPNode currentNode = _open.Pop(); + _closed.Add(currentNode.Hash, currentNode); + + //Log.Warn($"Closing node {currentNode.ToString()}"); + + if (start.Contains(currentNode.State) == true) + return currentNode.Hash; + + if (currentNode.Depth >= maxPlanSize) + continue; + + for (int i = availableActions.Length - 1; i >= 0; i--) + { + var action = availableActions[i]; + var actionData = _actionData[i]; + + if (actionData.IsProcessed == true && actionData.IsValid == false) + continue; + + // Check if action can satisfy state at least partially (backward search) + if (currentNode.State.ContainsAny(action.Effects) == false) + continue; + + // Check if action will not incorrectly override current state (backward search) + // Note: Next few lines are a bit difficult to understand. + // Do not modify it unless you know what you are doing. + // Mistake here will lead in failed backward search in more complex situations. + // Help: Continued state says how state should look like after this action will be executed. + // With applied state we are checking whether the current state contains continued state, + // merge helps with checking only part of the current state that matters. + var continuedState = GOAPState.Merge(action.Conditions, action.Effects); + var appliedState = GOAPState.Merge(continuedState, currentNode.State); + + if (appliedState.Contains(continuedState) == false) + continue; + + // We are validating action after we decide it fits the plan to not do this + // potentially expensive call unnecessary + if (actionData.IsProcessed == false) + { + actionData.IsValid = action.ValidateAction(frame, context, start, out FP cost) && cost > 0; + actionData.Cost = cost; + actionData.IsProcessed = true; + + Assert.Check(cost > 0, $"GOAP: Action cost has to be greater than zero. Action: {action.Path} Cost: {cost}"); + + if (actionData.IsValid == false) + { + Statistics.ValidationReturns++; + continue; + } + } + + Statistics.ValidationCalls++; + + // Remove effects, apply conditions to state (= backward apply action) + GOAPState newState = GOAPState.Remove(currentNode.State, action.Effects); + newState.Merge(action.Conditions); + + if (action.UsePlanStateValidation == true) + { + _planStateValidations.Clear(); + + action.ValidatePlanState(frame, context, newState, currentNode.State, actionData.Cost, _planStateValidations); + + Statistics.PlanStateValidationCalls++; + + // With plan state validation the plan can branch + for (int j = 0; j < _planStateValidations.Count; j++) + { + var validation = _planStateValidations[j]; + TryAddNode(currentNode, validation.ValidatedState, i, validation.CostToNextState, start, heuristic); + } + } + else + { + TryAddNode(currentNode, newState, i, actionData.Cost, start, heuristic); + } + } + } + + return 0; + } + + private void TryAddNode(GOAPNode currentNode, GOAPState newState, int actionIndex, FP actionCost, GOAPState start, HeuristicCost heuristic) + { + int newStateHash = newState.GetHashCode(); + + if (_closed.ContainsKey(newStateHash) == true) + { + Statistics.InClosedReturns++; + return; + } + + short h = (short)(heuristic(start, newState) * 100); + short g = (short)(currentNode.G + actionCost * 100); + + Statistics.ProcessedNodes++; + + GOAPNode node = new GOAPNode + { + Hash = newStateHash, + State = newState, + ActionIndex = (sbyte)actionIndex, + G = g, + F = (short)(h + g), + Parent = currentNode.Hash, + Depth = (byte)(currentNode.Depth + 1), + }; + + if (_active.TryGetValue(node.Hash, out GOAPNode existing)) + { + if (node.F >= existing.F) + return; + + _open.Update(node); + _active[node.Hash] = node; + + //Log.Warn($"Updating node {node.ToString()}"); + } + else + { + _open.Push(node); + _active.Add(node.Hash, node); + + //Log.Warn($"Adding node {node.ToString()}"); + } + } + + private static void PrepareActionData(ref ActionData[] actionData, int length) + { + int originalLength = actionData != null ? actionData.Length : 0; + + if (originalLength < length) + { + Array.Resize(ref actionData, length); + } + + for (int i = 0; i < length; i++) + { + if (i < originalLength) + { + actionData[i].IsProcessed = false; + } + else + { + actionData[i] = new ActionData(); + } + } + } + + private void PrintHeap(GOAPAction[] actions) + { + string heap = "HEAP: "; + int index = 0; + + foreach (GOAPNode node in _open) + { + heap += $"{index}: COST: {node.F} {actions[node.ActionIndex].Path}\n"; + index++; + } + + Log.Info(heap); + } + + private class ActionData + { + public bool IsProcessed; + public bool IsValid; + public FP Cost; + } + + public struct StatisticsData + { + public bool Success; + public int ClosedNodes; + public int OpenNodes; + public int ActiveNodes; + public int ValidationCalls; + public int ValidationReturns; + public int PlanStateValidationCalls; + public int InClosedReturns; + public int ProcessedNodes; + + public new string ToString() + { + return $"Success: {Success}, Closed: {ClosedNodes}, Open: {OpenNodes}, Active: {ActiveNodes}\nValidation calls: {ValidationCalls}\nValidation returns: {ValidationReturns}\nPlan State Validation calls: {PlanStateValidationCalls}\nIn Closed returns: {InClosedReturns}\n Processed nodes: {ProcessedNodes}"; + } + } + } +} \ No newline at end of file diff --git a/data/GOAPAction.cs b/data/GOAPAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..1ea9cc74ba2e37bfc4dbcb9e26cb98e6ab99947b --- /dev/null +++ b/data/GOAPAction.cs @@ -0,0 +1,53 @@ +using Photon.Deterministic; +using System.Collections.Generic; + +namespace Quantum +{ + public abstract unsafe partial class GOAPAction + { + public enum EResult + { + Continue, + IsDone, + IsFailed, + } + + // PUBLIC MEMBERS + + public string Label; + + [BotSDKHidden] + public GOAPState Conditions; + [BotSDKHidden] + public GOAPState Effects; + + public bool Interruptible; + + public abstract bool UsePlanStateValidation { get; } + + // PUBLIC METHODS + + public virtual bool ValidateAction(Frame frame, GOAPEntityContext context, GOAPState startState, out FP cost) + { + cost = 1; + return true; + } + + public virtual void ValidatePlanState(Frame frame, GOAPEntityContext context, GOAPState stateToValidate, GOAPState nextState, FP costToNextState, List validatedStates) + { + } + + public virtual void Activate(Frame frame, GOAPEntityContext context) + { + } + + public virtual EResult Update(Frame frame, GOAPEntityContext context) + { + return EResult.Continue; + } + + public virtual void Deactivate(Frame frame, GOAPEntityContext context) + { + } + } +} \ No newline at end of file diff --git a/data/GOAPBackValidation.cs b/data/GOAPBackValidation.cs new file mode 100644 index 0000000000000000000000000000000000000000..9b4bef4e9b17b142ada7a2feb6b4e317d1d336b8 --- /dev/null +++ b/data/GOAPBackValidation.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Photon.Deterministic; + + +namespace Quantum +{ + public abstract partial class GOAPBackValidation + { + public abstract void ValidatePlanState(Frame frame, EntityRef entity, GOAPState stateToValidate, GOAPState nextState, FP costToNextState, List validatedStates); + } + + public abstract class GOAPSingleBackValidation : GOAPBackValidation + { + public override sealed void ValidatePlanState(Frame frame, EntityRef entity, GOAPState stateToValidate, GOAPState nextState, FP costToNextState, + List validatedStates) + { + if (ValidatePlanState(frame, entity, ref stateToValidate, nextState, ref costToNextState) == true) + { + validatedStates.Add(new StateBackValidation(stateToValidate, costToNextState)); + } + } + + protected virtual bool ValidatePlanState(Frame frame, EntityRef entity, ref GOAPState stateToValidate, GOAPState nextState, ref FP costToNextState) + { + return false; + } + } +} \ No newline at end of file diff --git a/data/GOAPDefaultAction.cs b/data/GOAPDefaultAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..5452b51d23a9bf51c27b121edcc9ca97687649d9 --- /dev/null +++ b/data/GOAPDefaultAction.cs @@ -0,0 +1,122 @@ +using Photon.Deterministic; +using System.Collections.Generic; +using System; + +namespace Quantum +{ + [Serializable] + public unsafe partial class GOAPDefaultAction : GOAPAction + { + // PUBLIC MEMBERS + + public AIParamBool Validation = true; + public AIParamFP Cost = FP._1; + public AssetRefGOAPBackValidation PlanStateValidationLink; + + public AssetRefAIAction[] OnActivateLinks; + public AssetRefAIAction[] OnUpdateLinks; + public AssetRefAIAction[] OnDeactivateLinks; + + public AIParamBool IsDone; + public AIParamBool IsFailed; + + [NonSerialized] + public GOAPBackValidation PlanStateValidation; + + [NonSerialized] + public AIAction[] OnActivate; + [NonSerialized] + public AIAction[] OnUpdate; + [NonSerialized] + public AIAction[] OnDeactivate; + + public override bool UsePlanStateValidation => PlanStateValidation != null; + + // PUBLIC METHODS + + public override bool ValidateAction(Frame frame, GOAPEntityContext context, GOAPState startState, out FP cost) + { + cost = FP.MaxValue; + + if (Validation.Resolve(frame, context.Entity, context.Blackboard, context.Config) == false) + return false; + + cost = Cost.Resolve(frame, context.Entity, context.Blackboard, context.Config); + + return cost < FP.MaxValue; + } + + public override void ValidatePlanState(Frame frame, GOAPEntityContext context, GOAPState stateToValidate, GOAPState nextState, FP costToNextState, List validatedStates) + { + PlanStateValidation.ValidatePlanState(frame, context.Entity, stateToValidate, nextState, costToNextState, validatedStates); + } + + public override void Activate(Frame frame, GOAPEntityContext context) + { + ExecuteActions(frame, context.Entity, OnActivate); + } + + public override EResult Update(Frame frame, GOAPEntityContext context) + { + if (IsDone.Resolve(frame, context.Entity, context.Blackboard, context.Config) == true) + return EResult.IsDone; + + if (IsFailed.Resolve(frame, context.Entity, context.Blackboard, context.Config) == true) + return EResult.IsFailed; + + ExecuteActions(frame, context.Entity, OnUpdate); + + return EResult.Continue; + } + + public override void Deactivate(Frame frame, GOAPEntityContext context) + { + ExecuteActions(frame, context.Entity, OnDeactivate); + } + + // AssetObject INTERFACE + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + PlanStateValidation = (GOAPBackValidation)resourceManager.GetAsset(PlanStateValidationLink.Id); + + OnActivate = new AIAction[OnActivateLinks == null ? 0 : OnActivateLinks.Length]; + for (int i = 0; i < OnActivate.Length; i++) + { + OnActivate[i] = (AIAction)resourceManager.GetAsset(OnActivateLinks[i].Id); + } + + OnUpdate = new AIAction[OnUpdateLinks == null ? 0 : OnUpdateLinks.Length]; + for (int i = 0; i < OnUpdate.Length; i++) + { + OnUpdate[i] = (AIAction)resourceManager.GetAsset(OnUpdateLinks[i].Id); + } + + OnDeactivate = new AIAction[OnDeactivateLinks == null ? 0 : OnDeactivateLinks.Length]; + for (int i = 0; i < OnDeactivate.Length; i++) + { + OnDeactivate[i] = (AIAction)resourceManager.GetAsset(OnDeactivateLinks[i].Id); + } + } + + // PRIVATE METHODS + + private static void ExecuteActions(Frame frame, EntityRef entity, AIAction[] actions) + { + for (int i = 0; i < actions.Length; i++) + { + var action = actions[i]; + + action.Update(frame, entity); + + int nextAction = action.NextAction(frame, entity); + if (nextAction > i) + { + i = nextAction; + } + } + } + } +} \ No newline at end of file diff --git a/data/GOAPDefaultGoal.cs b/data/GOAPDefaultGoal.cs new file mode 100644 index 0000000000000000000000000000000000000000..d008fbeba8fb71e49cd56631a7fd13e4e04e1c5c --- /dev/null +++ b/data/GOAPDefaultGoal.cs @@ -0,0 +1,113 @@ +using System; +using Photon.Deterministic; + +namespace Quantum +{ + [Serializable] + public unsafe partial class GOAPDefaultGoal : GOAPGoal + { + // PUBLIC MEMBERS + + public AIParamBool Validation = true; + public AIParamFP Relevancy = FP._1; + public AIParamFP DisableTime; + public AssetRefAIAction[] OnInitPlanningLinks; + public AssetRefAIAction[] OnActivateLinks; + public AssetRefAIAction[] OnDeactivateLinks; + public AIParamBool IsFinished; + + [NonSerialized] + public AIAction[] OnInitPlanning; + [NonSerialized] + public AIAction[] OnActivate; + [NonSerialized] + public AIAction[] OnDeactivate; + + // PUBLIC METHODS + + public override FP GetRelevancy(Frame frame, GOAPEntityContext context) + { + if (Validation.Resolve(frame, context.Entity, context.Blackboard, context.Config) == false) + return 0; + + return Relevancy.Resolve(frame, context.Entity, context.Blackboard, context.Config); + } + + public override void InitPlanning(Frame frame, GOAPEntityContext context, ref GOAPState startState, ref GOAPState targetState) + { + base.InitPlanning(frame, context, ref startState, ref targetState); + + ExecuteActions(frame, context.Entity, OnInitPlanning); + } + + public override void Activate(Frame frame, GOAPEntityContext context) + { + base.Activate(frame, context); + + ExecuteActions(frame, context.Entity, OnActivate); + } + + public override void Deactivate(Frame frame, GOAPEntityContext context) + { + ExecuteActions(frame, context.Entity, OnDeactivate); + + base.Deactivate(frame, context); + } + + public override bool HasFinished(Frame frame, GOAPEntityContext context) + { + if (base.HasFinished(frame, context) == true) + return true; + + return IsFinished.Resolve(frame, context.Entity, context.Blackboard, context.Config); + } + + public override FP GetDisableTime(Frame frame, GOAPEntityContext context) + { + return DisableTime.Resolve(frame, context.Entity, context.Blackboard, context.Config); + } + + // AssetObject INTERFACE + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + OnInitPlanning = new AIAction[OnInitPlanningLinks == null ? 0 : OnInitPlanningLinks.Length]; + for (int i = 0; i < OnInitPlanning.Length; i++) + { + OnInitPlanning[i] = (AIAction)resourceManager.GetAsset(OnInitPlanningLinks[i].Id); + } + + OnActivate = new AIAction[OnActivateLinks == null ? 0 : OnActivateLinks.Length]; + for (int i = 0; i < OnActivate.Length; i++) + { + OnActivate[i] = (AIAction)resourceManager.GetAsset(OnActivateLinks[i].Id); + } + + OnDeactivate = new AIAction[OnDeactivateLinks == null ? 0 : OnDeactivateLinks.Length]; + for (int i = 0; i < OnDeactivate.Length; i++) + { + OnDeactivate[i] = (AIAction)resourceManager.GetAsset(OnDeactivateLinks[i].Id); + } + } + + // PRIVATE METHODS + + private static void ExecuteActions(Frame frame, EntityRef entity, AIAction[] actions) + { + for (int i = 0; i < actions.Length; i++) + { + var action = actions[i]; + + action.Update(frame, entity); + + int nextAction = action.NextAction(frame, entity); + if (nextAction > i) + { + i = nextAction; + } + } + } + } +} \ No newline at end of file diff --git a/data/GOAPDefaultHeuristic.cs b/data/GOAPDefaultHeuristic.cs new file mode 100644 index 0000000000000000000000000000000000000000..7d0c38e7726e49c91cdc7e97dfde03572499a9f9 --- /dev/null +++ b/data/GOAPDefaultHeuristic.cs @@ -0,0 +1,45 @@ +namespace Quantum +{ + public static class GOAPDefaultHeuristic + { + public static int BitmaskDifferenceUInt32(GOAPState start, GOAPState end) + { + EWorldState positiveAchieved = start.Positive & end.Positive; + EWorldState negativeAchieved = start.Negative & end.Negative; + + EWorldState positiveMissing = end.Positive & ~positiveAchieved; + EWorldState negativeMissing = end.Negative & ~negativeAchieved; + + return CountOnesUInt32((uint) positiveMissing) + CountOnesUInt32((uint) negativeMissing); + } + + public static int BitmaskDifferenceUInt64(GOAPState start, GOAPState end) + { + EWorldState positiveAchieved = start.Positive & end.Positive; + EWorldState negativeAchieved = start.Negative & end.Negative; + + EWorldState positiveMissing = end.Positive & ~positiveAchieved; + EWorldState negativeMissing = end.Negative & ~negativeAchieved; + + return CountOnesUInt64((ulong) positiveMissing) + CountOnesUInt64((ulong) negativeMissing); + } + + public static int CountOnesUInt32(uint x) + { + x = x - ((x >> 1) & 0x55555555u); + x = (x & 0x33333333u) + ((x >> 2) & 0x33333333u); + x = (x + (x >> 4)) & 0x0F0F0F0Fu; + + return (int) ((x * 0x01010101u) >> 24); + } + + public static int CountOnesUInt64(ulong x) + { + x = x - ((x >> 1) & 0x5555555555555555ul); + x = (x & 0x3333333333333333ul) + ((x >> 2) & 0x3333333333333333ul); + x = (x + (x >> 4)) & 0xF0F0F0F0F0F0F0Ful; + + return (int) (x * 0x101010101010101ul >> 56); + } + } +} \ No newline at end of file diff --git a/data/GOAPGoal.cs b/data/GOAPGoal.cs new file mode 100644 index 0000000000000000000000000000000000000000..b1b5dea69b735c771dbefd413f40ea7e3b213e43 --- /dev/null +++ b/data/GOAPGoal.cs @@ -0,0 +1,66 @@ +using Photon.Deterministic; + +namespace Quantum +{ + public abstract unsafe partial class GOAPGoal + { + public enum EInterruptionBehavior + { + Never, + Always, + BasedOnActions, + } + + // PUBLIC MEMBERS + + public string Label; + + [BotSDKHidden] + public GOAPState StartState; + [BotSDKHidden] + public GOAPState TargetState; + public EInterruptionBehavior InterruptionBehavior; + + // PUBLIC INTERFACE + + public virtual FP GetRelevancy(Frame frame, GOAPEntityContext context) + { + return 1; + } + + public virtual void InitPlanning(Frame frame, GOAPEntityContext context, ref GOAPState startState, ref GOAPState targetState) + { + startState.Merge(StartState); + targetState.Merge(TargetState); + } + + public virtual void Activate(Frame frame, GOAPEntityContext context) + { + } + + public virtual void Deactivate(Frame frame, GOAPEntityContext context) + { + } + + public virtual bool HasFinished(Frame frame, GOAPEntityContext context) + { + return context.Agent->CurrentState.Contains(context.Agent->GoalState); + } + + public bool IsInterruptible(GOAPAction currentAction) + { + if (InterruptionBehavior == EInterruptionBehavior.Never) + return false; + + if (InterruptionBehavior == EInterruptionBehavior.Always) + return true; + + return currentAction != null && currentAction.Interruptible; + } + + public virtual FP GetDisableTime(Frame frame, GOAPEntityContext context) + { + return 0; + } + } +} \ No newline at end of file diff --git a/data/GOAPHeap.cs b/data/GOAPHeap.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf75fbf82cb6cc2852ae5771f94e3d51954351ca --- /dev/null +++ b/data/GOAPHeap.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Quantum +{ + public class GOAPHeap : IEnumerable, IEnumerable + { + private GOAPNode[] _heap; + private int _size; + + public GOAPHeap(int capacity) + { + _heap = new GOAPNode[capacity]; + } + + public GOAPHeap() : this(1024) + { + } + + public int Size { get { return _size; } } + + public void Clear() + { + _size = 0; + + // remove all stuff from heap + Array.Clear(_heap, 0, _heap.Length); + } + + public void Update(GOAPNode updateNode) + { + int bubbleIndex = -1; + for (int i = 0; i < _size; i++) + { + var node = _heap[i]; + if (node.Hash == updateNode.Hash) + { + bubbleIndex = i; + break; + } + } + + if (bubbleIndex < 0) + { + Log.Error($"Cannot update node: Node with hash {updateNode.Hash} is not present in the heap"); + return; + } + + _heap[bubbleIndex] = updateNode; + + while (bubbleIndex != 0) + { + int parentIndex = (bubbleIndex - 1) / 2; + + if (_heap[parentIndex].F <= updateNode.F) + break; + + _heap[bubbleIndex] = _heap[parentIndex]; + _heap[parentIndex] = updateNode; + + bubbleIndex = parentIndex; + } + } + + public void Push(GOAPNode node) + { + if (_size == _heap.Length) + { + ExpandHeap(); + } + + int bubbleIndex = _size; + _heap[bubbleIndex] = node; + + _size++; + + while (bubbleIndex != 0) + { + int parentIndex = (bubbleIndex - 1) / 2; + if (_heap[parentIndex].F <= node.F) + break; + + _heap[bubbleIndex] = _heap[parentIndex]; + _heap[parentIndex] = node; + + bubbleIndex = parentIndex; + } + } + + public GOAPNode Pop() + { + GOAPNode returnItem = _heap[0]; + _heap[0] = _heap[_size - 1]; + + _size--; + + int swapItem = 0; + int parent = 0; + + do + { + parent = swapItem; + + int leftChild = 2 * parent + 1; + int rightChild = 2 * parent + 2; + + if (rightChild <= _size) + { + int smallerChild = _heap[leftChild].F < _heap[rightChild].F ? leftChild : rightChild; + + if (_heap[parent].F >= _heap[smallerChild].F) + { + swapItem = smallerChild; + } + } + else if (leftChild <= _size) + { + // Only one child exists + if (_heap[parent].F >= _heap[leftChild].F) + { + swapItem = leftChild; + } + } + + // One if the parent's children are smaller or equal, swap them + if (parent != swapItem) + { + GOAPNode tmpIndex = _heap[parent]; + + _heap[parent] = _heap[swapItem]; + _heap[swapItem] = tmpIndex; + } + } + while (parent != swapItem); + + return returnItem; + } + + private void ExpandHeap() + { + // Double the size + GOAPNode[] newHeap = new GOAPNode[_heap.Length * 2]; + + Array.Copy(_heap, newHeap, _heap.Length); + _heap = newHeap; + } + + IEnumerator IEnumerable.GetEnumerator() + { + for (int i = 0; i < _size; i++) + { + yield return _heap[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return (this as IEnumerable).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/data/GOAPHeuristic.cs b/data/GOAPHeuristic.cs new file mode 100644 index 0000000000000000000000000000000000000000..ed9e0c84fcb9aff2acefb6e1890e413b06c30003 --- /dev/null +++ b/data/GOAPHeuristic.cs @@ -0,0 +1,45 @@ +namespace Quantum +{ + public static class GOAPHeuristic + { + public static int BitmaskDifferenceUInt32(GOAPState start, GOAPState end) + { + EWorldState positiveAchieved = start.Positive & end.Positive; + EWorldState negativeAchieved = start.Negative & end.Negative; + + EWorldState positiveMissing = end.Positive & ~positiveAchieved; + EWorldState negativeMissing = end.Negative & ~negativeAchieved; + + return CountOnesUInt32((uint) positiveMissing) + CountOnesUInt32((uint) negativeMissing); + } + + public static int BitmaskDifferenceUInt64(GOAPState start, GOAPState end) + { + EWorldState positiveAchieved = start.Positive & end.Positive; + EWorldState negativeAchieved = start.Negative & end.Negative; + + EWorldState positiveMissing = end.Positive & ~positiveAchieved; + EWorldState negativeMissing = end.Negative & ~negativeAchieved; + + return CountOnesUInt64((ulong) positiveMissing) + CountOnesUInt64((ulong) negativeMissing); + } + + public static int CountOnesUInt32(uint x) + { + x = x - ((x >> 1) & 0x55555555u); + x = (x & 0x33333333u) + ((x >> 2) & 0x33333333u); + x = (x + (x >> 4)) & 0x0F0F0F0Fu; + + return (int) ((x * 0x01010101u) >> 24); + } + + public static int CountOnesUInt64(ulong x) + { + x = x - ((x >> 1) & 0x5555555555555555ul); + x = (x & 0x3333333333333333ul) + ((x >> 2) & 0x3333333333333333ul); + x = (x + (x >> 4)) & 0xF0F0F0F0F0F0F0Ful; + + return (int) (x * 0x101010101010101ul >> 56); + } + } +} \ No newline at end of file diff --git a/data/GOAPManager.cs b/data/GOAPManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..53e0548ebe669a2b632b4fb078598e86ef2a0929 --- /dev/null +++ b/data/GOAPManager.cs @@ -0,0 +1,450 @@ +using System; +using Photon.Deterministic; +using System.Collections.Generic; + +namespace Quantum +{ + public static unsafe class GOAPManager + { + // PUBLIC MEMBERS + + public static EntityRef DebugEntity; + + // PRIVATE MEMBERS + + private static GOAPAStar.HeuristicCost _heuristicCost; + + // PUBLIC METHODS + + public static void Initialize(Frame frame, EntityRef entity, GOAPRoot root, GOAPAStar.HeuristicCost heuristicCost = null) + { + var agent = frame.Unsafe.GetPointer(entity); + agent->Root = root; + + var disableTimes = frame.AllocateList(root.Goals.Length); + for (int i = 0; i < root.GoalRefs.Length; i++) + { + disableTimes.Add(0); + } + + agent->GoalDisableTimes = disableTimes; + + if (heuristicCost != null) + { + _heuristicCost = heuristicCost; + } + else if (_heuristicCost == null) + { + switch (sizeof(EWorldState)) + { + case 4: + _heuristicCost = GOAPHeuristic.BitmaskDifferenceUInt32; + break; + //case 8: + // _heuristicCost = GOAPHeuristic.BitmaskDifferenceUInt64; + // break; + default: + throw new NotImplementedException($"Heuristic for EWorldState size of {sizeof(EWorldState)} bytes is not implemented"); + } + } + } + + public static void Deinitialize(Frame frame, EntityRef entity) + { + var agent = frame.Unsafe.GetPointer(entity); + + agent->Root = default; + + frame.FreeList(agent->GoalDisableTimes); + agent->GoalDisableTimes = default; + } + + public static void Update(Frame frame, EntityRef entity, FP deltaTime) + { + var context = GetContext(frame, entity); + var agent = context.Agent; + + bool debug = DebugEntity == entity; + + // Update disable times + var goalDisableTimes = frame.ResolveList(agent->GoalDisableTimes); + for (int i = 0; i < goalDisableTimes.Count; i++) + { + goalDisableTimes[i] = FPMath.Max(FP._0, goalDisableTimes[i] - deltaTime); + } + + var currentGoal = agent->CurrentGoal.Id.IsValid == true ? frame.FindAsset(agent->CurrentGoal.Id) : null; + var currentAction = GetCurrentAction(frame, agent); + + if (currentGoal != null) + { + // Decrease interruption timer + agent->InterruptionCheckCooldown = FPMath.Max(agent->InterruptionCheckCooldown - deltaTime, 0); + + if (currentGoal.HasFinished(frame, context) == true) + { + StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); + } + } + + if (currentGoal == null || (agent->InterruptionCheckCooldown <= 0 && currentGoal.IsInterruptible(currentAction) == true)) + { + FindNewGoal(frame, context, ref currentGoal, ref currentAction); + } + + if (currentGoal != null) + { + UpdateCurrentGoal(frame, context, deltaTime, ref currentGoal, ref currentAction); + } + + Pool.Return(context); + } + + public static void StopCurrentGoal(Frame frame, EntityRef entity) + { + var context = GetContext(frame, entity); + + var currentGoal = context.Agent->CurrentGoal.Id.IsValid == true ? frame.FindAsset(context.Agent->CurrentGoal.Id) : null; + var currentAction = GetCurrentAction(frame, context.Agent); + + StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); + } + + public static void SetGoalDisableTime(Frame frame, EntityRef entity, AssetRefGOAPGoal goal, FP disableTime) + { + if (goal.Id.IsValid == false) + return; + + var agent = frame.Unsafe.GetPointer(entity); + + if (goal == agent->CurrentGoal) + { + StopCurrentGoal(frame, entity); + } + + var root = frame.FindAsset(agent->Root.Id); + int goalIndex = Array.IndexOf(root.GoalRefs, goal); + + if (goalIndex >= 0) + { + var disableTimes = frame.ResolveList(agent->GoalDisableTimes); + disableTimes[goalIndex] = disableTime; + } + } + + // PRIVATE METHODS + + private static void UpdateCurrentGoal(Frame frame, GOAPEntityContext context, FP deltaTime, ref GOAPGoal currentGoal, ref GOAPAction currentAction) + { + var agent = context.Agent; + bool debug = DebugEntity == context.Entity; + + if (currentAction != null && agent->CurrentState.Contains(currentAction.Effects) == true) + { + // This action is done, let's choose another one in next step + StopCurrentAction(frame, context, ref currentAction); + } + + // Activate next action from the plan if needed + if (currentAction == null && agent->CurrentPlanSize > 0) + { + while (agent->CurrentActionIndex < agent->CurrentPlanSize - 1) + { + agent->LastProcessedActionIndex = agent->CurrentActionIndex; + agent->CurrentActionIndex++; + + var nextAction = frame.FindAsset(agent->Plan[agent->CurrentActionIndex].Id); + + if (agent->CurrentState.Contains(nextAction.Conditions) == false) + { + // Conditions are not met, terminate whole plan + StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); + break; + } + + if (agent->CurrentState.Contains(nextAction.Effects) == false) + { + // This action is valid, activate it + currentAction = nextAction; + currentAction.Activate(frame, context); + + if (debug == true) + { + Log.Info($"GOAP: Action {currentAction.Path} activated"); + } + + agent->CurrentActionTime = 0; + break; + } + } + + if (currentAction == null && currentGoal != null) + { + if (debug == true) + { + Log.Info($"GOAP: Plan execution failed: Probably last action is finished but goal is not satisfied (state might change during execution). Goal: {currentGoal.Path}"); + } + + StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); + } + } + + // Update action + if (currentAction != null) + { + var result = currentAction.Update(frame, context); + + if (result == GOAPAction.EResult.IsFailed) + { + StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); + } + else if (result == GOAPAction.EResult.IsDone) + { + // This action claims to be done, apply effects and next action will be chosen next Update + agent->CurrentState.Merge(currentAction.Effects); + agent->LastProcessedActionIndex = agent->CurrentActionIndex; + + StopCurrentAction(frame, context, ref currentAction); + } + + agent->CurrentActionTime += deltaTime; + } + + if (currentGoal != null) + { + agent->CurrentGoalTime += deltaTime; + } + } + + private static void StopCurrentAction(Frame frame, GOAPEntityContext context, ref GOAPAction currentAction) + { + if (currentAction == null) + return; + + if (context.Agent->Plan[context.Agent->CurrentActionIndex] != currentAction) + { + Log.Error($"GOAP: Trying to stop action {currentAction.Path} that isn't currently active."); + return; + } + + currentAction.Deactivate(frame, context); + context.Agent->LastProcessedActionIndex = context.Agent->CurrentActionIndex; + + if (context.Entity == DebugEntity) + { + Log.Info($"GOAP: Action {currentAction.Path} deactivated"); + } + + currentAction = null; + } + + private static void StopCurrentGoal(Frame frame, GOAPEntityContext context, ref GOAPGoal currentGoal, ref GOAPAction currentAction) + { + var agent = context.Agent; + + StopCurrentAction(frame, context, ref currentAction); + + if (currentGoal != null) + { + currentGoal.Deactivate(frame, context); + + if (context.Entity == DebugEntity) + { + Log.Info($"GOAP: Goal {currentGoal.Path} deactivated"); + } + + FP disableTime = currentGoal.GetDisableTime(frame, context); + if (disableTime > 0) + { + var disableTimes = frame.ResolveList(agent->GoalDisableTimes); + + int goalIndex = Array.IndexOf(context.Root.Goals, currentGoal); + if (goalIndex >= 0) + { + disableTimes[goalIndex] = disableTime; + } + } + } + + agent->CurrentActionIndex = -1; + agent->LastProcessedActionIndex = -1; + agent->CurrentActionTime = 0; + + agent->CurrentPlanSize = 0; + agent->CurrentGoal = default; + agent->CurrentGoalTime = 0; + + currentGoal = null; + currentAction = null; + } + + private static void FindNewGoal(Frame frame, GOAPEntityContext context, ref GOAPGoal currentGoal, ref GOAPAction currentAction) + { + var agent = context.Agent; + var goals = context.Root.Goals; + + GOAPGoal bestGoal = null; + FP bestRelevancy = FP.MinValue; + + var disableTimes = frame.ResolveList(agent->GoalDisableTimes); + for (int i = 0; i < goals.Length; i++) + { + if (disableTimes[i] > 0) + continue; + + var goal = goals[i]; + + var startState = agent->CurrentState; + startState.Merge(goal.StartState); + + if (startState.Contains(goal.TargetState) == true) + continue; // Goal is satisfied + + FP relevancy = goal.GetRelevancy(frame, context); + + if (relevancy <= 0) + continue; + + if (relevancy > bestRelevancy) + { + bestRelevancy = relevancy; + bestGoal = goal; + } + } + + // Reset interruption timer + agent->InterruptionCheckCooldown = context.Root.InterruptionCheckInterval; + + if (bestGoal == null || bestGoal == currentGoal) + return; + + bool debug = context.Entity == DebugEntity; + + if (debug == true) + { + Log.Info($"GOAP: New best goal found: {bestGoal.Path}"); + } + + GOAPState currentState = agent->CurrentState; + GOAPState targetState = default; + + bestGoal.InitPlanning(frame, context, ref currentState, ref targetState); + + var aStar = Pool.Get(); + List plan = null; + + if (debug == true) + { + using (new StopwatchBlock("GOAP: Backward A* search")) + { + plan = aStar.Run(frame, context, currentState, targetState, bestGoal, context.Root.Actions, _heuristicCost, Constants.MAX_PLAN_SIZE); + } + + Log.Info($"GOAP: Search data - {aStar.Statistics.ToString()}"); + } + else + { + plan = aStar.Run(frame, context, currentState, targetState, bestGoal, context.Root.Actions, _heuristicCost, Constants.MAX_PLAN_SIZE); + } + + if (plan == null) + { + if (debug == true) + { + Log.Info($"GOAP: Failed to find plan for goal {bestGoal.Path}"); + } + + int goalIndex = Array.IndexOf(goals, bestGoal); + // Ensure there will be at least one planning without this failed goal + disableTimes[goalIndex] = FPMath.Max(FP._0_50, agent->InterruptionCheckCooldown + FP._0_10); + + Pool.Return(aStar); + + return; + } + + if (currentGoal != null) + { + StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); + } + + agent->CurrentGoal = bestGoal; + agent->CurrentGoalTime = 0; + agent->CurrentState = currentState; + agent->GoalState = targetState; + + agent->CurrentActionIndex = -1; + agent->LastProcessedActionIndex = -1; + agent->CurrentActionTime = 0; + agent->CurrentPlanSize = 0; + + currentGoal = bestGoal; + currentAction = null; + + for (int i = 0; i < plan.Count; i++) + { + var action = plan[i]; + if (action == null) + break; + + *agent->Plan.GetPointer(i) = action; + agent->CurrentPlanSize++; + } + + if (debug == true) + { + var planInfo = $"GOAP: Plan FOUND. Size: {agent->CurrentPlanSize} More..."; + for (int i = 0; i < agent->CurrentPlanSize; i++) + { + planInfo += $"\nAction {i + 1}: {plan[i].Path}"; + } + + Log.Info(planInfo); + } + + currentGoal.Activate(frame, context); + + if (debug == true) + { + Log.Info($"GOAP: Goal {currentGoal.Path} activated"); + } + + // Plan object is part of pooled GOAPAStar object + // so GOAPAStar needs to be returned after plan is no longer needed + Pool.Return(aStar); + } + + private static GOAPAction GetCurrentAction(Frame frame, GOAPAgent* agent) + { + if (agent->CurrentActionIndex < 0) + return null; + + if (agent->LastProcessedActionIndex >= agent->CurrentActionIndex) + return null; + + return frame.FindAsset(agent->Plan[agent->CurrentActionIndex].Id); + } + + private static GOAPEntityContext GetContext(Frame frame, EntityRef entity) + { + var context = Pool.Get(); + + context.Entity = entity; + context.Agent = frame.Unsafe.GetPointer(entity); + context.Blackboard = frame.Has(entity) ? frame.Unsafe.GetPointer(entity) : null; + context.Root = frame.FindAsset(context.Agent->Root.Id); + context.Config = frame.FindAsset(context.Agent->Config.Id); + + return context; + } + } + + public unsafe class GOAPEntityContext + { + public EntityRef Entity; + public GOAPAgent* Agent; + public GOAPRoot Root; + public AIConfig Config; + public AIBlackboardComponent* Blackboard; + } +} \ No newline at end of file diff --git a/data/GOAPRoot.cs b/data/GOAPRoot.cs new file mode 100644 index 0000000000000000000000000000000000000000..f44e8def57b434ece3d0d53f4dda577cdde8ca1e --- /dev/null +++ b/data/GOAPRoot.cs @@ -0,0 +1,40 @@ +using System; +using Photon.Deterministic; + +namespace Quantum +{ + public partial class GOAPRoot + { + // PUBLIC MEMBERS + + public string Label; + public FP InterruptionCheckInterval = FP._0_50; + + public AssetRefGOAPGoal[] GoalRefs; + public AssetRefGOAPAction[] ActionRefs; + + [NonSerialized] + public GOAPGoal[] Goals; + [NonSerialized] + public GOAPAction[] Actions; + + // AssetObject INTERFACE + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + Goals = new GOAPGoal[GoalRefs == null ? 0 : GoalRefs.Length]; + for (int i = 0; i < GoalRefs.Length; i++) + { + Goals[i] = (GOAPGoal)resourceManager.GetAsset(GoalRefs[i].Id); + } + + Actions = new GOAPAction[ActionRefs == null ? 0 : ActionRefs.Length]; + for (int i = 0; i < ActionRefs.Length; i++) + { + Actions[i] = (GOAPAction)resourceManager.GetAsset(ActionRefs[i].Id); + } + } + } +} \ No newline at end of file diff --git a/data/GOAPState.cs b/data/GOAPState.cs new file mode 100644 index 0000000000000000000000000000000000000000..d4f70e8e25af701dbff0dc5b6ff2ac94f59a2f2f --- /dev/null +++ b/data/GOAPState.cs @@ -0,0 +1,100 @@ +using System; + +namespace Quantum +{ + [Serializable] + public partial struct GOAPState + { + // PUBLIC METHODS + + public static GOAPState Merge(GOAPState state, GOAPState mergeState) + { + var newState = state; + newState.Merge(mergeState); + return newState; + } + + public static GOAPState Remove(GOAPState state, GOAPState removeState) + { + var newState = state; + newState.Remove(removeState); + return newState; + } + + public bool HasPositiveFlag(EWorldState state) + { + return (Positive & state) == state; + } + + public bool HasNegativeFlag(EWorldState state) + { + return (Negative & state) == state; + } + + public void SetFlag(EWorldState flagState, bool value) + { + if (value == true) + { + Positive = Positive | flagState; + Negative = Negative & ~Positive; + } + else + { + Negative = Negative | flagState; + Positive = Positive & ~Negative; + } + } + + public void ClearFlag(EWorldState clearState) + { + Positive = Positive & ~clearState; + Negative = Negative & ~clearState; + } + + public void Merge(GOAPState mergeState) + { + Positive = Positive | mergeState.Positive; + Negative = Negative & ~Positive; + + Negative = Negative | mergeState.Negative; + Positive = Positive & ~Negative; + + MergeUserData(mergeState); + } + + public void Remove(GOAPState removeState) + { + Positive = Positive & ~removeState.Positive; + Negative = Negative & ~removeState.Negative; + + RemoveUserData(removeState); + } + + public bool Contains(GOAPState state) + { + bool result = (state.Positive & Positive) == state.Positive && (state.Negative & Negative) == state.Negative; + + if (result == false) + return false; + + ContainsUserData(state, ref result); + return result; + } + + public bool ContainsAny(GOAPState state) + { + bool result = (state.Positive & Positive) != EWorldState.None || (state.Negative & Negative) != EWorldState.None; + + if (result == true) + return true; + + ContainsAnyUserData(state, ref result); + return result; + } + + partial void MergeUserData(GOAPState mergeState); + partial void RemoveUserData(GOAPState removeState); + partial void ContainsUserData(GOAPState state, ref bool result); + partial void ContainsAnyUserData(GOAPState state, ref bool result); + } +} \ No newline at end of file diff --git a/data/GOAPState.qtn b/data/GOAPState.qtn new file mode 100644 index 0000000000000000000000000000000000000000..fa6f5f3bc1c429a31ddff235e989bc4c53f49c5a --- /dev/null +++ b/data/GOAPState.qtn @@ -0,0 +1,14 @@ +struct GOAPState +{ + EWorldState Positive; + EWorldState Negative; +} + +[Flags] +enum EWorldState : UInt32 +{ + None = 0, + Idle = 1, + CarryingTarget = 2, + TargetDelivered = 4, +} \ No newline at end of file diff --git a/data/GameConfig.cs b/data/GameConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..44adf3ee5aacde00f9ce5ec9946d426a1c118b2d --- /dev/null +++ b/data/GameConfig.cs @@ -0,0 +1,10 @@ +using System; +using Photon.Deterministic; + +namespace Quantum { + public partial class GameConfig { + public FP MinStrikeForce; + public FP MaxStrikeForce; + public string HoleTriggerLayer; + } +} diff --git a/data/HFSM.Agent.cs b/data/HFSM.Agent.cs new file mode 100644 index 0000000000000000000000000000000000000000..97ca0901582a01ee18ee2df1c4aa2489d84332b0 --- /dev/null +++ b/data/HFSM.Agent.cs @@ -0,0 +1,13 @@ +namespace Quantum +{ + public partial struct HFSMAgent + { + // Used to setup info on the Unity debugger + public string GetRootAssetName(Frame frame) => frame.FindAsset(Data.Root.Id).Path; + + public AIConfig GetConfig(Frame frame) + { + return frame.FindAsset(Config.Id); + } + } +} diff --git a/data/HFSM.CheckBlackboardInt.cs b/data/HFSM.CheckBlackboardInt.cs new file mode 100644 index 0000000000000000000000000000000000000000..e381093f02bd829d9d642c4077e5a34e415527ed --- /dev/null +++ b/data/HFSM.CheckBlackboardInt.cs @@ -0,0 +1,40 @@ +using System; + +namespace Quantum +{ + public enum EValueComparison + { + None, + LessThan, + MoreThan, + EqualTo, + } + + [Serializable] + [AssetObjectConfig(GenerateLinkingScripts = true, GenerateAssetCreateMenu = false, GenerateAssetResetMethod = false)] + public partial class CheckBlackboardInt : HFSMDecision + { + public AIBlackboardValueKey Key; + public EValueComparison Comparison = EValueComparison.MoreThan; + public AIParamInt DesiredValue = 1; + + public override unsafe bool Decide(Frame frame, EntityRef entity) + { + var blackboard = frame.Unsafe.GetPointer(entity); + + var agent = frame.Unsafe.GetPointer(entity); + var aiConfig = agent->GetConfig(frame); + + var comparisonValue = DesiredValue.Resolve(frame, entity, blackboard, aiConfig); + var currentAmount = blackboard->GetInteger(frame, Key.Key); + + switch (Comparison) + { + case EValueComparison.LessThan: return currentAmount < comparisonValue; + case EValueComparison.MoreThan: return currentAmount > comparisonValue; + case EValueComparison.EqualTo: return currentAmount == comparisonValue; + default: return false; + } + } + } +} \ No newline at end of file diff --git a/data/HFSM.Decision.cs b/data/HFSM.Decision.cs new file mode 100644 index 0000000000000000000000000000000000000000..7eac18c1c1437e03f8ce8cb37a514aa27803ea38 --- /dev/null +++ b/data/HFSM.Decision.cs @@ -0,0 +1,12 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public abstract unsafe partial class HFSMDecision + { + public string Label; + + public abstract Boolean Decide(Frame frame, EntityRef entity); + } +} diff --git a/data/HFSM.LogicalDecisions.cs b/data/HFSM.LogicalDecisions.cs new file mode 100644 index 0000000000000000000000000000000000000000..9e6fc74f2072cf9de37b6f78ade490e18854b6f4 --- /dev/null +++ b/data/HFSM.LogicalDecisions.cs @@ -0,0 +1,68 @@ +using System; +using Photon.Deterministic; + +namespace Quantum +{ + public abstract partial class HFSMLogicalDecision : HFSMDecision + { + public AssetRefHFSMDecision[] Decisions; + + protected HFSMDecision[] _decisions; + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + _decisions = new HFSMDecision[Decisions == null ? 0 : Decisions.Length]; + if (Decisions != null) + { + for (Int32 i = 0; i < Decisions.Length; i++) + { + _decisions[i] = (HFSMDecision)resourceManager.GetAsset(Decisions[i].Id); + } + } + } + } + + [Serializable] + [AssetObjectConfig(GenerateLinkingScripts = true, GenerateAssetCreateMenu = false, GenerateAssetResetMethod = false)] + public partial class HFSMOrDecision : HFSMLogicalDecision + { + public override unsafe bool Decide(Frame frame, EntityRef entity) + { + foreach (var decision in _decisions) + { + if (decision.Decide(frame, entity)) + return true; + } + return false; + } + } + + + [Serializable] + [AssetObjectConfig(GenerateLinkingScripts = true, GenerateAssetCreateMenu = false, GenerateAssetResetMethod = false)] + public partial class HFSMAndDecision : HFSMLogicalDecision + { + public override unsafe bool Decide(Frame frame, EntityRef entity) + { + foreach (var decision in _decisions) + { + if (!decision.Decide(frame, entity)) + return false; + } + return true; + } + } + + + [Serializable] + [AssetObjectConfig(GenerateLinkingScripts = true, GenerateAssetCreateMenu = false, GenerateAssetResetMethod = false)] + public partial class HFSMNotDecision : HFSMLogicalDecision + { + public override unsafe bool Decide(Frame frame, EntityRef entity) + { + return !_decisions[0].Decide(frame, entity); + } + } +} diff --git a/data/HFSM.Manager.Threadsafe.cs b/data/HFSM.Manager.Threadsafe.cs new file mode 100644 index 0000000000000000000000000000000000000000..4b3469fb664c5a0bb834872c29b0b104a3d58ad5 --- /dev/null +++ b/data/HFSM.Manager.Threadsafe.cs @@ -0,0 +1,136 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public static unsafe partial class HFSMManager + { + public static unsafe partial class ThreadSafe + { + // ========== PUBLIC METHODS ================================================================================== + + /// + /// Initializes the HFSM, making the current state to be equals the initial state + /// + public static unsafe void Init(FrameThreadSafe frame, EntityRef entity, HFSMRoot root) + { + if (frame.TryGetPointer(entity, out HFSMAgent* agent)) + { + HFSMData* hfsmData = &agent->Data; + Init(frame, hfsmData, entity, root); + } + else + { + Log.Error("[Bot SDK] Tried to initialize an entity which has no HfsmAgent component"); + } + } + + /// + /// Initializes the HFSM, making the current state to be equals the initial state + /// + public static unsafe void Init(FrameThreadSafe frame, HFSMData* hfsm, EntityRef entity, HFSMRoot root) + { + hfsm->Root = root; + if (hfsm->Root.Equals(default) == false) + { + HFSMState initialState = frame.FindAsset(root.InitialState.Id); + ChangeState(initialState, frame, hfsm, entity, ""); + } + } + + /// + /// Update the state of the HFSM. + /// + /// Usually the current deltaTime so the HFSM accumulates the time stood on the current state + public static void Update(FrameThreadSafe frame, FP deltaTime, EntityRef entity) + { + if (frame.TryGetPointer(entity, out HFSMAgent* agent)) + { + HFSMData* hfsmData = &agent->Data; + Update(frame, deltaTime, hfsmData, entity); + } + else + { + Log.Error("[Bot SDK] Tried to update an entity which has no HFSMAgent component"); + } + } + + /// + /// Update the state of the HFSM. + /// + /// Usually the current deltaTime so the HFSM accumulates the time stood on the current state + public static void Update(FrameThreadSafe frame, FP deltaTime, HFSMData* hfsmData, EntityRef entity) + { + HFSMState currentState = frame.FindAsset(hfsmData->CurrentState.Id); + currentState.UpdateState(frame, deltaTime, hfsmData, entity); + } + + /// + /// Triggers an event if the target HFSM listens to it + /// + public static unsafe void TriggerEvent(FrameThreadSafe frame, EntityRef entity, string eventName) + { + if (frame.TryGetPointer(entity, out HFSMAgent* agent)) + { + HFSMData* hfsmData = &agent->Data; + TriggerEvent(frame, hfsmData, entity, eventName); + } + else + { + Log.Error("[Bot SDK] Tried to trigger an event to an entity which has no HFSMAgent component"); + } + } + + /// + /// Triggers an event if the target HFSM listens to it + /// + public static unsafe void TriggerEvent(FrameThreadSafe frame, HFSMData* hfsmData, EntityRef entity, string eventName) + { + Int32 eventInt = 0; + + HFSMRoot hfsmRoot = frame.FindAsset(hfsmData->Root.Id); + if (hfsmRoot.RegisteredEvents.TryGetValue(eventName, out eventInt)) + { + if (hfsmData->CurrentState.Equals(default) == false) + { + HFSMState currentState = frame.FindAsset(hfsmData->CurrentState.Id); + currentState.Event(frame, hfsmData, entity, eventInt); + } + } + } + + /// + /// Triggers an event if the target HFSM listens to it + /// + public static unsafe void TriggerEventNumber(FrameThreadSafe frame, HFSMData* hfsmData, EntityRef entity, Int32 eventInt) + { + if (hfsmData->CurrentState.Equals(default) == false) + { + HFSMState currentState = frame.FindAsset(hfsmData->CurrentState.Id); + currentState.Event(frame, hfsmData, entity, eventInt); + } + } + + // ========== INTERNAL METHODS ================================================================================ + + /// + /// Executes the On Exit actions for the current state, then changes the current state + /// + internal static void ChangeState(HFSMState nextState, FrameThreadSafe frame, HFSMData* hfsmData, EntityRef entity, string transitionId) + { + Assert.Check(nextState != null, "Tried to change HFSM to a null state"); + + HFSMState currentState = frame.FindAsset(hfsmData->CurrentState.Id); + currentState?.ExitState(nextState, frame, hfsmData, entity); + hfsmData->CurrentState = nextState; + + if (frame.IsVerified == true && entity != default(EntityRef)) + { + StateChanged?.Invoke(entity, hfsmData->CurrentState.Id.Value, transitionId); + } + + nextState.EnterState(frame, hfsmData, entity); + } + } + } +} diff --git a/data/HFSM.Manager.cs b/data/HFSM.Manager.cs new file mode 100644 index 0000000000000000000000000000000000000000..74fbb0c618547e8749eae3de4e1302557b10de4d --- /dev/null +++ b/data/HFSM.Manager.cs @@ -0,0 +1,133 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public static unsafe class HFSMManager + { + public static Action StateChanged; + + /// + /// Initializes the HFSM, making the current state to be equals the initial state + /// + public static unsafe void Init(Frame frame, EntityRef entity, HFSMRoot root) + { + if (frame.Unsafe.TryGetPointer(entity, out HFSMAgent* agent)) + { + HFSMData* hfsmData = &agent->Data; + Init(frame, hfsmData, entity, root); + } + else + { + Log.Error("[Bot SDK] Tried to initialize an entity which has no HfsmAgent component"); + } + } + + /// + /// Initializes the HFSM, making the current state to be equals the initial state + /// + public static unsafe void Init(Frame frame, HFSMData* hfsm, EntityRef entity, HFSMRoot root) + { + hfsm->Root = root; + if (hfsm->Root.Equals(default) == false) + { + HFSMState initialState = frame.FindAsset(root.InitialState.Id); + ChangeState(initialState, frame, hfsm, entity, ""); + } + } + + + /// + /// Update the state of the HFSM. + /// + /// Usually the current deltaTime so the HFSM accumulates the time stood on the current state + public static void Update(Frame frame, FP deltaTime, EntityRef entity) + { + if (frame.Unsafe.TryGetPointer(entity, out HFSMAgent* agent)) + { + HFSMData* hfsmData = &agent->Data; + Update(frame, deltaTime, hfsmData, entity); + } + else + { + Log.Error("[Bot SDK] Tried to update an entity which has no HFSMAgent component"); + } + } + + /// + /// Update the state of the HFSM. + /// + /// Usually the current deltaTime so the HFSM accumulates the time stood on the current state + public static void Update(Frame frame, FP deltaTime, HFSMData* hfsmData, EntityRef entity) + { + HFSMState curentState = frame.FindAsset(hfsmData->CurrentState.Id); + curentState.UpdateState(frame, deltaTime, hfsmData, entity); + } + + + /// + /// Triggers an event if the target HFSM listens to it + /// + public static unsafe void TriggerEvent(Frame frame, EntityRef entity, string eventName) + { + if (frame.Unsafe.TryGetPointer(entity, out HFSMAgent* agent)) + { + HFSMData* hfsmData = &agent->Data; + TriggerEvent(frame, hfsmData, entity, eventName); + } + else + { + Log.Error("[Bot SDK] Tried to trigger an event to an entity which has no HFSMAgent component"); + } + } + + /// + /// Triggers an event if the target HFSM listens to it + /// + public static unsafe void TriggerEvent(Frame frame, HFSMData* hfsmData, EntityRef entity, string eventName) + { + Int32 eventInt = 0; + + HFSMRoot hfsmRoot = frame.FindAsset(hfsmData->Root.Id); + if (hfsmRoot.RegisteredEvents.TryGetValue(eventName, out eventInt)) + { + if (hfsmData->CurrentState.Equals(default) == false) + { + HFSMState currentState = frame.FindAsset(hfsmData->CurrentState.Id); + currentState.Event(frame, hfsmData, entity, eventInt); + } + } + } + + /// + /// Triggers an event if the target HFSM listens to it + /// + public static unsafe void TriggerEventNumber(Frame frame, HFSMData* hfsmData, EntityRef entity, Int32 eventInt) + { + if (hfsmData->CurrentState.Equals(default) == false) + { + HFSMState currentState = frame.FindAsset(hfsmData->CurrentState.Id); + currentState.Event(frame, hfsmData, entity, eventInt); + } + } + + /// + /// Executes the On Exit actions for the current state, then changes the current state + /// + internal static void ChangeState(HFSMState nextState, Frame frame, HFSMData* hfsmData, EntityRef entity, string transitionId) + { + Assert.Check(nextState != null, "Tried to change HFSM to a null state"); + + HFSMState currentState = frame.FindAsset(hfsmData->CurrentState.Id); + currentState?.ExitState(nextState, frame, hfsmData, entity); + hfsmData->CurrentState = nextState; + + if (frame.IsVerified == true && entity != default(EntityRef)) + { + StateChanged?.Invoke(entity, hfsmData->CurrentState.Id.Value, transitionId); + } + + nextState.EnterState(frame, hfsmData, entity); + } + } +} diff --git a/data/HFSM.Root.cs b/data/HFSM.Root.cs new file mode 100644 index 0000000000000000000000000000000000000000..594fd481fd36abddfcba3f4c3c6da4ecf68b0669 --- /dev/null +++ b/data/HFSM.Root.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using Photon.Deterministic; + +namespace Quantum +{ + public partial class HFSMRoot : AssetObject + { + public string Label; + + public AssetRefHFSMState[] StatesLinks; + + public AssetRefHFSMState InitialState + { + get + { + if (StatesLinks != null) + { + return StatesLinks[0]; + } + return default; + } + } + + public string[] EventsNames; + + [NonSerialized] + public Dictionary RegisteredEvents = new Dictionary(); + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + RegisteredEvents.Clear(); + for (int i = 0; i < EventsNames.Length; i++) + { + RegisteredEvents.Add(EventsNames[i], i + 1); + } + } + + public string GetEventName(int eventID) + { + foreach (var kvp in RegisteredEvents) + { + if (kvp.Value == eventID) + return kvp.Key; + } + return ""; + } + } +} \ No newline at end of file diff --git a/data/HFSM.State.cs b/data/HFSM.State.cs new file mode 100644 index 0000000000000000000000000000000000000000..434ee8b28111733a6b481bbf5ab9601562da7d2a --- /dev/null +++ b/data/HFSM.State.cs @@ -0,0 +1,229 @@ +using Photon.Deterministic; +using System; +using System.Collections.Generic; + +namespace Quantum +{ + [AssetObjectConfig(GenerateLinkingScripts = true, GenerateAssetCreateMenu = false, GenerateAssetResetMethod = false)] + public unsafe partial class HFSMState : AssetObject + { + public string Label; + public AssetRefAIAction[] OnUpdateLinks; + public AssetRefAIAction[] OnEnterLinks; + public AssetRefAIAction[] OnExitLinks; + public HFSMTransition[] Transitions; + + public AssetRefHFSMState[] ChildrenLinks; + public AssetRefHFSMState ParentLink; + public int Level; + + [NonSerialized] + public AIAction[] OnUpdate; + [NonSerialized] + public AIAction[] OnEnter; + [NonSerialized] + public AIAction[] OnExit; + [NonSerialized] + public HFSMState[] Children; + [NonSerialized] + public HFSMState Parent; + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + OnUpdate = new AIAction[OnUpdateLinks == null ? 0 : OnUpdateLinks.Length]; + if (OnUpdateLinks != null) + { + for (Int32 i = 0; i < OnUpdateLinks.Length; i++) + { + OnUpdate[i] = (AIAction)resourceManager.GetAsset(OnUpdateLinks[i].Id); + } + } + OnEnter = new AIAction[OnEnterLinks == null ? 0 : OnEnterLinks.Length]; + if (OnEnterLinks != null) + { + for (Int32 i = 0; i < OnEnterLinks.Length; i++) + { + OnEnter[i] = (AIAction)resourceManager.GetAsset(OnEnterLinks[i].Id); + } + } + OnExit = new AIAction[OnExitLinks == null ? 0 : OnExitLinks.Length]; + if (OnExitLinks != null) + { + for (Int32 i = 0; i < OnExitLinks.Length; i++) + { + OnExit[i] = (AIAction)resourceManager.GetAsset(OnExitLinks[i].Id); + } + } + + Children = new HFSMState[ChildrenLinks == null ? 0 : ChildrenLinks.Length]; + if (ChildrenLinks != null) + { + for (Int32 i = 0; i < ChildrenLinks.Length; i++) + { + Children[i] = (HFSMState)resourceManager.GetAsset(ChildrenLinks[i].Id); + } + } + + Parent = (HFSMState)resourceManager.GetAsset(ParentLink.Id); + if (Transitions != null) + { + for (int i = 0; i < Transitions.Length; i++) + { + Transitions[i].Setup(resourceManager); + } + } + } + + internal Boolean UpdateState(Frame frame, FP deltaTime, HFSMData* hfsmData, EntityRef entity) + { + HFSMState parent = Parent; + Boolean transition = false; + + if (parent != null) + { + transition = parent.UpdateState(frame, deltaTime, hfsmData, entity); + } + + if (transition == true) + return true; + + *hfsmData->Times.GetPointer(Level) += deltaTime; + + DoUpdateActions(frame, entity); + return CheckStateTransitions(frame, hfsmData, entity, 0); + } + + internal Boolean Event(Frame frame, HFSMData* hfsmData, EntityRef entity, Int32 eventInt) + { + HFSMState p = Parent; + Boolean transition = false; + if (p != null) + { + transition = p.Event(frame, hfsmData, entity, eventInt); + } + + if (transition) + { + return true; + } + + return CheckStateTransitions(frame, hfsmData, entity, eventInt); + } + + private void DoUpdateActions(Frame frame, EntityRef entity) + { + for (int i = 0; i < OnUpdate.Length; i++) + { + OnUpdate[i].Update(frame, entity); + int nextAction = OnUpdate[i].NextAction(frame, entity); + if (nextAction > i) + { + i = nextAction; + } + } + } + private void DoEnterActions(Frame frame, EntityRef entity) + { + for (int i = 0; i < OnEnter.Length; i++) + { + OnEnter[i].Update(frame, entity); + int nextAction = OnEnter[i].NextAction(frame, entity); + if (nextAction > i) + { + i = nextAction; + } + } + } + private void DoExitActions(Frame frame, EntityRef entity) + { + for (int i = 0; i < OnExit.Length; i++) + { + OnExit[i].Update(frame, entity); + int nextAction = OnExit[i].NextAction(frame, entity); + if (nextAction > i) + { + i = nextAction; + } + } + } + + private bool CheckStateTransitions(Frame frame, HFSMData* hfsmData, EntityRef entity, Int32 eventKey = 0) + { + hfsmData->Time = *hfsmData->Times.GetPointer(Level); + + return CheckTransitions(frame, Transitions, hfsmData, entity, eventKey); + } + + private static bool CheckTransitions(Frame frame, HFSMTransition[] transitions, HFSMData* hfsmData, EntityRef entity, int eventKey, int depth = 0) + { + // Just to avoid accidental loops + if (depth == 10) + return false; + + if (transitions == null) + return false; + + for (int i = 0; i < transitions.Length; i++) + { + var transition = transitions[i]; + + if (transition.State == null && transition.TransitionSet == null) + continue; + + // Only consider evaluating the event if this transition HAS a event as requisite (EventKey != 0) + if (transition.EventKey != 0 && transition.EventKey != eventKey) + continue; + + if (transition.Decision != null && transition.Decision.Decide(frame, entity) == false) + continue; + + if (transition.State != null) + { + HFSMManager.ChangeState(transition.State, frame, hfsmData, entity, transition.Id); + return true; + } + else if (CheckTransitions(frame, transition.TransitionSet.Transitions, hfsmData, entity, eventKey, depth + 1) == true) + { + return true; + } + } + + return false; + } + + internal void EnterState(Frame frame, HFSMData* hfsmData, EntityRef entity) + { + *hfsmData->Times.GetPointer(Level) = FP._0; + DoEnterActions(frame, entity); + if (Children != null && Children.Length > 0) + { + HFSMState child = Children[0]; + HFSMManager.ChangeState(child, frame, hfsmData, entity, ""); + } + } + + internal void ExitState(HFSMState nextState, Frame frame, HFSMData* hfsmData, EntityRef entity) + { + if (nextState != null && nextState.IsChildOf(this) == true) + return; + + DoExitActions(frame, entity); + Parent?.ExitState(nextState, frame, hfsmData, entity); + } + + internal bool IsChildOf(HFSMState state) + { + HFSMState parent = Parent; + + if (parent == null) + return false; + + if (parent == state) + return true; + + return parent.IsChildOf(state); + } + } +} diff --git a/data/HFSM.TimerDecision.cs b/data/HFSM.TimerDecision.cs new file mode 100644 index 0000000000000000000000000000000000000000..3b02dee7f9ec17682a8f186b74068fb8f2e732c1 --- /dev/null +++ b/data/HFSM.TimerDecision.cs @@ -0,0 +1,25 @@ +using System; +using Photon.Deterministic; + +namespace Quantum +{ + [Serializable] + [AssetObjectConfig(GenerateLinkingScripts = true, GenerateAssetCreateMenu = false, GenerateAssetResetMethod = false)] + public partial class TimerDecision : HFSMDecision + { + public AIParamFP TimeToTrueState = FP._3; + + public override unsafe bool Decide(Frame frame, EntityRef entity) + { + var blackboard = frame.Has(entity) ? frame.Get(entity) : default; + + var agent = frame.Unsafe.GetPointer(entity); + var aiConfig = agent->GetConfig(frame); + + FP requiredTime = TimeToTrueState.Resolve(frame, entity, &blackboard, aiConfig); + + var hfsmData = &agent->Data; + return hfsmData->Time >= requiredTime; + } + } +} diff --git a/data/HFSM.Transition.cs b/data/HFSM.Transition.cs new file mode 100644 index 0000000000000000000000000000000000000000..4e500061d0a4d415fd9da466f498c76e2ecdbd5d --- /dev/null +++ b/data/HFSM.Transition.cs @@ -0,0 +1,29 @@ +using System; + +namespace Quantum +{ + [Serializable] + public class HFSMTransition + { + public string Id; + + public Int32 EventKey = 0; + public AssetRefHFSMDecision DecisionLink; + public AssetRefHFSMState StateLink; + public AssetRefHFSMTransitionSet TransitionSetLink; + + [NonSerialized] + public HFSMDecision Decision; + [NonSerialized] + public HFSMState State; + [NonSerialized] + public HFSMTransitionSet TransitionSet; + + public void Setup(IResourceManager resourceManager) + { + Decision = (HFSMDecision)resourceManager.GetAsset(DecisionLink.Id); + State = (HFSMState)resourceManager.GetAsset(StateLink.Id); + TransitionSet = (HFSMTransitionSet)resourceManager.GetAsset(TransitionSetLink.Id); + } + } +} diff --git a/data/HFSM.TransitionSet.cs b/data/HFSM.TransitionSet.cs new file mode 100644 index 0000000000000000000000000000000000000000..fac64831c35b1135e104c21f9f63a5f920bfd06a --- /dev/null +++ b/data/HFSM.TransitionSet.cs @@ -0,0 +1,32 @@ +using Photon.Deterministic; +using System; + +namespace Quantum +{ + public partial class HFSMTransitionSet + { + public string Label; + public AssetRefHFSMDecision PrerequisiteLink; + + public HFSMTransition[] Transitions; + + [NonSerialized] + public HFSMDecision Prerequisite; + + public override void Loaded(IResourceManager resourceManager, Native.Allocator allocator) + { + base.Loaded(resourceManager, allocator); + + Prerequisite = (HFSMDecision)resourceManager.GetAsset(PrerequisiteLink.Id); + + if (Transitions != null) + { + for (int i = 0; i < Transitions.Length; i++) + { + Transitions[i].Setup(resourceManager); + } + } + } + } +} + diff --git a/data/HFSM.qtn b/data/HFSM.qtn new file mode 100644 index 0000000000000000000000000000000000000000..d84d43f7b24deef26be6ae33b9d4d640a254ed60 --- /dev/null +++ b/data/HFSM.qtn @@ -0,0 +1,17 @@ +asset HFSMRoot; +asset HFSMState; +asset HFSMDecision; +asset HFSMTransitionSet; + +component HFSMAgent{ + HFSMData Data; + AssetRefAIConfig Config; +} + +struct HFSMData +{ + asset_ref Root; + asset_ref CurrentState; + FP Time; + array[8] Times; +} \ No newline at end of file diff --git a/data/callbacks.txt b/data/callbacks.txt new file mode 100644 index 0000000000000000000000000000000000000000..5c84e7439f330fd7e84cad400ac764a4655ce7ac --- /dev/null +++ b/data/callbacks.txt @@ -0,0 +1,201 @@ +Introduction +Collision and Trigger callbacks in Quantum are handled through system signals. There are two steps required to have callbacks executed for any specific entity: + +enabling the specific type(s) of callback to the entity(ies). +implementing the corresponding signal(s). +Before learning how to do enable callbacks and how to write code, it is important to first understand the different callback types and what causes them to be executed. + +Back To Top + + +Callback Types +Collisions and triggers start as either a two-entity or an entity-static pair generated by the collision detection step in the physics engine. Depending on the combination of physics components attached to entities, and the value of the trigger property (true/false), the tables below illustrate the types of callbacks that will be executed. + + +Entity Vs Entity +According to their component composition, physics-enabled entities can be classified as either: + +Non-Trigger Collider: Entity with a non-trigger Physics Collider and, optionally, a kinematic Physics Body. +Trigger Collider: Entity with a trigger Physics Collider and, optionally, a kinematic Physics Body. +Dynamic Body: Entity with a non-trigger Physics Collider and a dynamic (non-kinematic) Physics Body. +When a collision pair is composed of two entities A and B, these are the possibly executed callbacks (depending on the group each entity belongs to): + +Entities A x B Non-Trigger Collider Trigger Collider Dynamic Body +Non-Trigger Collider None OnTrigger OnCollision +Trigger Collider OnTrigger None OnTrigger +Dynamic Body OnCollision OnTrigger OnCollision +Back To Top + + +Entity Vs Static Collider +Static colliders, on the other hand, can be either Trigger or Non-Trigger, according to their IsTrigger property. These are the possible combinations when the collision pair is composed of an entity and a static collider: + +Components (Entity and Static) Non-Trigger Static Collider Trigger Static Collider +Non-Trigger Collider None OnTrigger +Trigger Collider OnTrigger None +Dynamic Body OnCollision OnTrigger +Back To Top + + +Enabling Callbacks On Entities +It is possible to control which callback types (and against which kinds of other collider) are enabled for each individual entity. This is done via the Entity Prototype in Unity or in code via the SetCallbacks function in the physics engine API, which takes the entity and a collision callbacks flag. + + +Via Entity Prototypes +The physics callbacks can be set on any Entity Prototype with a PhysicsCollider (2D/3D). + +Setting Physics Callbacks via the Entity Prototype's Physics Properties in the Unity Editor +Setting Physics Callbacks via the Entity Prototype's Physics Properties in the Unity Editor. +Each entity can have several callbacks. + +N.B.: Enabling the callbacks on an Entity Prototype only sets the callbacks for that particular entity. You still have to implement the corresponding signals in code. See the section on Callback Signals below for more information. + +Back To Top + + +Via Code +The callbacks flag is a bitmask and can specify multiple callback types by the use of bitwise operations as exemplified next. + +The following snippet enables the full set of OnTrigger callbacks against another dynamic entity (OnDynamicTrigger, OnDynamicTrigger OnDynamicTriggerEnter and OnDynamicTriggerExit). + +CallbackFlags flags = CallbackFlags.OnDynamicTrigger; +flags |= CallbackFlags.OnDynamicTriggerEnter; +flags |= CallbackFlags.OnDynamicTriggerExit; + +// for 2D +f.Physics2D.SetCallbacks(entity, flags); + +// for 3D +f.Physics3D.SetCallbacks(entity, flags); +These are the basic callback flags (the corresponding signals are called every tick until the collision is not being detected anymore by the physics engine): + +CallbackFlags.OnDynamicCollision +CallbackFlags.OnDynamicTrigger +CallbackFlags.OnStaticTrigger +And these are the corresponding Enter/Exit callbacks (that can be enabled independently from the above): + +CallbackFlags.OnDynamicCollisionEnter +CallbackFlags.OnDynamicCollisionExit +CallbackFlags.OnDynamicTriggerEnter +CallbackFlags.OnDynamicTriggerExit +CallbackFlags.OnStaticCollisionEnter +CallbackFlags.OnStaticCollisionExit +CallbackFlags.OnStaticTriggerEnter +CallbackFlags.OnStaticTriggerExit +Having to enable callbacks on a per-entity basis is an intentional design choice to make the default simulation as fast as possible. Also notice that Enter/Exit callbacks incur in a bit more memory and more CPU usage (compared to basic callbacks), so for the leanest possible simulation you should avoid these whenever possible. + +Back To Top + + +Callback Signals +N.B.: Collision and Trigger callbacks for both Entity vs Entity and Entity vs Static pairs are grouped into a unified signals API. + +These are the 2D Physics signals: + +namespace Quantum { + public interface ISignalOnCollision2D : ISignal { + void OnCollision2D(Frame f, CollisionInfo2D info); + } + public interface ISignalOnCollisionEnter2D : ISignal { + void OnCollisionEnter2D(Frame f, CollisionInfo2D info); + } + public interface ISignalOnCollisionExit2D : ISignal { + void OnCollisionExit2D(Frame f, ExitInfo2D info); + } + public interface ISignalOnTrigger2D : ISignal { + void OnTrigger2D(Frame f, TriggerInfo2D info); + } + public interface ISignalOnTriggerEnter2D : ISignal { + void OnTriggerEnter2D(Frame f, TriggerInfo2D info); + } + public interface ISignalOnTriggerExit2D : ISignal { + void OnTriggerExit2D(Frame f, ExitInfo2D info); + } +} +And the 3D Physics signals: + +namespace Quantum { + public interface ISignalOnCollision3D : ISignal { + void OnCollision3D(Frame f, CollisionInfo3D info); + } + public interface ISignalOnCollisionEnter3D : ISignal { + void OnCollisionEnter3D(Frame f, CollisionInfo3D info); + } + public interface ISignalOnCollisionExit3D : ISignal { + void OnCollisionExit3D(Frame f, ExitInfo3D info); + } + public interface ISignalOnTrigger3D : ISignal { + void OnTrigger3D(Frame f, TriggerInfo3D info); + } + public interface ISignalOnTriggerEnter3D : ISignal { + void OnTriggerEnter3D(Frame f, TriggerInfo3D info); + } + public interface ISignalOnTriggerExit3D : ISignal { + void OnTriggerExit3D(Frame f, ExitInfo3D info); + } +} +To receive the callback, you need to implement the corresponding signal interface in at least one active system (disabled systems do not get any signal executed in them): + +public class PickUpSystem : SystemSignalsOnly, ISignalOnTriggerEnter3D +{ + public void OnTriggerEnter3D(Frame f, TriggerInfo3D info) + { + if (!f.Has(info.Entity)) return; + if (!f.Has(info.Other)) return; + + var item = f.Get(info.Entity).Item; + var itemAsset = f.FindAsset(item.Id); + itemAsset.OnPickUp(f, info.Other, itemAsset); + + f.Destroy(info.Entity); + } +} +The code above exemplifies yet another optimization one can use when implementing signals. Inheriting from SystemSignalsOnly lets the system handle signals while not needing to schedule an empty update function (that would unecessarily incur into task system overhead). + +Back To Top + + +CollisionInfo +The signals for OnCollisionEnter and OnCollision offer up additional information on the colliding entities via the CollisionInfo struct. + + +Contact Points +The information on the contact points can be accessed through the ContactPoints API. + +Average : the average of all contact points +Count : the number of contact points +Length : a buffer with all contact points +First : returns the first contact point of the first Triangle +First can help saving computations if only one/any contact point is needed, and it does not have to be an averaged one. + +ContactPoints is also an iterator. When colliding with a mesh, it can be used to iterate through all triangle collisions contact points. + +while(info.ContactPoints.Next(out var cp)) { + Draw.Sphere(cp, radius); +} +Sphere-Triangle collisions have a single contact point; therefore, both Average and ContactPoints[0] will return the same contact. Other types of collision can have more contact points. + +Back To Top + + +Mesh Collision +When an entity is colliding with a mesh, Count and Average take all triangle collisions belonging to that specific mesh into account. Instead of receiving a callback for each triangle an entity collides with, these colliding triangles are grouped into a single CollisionInfo struct. + +In the case of a mesh collision, info.ContactNormal and info.Penetration will return the average value of that mesh's triangle collisions; the same data available through info.MeshTriangleCollisions.AverageNormal and info.MeshTriangleCollisions.AveragePenetration. + +In addition to the average normal an penetration, you can iterate through each triangle collision and access specific info such as the triangle data itself. MeshTriangleCollisions is also an iterator; it can be used to iterate through each triangle's collision data and retrieve mesh-specific collision data. + +if (info.IsMeshCollision) { + while(info.MeshTriangleCollisions.Next(out var triCollision)) { + Draw.Ray(triCollision.Triangle->Center, triCollision.ContactNormal * triCollision.Penetration); + } +} +Back To Top + + +Misc And FAQ +Useful information when working with collision callbacks: + +If 2 entities have the same callback set, the callback will be called twice - once per entity it is associated with. The only difference between the two calls are the values for Entity and Other which will be swapped. +Collisions with triggers and static colliders do not compute the Normal/Point/Penetration data. +Static Colliders on Unity can have a Quantum DB asset attached to their "Asset" field. Casting it to your expected custom asset types is a good way to add arbitrary data to your static collision callbacks. \ No newline at end of file diff --git a/data/cheaters.txt b/data/cheaters.txt new file mode 100644 index 0000000000000000000000000000000000000000..1487bf1dab49236308e3a0610cf1b80980721bf6 --- /dev/null +++ b/data/cheaters.txt @@ -0,0 +1,124 @@ +Cheat Protection +Introduction +Cheat Protection By Determinism +Trustable Match Results +How To Access The Replay Data +Self-Hosted Spectators Referee +Custom Quantum Server +Protect Client-Controlled Game Configs +Custom Authentication +Determinism As A Drawback +Perfect Information Problem +Detecting Cheaters Using Checksums + +Introduction +Security and cheat-protection are important aspects of online multiplayer games. Quantum's determinism offers unique features to address them. This page dives into the details on the built-in protection and offers best-practices to create production-ready online games with Photon Quantum. + +It is crucial for developers to be aware of all security related issues and the steps to mitigated those as well as at which time these should be taken. Although it is possible to run the full simulation on the server, it extremely rare this is sensible from a practical and cost perspective. + +Running servers is costly especially when the game does not generate revenue, yet. +In a most cases cheaters only make up a very small portion of the user base. +Making a game 100% cheat proof is an utopian idea, even keeping just one step ahead of them is a huge task. +There are game genres which rely on as much security as possible. +The single most important advice is: write the game now and add these more complex safety checks incrementally. It is perfectly viable to go live without a custom server and be successful. +In the documentation the following terms are used: + +a game backend refers to online services created and hosted by the customer; +a custom server (/plugin) refers to a customer-customized Quantum plugin hosted by Photon; and, +Quantum Public Cloud refers to non-customized Quantum servers hosted by Photon. +Back To Top + + +Cheat Protection By Determinism +The huge upside of a deterministic game, even without the server running the simulation, is its cheat-robustness; if one player modifies their client, for example by changing their character's speed, it will not affect any other player. They might notice the cheating player behaving strangely (e.g. continuously bumping into a wall) but otherwise their experience remains untainted. + +The reason for this is simple: each client deterministically runs the complete simulation locally and only shares input with other clients. + +Back To Top + + +Trustable Match Results +A match result is used to update the player progression in a game backend. In the most secure scenarios this is done from the server where the game logic ran. + +However, there are a few iterations possible to secure the results in a cost-efficient way before going towards an authoritative game server. + +Prototyping Clients push their individual results to the developers game backend. This is good for prototyping and games can also launch with this setup. Having a data structure which can be filled with the results to be sent to a backend is the first thing to have. +Majority Vote Clients push the match result individually but the backend choses the result based on a majority vote. Outliers that send (potentially) tampered data can be identified. There is no need to wait for a confirmation on the clients, just display them their result right away and save the progression only _after_ having had it validated on the backend. +Resimulation Replays If there is no agreement via the majority vote or a statistical evaluation smell, flag the match for a revalidation and collect the input history of the players. With the input history, the config files and the asset database the Quantum simulation can run outside Unity to recheck the result (replay). See the quantum-console-runner project in the Quantum SDK and read the following section on how to access the replay data. +Self-Hosted Spectators Referee Use a referee spectator, a .Net application, which can connect to live matches and run the simulation simultaneously, then submit the final results from it rather than letting the players send it. +Custom Quantum Server Run the simulation on the server and submit its result (see the quantum-custom-plugin SDK). +Back To Top + + +How To Access The Replay Data +Quantum 2.1 and earlier + +Request to be send by a client +Send from the Self-Hosted Spectators Referee +Send from the Custom Quantum Server +Quantum 3.0. (planned) + +Configurable webhook for Quantum Public Cloud users to stream the verified input history to another backend +Back To Top + + +Self-Hosted Spectators Referee +The Quantum simulation can be joined as a spectator; a spectator is a client which can connect to the server and run the full game without the ability of sending input. This can be used to start and initialize the game from a trusted source. + +Create a spectator application (runs outside Unity). +Run it anywhere to communicate securely with the game backend to start and prepare a Photon room. +Start a Quantum game. +Let other clients late-join the simulation (see the quantum-console-spectator project in the Quantum SDK). +Adding another, artificial client to the Photon Room for spectating purposes will increase the CCU count, which is the basis for the server cost. This might be a problem for games with low player counts (1vs1) as increases the CCU count perceptibly. +Back To Top + + +Custom Quantum Server +Running custom code on the Quantum server requires renting Photon Enterprise servers. It will enable authority over the following aspects: + +Option to run the game simulation on the server to: +Have a trusting source for game results +Enable server snapshots (are send by clients when using the Public Quantum Cloud) +Option to inject server controlled secrets into the game (server command) +Option to intercept and replace DeterministicConfig, RuntimeConfig and RuntimePlayer +Option to save validated replays +Back To Top + + +Protect Client-Controlled Game Configs +Two important shared configuration files are sent by the clients in the beginning of a match: DeterministicConfig (Quantum settings) and RuntimeConfig (game settings). The server will accept the first ones (DeterministicConfig, RuntimeConfig) it receives which is more or less random in the most cases. + +Clients also sending another config which is describing the player loadout: RuntimePlayer. To verify it against the player progression saved on a backend a custom Quantum server plugin is required. + +Public Quantum Cloud Chooses a random source for DeterministicConfig and RuntimeConfig +Custom Quantum Server All configs can be intercepted and replaced after retrieving it from the developers backend +Dashboard (coming in Quantum 3.0) DeterministicConfig can be forced on Public Cloud server by associating it with an AppId inside the Photon dashboard. +Webhooks (coming in Quantum 3.0) RuntimeConfig and RuntimePlayer can be verified by the developers backend calling a webrequest configured on the Photon dashboard. +Back To Top + + +Custom Authentication +We do not offer an authentication service nor a player database ourselves but we absolutely recommend to add a proprietary or third-party authentication service. + +photon realtime custom authentication + +Back To Top + + +Determinism As A Drawback +While Determinism has a lot of advantages, it comes with a few notable drawbacks inherent to this type of technology. + + +Perfect Information Problem +With Quantum every client has access to all information required to simulation the game locally (apart from other players' input). This means client controlled secrets used in a card game and Fog Of War-like features are easily hackable. + +There are also fringe cases which let clients "guess" a next random number or the ability to create bots. + +Back To Top + + +Detecting Cheaters Using Checksums +It is not recommended to use Quantum checksum detection for live games as a way to detect cheaters. + +Checksum calculation is expensive and could lead to hiccups; and, +The build-in mechanism stops the simulation for every client in the game session immediately. diff --git a/data/collider.txt b/data/collider.txt new file mode 100644 index 0000000000000000000000000000000000000000..98672509323b0c682ca4a9f64a8b6a6ad789076c --- /dev/null +++ b/data/collider.txt @@ -0,0 +1,205 @@ +Introduction +The collision and physics behaviour each have their own component in Quantum 2. + +Adding a PhysicsCollider2D/PhysicsCollider3D to an entity, turns entity into a dynamic obstacle or trigger which can be moved via its transform. +Adding a PhysicsBody2D/PhysicsBody3D allows the entity to be controlled by the physics solver. +Back To Top + + +Requirements +The Transform2D/Transform3D, PhysicsCollider2D/PhysicsCollider3D and PhysicsBody2D/PhysicsBody3D components are tightly intertwined. As such some of them are requirements for others to function. The complete dependency list can be found below: + +Requirement Transform PhysicsCollider PhysicsBody +Component +Transform ✓ ✗ ✗ +PhysicsCollider ✓ ✓ ✗ +PhysicsBody ✓ ✓ ✓ +These dependencies build on one another, thus you have to add the components to an entity in the following order if you wish to enable a PhysicsBody: + +Transform +PhysicsCollider +PhysicsBody +Back To Top + + +The PhysicsBody Component +Adding the PhysicsBody ECS component to an entity enables this entity to be taken into account by the physics engine. N.B.: the use of a PhysicsBody requires the entity to already have a Transform and a PhysicsCollider . + +You can create and initialize the components either manually in code, or via the EntityPrototype component in Unity. + +var entity = f.Create(); +var transform = new Transform2D(); +var collider = PhysicsCollider2D.Create(f, Shape2D.CreateCircle(1)); +var body = PhysicsBody2D.CreateDynamic(1); + +f.Set(entity, transform); +f.Set(entity, collider); +f.Set(entity, body); +The same rule applies to the 3D Physics: + +var entity = f.Create(); +var transform = Transform3D.Create(); + +var shape = Shape3D.CreateSphere(FP._1); + +var collider = PhysicsCollider3D.Create(shape); +var body = PhysicsBody3D.CreateDynamic(FP._1); + +f.Set(entity, transform); +f.Set(entity, collider); +f.Set(entity, body); +In case of the EntityPrototype method, the components will be initialized with their saved values. + +Adjusting an Entity Prototype's Physics Properties via the Unity Editor +Adjusting an Entity Prototype's Physics Properties via the Unity Editor. +The PhysicsCollider3D supports only supports the following Shape3D for dynamic entities: + +Sphere +Box +Back To Top + + +Center Of Mass +The Center of Mass, simply referred to as CoM from here on out, can be set on the PhysicsBody component. The CoM represents an offset relative to the position specified in the Transform component. Changing the position of the CoM allows to affect how forces are applied to the PhysicsBody. + +Animated examples of how various CoM affect the same PhysicsBody +Animated examples showcasing how various CoM affect the same PhysicsBody. +By default, the CoM is set to the centroid of the PhysicsCollider's shape. This is enforced by the Reset Center of Mass On Added in the PhysicsBody Config drawer. + +N.B.: To customize the CoM position, you HAVE TO uncheck the Reset Center of Mass On Added flag; otherwise the CoM will be reset to the Collider's centroid when the PhysicsBody component gets added to the entity. + +Defaults Flags in the PhysicsBody Config +Defaults Flags in the PhysicsBody Config viewed in the Unity Editor. +The above configuration is the commonly used for an entity behaving like a uniformly dense body, a.k.a. body with a uniform density. However, the CoM and collider offset are configured separately. The combinations are explained in the table below. + +PhysicsCollider Offset PhysicsBody CoM Reset Center of Mass On Added flag Resulting positions +Default Position = 0, 0, 0 +Custom Value = any position differing from the default position +Default Position Default Position On / Off Collider Centroid and the CoM positions are both equal to the transform position. +Custom Value Default Position On Collider Centroid is offset from the transform, and the CoM is equal to the Collider Centroid position. +Custom Value Default Position Off Collider Centroid is offset from the transform position. +The CoM is equal to the transform position. +Custom Value Custom Position On Collider Centroid is offset from the transform position. +The CoM is equal to the Collider Centroid position. +Custom Value Custom Position Off Collider Centroid is offset from the transform position. +The CoM is offset from the transform position. +Back To Top + + +Compound Collider CoM +A compound shape's CoM is a combination of all the shape's elements' centroids based on the weighted average of their areas (2D) or volumes (3D). + +Back To Top + + +Key Points +In summary, these are the main points you need to takeaway regarding the CoM configuration. + +The PhysicsCollider offset and PhysicsBody CoM positions are distinct from one another. +By default the PhysicsBody Config has the flags Reset Center of Mass On Added and Reset Inertia on Added. +To set a custom CoM, uncheck the Reset Center of Mass On Added flag in the PhysicsBody Config. +If the Reset Center of Mass On Added flag is checked on the PhysicsBody Config, the CoM will be automatically set to the PhysicsCollider centroid upon being added to the entity - regardless of the CoM position specified in the Editor. +Back To Top + + +Applying External Forces +The PhysicsBody API allows for the manual application of external forces to a body. + +// This is the 3D API, the 2D one is identical. + +public void AddTorque(FPVector3 amount) +public void AddAngularImpulse(FPVector3 amount) + +public void AddForce(FPVector3 amount, FPVector3? relativePoint = null) +public void AddLinearImpulse(FPVector3 amount, FPVector3? relativePoint = null) +// relativePoint is a vector from the body's center of mass to the point where the force is being applied, both in world space. +// If a relativePoint is provided, the resulting Torque is computed and applied. + +public void AddForceAtPosition(FPVector3 force, FPVector3 position, Transform3D* transform) +public void AddImpulseAtPosition(FPVector3 force, FPVector3 position, Transform3D* transform) +// Applies the force/impulse at the position specified while taking into account the CoM. +As you can gather from the API, angular and linear momentum of the PhysicsBody can be affected by applying: + +forces; or +impulses. +Although they are similar, there is a key different; forces are applying over a period of time, while impulses are immediate. You can think of them as: + +Force = Force per deltatime +Impulse = Force per frame +Note: In Quantum deltatime is fixed and depended on the simulation rate set in Simulation Config asset. + +An impulse will produce the same effect, regardless of the simulation rate. However, a force depends on the simulation rate - this means applying a force vector of 1 to a body at a simulation rate of 30, if you increase the simulation rate to 60 the deltatime with be half and thus the integrated force will be halved as well. + +Generally speaking, it is advisable to use an impulse when a punctual and immediate change is meant to take place; while a force should be used for something that is either constantly, gradually, or applied over a longer period of time. + +Back To Top + + +Initializing The Components +To initialize a PhysicsBody as either a Dynamic or Kinematic body, you can use their respective Create functions. These methods are accessible via the PhysicsBody2D and PhysicsBody3D classes, e.g.: + +PhysicsBody3D.CreateDynamic +PhysicsBody3D.CreateKinematic +Back To Top + + +ShapeConfigs +To initialize PhysicsCollider and PhysicsBody via data-driven design, you can use the ShapeConfig types (Shape2DConfig, and Shape3DConfig). These structs can be added as a property to any Quantum data-asset, editable from Unity (for shape, size, etc). + +// data asset containing a shape config property +partial class CharacterSpec { + // this will be edited from Unity + public Shape2DConfig Shape2D; + public Shape3DConfig Shape3D; + public FP Mass; +} +When initializing the body, we use the shape config instead of the shape directly: + +// instantiating a player entity from the Frame object +var playerPrototype = f.FindAsset(PLAYER_PROTOTYPE_PATH); +var playerEntity = playerPrototype.Container.CreateEntity(f); + +var playerSpec = f.FindAsset("PlayerSpec"); + +var transform = Transform2D.Create(); +var collider = PhysicsCollider2D.Create(playerSpec.Shape2D.CreateShape(f)); +var body = PhysicsBody2D.CreateKinematic(playerSpec.Mass); + +// or the 3D equivalent: +var transform = Transform3D.Create(); +var collider = PhysicsCollider3D.Create(playerSpec.Shape3D.CreateShape()) +var body = PhysicsBody3D.CreateKinematic(playerSpec.Mass); + +// Set the component data +f.Set(playerEntity, transform); +f.Set(playerEntity, collider); +f.Set(playerEntity, body); +Back To Top + + +Enabling Physics Callbacks +An entity can have a set of physics callbacks associated with it. These can be enabled either via code or in the Entity Prototype's PhysicsCollider component. + +Setting Physics Callbacks via the Entity Prototype's Physics Properties in the Unity Editor +Setting Physics Callbacks via the Entity Prototype's Physics Properties in the Unity Editor. +For information on how to set the physics callbacks in code and implement their respective signals, please refer to the Callbacks entry in the Physics manual. + +Back To Top + + +Kinematic +In Quantum v2 there are 4 different ways for a physics entity to have kinematic-like behaviour: + +By having only a PhysicsCollider component. In this case the entity does not have a PhysicsBody component; i.e. no mass, drag, force/torque integrations, etc... . You can manipulate the entity transform at will, however, when colliding with dynamic bodies, the collision impulses are solved as if the entity was stationary (zeroed linear and angular velocities). +By disabling the PhysicsBody component. If you set the IsEnabled property on a PhysicsBody to false, the physics engine will treat the entity in same fashion as presented in Point 1 - i.e as having only a collider component. No forces or velocities are integrated. This is suitable if you want the body to behave like a stationary entity temporarily and its config (mass, drag coefficients, etc) for when you re-enable it at a later point. +By setting the IsKinematic property on a PhysicsBody component to true. In this case the physics engine will not move affect the PhysicsBody itself, but the body's linear and angular velocities will still have affect other bodies when resolving collisions. Use this if you want to control the entity movement instead of letting the physics engine do it, and know that you have the responsibility to move an entity and control a body's velocity manually, while still having other dynamic bodies react to it. +By initializing the PhysicsBody with CreateKinematic. If the body is expected to behave as kinematic during its entire lifetime, you can simply create it as a kinematic body. This will have the PhysicsBody behave like in 3 from the very beginning. If the body needs to eventually become dynamic one, you simply create a new one with the CreateDynamic method and set IsKinematic = true. Setting IsKinematic to true/false and re-initializing the PhysiscBody component as dynamic/kinematic can be done seamlessly at any time. +Back To Top + + +The PhysicsCollider Component + +Disabling / Enabling The Component +Since Quantum 2.1, the PhysicsCollider component is equipped with an Enabled property. When setting this property to false, the entity with the PhysicsCollider will be ignored in the PhysicsSystem. + +As the PhysicsBody requires an active PhysicsCollider, it will be effectively disabled as well. \ No newline at end of file diff --git a/data/command.txt b/data/command.txt new file mode 100644 index 0000000000000000000000000000000000000000..e3f028bd461d38015f508844c36e9bf0da1483f0 --- /dev/null +++ b/data/command.txt @@ -0,0 +1,308 @@ +Commands +Introduction +Command System Setup In The Simulation +DeterministicCommandSetup +CommandSetup (Only For 2.0!) +Sending Commands From The View +Sending CompoundCommands From The View +Overloads +Polling Commands From The Simulation +Note +Examples For Collections +List +Array +Compound Command Example + +Introduction +Quantum Commands are an input data paths to Quantum standard APIs. They are similar to Quantum Inputs but are not required to be sent every tick. + +Quantum Commands are fully reliable. The server will always accept them and confirm it, regardless of the time at which they are sent. This comes with a trade-off; namely local clients, including the one who sent the command, cannot predict the tick in which the command is received by the simulation. While visual prediction can be shown if needed, the simulation will only receive the Command after the server has confirmed it as part of a tick. + +Commands are implemented as regular C# classes that inherit from Photon.Deterministic.DeterministicCommand. They can contain any serializable data. + +using Photon.Deterministic; +namespace Quantum +{ + public class CommandSpawnEnemy : DeterministicCommand + { + public long enemyPrototypeGUID; + + public override void Serialize(BitStream stream) + { + stream.Serialize(ref enemyPrototypeGUID); + } + + public void Execute(Frame f) + { + var enemyPrototype = f.FindAsset(enemyPrototypeGUID); + enemyPrototype.Container.CreateEntity(f); + } + } +} +Back To Top + + +Command System Setup In The Simulation +In order for commands to be sent by Quantum, a system needs to be created for them and be made aware of the available commands. + +In Quantum 2.1 and onwards, Commands need to be registered in CommandSetup.User.cs. +In Quantum 2.0, Commands need to be registered in CommandSetup.cs. +Back To Top + + +DeterministicCommandSetup +Commands need to be added to the command factories found in CommandSetup.User.cs to be available at runtime. + +N.B.: CommandSetup.Legacy.cs is not directly used in this setup; however, it needs to be kept in 2.1 for compatibility reasons. + +// CommandSetup.User.cs + +namespace Quantum { + public static partial class DeterministicCommandSetup { + static partial void AddCommandFactoriesUser(ICollection factories, RuntimeConfig gameConfig, SimulationConfig simulationConfig) { + // user commands go here + // new instances will be created when a FooCommand is received (de-serialized) + factories.Add(new FooCommand()); + + // BazCommand instances will be acquired from/disposed back to a pool automatically + factories.Add(new DeterministicCommandPool()); + } + } +} + +---------- + +// CommandSetup.Legacy.cs + +using Photon.Deterministic; +namespace Quantum { + public static class CommandSetup { + public static DeterministicCommand[] CreateCommands(RuntimeConfig gameConfig, SimulationConfig simulationConfig) { + return new null; + } + } +} +Back To Top + + +CommandSetup (Only For 2.0!) +In Quantum 2.0 Commands need to be registered in CommandSetup.cs to be available at runtime. + +N.B.: This system is obsolete in newer versions of Quantum; refer to DeterministicCommandSetup for more information. + +using Photon.Deterministic; +namespace Quantum { + public static class CommandSetup { + public static DeterministicCommand[] CreateCommands(RuntimeConfig gameConfig, SimulationConfig simulationConfig) { + return new DeterministicCommand[] { + + // user commands go here + new CommandSpawnEnemy(), + }; + } + } +} +Back To Top + + +Sending Commands From The View +Commands can be send from anywhere inside Unity. + +using Quantum; +using UnityEngine; + +public class UISpawnEnemy : MonoBehaviour +{ + [SerializeField] private EntityPrototypeAsset enemyPrototype = null; + private PlayerRef _playerRef; + + public void Initialize(PlayerRef playerRef) { + _playerRef = playerRef; + } + + public void SpawnEnemy() { + CommandSpawnEnemy command = new CommandSpawnEnemy() + { + enemyPrototypeGUID = enemyPrototype.Settings.Guid.Value, + }; + QuantumRunner.Default.Game.SendCommand(command); + } +} +Back To Top + + +Sending CompoundCommands From The View +N.B.: This is only available from Quantum 2.1 onwards. + +To send multiple commands at once, simply create a CompoundCommand and add each individual DeterministicCommand to it before sending it. + +var compound = new Quantum.Core.CompoundCommand(); +compound.Commands.Add(new FooCommand()); +compound.Commands.Add(new BazCommand()); + +QuantumRunner.Default.Game.SendCommand(compound); +Back To Top + + +Overloads +SendCommand() has two overloads. + +void SendCommand(DeterministicCommand command); +void SendCommand(Int32 player, DeterministicCommand command); +Specify the player index (PlayerRef) if multiple players are controlled from the same machine. Games with only one local player can ignore the player index field. + +Back To Top + + +Polling Commands From The Simulation +To receive and handle Commands inside the simulation poll the frame for a specific player: + +using Photon.Deterministic; +namespace Quantum +{ + public class PlayerCommandsSystem : SystemMainThread + { + public override void Update(Frame f) + { + for (int i = 0; i < f.PlayerCount; i++) + { + var command = f.GetPlayerCommand(i) as CommandSpawnEnemy; + command?.Execute(f); + } + } + } +} +Like any other sytem, the system handling the command polling and consumption needs to be included in SystemSetup.cs + +namespace Quantum { + public static class SystemSetup { + public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig) { + return new SystemBase[] { + // pre-defined core systems + [...] + + // user systems go here + new PlayerCommandsSystem(), + + }; + } + } +} +Back To Top + + +Note +The API does neither enforce, nor implement, a specific callback mechanism or design pattern for Commands. It is up to the developer to chose how to consume, interpret and execute Commands; for example by encoding them into signals, using a Chain of Responsibility, or implementing the command execution as a method in them. + +Back To Top + + +Examples For Collections + +List +using System.Collections.Generic; +using Photon.Deterministic; + +namespace Quantum +{ + public class ExampleCommand : DeterministicCommand + { + public List Entities = new List(); + + public override void Serialize(BitStream stream) + { + var count = Entities.Count; + stream.Serialize(ref count); + if (stream.Writing) + { + foreach (var e in Entities) + { + var copy = e; + stream.Serialize(ref copy.Index); + stream.Serialize(ref copy.Version); + } + } + else + { + for (int i = 0; i < count; i++) + { + EntityRef readEntity = default; + stream.Serialize(ref readEntity.Index); + stream.Serialize(ref readEntity.Version); + Entities.Add(readEntity); + } + } + } + } +} +Back To Top + + +Array +using Photon.Deterministic; + +namespace Quantum +{ + public class ExampleCommand : DeterministicCommand + { + public EntityRef[] Entities; + + public override void Serialize(BitStream stream) + { + stream.SerializeArrayLength(ref Entities); + for (int i = 0; i < Cars.Length; i++) + { + EntityRef e = Entities[i]; + stream.Serialize(ref e.Index); + stream.Serialize(ref e.Version); + Entities[i] = e; + } + } + } +} +Back To Top + + +Compound Command Example +Only one command can be attached to an input stream per tick. Even though a client can send multiple Deterministic Commands per tick, the commands will not reach the simulation at the same tick, rather they will arrive separately on consecutive ticks. To go around this limitation, you can pack multiple Deterministic Commands into a single CompoundCommand. + +Quantum 2.1 already offers this class. And for previous versions it can be added like this: + +public class CompoundCommand : DeterministicCommand { + public static DeterministicCommandSerializer CommandSerializer; + public List Commands = new List(); + + public override void Serialize(BitStream stream) { + if (CommandSerializer == null) { + CommandSerializer = new DeterministicCommandSerializer(); + CommandSerializer.RegisterPrototypes(CommandSetup.CreateCommands(null, null)); + } + + var count = Commands.Count; + stream.Serialize(ref count); + + if (stream.Reading) { + Commands.Clear(); + } + + for (var i = 0; i < count; i++) { + if (stream.Reading) { + CommandSerializer.ReadNext(stream, out var cmd); + Commands.Add(cmd); + } else { + CommandSerializer.PackNext(stream, Commands[i]); + } + } + } +} +To then dispatch the compounded commands: + +public override void Update(Frame f) { + for (var i = 0; i < f.PlayerCount; i++) { + var compoundCommand = f.GetPlayerCommand(i) as CompoundCommand; + if (compoundCommand != null) { + foreach (var cmd in compoundCommand.Commands) { + } + } + } +} diff --git a/data/components.txt b/data/components.txt new file mode 100644 index 0000000000000000000000000000000000000000..f11e177176c840dbe85cffa70c4ed0fc719cdd0d --- /dev/null +++ b/data/components.txt @@ -0,0 +1,326 @@ +Introduction +Components are special structs that can be attached to entities, and used for filtering them (iterating only a subset of the active entities based on its attached components). + +Aside from custom components, Quantum comes with several pre-built ones: + +Transform2D/Transform3D: position and rotation using Fixed Point (FP) values; +PhysicsCollider, PhysicsBody, PhysicsCallbacks, PhysicsJoints (2D/3D): used by Quantum's stateless physics engines; +PathFinderAgent, SteeringAgent, AvoidanceAgent, AvoidanceOBstacle: navmesh-based path finding and movement. +Back To Top + + +Component +This is a basic example definition of a component in the DSL: + +component Action +{ + FP Cooldown; + FP Power; +} +Labeling them as components (like above), instead of structs, will generate the appropriate code structure (marker interface, id property, etc). Once compiled, these will also be available in the Unity Editor for use with the Entity Prototype. In the editor, custom components are named Entity Component ComponentName. + +The API to work on components is presented via the Frame class. You have the option of working on copies on the components, or on them components via pointers. To distinguish between the access type, the API for working on copies is accessible directly viaFrame and the API for accessing pointers is available under Frame.Unsafe - as the latter modifies the memory. + +The most basic functions you will require to add, get and set components are the functions of the same name. + +Add is used to add a component to an entity. Each entity can only carry one copy of a certain component. To aid you in debugging, Add returns an AddResult enum. + +public enum AddResult { + EntityDoesNotExist = 0, // The EntityRef passed in is invalid. + ComponentAlreadyExists = 1, // The Entity in question already has this component attached to it. + ComponentAdded = 2 // The component was successfully added to the entity. +} +Once an entity has a component, you can retrieve it with Get. This will return a copy of the component value. Since you are working on a copy, you will need to save the modified values on the component using Set. Similarly to the Add method, it returns a SetResult which can be used to verify the operation's result or react to it. + +public enum SetResult { + EntityDoesNotExist = 0, // The EntityRef passed in is invalid. + ComponentUpdated = 1, // The component values were successfully updated. + ComponentAdded = 2 // The Entity did not have a component of this type yet, so it was added with the new values. +} +For example if you were to set the starting value of a health component, you would do the following: + +private void SetHealth(Frame f, EntityRef entity, FP value){ + var health = f.Get(entity); + health.Value = value; + f.Set(entity, health); +} +This table recaps the methods already presented and the others offered to you to manipulate components and their values are: + +Method Return Additional Info +Add(EntityRef entityRef) AddResult enum, see above. allows an invalid entity ref. +Get(EntityRef entityRef) T +a copy of T with the current values. does NOT allow an invalid entity ref. +Throws an exception if the component T is not present on the entity. +Set(EntityRef entityRef) SetResult enum, see above. allows an invalid entity ref. +Has(EntityRef entityRef) bool +true = entity exists and the component is attached +false = entity does not exist, or component is not attached. allows invalid entity ref, +and component to not exist. +TryGet(EntityRef entityRef, out T value) bool +true = entity exists and component is attached to it. +false = entity does not exist, or component not attached to it. allows an invalid entity ref. +TryGetComponentSet(EntityRef entityRef, +out ComponentSet componentSet) bool +true = entity exists and all components of the components are attached +false = entity does not exist, or one or more components of the set are +not attached. allows an invalid entity ref. +Remove(EntityRef entityRef) No return value. +Will remove component if the entity exists and carries the component. +Otherwise does nothing. allows an invalid entity ref. +To facilitate working on components directly and avoid the -small- overhead from using Get/Set, Frame.Unsafe offers unsafe versions of Get and TryGet (see table below). + +Method Return Additional Info +GetPointer(EntityRef entityRef) T* does NOT allow invalid entity ref. +Throws an exception if the component T is not present on the entity. +TryGetPointer(EntityRef entityRef +out T* value) bool +true = entity exists and component is attached to it. +false = entity does not exist, or component not attached to it. allows an invalid entity ref. +Back To Top + + +Singleton Component +A Singleton Component is a special type of component of which only one can exist at any given time. There can ever only be one instance of a specific T singleton component, on any entity in the entire game state - this is enforced deep in the core of the ECS data buffers. This is strictly enforced by Quantum. + +A custom Singleton Component can be defined in the DSL using singleton component. + +singleton component MySingleton{ + FP Foo; +} +Singletons inherit an interface called IComponentSingleton which itself inherits from IComponent. It can therefore do all the common things you would expect from regular components: + +It can be attached to any entity. +It can be managed with all the regular safe & unsafe methods (e.g. Get, Set, TryGetPointer, etc...). +It can be put on entity prototypes via the Unity Editor, or instantiated in code on an entity. +In addition to the regular component related methods, there are several special methods dedicated to singletons. Just like for regular components, the methods are separated in Safe and Unsafe based on whether they return a value type or a pointer. + +Method Return Additional Info +API - Frame +SetSingleton (T component, +EntityRef optionalAddTarget = default) void Sets a singleton IF the singleton does not exist. +------- +EntityRef (optional), specifies which entity to add it to. +IF none is given, a new entity will be created to add the singleton to. +GetSingleton() T Throws exception if singleton does not exist. +No entity ref is needed, it will find that automatically. +TryGetSingleton(out T component) bool +true = singleton exists +false = singleton does NOT exist Does NOT throw an exception if singleton does not exist. +No entity ref is needed, it will find that automatically. +GetOrAddSingleton(EntityRef optionalAddTarget = default) T Gets a singleton and returns it. +IF the singleton does not exist, it will be created like in SetSingleton. +----- +EntityRef (optional), specifies which entity to add it to if it has to be created. +A new entity will be created to add the singleton to if no EntityRef is passed in. +GetSingletonEntityRef() EntityRef Returns the entity which currently holds the singleton. +Throws if the singleton does not exist. +TryGetSingletonEntityRef(out EntityRef entityRef) bool +true = singleton exists. +false = singleton does NOT exist. Get the entity which currently holds the singleton.Does NOT throw if the single does not exist. +API - Frame.Unsafe +Unsafe.GetPointerSingleton() T* Gets a singleton pointer. +Throws exception if it does not exist. +TryGetPointerSingleton(out T* component) bool +true = singleton exists. +false = singleton does NOT exist. Gets a singleton pointer. +GetOrAddSingletonPointer(EntityRef optionalAddTarget = default) T* Gets or Adds a singleton and returns it. +IF the singleton does not exist, it will be created. +----- +EntityRef (optional), specifies which entity to add it to if it has to be created. +A new entity will be created to add the singleton to if no EntityRef is passed in. +Back To Top + + +Adding Functionality +Since components are special structs, you can extend them with custom methods by writing a partial struct definition in a C# file. For example, if we could extend our Action component from before as follows: + +namespace Quantum +{ + public partial struct Action + { + public void UpdateCooldown(FP deltaTime){ + Cooldown -= deltaTime; + } + } +} +Back To Top + + +Reactive Callbacks +There are two component specific reactive callbacks: + +ISignalOnAdd: called when a component type T is added to an entity. +ISignalOnRemove: called when a component type T is removed from an entity. +These are particularly useful in case you need to manipulate part of the component when it is added/removed - for instance allocate and deallocate a list in a custom component. + +To receive these signals, simply implement them in a system. + +Back To Top + + +Components Iterators +If you were to require a single component only, ComponentIterator (safe) and ComponentBlockIterator (unsafe) are best suited. + +foreach (var pair in frame.GetComponentIterator()) +{ + var component = pair.Component; + component.Position += FPVector3.Forward * frame.DeltaTime; + frame.Set(pair.Entity, component); +} +Component block iterators give you the fastest possible access via pointers. + +// This syntax returns an EntityComponentPointerPair struct +// which holds the EntityRef of the entity and the requested Component of type T. +foreach (var pair in frame.Unsafe.GetComponentBlockIterator()) +{ + pair.Component->Position += FPVector3.Forward * frame.DeltaTime; +} + +// Alternatively, it is possible to use the following syntax to deconstruct the struct +// and get direct access to the EntityRef and the component +foreach (var (entityRef, transform) in frame.Unsafe.GetComponentBlockIterator()) +{ + transform->Position += FPVector3.Forward * frame.DeltaTime; +} +Back To Top + + +Filters +Filters are a convenient way to filter entities based on a set of components, as well as grabbing only the necessary components required by the system. Filters can be used for both Safe (Get/Set) and Unsafe (pointer) code. + + +Generic +To create a filter simply use the Filter() API provided by the frame. + +var filtered = frame.Filter(); +The generic filter can contain up to 8 components. If you need to more specific by creating without and any ComponentSet filters. + +var without = ComponentSet.Create(); +var any = ComponentSet.Create(); +var filtered = frame.Filter(without, any); +A ComponentSet can hold up to 8 components. The ComponentSet passed as the without parameter will exclude all entities carrying at least one of the components specified in the set. The any set ensures entities have at least one or more of the specified components; if an entity has none of the components specified, it will be excluded by the filter. + +Iterating through the filter is as simple as using a while loop with filter.Next(). This will fill in all copies of the components, and the EntityRef of the entity they are attached to. + +while (filtered.Next(out var e, out var t, out var b)) { + t.Position += FPVector3.Forward * frame.DeltaTime; + frame.Set(e, t); +} +N.B.: You are iterating through and working on copies of the components. So you need to set the new data back on their respective entity. + +The generic filter also offers the possibility to work with component pointers. + +while (filtered.UnsafeNext(out var e, out var t, out var b)) { + t->Position += FPVector3.Forward * frame.DeltaTime; +} +In this instance you are modifying the components' data directly. + +Back To Top + + +FilterStruct +In addition to regular filters, you may use the FilterStruct approach. For this you need to first define a struct with public properties for each component type you would like to receive. + +struct PlayerFilter +{ + public EntityRef Entity; + public CharacterController3D* KCC; + public Health* Health; + public FP AccumulatedDamage; +} +Just like a ComponentSet, a FilterStruct can filter up to 8 different component pointers. + +N.B.: A struct used as a FilterStruct is required to have an EntityRef field! + +The component type members in a FilterStruct HAVE TO BE pointers; only those will be filled by the filter. In addition to component pointers, you can also define other variables, however, these will be ignored by the filter and are left to you to manage. + +var players = f.Unsafe.FilterStruct(); +var playerStruct = default(PlayerFilter); + +while (players.Next(&playerStruct)) +{ + // Do stuff +} +Frame.Unsafe.FilterStruct() has an overload utilizing the optional ComponentSets any and without to further specify the filter. + +Back To Top + + +Note On Count +A filter does not know in advance how many entities it will touch and iterate over. This is due to the way filters work in Sparse-Set ECS: + +the filter finds which among the components provided to it has the least entities associated with it (smaller set to check for intersection); and then, +it goes through the set and discards any entity that does not have the other queried components. +Knowing the exact number in advance would require traversing the filter once; as this is an (O(n) operation, it would not be efficient. + +Back To Top + + +Components Getter +Should you want to get a specific set of components from a known entity, use a filter struct in combination with the Frame.Unsafe.ComponentGetter. N.B.: This is only available in an unsafe context! + +public unsafe class MySpecificEntitySystem : SystemMainThread + + struct MyFilter { + public EntityRef Entity; // Mandatory member! + public Transform2D* Transform2D; + public PhysicsBody2D* Body; + } + + public override void Update(Frame f) { + MyFilter result = default; + + if (f.Unsafe.ComponentGetter().TryGet(f, f.Global->MyEntity, &result)) { + // Do Stuff + } + } +If this operation has to performed often, you can cache the look-up struct in the system as shown below (100% safe). + +public unsafe class MySpecificEntitySystem : SystemMainThread + + struct MyFilter { + public EntityRef Entity; // Mandatory member! + public Transform2D* Transform2D; + public PhysicsBody2D* Body; + } + + ComponentGetter _myFilterGetter; + + public override void OnInit(Frame f) { + _myFilterGetter = f.Unsafe.ComponentGetter(); + } + + public override void Update(Frame f) { + MyFilter result = default; + + if (_myFilterGetter.TryGet(f, f.Global->MyEntity, &result)) { + // Do Stuff + } + } +Back To Top + + +Filtering Strategies +Often times you will be running into a situation where you will have many entities, but you only want a subset of them. Previously we introduced the components and tools available in Quantum to filter them; in this section, we will present some strategies that utilize these. N.B.: The best approach will depend on your own game and its systems. We recommend taking the strategies below as a jumping off point to create a fitting one to your unique situation. + +Note: All terminology used below has been created in-house to encapsulate otherwise wordy concepts. + +Back To Top + + +Micro-component +Although many entities may be using the same component types, few entities use the same component composition. One way to further specialize their composition is by the use of micro-components . Micro-components are highly specialized components with data for a specific system or behaviour. Their uniqueness will allow you to create filters that can quickly identify the entities carrying it. + +Back To Top + + +Flag-component +One common way to identify entities is by adding a flag-component to them. In ECS the concept of flags does not exist per-se, nor does Quantum support entity types; so what exactly are flag-components ? They are components holding little to no data and created for the exclusive purpose of identifying entities. + +For instance, in a team based game you could have: + +a "Team" component with an enum for TeamA and TeamB; or +a "TeamA" and "TeamB" component. +Option 1. is helpful when the main purpose is polling the data from the View, while option 2. will enable you to benefit from the filtering performance in the relevant simulation systems. + +Note: Sometimes a flag-component are also referred to as tag-component because tagging and flagging entities is used interchangeably. \ No newline at end of file diff --git a/data/config-class ref.txt b/data/config-class ref.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc5d2340ee21563638371d90281ad2f93ec63ded --- /dev/null +++ b/data/config-class ref.txt @@ -0,0 +1,165 @@ +Photon.Deterministic.DeterministicSessionConfig Class Reference +Parameterize internals of the Deterministic simulation and plugin (the Quantum server component). More... + +Public Attributes +Boolean AggressiveSendMode = false + If the server should skip buffering and perform aggressive input sends, only suitable for games with less or equal 4 players. More... + +Boolean ChecksumCrossPlatformDeterminism = false + If Quantum should skip performing rollbacks and re-predict when it's not needed to retain determinism. Not used in lockstep mode. Mutually exclusive with the _BW_COMPAT_ExposeVerifiedStatusInsideSimulation setting. More... + +Int32 ChecksumInterval = 60 + How often we should send checksums of the frame state to the server for verification (useful during development, set to zero for release). Defined in frames. More... + +Int32 InputDelayMax = 60 + The maximum input offset a player can have. More... + +Int32 InputDelayMin = 0 + The minimum input offset a player can have. More... + +Int32 InputDelayPingStart = 100 + At what ping value that Quantum starts applying input offset. Defined in milliseconds. More... + +Int32 InputFixedSize + Fixed input size. More... + +Boolean InputFixedSizeEnabled + If the input data has a fixed byte length, enabling this saves bandwidth. More... + +Int32 InputHardTolerance = 8 + How many frames the server will wait until it expires a frame and replaces all non-received inputs with repeated inputs or null's and sends it out to all players. More... + +Int32 InputRedundancy = 3 + How much staggering the Quantum client should apply to redundant input resends. 1 = Wait one frame, 2 = Wait two frames, etc. More... + +Int32 InputRepeatMaxDistance = 10 + How many frames Quantum will scan for repeatable inputs. 5 = Scan five frames forward and backwards, 10 = Scan ten frames, etc. More... + +Boolean LockstepSimulation = false + Runs the quantum simulation in lockstep mode, where no rollbacks are performed. s recommended to set input InputDelayMin to at least 10 and _BW_COMPAT_InputPacking to 1. More... + +Int32 MinOffsetCorrectionDiff = 1 + How many frames the current local input delay must diff to the current requested offset for Quantum to update the local input offset. Defined in frames. More... + +Int32 MinTimeCorrectionFrames = 1 + How much the local client time must differ with the server time when a time correction package is received for the client to adjust it's local clock. Defined in frames. More... + +Int32 PlayerCount + Player count the simulation is initialized for. More... + +Int32 RollbackWindow = 60 + How many frames are kept in the local ring buffer on each client. Controls how much Quantum can predict into the future. Not used in lockstep mode. More... + +Int32 SessionStartTimeout = 1 + How long the Quantum server will wait for the room to become full until it forces a start of the Quantum session. Defined in seconds. More... + +Int32 TimeCorrectionRate = 4 + How many times per second the server will send out time correction packages to make sure every clients time is synchronized. More... + +Int32 TimeScaleMin = 100 + The smallest timescale that can be applied by the server. Defined in percent. More... + +Int32 TimeScalePingMax = 300 + The ping value that the server will reach the 'Time Scale Minimum' value at, i.e. be at its slowest setting. Defined in milliseconds. More... + +Int32 TimeScalePingMin = 100 + The ping value that the server will start lowering the time scale towards 'Time Scale Minimum'. Defined in milliseconds. More... + +Int32 UpdateFPS = 60 + How many ticks per second Quantum should execute. More... + +Detailed Description +Parameterize internals of the Deterministic simulation and plugin (the Quantum server component). + +This config file will be synchronized between all clients of one session. Though each player starts its own simulation locally with his own version of the DeterministicConfig the server will distribute the config file instance of the first player that joined the plugin. + +Member Data Documentation +◆ PlayerCount +Int32 Photon.Deterministic.DeterministicSessionConfig.PlayerCount +Player count the simulation is initialized for. + +◆ ChecksumCrossPlatformDeterminism +Boolean Photon.Deterministic.DeterministicSessionConfig.ChecksumCrossPlatformDeterminism = false +If Quantum should skip performing rollbacks and re-predict when it's not needed to retain determinism. Not used in lockstep mode. Mutually exclusive with the _BW_COMPAT_ExposeVerifiedStatusInsideSimulation setting. + +This allows Quantum frame checksumming to be deterministic across different runtime platforms, however it comes with quite a cost and should only be used during debugging. + +◆ LockstepSimulation +Boolean Photon.Deterministic.DeterministicSessionConfig.LockstepSimulation = false +Runs the quantum simulation in lockstep mode, where no rollbacks are performed. s recommended to set input InputDelayMin to at least 10 and _BW_COMPAT_InputPacking to 1. + +◆ AggressiveSendMode +Boolean Photon.Deterministic.DeterministicSessionConfig.AggressiveSendMode = false +If the server should skip buffering and perform aggressive input sends, only suitable for games with less or equal 4 players. + +◆ UpdateFPS +Int32 Photon.Deterministic.DeterministicSessionConfig.UpdateFPS = 60 +How many ticks per second Quantum should execute. + +◆ ChecksumInterval +Int32 Photon.Deterministic.DeterministicSessionConfig.ChecksumInterval = 60 +How often we should send checksums of the frame state to the server for verification (useful during development, set to zero for release). Defined in frames. + +◆ RollbackWindow +Int32 Photon.Deterministic.DeterministicSessionConfig.RollbackWindow = 60 +How many frames are kept in the local ring buffer on each client. Controls how much Quantum can predict into the future. Not used in lockstep mode. + +◆ InputHardTolerance +Int32 Photon.Deterministic.DeterministicSessionConfig.InputHardTolerance = 8 +How many frames the server will wait until it expires a frame and replaces all non-received inputs with repeated inputs or null's and sends it out to all players. + +◆ InputRedundancy +Int32 Photon.Deterministic.DeterministicSessionConfig.InputRedundancy = 3 +How much staggering the Quantum client should apply to redundant input resends. 1 = Wait one frame, 2 = Wait two frames, etc. + +◆ InputRepeatMaxDistance +Int32 Photon.Deterministic.DeterministicSessionConfig.InputRepeatMaxDistance = 10 +How many frames Quantum will scan for repeatable inputs. 5 = Scan five frames forward and backwards, 10 = Scan ten frames, etc. + +◆ SessionStartTimeout +Int32 Photon.Deterministic.DeterministicSessionConfig.SessionStartTimeout = 1 +How long the Quantum server will wait for the room to become full until it forces a start of the Quantum session. Defined in seconds. + +◆ TimeCorrectionRate +Int32 Photon.Deterministic.DeterministicSessionConfig.TimeCorrectionRate = 4 +How many times per second the server will send out time correction packages to make sure every clients time is synchronized. + +◆ MinTimeCorrectionFrames +Int32 Photon.Deterministic.DeterministicSessionConfig.MinTimeCorrectionFrames = 1 +How much the local client time must differ with the server time when a time correction package is received for the client to adjust it's local clock. Defined in frames. + +◆ MinOffsetCorrectionDiff +Int32 Photon.Deterministic.DeterministicSessionConfig.MinOffsetCorrectionDiff = 1 +How many frames the current local input delay must diff to the current requested offset for Quantum to update the local input offset. Defined in frames. + +◆ TimeScaleMin +Int32 Photon.Deterministic.DeterministicSessionConfig.TimeScaleMin = 100 +The smallest timescale that can be applied by the server. Defined in percent. + +◆ TimeScalePingMin +Int32 Photon.Deterministic.DeterministicSessionConfig.TimeScalePingMin = 100 +The ping value that the server will start lowering the time scale towards 'Time Scale Minimum'. Defined in milliseconds. + +◆ TimeScalePingMax +Int32 Photon.Deterministic.DeterministicSessionConfig.TimeScalePingMax = 300 +The ping value that the server will reach the 'Time Scale Minimum' value at, i.e. be at its slowest setting. Defined in milliseconds. + +◆ InputDelayMin +Int32 Photon.Deterministic.DeterministicSessionConfig.InputDelayMin = 0 +The minimum input offset a player can have. + +◆ InputDelayMax +Int32 Photon.Deterministic.DeterministicSessionConfig.InputDelayMax = 60 +The maximum input offset a player can have. + +◆ InputDelayPingStart +Int32 Photon.Deterministic.DeterministicSessionConfig.InputDelayPingStart = 100 +At what ping value that Quantum starts applying input offset. Defined in milliseconds. + +◆ InputFixedSizeEnabled +Boolean Photon.Deterministic.DeterministicSessionConfig.InputFixedSizeEnabled +If the input data has a fixed byte length, enabling this saves bandwidth. + +◆ InputFixedSize +Int32 Photon.Deterministic.DeterministicSessionConfig.InputFixedSize +Fixed input size. \ No newline at end of file diff --git a/data/config.txt b/data/config.txt new file mode 100644 index 0000000000000000000000000000000000000000..b81433fba824d8aeb8d22bc1755fa34b3203cdb6 --- /dev/null +++ b/data/config.txt @@ -0,0 +1,88 @@ +Quantum.SimulationConfig Class Reference +The SimulationConfig holds parameters used in the ECS layer and inside core systems like physics and navigation. More... + +Inherits Quantum.AssetObject, and Quantum.AssetObject. + +Public Attributes +AutoLoadSceneFromMapMode AutoLoadSceneFromMap = AutoLoadSceneFromMapMode.UnloadPreviousSceneThenLoad + This option will trigger a Unity scene load during the Quantum start sequence. +This might be convenient to start with but once the starting sequence is customized disable it and implement the scene loading by yourself. "Previous Scene" refers to a scene name in Quantum Map. More... + +SimulationConfigChecksumErrorDumpOptions ChecksumErrorDumpOptions + Additional options for checksum dumps, if the default settings don't provide a clear picture. More... + +FP ChecksumSnapshotHistoryLengthSeconds = 3 + How long to store checksumed verified frames. The are used to generate a frame dump in case of a checksum error happening. Not used in Replay and Local mode. More... + +SimulationUpdateTime DeltaTimeType = SimulationUpdateTime.Default + Configure how the client tracks the time to progress the Quantum simulation from the QuantumRunner class. More... + +FrameBase.EntitiesConfig Entities + Global entities configuration More... + +int HeapExtraCount = 0 + Sets extra heaps to allocate for a session in case you need to create 'auxiliary' frames than actually required for the simulation itself More... + +int HeapPageCount = 256 + Define the max heap page count for memory the frame class uses for custom allocations like QList<> for example. More... + +int HeapPageShift = 15 + Define the max heap size for one page of memory the frame class uses for custom allocations like QList<> for example. More... + +Navigation.Config Navigation + Global navmesh configurations. More... + +PhysicsCommon.Config Physics + Global physics configurations. More... + +int ThreadCount = 2 + Override the number of threads used internally. More... + +Detailed Description +The SimulationConfig holds parameters used in the ECS layer and inside core systems like physics and navigation. + +Member Data Documentation +◆ Navigation +Navigation.Config Quantum.SimulationConfig.Navigation +Global navmesh configurations. + +◆ Physics +PhysicsCommon.Config Quantum.SimulationConfig.Physics +Global physics configurations. + +◆ Entities +FrameBase.EntitiesConfig Quantum.SimulationConfig.Entities +Global entities configuration + +◆ AutoLoadSceneFromMap +AutoLoadSceneFromMapMode Quantum.SimulationConfig.AutoLoadSceneFromMap = AutoLoadSceneFromMapMode.UnloadPreviousSceneThenLoad +This option will trigger a Unity scene load during the Quantum start sequence. +This might be convenient to start with but once the starting sequence is customized disable it and implement the scene loading by yourself. "Previous Scene" refers to a scene name in Quantum Map. + +◆ DeltaTimeType +SimulationUpdateTime Quantum.SimulationConfig.DeltaTimeType = SimulationUpdateTime.Default +Configure how the client tracks the time to progress the Quantum simulation from the QuantumRunner class. + +◆ HeapPageShift +int Quantum.SimulationConfig.HeapPageShift = 15 +Define the max heap size for one page of memory the frame class uses for custom allocations like QList<> for example. + +2^15 = 32.768 bytes + +TotalHeapSizeInBytes = (1 << HeapPageShift) * HeapPageCount +◆ HeapPageCount +int Quantum.SimulationConfig.HeapPageCount = 256 +Define the max heap page count for memory the frame class uses for custom allocations like QList<> for example. + +TotalHeapSizeInBytes = (1 << HeapPageShift) * HeapPageCount +◆ HeapExtraCount +int Quantum.SimulationConfig.HeapExtraCount = 0 +Sets extra heaps to allocate for a session in case you need to create 'auxiliary' frames than actually required for the simulation itself + +◆ ThreadCount +int Quantum.SimulationConfig.ThreadCount = 2 +Override the number of threads used internally. + +◆ ChecksumSnapshotHistoryLengthSeconds +FP Quantum.SimulationConfig.ChecksumSnapshotHistoryLengthSeconds = 3 +How long to store checksumed verified frames. The are used to generate a frame dump in case of a checksum error happening. Not used in Replay and Local mode. \ No newline at end of file diff --git a/data/configuration.txt b/data/configuration.txt new file mode 100644 index 0000000000000000000000000000000000000000..e2c44c2e1979712435d679c8c70638bb7b0434a3 --- /dev/null +++ b/data/configuration.txt @@ -0,0 +1,190 @@ +Introduction +Quantum Start Sequence +Config Files +PhotonServerSettings +DeterministicConfig +SimulationConfig +Delta Time Type +RuntimeConfig +RuntimePlayer +Using DSL Generated Code With RuntimePlayer And RuntimeConfig Serialization + +Introduction +There are a few Quantum config files that have specific roles and purposes. + +These config files are placed in different folders in the Unity project. Finding them quickly is made easy with the shortcuts (unity) editor window found in "Menu/Quantum/Show Shortcuts". + +Most of default config instances reside as Scriptable Objects inside the "Resources" folder at the root level of the Unity project Assets, and will end up in your app build from there (see DeterministicSessionConfigAsset.Instance for example) while others (RuntimeConfig, RuntimePlayer) can be assembled during run-time. + +Back To Top + + +Quantum Start Sequence +Which config is used by whom and send when is shown in the diagram below. + +Config Sequence Diagram +Config Sequence Diagram +Back To Top + + +Config Files + +PhotonServerSettings +Assets/Resources/PhotonServerSettings.asset +Quantum, from version 2.0, uses Photon Realtime to connect and communicate to the Photon Cloud. This config describes where the client connects to (cloud + region, local ip, ..). + +photon realtime introduction + +Also a valid AppId (referring to an active Quantum plugin) is set here. + +Only one instance of this config file is allowed. The loading is tightly integrated into the PhotonNetwork class. See PhotonNetwork.PhotonServerSettings. + +Photon Server Settings +Photon Server Settings +Back To Top + + +DeterministicConfig +Assets/Resouces/DeterministicConfig.asset +Via the DeterministicConfig developers can parametrize internals of the deterministic simulation and plugin (the Quantum server component). Toggle Show Help Info in the inspector of this config for details of each parameter. + +The default way only allows one instance of this asset but as long as it is passed into QuantumRunner.StartParameters it does not matter how the file is retrieved. + +This config file will be synchronized between all clients of one session. Although each player starts their own simulation locally with their own version of the DeterministicConfig, the server will distribute the config file instance of the first player who joined the plugin. + +The data on this config is included in the checksum generation. + +Deterministic Config +Deterministic Config +Back To Top + + +SimulationConfig +Assets/Resources/DB/Configs/SimulationConfig.asset +This config file holds parameters used in the ECS layer and inside core systems like physics and navigation. See the related system sections in the manual for more details of each value. + +The SimulationConfig is part of the Quantum DB and multiple instances of this config are supported. Add the config asset GUID to the RuntimeConfig to select which SimualtionConfig should be used. + +Developers can "extend" (create a partial class) the quantum_code/quantum.state/Core/SimulationConfig.cs class and add more data to it. + +Simulation Config +Simulation Config +Back To Top + + +Delta Time Type +You can customize how the QuantumRunner will accumulate elapsed time to update the Quantum simulation (see the QuantumRunner.DeltaTime property). + +The Default setting will use an internal stopwatch and is the recommended setting for production. +EngineDeltaTime will use, for example Unity.deltaTime, to track when to trigger simulation updates. This is very handy when debugging the project using break points, because upon resuming the simulation with not fast-forward but continue from the exact time the simulation was paused. Alas, this setting can cause issues with time synchronization when initializing online matches: the time tracking can be inaccurate under load (e.g. level loading) and result in a lot of large extra time syncs request and cancelled inputs for a client when starting an online game. +Back To Top + + +RuntimeConfig +In contrast to the SimulationConfig, which has only static configuration data, the RuntimeConfig holds information that can be different from game to game. By default is defines for example what map to load and the random start seed. It is assembled from scratch each time starting a game. + +Developers can add custom data to quantum_code/quantum.state/RuntimeConfig.User.cs (don't forget to fill out the serialization methods). + +Like the DeterministicConfig this "file" is distributed to every other client after the first player connected and joined the Quantum plugin. + +A convenient way of using this config is by creating a MonoBehaviour that stores an instance of RuntimeConfig (and RuntimePlayer) with default values and asset links (GUIDs) for example pointing to other asset files containing specific balancing data. When the player is inside a game lobby parts of the Runtime configs can be overwritten with his custom load-out before connecting and starting the game. See QuantumRunnerLocalDebug.cs or the sample below: + +Runtime Setup +Runtime Setup +using Quantum; +using UnityEngine; + +public sealed class RuntimeSetup : MonoBehaviour +{ + public static RuntimeSetup Instance { get; private set; } + + public RuntimeConfig GameConfig { get { return _gameConfig; } } + public RuntimePlayer PlayerConfig { get { return _playerConfig; } } + + [SerializeField] private RuntimeConfig _gameConfig; + [SerializeField] private RuntimePlayer _playerConfig; + + private void Awake() { + Instance = this; + } +} +Back To Top + + +RuntimePlayer +Similar to the RuntimeConfig the RuntimePlayer describes dynamic properties for one player (quantum_code/quantum.state/RuntimePlayer.User.cs). + +The data for a player behaves differently to the other configs, because it is send by each player individually after the actual game has been started. See the Player document in the manual for more information. + +Back To Top + + +Using DSL Generated Code With RuntimePlayer And RuntimeConfig Serialization +RuntimeConfig and RuntimePlayer require to write manual serialization code. When using DSL generated structs of component prototypes the serialization code can be simplified. + +Caveat: Never use objects that are actually pointers that require a frame to be resolved (e.g. Quantum collections). + +The following struct Foo43 and components prototype Component43 will be used in the RuntimePlayer. + +struct Foo43 { + int Integer; + array[8] Bytes; + asset_ref MapAssetReference; + Bar43 Bar43; +} + +struct Bar43 { + FPVector3 Vector3; +} + +component Component43 { + int Integer; + OtherComponent43 OtherComponent; +} + +component OtherComponent43 { + int Integer; + FP FP; +} +The partial RuntimePlayer.User implementation looks like this. + +partial class RuntimePlayer { + // A) Use a DSL generated struct on RuntimePlayer + public Foo43 Foo; + + // B) Piggyback on a component prototype to set data + public Component43_Prototype Component43 = new Component43_Prototype { OtherComponent = new OtherComponent43_Prototype() }; + + partial void SerializeUserData(BitStream stream) { + // A) Because the struct is memory alined we can pin the memory and serialize it as a byte array which will work platform indenpentently. + unsafe { + fixed (Foo43* p = &Foo) { + stream.SerializeBuffer((byte*)p, sizeof(Foo43)); + } + } + + // B) Initialized the references in the field declaration with new and serialize all fields here. + stream.Serialize(ref Component43.Integer); + stream.Serialize(ref Component43.OtherComponent.Integer); + stream.Serialize(ref Component43.OtherComponent.FP); + } +} +Send the RuntimePlayer from the client: + +var runtimePlayer = new Quantum.RuntimePlayer { + Component43 = new Quantum.Prototypes.Component43_Prototype { + Integer = 1, + OtherComponent = new Quantum.Prototypes.OtherComponent43_Prototype { FP = 2, Integer = 3 } }, + Foo = new Foo43 { + Bar43 = new Bar43 { Vector3 = FPVector3.One }, + Integer = 4, + MapAssetReference = new AssetRefMap() { Id = 66 } + } +}; + +unsafe { + runtimePlayer.Foo.Bytes[0] = 7; + runtimePlayer.Foo.Bytes[1] = 6; +} + +game.SendPlayerData(lp, runtimePlayer); \ No newline at end of file diff --git a/data/continuous collison.txt b/data/continuous collison.txt new file mode 100644 index 0000000000000000000000000000000000000000..41b63ec5627460dc601b4e81147a6b2dc1a502d6 --- /dev/null +++ b/data/continuous collison.txt @@ -0,0 +1,60 @@ +Overview +Continous Collision Detection is used to fast moving physics entities collide with other physics colliders instead of tunnelling through them. + +There are two common approaches for CCD algorithms, speculative and sweep based. Quantum implements a speculative Continuous Collision Detection due to the performance considerations tied to its stateless physics engine. The speculative CCD approach is better suited for parallelism while also handling angular motion well; the former is needed for performance and the latter is necessary in many gameplay scenarios. + +The speculative CCD algorithm increases the minimum bounding box used during the broad-phase for an entity based on its PhyiscsBody component linear Velocity and AngularVelocity. It is called speculative because it "speculates" the entity may collide with any of the other physics objects in that area and feeds all these candidates into the solver. This speculation ensures all contact contrains are taken into account when solving the collision thus preventing tunnelling. + +Back To Top + + +Set-up +Two simple steps are required to set up the CCD; both of which can done at edit-time and / or runtime. + +N.B.: Given the performance impact CCD has on the simulation, the CCD functionality is enabled on a per-entity basis and NOT globally! + +Back To Top + + +Edit-Time +Step 1: Check the Allow CCD boolean in the Physics section of the Simulation Config asset. + +Allow CCD in the Simulation Config +Enable the CCD in the Simulation Config. +Step 2: Enable the Use Continuous Collision Detected flag in the Config found on the PhysicsBody component of the Entity Prototype$. + +CCD Flag in the PhysicsBody Config +Select the CCD Flag in the PhysicsBody Config. +Back To Top + + +Runtime +Should the CCD only be necessary in particular situation or moments of the game, it is possible to dynamically toggle the CCD and entities using it on and off. + +Step 1: Toggle the AllowCCD property in the current game state's PhysicsSceneSettings. The PhysicsSceneSettings are part of the frame and initialized with the Physics values found in the SimulationConfig asset. IMPORTANT: Do NOT modify the SimulationConfig asset at runtime, this is undeterministic and will result in desynchronization! + +frame.PhysicsSceneSettings->CCDSettings.AllowCCD = true; +Step 2: Toggle the UseContinuousCollisionDetection property on the PhysicsBody component for the entity which should be using CCD. + +var physicsBody = f.Get(myEntityRef); +physicsBody.UseContinuousCollisionDetection = true; +Back To Top + + +Config +The SimulationConfig assets hold the default values for initializing the physics engine; including the aspects related to the CCD. The default values found in the Continuous Collision Detection (CCD) section are optimal for most games and should only be tweaked with care if edge cases were to arise. + +AllowCCD: Allows CCD to be performed if the Physics Body has CCD enabled on its Config flags. +CCDLinearVelocityThreshold: If CCD is allowed, it will be performed on all Physics Bodies that have it enabled and have a linear velocity magnitude above this threshold. +CCDAngularVelocityThreshold: If CCD is allowed, it will be performed on all Physics Bodies that have it enabled and have a angular velocity magnitude above this threshold. +CCDDistanceTolerance: The absolute distance value below which the Physics Bodies under CCD check can be considered as touching. +MaxTimeOfImpactIterations: The maximum number of iterations performed by the CCD algorithm when computing the time of impact between two Physics Bodies. +MaxRootFindingIterations: The maximum number of iterations performed when computing the point in time when the distance between two Physics Bodies in a given separation axis is below the tolerance. +Back To Top + + +Known Limitations +Although the speculative CCD is feature complete, one needs to be aware of the know limitations of the speculative approach. + +The current algorithm runs a single CCD iteration alongside the regular physics collision resolution. In other words, after a CCD collision is detected and resolved, the remaining delta-time for that entity is integrated regardless of CCD. Thus there is a chance of tunnelling occurring in highly dense environments with extremely fast moving entities. + diff --git a/data/custom-plug-over.txt b/data/custom-plug-over.txt new file mode 100644 index 0000000000000000000000000000000000000000..5954af1c4dea45dc97ad17b12bc13194adaac511 --- /dev/null +++ b/data/custom-plug-over.txt @@ -0,0 +1,40 @@ +Overview +You can get Quantum Custom Server Plugin SDK from our quantum sdk page. + +It contains Quantum server libraries, the Photon-Server and a sample project to enable you to run your own Photon-Server Quantum plugin. + +Common features: + +Run your game simulation on the server +Send server snapshots to late-joining or reconnecting clients +Forward the input or replays to another backend +Safely retrieve player configurations/load-outs/inventories from another backend +Forward game results to another backend +Add additional user authentication +Start by watching the intro video below. + +The set up tutorial demonstrates how the sample project runs on a local Photon-Server. + +Use the API docs to gather more information about the features you want to implement. + +Back To Top + + +Quantum Custom Server Plugin Intro Video (YouTube) + +Back To Top + + +API +The PhotonDeterministic.Plugin and PhotonDeterministic.Server classes have been added to the Quantum offline API documentation that can be found in the Quantum SDK folder (unblock the zip before extracting please): + +PhotonQuantum-Documentation.chm +Additional the API information is available as Visual Studio xml documentation alongside the dlls in the Custom Plugin SDK folder: + +assemblies\PhotonDeterministic.Plugin.xml +assemblies\PhotonDeterministic.Server.xml +The comments are visible when hovering over classes and methods: + + +And when opening the classes in your IDE: + diff --git a/data/custom-setup tutorial.txt b/data/custom-setup tutorial.txt new file mode 100644 index 0000000000000000000000000000000000000000..e59949c165c931c28c59de341c514bf3cf289f92 --- /dev/null +++ b/data/custom-setup tutorial.txt @@ -0,0 +1,148 @@ +Set Up Tutorial +Download The Plugin SDK +Run The Quantum Plugin Locally +License +Start And Debug The Plugin +Connect Clients To The Photon-Server Plugin +1) Export Quantum Asset Database +2) Copy Math Look-Up Tables +3) Configure Asset Paths +4) Configure Unity +Finally +The Quantum custom plugin is based of Photon-Server V5 and follows the workflow described in the Photon Server docs. Dive into these docs for further reading: photon-server v5 step by step guide + +Here we present a tutorial like introduction with all essential steps to get started with the Quantum custom plugin and finally run and debug it locally including the server side game simulation. + +Watch Erick from Exit Games go through the installation and set up process: Quantum Custom Server Plugin Intro (YouTube) + + +Download The Plugin SDK +Download the Plugin SDK package here: quantum sdk & release notes. If a matching Quantum CustomPlugin SDK version is not available please ask us. + +On Windows it's better to unblock the zip file before extracting it (right-click the zip file, select Properties, check the unblock toggle and press OK). If you have problems getting the Photon Server to run locally this might be the cause. + +Extract the zip file to a folder inside your Quantum project to have this setup: + +Quantum project + assemblies + quantum_code + quantum_custom (or any other name) + assemblies + Photon-Server + quantum.custom.plugin + quantum_unity + ... +If you chose another folder location adjust the following paths in quantum.custom.plugin\quantum.custom.plugin.csproj to point to the release versions of the Quantum SDK library files (PhotonDeterministic.dll, quantum.core.dll) and your quantum.code.dll. + + + ..\..\assemblies\release\PhotonDeterministic.dll + + + ..\..\assemblies\release\quantum.core.dll + + + ..\..\quantum_unity\Assets\Photon\Quantum\Assemblies\quantum.code.dll + +SDK Content + +assemblies folder includes the Quantum server plugin (PhotonDeterministic*.dll) and Photon Server libs (PhotonHivePlugin.dll) +Photon-Server folder structure that include necessary tools and programs to run the Photon-Server locally +deploy +bin_Win64 contains PhotonControl.exe, a tray-based tool to control the local Photon Server, PhotonSocketServer.exe which is the Photon Server main application and this folder is also where the license file should go +LoadBalancing\GameServer\bin the location of the plugin.config that supplies settings to our local plugin +log server log files +Plugins\DeterministicPlugin\bin the location of the compiled plugin project and its assets +quantum.custom.plugin source code that runs a Quantum Photon Server plugin +Back To Top + + +Run The Quantum Plugin Locally + +License +If you have not already, create an Exit Games account on our website SignUp. +Visit the website Your Photon Server Licenses log into your account and download your free license file. +If you require licenses to publish a game please contact us +Place the .license file inside the Photon-Server\deploy\bin_Win64 folder +Back To Top + + +Start And Debug The Plugin +Open the quantum_custom.sln and make sure that the references to PhotonDeterministic.dll, quantum.core.dll and quantum.code.dll (your game project) are valid (see section above). Select dlls that are build with release configuration because debug will running be considerably slower. +(Only Until Quantum 2.0 / PhotonServer 4) Run Photon-Server\deploy\bin_Win64\PhotonControl.exe once (it is started inside the Windows tray) and select Game Server IP Config > Set Local IP: 192.168.XXX.XXX + +Press F5 to start and debug the Quantum plugin running inside the Photon-Server +You can set breakpoints but it may disconnect clients due to timeouts when resuming +Common Errors: no license file, Quantum library references not found or not compatible, zip file was not unblocked before extracting, check the Photon-Server\deploy\log for more information.. + +Back To Top + + +Connect Clients To The Photon-Server Plugin +The plugin project is set up in a way that it runs your quantum simulation as well. This can be interesting to add authority over the final game results or being able to send game snapshots to reconnecting players directly from the server. + +The plugin requires the identical game libraries and data: + +Back To Top + + +1) Export Quantum Asset Database +Export the asset db from Unity by selecting Quantum > Export > Asset DB. Chose the folder Photon-Server\deploy\Plugins\DeterministicPlugin\bin\assets as destination. + +Quantum 2.0: + +Rename the file from assetDb.json to db.json (or change the path inside the plugin.config file) +additional .bytes files are created alongside the asset db + +Quantum 2.1: + +No additional files are created. All binary data is included in the json. + +Optionally the export asset db can be embedded into the quantum.code.dll (only Quantum 2.1). There is already an empty placeholder file inside the Quantum code project ( quantum_code\quantum.code\db.json ). If not create it and add to the quantum.code.csproj: + + + + ... + +Export the asset db via the Unity menu to quantum_code\quantum.code\db.json. Recompile the solution and make sure the plugin the just created dll. + +The plugin will always try to load the db from file first. If non was found it will try to load the db from the assembly. + +Back To Top + + +2) Copy Math Look-Up Tables +Copy the LUT folder (quantum_unity\Assets\Photon\Quantum\Resources\LUT) from your Quantum Unity project to Photon-Server\deploy\Plugins\DeterministicPlugin\bin\assets. You can delete the .meta files. + +Back To Top + + +3) Configure Asset Paths +The location of the assets that you just used can be changed in this file: Photon-Server\deploy\LoadBalancing\GameServer\bin\plugin.config (PathToDBFile and PathToLUTFolder). The paths are relative to quantum_custom\Photon-Server\deploy\Plugins\DeterministicPlugin\bin. + +EmbeddedDBFile is the name of the asset db that is optionally embedded into the quantum.code assembly. The file name is db.json but requires the Quantum. prefix. + + + + + + +When you publish the plugin online you need to configure these paths in the Photon dashboard. + +Back To Top + + +4) Configure Unity +The Photon Server Settings file needs the information to connect to a local URL instead the Photon cloud. Open your Unity project select the Photon Server Settings file and press the Configure App Settings - Local Master Server button. + +Settings required: UseNameserver=off, Host=your local IP address, Port=5055 + + + +Finally +Start the plugin (open the custom plugin project and press F5) +Start the Menu scene in Unity, connect and play diff --git a/data/data.txt b/data/data.txt new file mode 100644 index 0000000000000000000000000000000000000000..e87dd5fe9c4f2d0ffd0238d055be181e80cea9c1 --- /dev/null +++ b/data/data.txt @@ -0,0 +1,162 @@ +Overview +Photon Quantum is a high-performance deterministic ECS (Entity Component System) framework for online multiplayer games made with Unity. + +It is based on the predict/rollback approach which is ideal for latency-sensitive online games such as action RPGs, sports games, fighting games, FPS and more. + +Quantum also helps the developer to write clean code, fully decoupling simulation logic (Quantum ECS) from view/presentation (Unity), while also taking care of the network implementations specifics (internal predict/rollback + transport layer + game agnostic server logic): + +Quantum implements a state-of-the-art tech stack composed of the following pieces: + +Server-managed predict/rollback simulation core. +Sparse-set ECS memory model and API. +Complete set of stateless deterministic libraries (math, 2D and 3D physics, navigation, etc.). +Rich Unity editor integration and tooling. +All built on top of mature and industry-proven existing Photon products and infrastructure (photon realtime transport layer, photon server plugin to host server logic, etc.); + +Determinism Without Lockstep +In deterministic systems, game clients only exchange player input with the simulation running locally on all clients. In the past, this has used a lockstep approach, in which game clients would wait for the input from all other players before updating each tick/frame of the simulation. + +In Quantum, however, game clients are free to advance the simulation locally using input prediction, and an advanced rollback system takes care of restoring game state and re-simulating any mispredictions. + +Because Quantum also relies on a game-agnostic authoritative server component (photon server plugin) to manage input latency and clock synchronization, clients never need to wait for the slowest one to rollback/confirm the simulation forward: + +Quantum Server-Managed predict/Rollback +In Quantum, deterministic input exchange is managed via game-agnostic server logic. This prevents game clients with bad network from interfering with the experience of players on good networks. +These are the basic building blocks of a Quantum game: +Quantum Server Plugin: manages input timing and delivery between game clients, acts as clock synchronization source. Can be extended to integrate with customer-hosted back-end systems (matchmaking, player services, etc.). +Game Client Simulator: communicates with Quantum server, runs local simulation, performing all input prediction and rollbacks. +Custom Gameplay Code: developed by the customer as an isolated pure C# simulation (decoupled from Unity) using the Quantum ECS. Besides providing a framework for how to organize high-performance code, Quantum's API offers a great range of pre-built components (data) and systems (logic) that can be reused in any game such as deterministic 3D vector math, 2D and 3D physics engines, navmesh pathfinder, etc. + +Old School Coding +Starting from the assumption that all simulation code must be high-performance out of the box, Quantum internal systems are all designed with that in mind from the ground up. + +The key to Quantum's high performance is the use of pointer-based C# code combined with its sparse-set ECS memory model (all based memory aligned data-structs and a custom heap allocator - no garbage collection at runtime from simulation code). + +The goal is to leave most of the CPU budget for view/rendering code (Unity), including here the re-simulations induced by input mispredictions, inherent to the predict/rollback approach: + +Quantum hyper-fast predict/rollback +Quantum is designed to make your simulation code run as fast as possible, leaving most of the CPU budget for rendering updates. +Although the use of pointer-based C# is exposed (for performance), most of the complexity is hidden away from the developer by the clever use of a custom DSL and automatic code generation. + +Back To Top + + +Code Generation +In Quantum, all gameplay data (game state) is kept either in the sparse-set ECS data structures (entities and components) or in our custom heap-allocator (dynamic collections and custom data), always as blittable memory-aligned C# structs. + +To define all data structures that go into that, the developer uses a custom DSL (domain specific language) that lets him concentrate on the game concepts instead of performance-oriented restrictions: + +// components define reusable game state data groups + +component Resources +{ + Int32 Mana; + FP Health; +} + +// structs, c-style unions, enums, flags, etc, can be defined directly from the DSL as well +struct CustomData +{ + FP Resources; + Boolean Active; +} +The code-snippet above would generate the corresponding types (with explicit memory alignment), serialization code, and a all boiler plate control logic for special types (like components). + +The auto-generated API lets you both query and modify the game state with comprehensive functions to iterate, modify, create or destroy entities (based on composition): + +var es = frame.Filter(); +// Next fills in copies of each of the components + the EntityRef +while (es.NextUnsafe(out var entity, out var transform, out var resources)) { + transform->Position += FPVector3.Forward * frame.DeltaTime; +} +Back To Top + + +Stateless Systems +While Quantum's DSL covers game state data definition with concepts such as entities, components and auxiliary structures (structs, enums, unions, bitsets, collections, etc.), there needs to be a way to organize the custom game logic that will update this game state. + +You write custom logic by implementing Systems, which are stateless pieces of logic that will be executed every tick update by Quantum's client simulation loop: + +public unsafe class LogicSystem : SystemMainThread +{ + public override void Update(Frame f) + { + // your game logic here (f is a reference for the generated game state container). + } +} +The Systems API game loop call order, signals for system intercommunication (both custom and pre-built, such as the physics engine collision callbacks), events and several other extension hooks. + +Back To Top + + +Events +While the simulation is implemented in pure C#, without referencing Unity's API directly, there are two important features to let gameplay code communicate with the rendering engine: events and the asset linking system. + +Events are a way for the game code to inform the rendering engine that important things happened during the simulation. One good example is when something results in damage to a character. + +Using the state from the previous section as a basis, imagine that damage reduces the health value from the resources component of a character entity. From the Unity rendering scripts, the only noticeable data will be the new health value itself, without any way to know what caused the damage, and also what was the previous health value, etc. + +Event definition in a file DSL: + +event Damage +{ + entity_ref Character; + FP Amount; +} +Gameplay code raises events as a simple API call (generated): + +public void ApplyDamage(Frame f, EntityRef c, FP amount) +{ + // missing here, the logic to apply damage to the character itself + + // this sends an event to the "view" (Unity) + f.Events.Damage(amount, c); +} +Quantum's event processor will handle all generated events after the tick update is done, taking care of events that require server-confirmed input, event repetitions, and also cancelation/confirmation when simulation rollbacks do occur. + +Events raised from the simulation code can then be consumed in runtime from callbacks created in Unity scripts: + +public void OnDamage(DamageEvent dmg) +{ + // instantiate and show floating damage number above the target character, etc +} +Back To Top + + +Asset Linking +Unity is known for its flexible editor and smooth asset pipeline. The Asset Linking system allows game and level designers to create and edit data-driven simulation objects from the Unity Editor, which are then fed into the simulation. This is essential to prototype and to add final balancing touches to the gameplay. + +From the C# simulation project, the developer creates a data-driven class exposing the desired attributes: + +public partial class CharacterClass +{ + public Int32 MaxMana; + public FP MaxHealth; +} +Then from Unity, level designers can create as many instances of this asset as needed, each one being automatically assigned with a Unique GUID: + +Character Classes - Asset Linking +Example of the Asset Linking system: data-driven character-class asset containers being created/modified directly from the Unity Editor. +Then, programmers can use data from these assets directly from inside the simulation: + +var data = frame.FindAsset("character_class_id"); +var mana = data.MaxMana; +It's also possible to reference these assets directly in components from the state definition DSL: + +component CharacterAbilities +{ + asset_ref CharacterData; +} +Back To Top + + +Deterministic Library +In Quantum, the simulation needs to compute the same results on all clients, given they use the same input values. This means it has to be deterministic, which implies neither using any float or doubles variables, nor anything from the Unity API such as their vectors, physics engine, etc. + +To help game developers to implement this style of gameplay, Quantum comes bundled with a flexible and extensible deterministic library, both with a set of classes/struct/functions and also some components that can be used directly when defining entities with the DSL. + +The following modules are available: + +Deterministic math library: FP (Fixed Point) type (Q48.16) to replace floats/doubles, FPVector2, FPVector3, FPMatrix, FPQuaternion, RNGSession, FPBounds2, and all extra math utils including safe casts, and parsers from native types. The math library is implemented with performance as a primary goal, so we make intense use of inlining, lookup tables and fast operators whenever possible. +2D and 3D Physics Engines: high performance stateless 2D/3D physics engines with support for static and dynamic objects, callbacks, joints, etc. +NavMesh/PathFinder/Agents: includes both an exporter from an existing Unity navmesh or an editor to manipulate the mesh directly. Also includes industry standard HRVO collision avoidance, funneled paths, and many more features. diff --git a/data/db.json b/data/db.json new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/data/entity.txt b/data/entity.txt new file mode 100644 index 0000000000000000000000000000000000000000..ad421faf580899020f73b92cd7ddbf033989a1b3 --- /dev/null +++ b/data/entity.txt @@ -0,0 +1,149 @@ +Entity Prototypes +Introduction +Setting Up A Prototype +Basic +Custom Components +Note On Collections +Hierarchy +Creating/Instantiating A Prototype +Baked In The Scene/Map +In Code +Note +Entity View +Self +Separate From Prototype +Important + +Introduction +To facilitate data driven design, Quantum 2.0 introduced Entity Prototypes. + +An Entity Prototype is a serialized version of an entity that includes: + +composition (i.e. which components it is made of); and, +data (i.e. the components' properties and their initial value). +This allows for a clean separation of data and behaviour, while enabling designers to tweak the former without programmers having to constantly edit the latter. + +Back To Top + + +Setting Up A Prototype +Entity prototypes can be set up in Unity Editor. + + +Basic +To create an Entity Prototype simply add the Entity Prototype script to any GameObject. + +Entity Prototype Script on an empty GameObjet +Basic Entity Prototype (Empty GameObject + Entity Prototype Script). +The Entity Prototype script allows you to set up and define the parameters for the most commonly used components for both 2D and 3D. + +Transform (including Transform2DVertical for 2D) +PhysicsCollider +PhysicsBody +NavMeshPathFinder +NavMeshSteeringAgent +NavMeshAvoidanceAgent +The dependencies for the Physics and NavMesh related agents are respected. For more information, please read their respective documentation. + +Back To Top + + +Custom Components +Additional components can be added to an Entity Prototype via either: + +the Add Entity Component drop-down; or, +the regular Unity Add Component button by searching for the right Entity Component. +Back To Top + + +Note On Collections +Dynamic collections in components are only automatically allocated IF there is at least one item in them. Otherwise, the collection will have to be allocated manually. For more information on the subject, refer to the dynamics collection entry on the dsl page. + +Back To Top + + +Hierarchy +In ECS the concept of entity/GameObject hierarchy does not exist. As such entity prototypes do not support hierarchies or nesting. + +Although child prototypes are not supported directly, you can: + +Create separate prototypes in the scene and bake them. +Link them by keeping a reference in a component. +Update the position of the "child" manually. +Note: Prototypes that are not baked in scene will have to follow a different workflow where the entities are created and linked in code. + +You can have hierarchies in objects (View), however hierarchies in entities (Simulation) will have to be handled by you. + +Back To Top + + +Creating/Instantiating A Prototype +Once a Entity Prototype has been defined in Unity, there are various ways to include it in the simulation. + + +Baked In The Scene/Map +If the Entity Prototype is created as part of a Unity Scene, it will be baked into the corresponding Map Asset. The baked Entity Prototype will be loaded when the Map is and initialized with the values it was baked with. + +N.B.: If a Scene's Entity Prototype is edited or has its values changed, the Map Data has to be re-baked. + +Back To Top + + +In Code +To create a new entity from an Entity Prototype, you need to follow these steps: + +Create a Unity Prefab of the GameObject carrying the EntityPrototype script. +Place the Prefab in Resources\DB. +Entity Prototype Asset +Entity Prototype Prefab + Nested Entity Prototype Asset. +=> This will generate a nested *Entity Prototy* **Asset**. +Refresh the Quantum Database Quantum -> Generate Asset Resources. +Make the Entity Prototy Asset Path or GUID available to your simulation. +Entity Prototype Asset GUID & Path +Entity Prototype Asset Window. +=> To copy the Path or GUID, click on their corresponding `Edit` button. +Call Create() via the frame and pass, for example, the EntityPrototype reference, or an instance of it: +void CreateExampleEntity(Frame f){ + // using a reference + var exampleEntity = f.Create(myPrototypeReference); + + // OR, getting an instance before, using the asset's path as a parameter, and then creating the entity + var entityPrototype = f.FindAsset("Resources/DB/Prefabs/Example|EntityPrototype"); + var exampleEntity = f.Create(entityPrototype); +} +Back To Top + + +Note +Entity Prototypes present in the Scene are baked into the Map Asset, while prefabed Entity Prototypes are individual Assets that are part of the Quantum Asset DataBase. + +Back To Top + + +Entity View +The Entity View corresponds to the visual representation of an entity in Unity. In the spirit of data driven design, an Entity Prototype can either incorporate its View component or point to a separate EntityView Asset. + +Back To Top + + +Self +To set an Entity Prototype's view to itself, simply add the Entity View component to it. + +Entity Prototype with Entity View +Entity Prototype with "Self" View. +Once the component has been added, the *Entity Prototype* script will list **Self** as the value for the *View* parameter. This will also create a nested *Entity View* **Asset** in the same prefab. +Entity Prototype Asset and +Entity Prototype Asset and "Self" View Asset. +Back To Top + + +Separate From Prototype +To set up and link a view separate from the Entity Prototype asset: + +Add the Entity View to the GameObject you would like to represent the view. +Prefab the GameObject carrying the Entity View. +Place the prefab in Resources\DB, this will create an Entity View Asset nested in the prefab. +Entity Prototype with Entity View +Entity Prototype Asset and separate Entity View Asset. +Refresh the database Quantum -> Generate Asset Resources. +Link the View field from the Entity Prototype with the newly created Entity View Asset. This can be done via drag-and-drop or the Unity context search menu. \ No newline at end of file diff --git a/data/events callback.txt b/data/events callback.txt new file mode 100644 index 0000000000000000000000000000000000000000..bbd8f118c3eae8902546cd6bb75ac090dce0fb74 --- /dev/null +++ b/data/events callback.txt @@ -0,0 +1,366 @@ +Introduction +The split between simulation (Quantum) and view (Unity) allows for great modularity during the development of the game state and the visuals. However, the view requires information from the game state to update itself. Quantum offers two ways: + +Polling the game state +Events/Callbacks +Although both are valid approaches, their use-cases are slightly different. Generally speaking, polling Quantum information from Unity is preferable for on-going visuals while events are used for punctual occurrences where the game simulation triggers a reaction in the view. This document will focus on Frame Events & Callbacks. + +Back To Top + + +Frame Events +Events are a fire-and-forget mechanism to transfer information from the simulation to the view. They should never be used to modify or updates parts of the games state (Signals are used for that). Events have a couple of important aspects to understand that help manage them during prediction and rollbacks. + +Events do not synchronize anything between clients and they are fired by each client's own simulation. +Since the same Frame can be simulated more than once (prediction, rollback), it is possible to have events being triggered multiple times. To avoid undesired duplicated Events Quantum identifies duplicates using a hash code function over the Event data members, the Event id and the tick. See nothashed keyword for further information. +Regular, non-synced, Events will be either cancelled or confirmed once the predicted Frame from which they were fired has been verified. See Canceled And Confirmed Events for further information. +Events are dispatched after all Frames have been simulated right after the OnUpdateView callback. Events are called in the same order they were invoked with the exception of non-synced Events which can be skipped when identified as duplicated. Due to this timing, the targeted EntityView may already have been destroyed. +The simplest Event and its usage looks like this: + +Define an Event using the Quantum DSL +event MyEvent { + int Foo; +} +Trigger the Event from the simulation +f.Events.MyEvent(2022); +And subscribe and consume the Event in Unity, where we generate a class for the event, with the prefix Event +QuantumEvent.Subscribe(listener: this, handler: (EventMyEvent e) => Debug.Log($"MyEvent {e.Foo}")); +Back To Top + + +DSL Structure +Events and their data are defined using the Quantum DSL inside a qtn-file. Compile the project to make them become available via the Frame.Events API in the simulation. + +event MyEvent { + FPVector3 Position; + FPVector3 Direction; + FP Length +} +Class inheritance allows to share base Events classes and members. + +event MyBaseEvent {} +event SpecializedEventFoo : MyBaseEvent {} +event SpecializedEventBar : MyBaseEvent {} +The synced keyword cannot be inherited. +Use abstract classes to prevent base-Events to be triggered directly. + +abstract event MyBaseEvent {} +event MyConcreteEvent : MyBaseEvent {} +Reuse DSL generated structs inside the Event. + +struct FooEventData { + FP Bar; + FP Par; + FP Rap; +} + +event FooEvent { + FooEventData EventData; +} +Back To Top + + +Keywords +Synced +Nothashed +Local, Remote +Client, Server + +Synced +To avoid rollback-induced false positive Events, they can be marked with the synced keyword. This will guarantee the events will only be dispatched (to Unity) when the input for the Frame has been confirmed by the server. + +Synced Events will add a delay between the time it is issued in the simulation (during a predicted Frame) and its manifestation in the view which can be used to inform players. + +synced event MyEvent {} +Synced Events never create false positives or false negatives +Non-synced Events are never called twice on Unity +Back To Top +Back To "Keywords" + + +Nothashed +To prevent an Event which has already been consumed by the view in an earlier predicted Frame to be dispatched again a hash-code is calculated for each Event instance. Before dispatching an Event, the hash-code is used to check if the event is a duplicate. + +This can lead to the following situation: Minimal rollback-induced position changes of one Event are wrongly interpreted as two different Events. + +The nothashed keyword can be used to control what key-candidate data is used to test the Event uniqueness by ignoring parts of the Event data. + +abstract event MyEvent { + nothashed FPVector2 Position; + Int32 Foo; +} +Back To Top +Back To "Keywords" + + +Local, Remote +If an event has a player_ref member special keywords are available : remote and local + +Before the Event is dispatched in Unity on a client the keywords will cause the player_ref to be checked if assigned to a local or remote player respectively. If all conditions match, the event is dispatched on this client. + +event LocalPlayerOnly { + local player_ref player; +} +event RemotePlayerOnly { + remote player_ref player; +} +To recap: the simulation itself is agnostic to the concept of remote and local. The keywords only alter if a particular event is raised in the view of an individual client. + +Should an Event have multiple player_ref parameters, local and remote can be combined. This event will only trigger on the client who controls the LocalPlayer and when the RemotePlayer is assigned to different player. + +event MyEvent { + local player_ref LocalPlayer; + remote player_ref RemotePlayer; + player_ref AnyPlayer; +} +If a client controls several players (e.g. split-screen), all their player_ref will be considered local. + +Back To Top +Back To "Keywords" + + +Client, Server +Since Quantum 2.1 + +This is only relevant when running server side simulation on a custom Quantum plugin. +Events can be qualified using client and server keywords to scope where they will executed. By default all Events will be dispatched on the client and server. + +server synced event MyServerEvent {} +client event MyClientEvent {} +Back To Top + + +Using Events +Trigger Events +Choosing Event Data +Event Subscriptions In Unity +Unsubscribing From Events +Event Subscriptions In CSharp +Canceled And Confirmed Events + +Trigger Events +Events types and signatures are code-generated into the Frame.FrameEvents struct which is accessible over Frame.Events. + +public override void Update(Frame f) { + f.Events.MyEvent(2022); +} +Back To Top +Back To "Using Events" + + +Choosing Event Data +Ideally, the Event data should be self-contained and carry all information the subscriber will need to handle it on the view. + +The Frame at which the Event was raised in the simulation might no longer be available when the Event is actually called on the view. Meaning that information to be retrieved from the Frame needed to handle the Event could be lost. + +A QCollection or QList on an Event is actually only passed as a Ptr to memory on the Frame heap. Resolving the pointer may fail because the buffer is no longer available. The same can be true for EntityRefs, when accessing Components from the most current Frame at the time the Event is dispatched the data may not be the same as when the Event was originally invoked. + +Ways to enrich the Event data with an array or a List: + +If the collection data payload is of a known and reasonable max size a fixed array can be wrapped inside a struct and added to the Event. Unlike QCollections the arrays do not store the data on the Frame heap but carry it on the value itself. +struct FooEventData { + array[4] ArrayOfValues; +} +event FooEvent { + FooEventData EventData; +} +The DSL currently does not allow to declare an Event with a regular C# List type in it. But the Event can be extended using partial classes. See the Extend Event Implementation section for more details. +Back To Top +Back To "Using Events" + + +Event Subscriptions In Unity +Quantum supports a flexible Event subscription API in Unity via QuantumEvent. + +QuantumEvent.Subscribe(listener: this, handler: (EventPlayerHit e) => Debug.Log($"Player hit in Frame {e.Tick}")); +In the example above, the listener is simply the current MonoBehaviour and the handler an anonymous function. Alternatively, a delegate function can be passed in. + +QuantumEvent.Subscribe(listener: this, handler: OnEventPlayerHit); + +private void OnEventPlayerHit(EventPlayerHit e){ + Debug.Log($"Player hit in Frame {e.Tick}"); +} +QuantumEvent.Subscribe offers a few optional QoL arguments that allow to qualify the subscription in various ways. + +// only invoked once, then removed +QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, once: true); + +// not invoked if the listener is not active +// and enabled (Behaviour.isActiveAndEnabled or GameObject.activeInHierarchy is checked) +QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, onlyIfActiveAndEnabled: true); + +// only called for runner with specified id +QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, runnerId: "SomeRunnerId"); + +// only called for a specific +QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, runner: runnerReference); + +// custom filter, invoked only if player 4 is local +QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, filter: (QuantumGame game) => game.PlayerIsLocal(4)); + +// only for replays +QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, gameMode: DeterministicGameMode.Replay); + +// not for replays (Quantum SDK v2.0) +QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, excludeGameMode: DeterministicGameMode.Replay); + +// for all types except replays (Quantum SDK 2.1+) +QuantumEvent.Subscribe(this, (EventPlayerHit e) => {}, gameMode: DeterministicGameMode.Replay, exclude: true); +//=> The gameMode parameter accepts and array of DeterministicGameMode +Back To Top +Back To "Using Events" + + +Unsubscribing From Events +Unity manages the lifetime of MonoBehaviours, so there is no need to be unregistered as listeners get cleaned up automatically. + +If tighter control is required unsubscribing can be handled manually. + +var subscription = QuantumEvent.Subscribe(); + +// cancels this specific subscription +QuantumEvent.Unsubscribe(subscription); + +// cancels all subscriptions for this listener +QuantumEvent.UnsubscribeListener(this); + +// cancels all listeners to EventPlayerHit for this listener +QuantumEvent.UnsubscribeListener(this); +Back To Top +Back To "Using Events" + + +Event Subscriptions In CSharp +If an Event is subscribed outside of a MonoBehaviour the subscription has to be handled manually. + +var disposable = QuantumEvent.SubscribeManual((EventPlayerHit e) => {}); // subscribes to the event +// ... +disposable.Dispose(); // disposes the event subscription +Back To Top +Back To "Using Events" + + +Canceled And Confirmed Events +Non-synced Events are either cancelled or confirmed once the verified Frame has been simulation. Quantum offers the callbacks CallbackEventCanceled and CallbackEventConfirmed to react to them. + +QuantumCallback.Subscribe(this, (Quantum.CallbackEventCanceled c) => Debug.Log($"Cancelled event {c.EventKey}")); +QuantumCallback.Subscribe(this, (Quantum.CallbackEventConfirmed c) => Debug.Log($"Confirmed event {c.EventKey}")); +Event instances are identified by the EventKey struct. The previously received Event can be added into a dictionary for example by creating the EventKey like this. + +public void OnEvent(MyEvent e) { + EventKey eventKey = (EventKey)e; + // ... +} +Back To Top + + +Extend Event Implementation +Although Events support using a QList. When resolving the list the corresponding Frame might not be available anymore. Additional data types can be added using partial class declarations. + +event ListEvent { + Int32 Foo; +} +public partial class EventListEvent { + public List ListOfFoo; +} +To be able to raise the customized Event via the Frame.Event API extend the FrameEvents struct. + +f.Events.TestListEvent(f, 1, new List() {2, 3, 4});. +namespace Quantum { + public partial class Frame { + public partial struct FrameEvents { + public EventListEvent ListEvent(Frame f, Int32 foo, List listOfFoo) { + var ev = f.Events.ListEvent(foo); + ev.ListOfFoo = listOfFoo; + return ev; + } + } + } +} +Back To Top + + +Callbacks +Callbacks are a special type of event triggered internally by the Quantum Core. The ones made available to the user are: + +Callback Description +CallbackPollInput Is called when the simulation queries local input. +CallbackInputConfirmed Is called when local input was confirmed. +CallbackGameStarted Is called when the game has been started. +CallbackGameResynced Is called when the game has been re-synchronized from a snapshot. +CallbackGameDestroyed Is called when the game was destroyed. +CallbackUpdateView Is guaranteed to be called every rendered frame. +CallbackSimulateFinished Is called when frame simulation has completed. +CallbackEventCanceled Is called when an event raised in a predicted frame was cancelled in a verified frame due to a roll-back / missed prediction. Synchronized events are only raised on verified frames and thus will never be cancelled; this is useful to graciously discard non-synced events in the view. +CallbackEventConfirmed Is called when an event was confirmed by a verified frame. +CallbackChecksumError Is called on a checksum error. +CallbackChecksumErrorFrameDump Is called when due to a checksum error a frame is dumped. +CallbackChecksumComputed Is called when a checksum has been computed. +CallbackPluginDisconnect Is called when the plugin disconnects the client with an error. The reason parameter is filled with an error discription (e.g. "Error #15: Snapshot request timed out"). The client state is unrecoverable after that and needs to reconnect and restart the simulation. The current QuantumRunner should be shutdown immediately. +Back To Top + + +Unity-side Callbacks +By tweaking the value of Auto Load Scene From Map in the SimulationConfig asset, it is possible to determine if the game scene will be loaded automatically or not and it is also possible to determine whether the preview scene unloading will happen before or after the game scene is loaded. + +There are four callbacks that are called when the scenes are being loaded and unloaded: CallbackUnitySceneLoadBegin, CallbackUnitySceneLoadDone, CallbackUnitySceneUnloadBegin, CallbackUnitySceneUnloadDone. + +Back To Top + + +MonoBehaviour +Callbacks are subscribed to and unsubscribe from in the same way one as Frame Events presented earlier, albeit through QuantumCallback instead of QuantumEvent. + +var subscription = QuantumCallback.Subscribe(...); +QuantumCallback.Unsubscribe(subscription); // cancels this specific subscription +QuantumCallback.UnsubscribeListener(this); // cancels all subscriptions for this listener +QuantumCallback.UnsubscribeListener(this); // cancels all listeners to CallbackPollInput for this listener +Unity manages the lifetime of its objects. Therefore, Quantum can detect whether the listener is alive or not. "Dead" listeners are removed with each LateUpdate and with each event invocation for specific event type. + +For example, to subscribe to the PollInput method and set up the player input, the following steps are necessary: + +public class LocalInput : MonoBehaviour { + private DispatcherSubscription _pollInputDispatcher; + private void OnEnable() { + _pollInputDispatcher = QuantumCallback.Subscribe(this, (CallbackPollInput callback) => PollInput(callback)); + } + + public void PollInput(CallbackPollInput callback) { + Quantum.Input i = new Quantum.Input(); + callback.SetInput(i, DeterministicInputFlags.Repeatable); + } + + private void OnDisable(){ + QuantumCallback.Unsubscribe(_pollInputDispatcher); + } +} +Back To Top + + +Pure CSharp +If a callback is subscribed outside of a MonoBehaviour the subscription has to be handled manually. + +var disposable = QuantumCallback.SubscribeManual((CallbackPollInput pollInput) => {}); // subscribes to the callback +// ... +disposable.Dispose(); // disposes the callback subscription +Back To Top + + +Entity Instantiation Order +When creating entities using Frame.Create() and the Frame simulation is finished, the following callbacks will be executed in order: + +OnUpdateView, the view for the newly created entities are instantiated. +Monobehaviour.Awake +Monobehaviour.OnEnabled +EntityView.OnEntityInstantiated +Frame.Events are called. +Event and Callback subscription can be done in either Monobehaviour.OnEnabled or EntityView.OnEntityInstantiated. + +MonoBehaviour.OnEnabled, it is possible to subscribe to events in code here; however, the EntityView's EntityRef and Asset GUID will not have been set yet. +EntityView.OnEntityInstantiated is a UnityEvent part of the EntityView component. It can be subscribed to via the in-editor menu. When OnEntityInstantiated is called, the EntityRef and Asset GUID of the EntityView are guaranteed to be set. If the event subscription or custom logic requires either of those parameters, this is where it should be executed. +OnEntityInstantiated subscription menu in Editor +OnEntityInstantiated subscription menu as seen in the Editor. +To unsubscribe from an event or callback, simply use the complementary functions: + +Unsubscribe in OnDisabled for any subscription made in OnEnabled. +Unsubscribe in OnEntityDestroyed for any subscription made in OnEntityInstantiated. \ No newline at end of file diff --git a/data/extended assets.txt b/data/extended assets.txt new file mode 100644 index 0000000000000000000000000000000000000000..33f49758189ec1fc42af632abd1c22b353cfdf15 --- /dev/null +++ b/data/extended assets.txt @@ -0,0 +1,56 @@ +Extending Assets for Unity +Overview +Example +Access At Runtime +Access At Edit-time + +Overview +Quantum assets can be extended with Unity-specific data not relevant for the simulation like data for the UI (colors, texts, icons...). This is done with the use of partial classes. + +Back To Top + + +Example +Let's take the CharacterSpec asset as an example. Its ScriptableObject-based wrapper in Unity is called CharacterSpecAsset and is the type which needs to extended. + +public partial class CharacterSpecAsset { + [Header("Unity")] + public Sprtie Icon; + public Color Color; + public string DisplayName; +} +These fields can only be accessed in the View (Unity) and cannot be accessed or used in the simulation (Quantum). +The newly created partial class needs to be added to the same assembly as the original definition of CharacterSpecAsset. By default, all Unity-side Quantum code belongs to the PhotonQuantum assembly. + +To ensure the partial class belongs to the correct assembly use one of the following approaches: + +Save the class in Assets/Photon/Quantum/User directory. +Save the class in any directory that has an AssemblyDefinitionReference asset pointing to the PhotonQuantum assembly. +Delete Assets/Photon/Quantum/PhotonQuantum.asmdef. This will make Quantum a part of the main assembly. Note that this step needs to be repeated after each Quantum SDK update. +Back To Top + + +Access At Runtime +To access the extra fields at runtime, use the UnityDB.FindAsset() method. + +CharacterSpecAsset characterSpecAsset = UnityDB.FindAsset(guid); +Debug.Log(characterSpecAsset.DisplayName); +Alternatively, the code-generated GetUnityAsset() extension methods can be used: + +CharacterSpec characterSpec = frame.FindAsset(guid); +CharacterSpecAsset characterSpecAsset = characterSpec.GetUnityAsset(); +Debug.Log(characterSpecAsset.DisplayName); +Both of the approaches will result in the asset being loaded into Quantum's AssetDB using the appropriate method, as discussed here: resources, addressables and asset bundles. + +Back To Top + + +Access At Edit-time +To load an asset using its path while in the Unity Editor, the UnityEditor.AssetDataBase.LoadAssetAtPath() method can be used. + +CharacterSpecAsset characterSpecAsset = UnityEditor.AssetDatabase.LoadAssetAtPath(path); +Debug.Log(characterSpecAsset.DisplayName); +Alternatively, the asset can be loaded using its AssetGuid via the UnityDB.FindAssetForInspector() method and casting the result to the correct type. + +CharacterSpecAsset characterSpecAsset = (CharacterSpecAsset)UnityDB.FindAssetForInspector(guid); +Debug.Log(characterSpecAsset.DisplayName); \ No newline at end of file diff --git a/data/frame.txt b/data/frame.txt new file mode 100644 index 0000000000000000000000000000000000000000..e00e5364c6b53d8a8ca8c7e88ae4795ced6238bf --- /dev/null +++ b/data/frame.txt @@ -0,0 +1,41 @@ +Frame Classes +The FrameBase class the hub for all of the Quantum game data. More... + +Classes +class Quantum.Frame + The user implementation of FrameBase that resides in the project quantum_state and has access to all user relevant classes. More... + +class Quantum.Core.FrameBase + The Frame class is the container for all the transient and static game state data, including the API for entities, physics, assets and others. More... + +struct Quantum.Core.FrameBase.FrameBaseUnsafe + Frame API to give access to C# unsafe pointers and advanced immediate operations. More... + +Variables +Physics2D.PhysicsEngine2D.Api Quantum.Core.FrameBase.Physics2D + Access to the Physics2D API. More... + +Physics3D.PhysicsEngine3D.Api Quantum.Core.FrameBase.Physics3D + Access to the Physics3D API. More... + +Properties +Navigation Quantum.Core.FrameBase.Navigation[get] + Access to the Navigation API. More... + +Detailed Description +The FrameBase class the hub for all of the Quantum game data. + +Variable Documentation +◆ Physics2D +Physics2D.PhysicsEngine2D.Api Quantum.Core.FrameBase.Physics2D +Access to the Physics2D API. + +◆ Physics3D +Physics3D.PhysicsEngine3D.Api Quantum.Core.FrameBase.Physics3D +Access to the Physics3D API. + +Properties +◆ Navigation +Navigation Quantum.Core.FrameBase.Navigation +get +Access to the Navigation API. \ No newline at end of file diff --git a/data/frames.txt b/data/frames.txt new file mode 100644 index 0000000000000000000000000000000000000000..acb172ce2bf396e8da8918626ab2968272b733d7 --- /dev/null +++ b/data/frames.txt @@ -0,0 +1,48 @@ +Quantum's predict-rollback architecture allows to mitigate latency. Quantum always rolls-back and re-simulates frames. It is a necessary for determinism and involves the validation of player input by the server. Once the server has either confirmed the player input or overwritten/replaced it (only in cases were the input did not reach the server in time), the validated input of all players for a given frame is sent to the clients. Once the validated input is received, the last verified frame advances using the confirmed input. + +N.B.: A player's own input will be rolled back if it has not reached the server in time or could not be validated. + +Back To Top + + +Types Of Frame +Quantum differenciates between two types of frame: + +verified; and, +predicted. +Back To Top + + +Verified +A Verified frame is a trusted simulation frame. A verified frame is guaranteed to be deterministic and identical on all client simulations. The verified simulation only simulates the next verified frame once it has received the server-confirmed inputs; as such, it moves forward proportional to RTT/2 from the server. + +A frame is verified if both of the following condition are both true: + +the input from ALL players is confirmed by the server for this tick; and, +all previous ticks it follows are verified. +A partial tick confirmation where the input from only a subset of player has been validated by the server will not result in a verified tick/frame. + +Back To Top + + +Predicted +Contrary to verified frames, predicted frames do not require server-confirmed input. This means the predicted frame advances with prediction as soon as the simulation has accumulated enough delta time in the local session. + +The Unity-side API offers access to various versions of the predicted frame, see the API explanation below. + +Predicted : the simulation "head", based on the synchronised clock. +PredictedPrevious (predicted - 1): used for main clock-aliasing interpolation (most views will use this to stay smooth, as Unity's local clock may slightly drift from the main server clock. Quantum runs from a separate clock, in sync with the server clock - smoothly corrected). +PreviousUpdatePredicted: this is the exact frame that was the "Predicted/Head" the last time Session.Update was called (with the "corrected" data in it). Used for error correction interpolation (most of the time there will be no error). +Back To Top + + +API +The concept of Verified and Predicted frames exists in both the simulation and the view, albeit with a slightly different API. + + +Simulation +In the simulation, one can access the state of the currently simulated frame via the Frame class. + +Method Return Value Description +IsVerified bool Returns true if the frame is deterministic across all clients and uses server-confirmed input. +IsPredicted bool Returns true if the frame is a locally predicted one. \ No newline at end of file