add spirit trail rec reader
This commit is contained in:
		@ -3,6 +3,8 @@
 | 
			
		||||
    <TargetFramework>net472</TargetFramework>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Brigadier.NET" Version="1.2.13" />
 | 
			
		||||
    <PackageReference Include="Google.Protobuf" Version="3.21.1" />
 | 
			
		||||
    <PackageReference Include="zlib.net" Version="1.0.4" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
using System;
 | 
			
		||||
using BallanceStalker.Cores.Utils;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
@ -6,5 +7,23 @@ using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace BallanceStalker.Cores.Managers {
 | 
			
		||||
    public class RecManager {
 | 
			
		||||
 | 
			
		||||
        public event Action<Guid, IRec> RecAdded;
 | 
			
		||||
        public event Action<Guid, IRec> RecRemoved;
 | 
			
		||||
 | 
			
		||||
        private Dictionary<Guid, IRec> mRecDict = new Dictionary<Guid, IRec>();
 | 
			
		||||
 | 
			
		||||
        public Guid AddSpiritTrailRec(string rec_folder) {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void RemoveRec(Guid uuid) {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Tick(float delta) {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,51 +5,217 @@ using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.IO.Compression;
 | 
			
		||||
using zlib;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using System.Numerics;
 | 
			
		||||
 | 
			
		||||
namespace BallanceStalker.Cores.Rec {
 | 
			
		||||
 | 
			
		||||
    public struct SpiritTrailTrafo {
 | 
			
		||||
        public int Frame;
 | 
			
		||||
        public VxBallType TrafoType;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum SpiritTrailRecType {
 | 
			
		||||
        HS,
 | 
			
		||||
        SR
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class SpiritTrailRec : Utils.IRec {
 | 
			
		||||
        public SpiritTrailRec(string rec_folder) {
 | 
			
		||||
 | 
			
		||||
        static readonly float REC_DELTA = 1f / 8f;
 | 
			
		||||
 | 
			
		||||
        public SpiritTrailRec(string rec_folder, SpiritTrailRecType t) {
 | 
			
		||||
            mIsReady = mIsPlaying = false;
 | 
			
		||||
            Task.Run(() => {
 | 
			
		||||
                // load data
 | 
			
		||||
 | 
			
		||||
            mFileFolder = rec_folder;
 | 
			
		||||
            mFileType = t;
 | 
			
		||||
            mFileCursor = 1;
 | 
			
		||||
 | 
			
		||||
                lock (mStatusLock) {
 | 
			
		||||
                    mIsReady = true;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            mRecStateCursor = mRecTrafoCursor = 0;
 | 
			
		||||
            mRecRemainDelta = 0f;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public event Action<long, string> RecRegisterBall;
 | 
			
		||||
        public event Action RecPlaying;
 | 
			
		||||
        public event Action RecPaused;
 | 
			
		||||
        public event Action<long> RecUnregisterBall;
 | 
			
		||||
        public event Action<RecBallStateReport> RecNewBallState;
 | 
			
		||||
        public event Action<List<RecBallStateReport>> RecNewBallStates;
 | 
			
		||||
 | 
			
		||||
        object mStatusLock;
 | 
			
		||||
        bool mIsReady, mIsPlaying;
 | 
			
		||||
 | 
			
		||||
        string mFileFolder;
 | 
			
		||||
        SpiritTrailRecType mFileType;
 | 
			
		||||
        int mFileCursor;
 | 
			
		||||
 | 
			
		||||
        int mRecHSScore;
 | 
			
		||||
        float mRecSRScore;
 | 
			
		||||
        VxBallState[] mRecStates;
 | 
			
		||||
        SpiritTrailTrafo[] mRecTrafos;
 | 
			
		||||
 | 
			
		||||
        int mRecStateCursor, mRecTrafoCursor;
 | 
			
		||||
        float mRecRemainDelta;
 | 
			
		||||
 | 
			
		||||
        private void LoadData() {
 | 
			
		||||
            // load data
 | 
			
		||||
            bool success = true;
 | 
			
		||||
            try {
 | 
			
		||||
                // check exist
 | 
			
		||||
                var filename = System.IO.Path.Combine(mFileFolder, mFileType == SpiritTrailRecType.HS ? $"hs{mFileCursor}.rec" : $"sr{mFileCursor}.rec");
 | 
			
		||||
                if (!System.IO.File.Exists(filename))
 | 
			
		||||
                    throw new ArgumentOutOfRangeException("spirit trail index overflow.");
 | 
			
		||||
 | 
			
		||||
                // analyse file
 | 
			
		||||
                using (var ms = new MemoryStream()) {
 | 
			
		||||
                    // decompress
 | 
			
		||||
                    int decomp;
 | 
			
		||||
                    using (var zo = new zlib.ZOutputStream(ms, 9)) {
 | 
			
		||||
                        using (var fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) {
 | 
			
		||||
                            // read header
 | 
			
		||||
                            var data = new byte[4];
 | 
			
		||||
                            fs.Read(data, 0, 4);
 | 
			
		||||
                            decomp = BitConverter.ToInt32(data, 0);
 | 
			
		||||
 | 
			
		||||
                            fs.CopyTo(zo);
 | 
			
		||||
                        }
 | 
			
		||||
                        zo.finish();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // check size
 | 
			
		||||
                    if (ms.Length != decomp)
 | 
			
		||||
                        throw new ArgumentOutOfRangeException("expected stream size if not matched real size in zlib decompression.");
 | 
			
		||||
 | 
			
		||||
                    // read data
 | 
			
		||||
                    using (var br = new BinaryReader(ms, Encoding.ASCII, true)) {
 | 
			
		||||
                        // read basic data
 | 
			
		||||
                        mRecHSScore = br.ReadInt32();
 | 
			
		||||
                        mRecSRScore = br.ReadSingle();
 | 
			
		||||
                        int states_count = br.ReadInt32();
 | 
			
		||||
                        int trafo_count = br.ReadInt32();
 | 
			
		||||
 | 
			
		||||
                        // read states data
 | 
			
		||||
                        mRecStates = new VxBallState[states_count];
 | 
			
		||||
                        byte[] buffer = br.ReadBytes(states_count * 4 * (3 + 4));
 | 
			
		||||
                        Buffer.BlockCopy(buffer, 0, mRecStates, 0, states_count);
 | 
			
		||||
                        //for (int i = 0; i < states_count; ++i) {
 | 
			
		||||
                        //    mRecStates[i].Pos.X = br.ReadSingle();
 | 
			
		||||
                        //    mRecStates[i].Pos.Y = br.ReadSingle();
 | 
			
		||||
                        //    mRecStates[i].Pos.Z = br.ReadSingle();
 | 
			
		||||
                        //    mRecStates[i].Quat.X = br.ReadSingle();
 | 
			
		||||
                        //    mRecStates[i].Quat.Y = br.ReadSingle();
 | 
			
		||||
                        //    mRecStates[i].Quat.Z = br.ReadSingle();
 | 
			
		||||
                        //    mRecStates[i].Quat.W = br.ReadSingle();
 | 
			
		||||
                        //}
 | 
			
		||||
 | 
			
		||||
                        // read trafo data
 | 
			
		||||
                        mRecTrafos = new SpiritTrailTrafo[trafo_count];
 | 
			
		||||
                        buffer = br.ReadBytes(states_count * 8);
 | 
			
		||||
                        Buffer.BlockCopy(buffer, 0, mRecTrafos, 0, trafo_count);
 | 
			
		||||
                        //for (int i = 0; i < trafo_count; ++i) {
 | 
			
		||||
                        //    mRecTrafo[i].Frame = br.ReadInt32();
 | 
			
		||||
                        //    mRecTrafo[i].TrafoType = (BallType)br.ReadInt32();
 | 
			
		||||
                        //}
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // increase sector
 | 
			
		||||
                mFileCursor++;
 | 
			
		||||
            } catch {
 | 
			
		||||
                success = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // feedback status
 | 
			
		||||
            lock (mStatusLock) {
 | 
			
		||||
                mIsReady = success;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        public void Startup() {
 | 
			
		||||
            RecRegisterBall?.Invoke(0, Path.GetFileName(mFileFolder));
 | 
			
		||||
 | 
			
		||||
            Task.Run(() => {
 | 
			
		||||
                LoadData();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Shutdown() {
 | 
			
		||||
            RecUnregisterBall?.Invoke(0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Pause() {
 | 
			
		||||
            lock (mStatusLock) {
 | 
			
		||||
                mIsPlaying = false;
 | 
			
		||||
            }
 | 
			
		||||
            //RecPaused?.Invoke();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Play() {
 | 
			
		||||
            lock (mStatusLock) {
 | 
			
		||||
                mIsPlaying = true;
 | 
			
		||||
            }
 | 
			
		||||
            //RecPlaying?.Invoke();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Seek(float sec) {
 | 
			
		||||
            ;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Tick(float delta_sec, List<RecBallStateReport> report) {
 | 
			
		||||
        public void Tick(float delta_sec) {
 | 
			
		||||
            lock (mStatusLock) {
 | 
			
		||||
                if (!mIsPlaying || !mIsReady) return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            // increase delta
 | 
			
		||||
            mRecRemainDelta += delta_sec;
 | 
			
		||||
            if (mRecRemainDelta > REC_DELTA) {
 | 
			
		||||
                var inc = (int)(mRecRemainDelta / REC_DELTA);
 | 
			
		||||
                mRecStateCursor += inc;
 | 
			
		||||
                mRecRemainDelta -= inc * REC_DELTA;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // check cursor
 | 
			
		||||
            if (mRecStateCursor >= mRecStates.Length - 1) {
 | 
			
		||||
                // try to load next sector
 | 
			
		||||
                var remain = mRecStates.Length - mRecStateCursor;
 | 
			
		||||
 | 
			
		||||
                lock (mStatusLock) {
 | 
			
		||||
                    mIsReady = false;
 | 
			
		||||
                }
 | 
			
		||||
                Task.Run(() => {
 | 
			
		||||
                    LoadData();
 | 
			
		||||
                    mRecStateCursor = remain;
 | 
			
		||||
                    mRecTrafoCursor = 0;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // return directly until next sector load successfully
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // try shift trafo cursor
 | 
			
		||||
            while ((mRecTrafoCursor < mRecTrafos.Length - 1) && mRecTrafos[mRecTrafoCursor + 1].Frame <= mRecStateCursor) {
 | 
			
		||||
                mRecTrafoCursor++;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // otherwise, output data
 | 
			
		||||
            RecNewBallState?.Invoke(new RecBallStateReport() {
 | 
			
		||||
                BallState = mRecStates[mRecStateCursor].Slerp(mRecStates[mRecStateCursor + 1], mRecRemainDelta / REC_DELTA),
 | 
			
		||||
                BallType = mRecTrafos[mRecTrafoCursor].TrafoType,
 | 
			
		||||
                Identifier = 0
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string GetProfile() {
 | 
			
		||||
            lock (mStatusLock) {
 | 
			
		||||
                return $@"Rec folder: {mFileFolder}
 | 
			
		||||
Rec folder cursor: {mFileCursor}
 | 
			
		||||
Rec folder type: {mFileType}
 | 
			
		||||
Rec ready/play status: {mIsReady}/{mIsPlaying}
 | 
			
		||||
SubRec HS/SR: {mRecHSScore}/{mRecSRScore}
 | 
			
		||||
SubRec state/trafo size: {mRecStates?.Length}/{mRecTrafos?.Length}";
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,30 +1,144 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace BallanceStalker.Cores.Utils {
 | 
			
		||||
    public enum BallType : UInt32 {
 | 
			
		||||
    public enum VxBallType : UInt32 {
 | 
			
		||||
        Stone,
 | 
			
		||||
        Wood,
 | 
			
		||||
        Paper
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct VxVector {
 | 
			
		||||
        float X, Y, Z;
 | 
			
		||||
    }
 | 
			
		||||
    public struct VxQuaternion {
 | 
			
		||||
        float X, Y, Z, W;
 | 
			
		||||
    public static class QuaternionHelper {
 | 
			
		||||
        public static float Dot(this System.Numerics.Quaternion f, System.Numerics.Quaternion p_to) {
 | 
			
		||||
            return f.X * p_to.X + f.Y * p_to.Y + f.Z * p_to.Z + f.W * p_to.W;
 | 
			
		||||
        }
 | 
			
		||||
        public static System.Numerics.Quaternion Slerp(this System.Numerics.Quaternion f, System.Numerics.Quaternion p_to, float delta) {
 | 
			
		||||
            float omega, cosom, sinom, scale0, scale1;
 | 
			
		||||
 | 
			
		||||
            // calc cosine
 | 
			
		||||
            cosom = f.Dot(p_to);
 | 
			
		||||
 | 
			
		||||
            // adjust signs (if necessary)
 | 
			
		||||
            bool is_minus = cosom < 0f;
 | 
			
		||||
            if (is_minus) cosom = -cosom;
 | 
			
		||||
 | 
			
		||||
            // calculate coefficients
 | 
			
		||||
            if ((1f - cosom) >= 0.01f) {
 | 
			
		||||
                // standard case (slerp)
 | 
			
		||||
                omega = (float)Math.Acos(cosom);
 | 
			
		||||
                sinom = (float)Math.Sin(omega);
 | 
			
		||||
                scale0 = (float)Math.Sin((1.0 - delta) * omega) / sinom;
 | 
			
		||||
                scale1 = (float)Math.Sin(delta * omega) / sinom;
 | 
			
		||||
            } else {
 | 
			
		||||
                // "from" and "to" quaternions are very close
 | 
			
		||||
                //  ... so we can do a linear interpolation
 | 
			
		||||
                scale0 = 1.0f - delta;
 | 
			
		||||
                scale1 = delta;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // adjust signs (if necessary)
 | 
			
		||||
            if (is_minus) scale1 = -scale1;
 | 
			
		||||
 | 
			
		||||
            return f * scale0 + p_to * scale1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //public struct VxVector {
 | 
			
		||||
    //    public VxVector(float x, float y, float z) {
 | 
			
		||||
    //        X = x;
 | 
			
		||||
    //        Y = y;
 | 
			
		||||
    //        Z = z;
 | 
			
		||||
    //    }
 | 
			
		||||
    //    public float X;
 | 
			
		||||
    //    public float Y;
 | 
			
		||||
    //    public float Z;
 | 
			
		||||
 | 
			
		||||
    //    public static VxVector operator+(VxVector a, VxVector b) {
 | 
			
		||||
    //        return new VxVector(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
 | 
			
		||||
    //    }
 | 
			
		||||
    //    public static VxVector operator -(VxVector a, VxVector b) {
 | 
			
		||||
    //        return new VxVector(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
 | 
			
		||||
    //    }
 | 
			
		||||
    //    public static VxVector operator *(float m, VxVector a) {
 | 
			
		||||
    //        return new VxVector(m * a.X, m * a.Y, m * a.Z);
 | 
			
		||||
    //    }
 | 
			
		||||
    //    public static VxVector operator *(VxVector a, float m) {
 | 
			
		||||
    //        return new VxVector(m * a.X, m * a.Y, m * a.Z);
 | 
			
		||||
    //    }
 | 
			
		||||
    //    public static VxVector operator /(VxVector a, float m) {
 | 
			
		||||
    //        return new VxVector(a.X / m, a.Y / m, a.Z / m);
 | 
			
		||||
    //    }
 | 
			
		||||
    //}
 | 
			
		||||
    //public struct VxQuaternion {
 | 
			
		||||
    //    public VxQuaternion(float x, float y, float z, float w) {
 | 
			
		||||
    //        X = x;
 | 
			
		||||
    //        Y = y;
 | 
			
		||||
    //        Z = z;
 | 
			
		||||
    //        W = w;
 | 
			
		||||
    //    }
 | 
			
		||||
    //    public float X;
 | 
			
		||||
    //    public float Y;
 | 
			
		||||
    //    public float Z;
 | 
			
		||||
    //    public float W;
 | 
			
		||||
    //    public float Dot(VxQuaternion p_to) {
 | 
			
		||||
    //        return X * p_to.X + Y * p_to.Y + Z * p_to.Z + W * p_to.W;
 | 
			
		||||
    //    }
 | 
			
		||||
    //    // Reference: https://github.com/godotengine/godot/blob/master/core/math/quaternion.cpp
 | 
			
		||||
    //    public VxQuaternion Slerp(VxQuaternion p_to, float delta) {
 | 
			
		||||
    //        float omega, cosom, sinom, scale0, scale1;
 | 
			
		||||
 | 
			
		||||
    //        // calc cosine
 | 
			
		||||
    //        cosom = Dot(p_to);
 | 
			
		||||
 | 
			
		||||
    //        // adjust signs (if necessary)
 | 
			
		||||
    //        bool is_minus = cosom < 0f;
 | 
			
		||||
    //        if (is_minus) cosom = -cosom;
 | 
			
		||||
 | 
			
		||||
    //        // calculate coefficients
 | 
			
		||||
    //        if ((1f - cosom) >= 0.01f) {
 | 
			
		||||
    //            // standard case (slerp)
 | 
			
		||||
    //            omega = (float)Math.Acos(cosom);
 | 
			
		||||
    //            sinom = (float)Math.Sin(omega);
 | 
			
		||||
    //            scale0 = (float)Math.Sin((1.0 - delta) * omega) / sinom;
 | 
			
		||||
    //            scale1 = (float)Math.Sin(delta * omega) / sinom;
 | 
			
		||||
    //        } else {
 | 
			
		||||
    //            // "from" and "to" quaternions are very close
 | 
			
		||||
    //            //  ... so we can do a linear interpolation
 | 
			
		||||
    //            scale0 = 1.0f - delta;
 | 
			
		||||
    //            scale1 = delta;
 | 
			
		||||
    //        }
 | 
			
		||||
 | 
			
		||||
    //        // adjust signs (if necessary)
 | 
			
		||||
    //        if (is_minus) scale1 = -scale1;
 | 
			
		||||
 | 
			
		||||
    //        return new VxQuaternion(
 | 
			
		||||
    //            scale0 * X + scale1 * p_to.X,
 | 
			
		||||
    //            scale0 * Y + scale1 * p_to.Y,
 | 
			
		||||
    //            scale0 * Z + scale1 * p_to.Z,
 | 
			
		||||
    //            scale0 * W + scale1 * p_to.W
 | 
			
		||||
    //        );
 | 
			
		||||
    //    }
 | 
			
		||||
 | 
			
		||||
    //}
 | 
			
		||||
    public struct VxBallState {
 | 
			
		||||
        VxVector Pos;
 | 
			
		||||
        VxQuaternion Quat;
 | 
			
		||||
        public System.Numerics.Vector3 Pos;
 | 
			
		||||
        public System.Numerics.Quaternion Quat;
 | 
			
		||||
        public VxBallState Slerp(VxBallState to, float delta) {
 | 
			
		||||
            return new VxBallState() {
 | 
			
		||||
                Pos = (to.Pos - this.Pos) * delta + this.Pos,
 | 
			
		||||
                Quat = this.Quat.Slerp(to.Quat, delta)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct RecBallStateReport {
 | 
			
		||||
        long Identifier;
 | 
			
		||||
        VxBallState BallState;
 | 
			
		||||
        public long Identifier;
 | 
			
		||||
        public VxBallType BallType;
 | 
			
		||||
        public VxBallState BallState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,18 +6,23 @@ using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace BallanceStalker.Cores.Utils {
 | 
			
		||||
    public interface IRec {
 | 
			
		||||
        void Startup();
 | 
			
		||||
        void Shutdown();
 | 
			
		||||
        void Play();
 | 
			
		||||
        void Pause();
 | 
			
		||||
        void Seek(float sec);
 | 
			
		||||
        void Tick(float delta_sec, List<RecBallStateReport> report);
 | 
			
		||||
        void Tick(float delta_sec);
 | 
			
		||||
        string GetProfile();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// register a ball
 | 
			
		||||
        /// long is identifier, string is name
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        event Action<long, string> RecRegisterBall;
 | 
			
		||||
        event Action RecPlaying;
 | 
			
		||||
        event Action RecPaused;
 | 
			
		||||
        event Action<RecBallStateReport> RecNewBallState;
 | 
			
		||||
        event Action<List<RecBallStateReport>> RecNewBallStates;
 | 
			
		||||
        //event Action RecPlaying;
 | 
			
		||||
        //event Action RecPaused;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// unregister a ball
 | 
			
		||||
        /// long is identifier
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user