v2019.08.19

This commit is contained in:
pypy
2019-08-19 23:21:08 +09:00
parent d725c8f1e3
commit 26b371fd0f
19 changed files with 768 additions and 515 deletions

View File

@@ -6,13 +6,14 @@
using CefSharp;
using CefSharp.OffScreen;
using SharpDX.Direct3D11;
using System;
using System.Drawing;
using System.Threading;
namespace VRCX
{
public class Browser : ChromiumWebBrowser
{
private readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim();
private Texture2D m_Texture;
public Browser(Texture2D texture, string address)
@@ -24,38 +25,46 @@ namespace VRCX
m_Texture = texture;
Size = new Size(texture.Description.Width, texture.Description.Height);
RenderHandler.Dispose();
RenderHandler = new RenderHandler(this);
RenderHandler = new RenderHandler(this, m_Lock);
var options = new BindingOptions()
{
CamelCaseJavascriptNames = false
};
JavascriptObjectRepository.Register("VRCX", new VRCX(), true, options);
JavascriptObjectRepository.Register("VRCXStorage", new VRCXStorage(), false, options);
JavascriptObjectRepository.Register("SQLite", new SQLite(), true, options);
}
public new void Dispose()
{
RenderHandler.Dispose();
RenderHandler = null;
base.Dispose();
}
public void Render()
{
var handler = (RenderHandler)RenderHandler;
lock (handler.BufferLock)
m_Lock.EnterReadLock();
try
{
if (handler.Buffer.IsAllocated)
var H = (RenderHandler)RenderHandler;
if (H.Buffer.IsAllocated)
{
var context = m_Texture.Device.ImmediateContext;
var box = context.MapSubresource(m_Texture, 0, MapMode.WriteDiscard, MapFlags.None);
if (box.DataPointer != IntPtr.Zero)
if (box.IsEmpty)
{
var width = handler.Width;
var height = handler.Height;
if (box.RowPitch == width * 4)
if (box.RowPitch == H.Width * 4)
{
WinApi.CopyMemory(box.DataPointer, handler.Buffer.AddrOfPinnedObject(), (uint)handler.BufferSize);
WinApi.CopyMemory(box.DataPointer, H.Buffer.AddrOfPinnedObject(), (uint)H.BufferSize);
}
else
{
var dest = box.DataPointer;
var src = handler.Buffer.AddrOfPinnedObject();
var src = H.Buffer.AddrOfPinnedObject();
var pitch = box.RowPitch;
var length = width * 4;
var length = H.Width * 4;
var height = H.Height;
for (var i = 0; i < height; ++i)
{
WinApi.CopyMemory(dest, src, (uint)length);
@@ -67,6 +76,10 @@ namespace VRCX
context.UnmapSubresource(m_Texture, 0);
}
}
finally
{
m_Lock.ExitReadLock();
}
}
}
}

View File

@@ -13,36 +13,32 @@ namespace VRCX
public static float CpuUsage { get; private set; }
private static Thread m_Thread;
public static void Start()
public static void Init()
{
if (m_Thread == null)
m_Thread = new Thread(() =>
{
m_Thread = new Thread(() =>
PerformanceCounter counter = null;
try
{
PerformanceCounter cpuCounter = null;
try
{
cpuCounter = new PerformanceCounter("Processor Information", "% Processor Utility", "_Total", true);
}
catch
{
}
try
{
if (cpuCounter == null)
{
cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total", true);
}
}
catch
counter = new PerformanceCounter("Processor Information", "% Processor Utility", "_Total", true);
}
catch
{
}
try
{
if (counter == null)
{
counter = new PerformanceCounter("Processor", "% Processor Time", "_Total", true);
}
}
catch
{
}
if (counter != null)
{
while (m_Thread != null)
{
if (cpuCounter != null)
{
CpuUsage = cpuCounter.NextValue();
}
try
{
Thread.Sleep(1000);
@@ -51,31 +47,23 @@ namespace VRCX
{
// ThreadInterruptedException
}
CpuUsage = counter.NextValue();
}
if (cpuCounter != null)
{
cpuCounter.Dispose();
}
});
m_Thread.Start();
}
counter.Dispose();
}
})
{
IsBackground = true
};
m_Thread.Start();
}
public static void Stop()
public static void Exit()
{
var thread = m_Thread;
if (thread != null)
{
m_Thread = null;
try
{
thread.Interrupt();
thread.Join();
}
catch
{
}
}
var T = m_Thread;
m_Thread = null;
T.Interrupt();
T.Join();
}
}
}

View File

@@ -4,38 +4,80 @@
// For a copy, see <https://opensource.org/licenses/MIT>.
using DiscordRPC;
using System.Threading;
namespace VRCX
{
public class Discord
{
private static RichPresence m_Presence = new RichPresence();
private static DiscordRpcClient m_Client;
private static readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim();
private static readonly RichPresence m_Presence = new RichPresence();
private static Thread m_Thread;
private static bool m_Active;
public static void Update()
public static void Init()
{
if (m_Client != null)
m_Thread = new Thread(() =>
{
m_Client.Invoke();
}
if (m_Active)
{
if (m_Client == null)
DiscordRpcClient client = null;
while (m_Thread != null)
{
m_Client = new DiscordRpcClient("525953831020920832");
m_Client.Initialize();
try
{
Thread.Sleep(1000);
}
catch
{
// ThreadInterruptedException
}
if (client != null)
{
m_Lock.EnterReadLock();
try
{
client.SetPresence(m_Presence);
}
finally
{
m_Lock.ExitReadLock();
}
client.Invoke();
}
if (m_Active)
{
if (client == null)
{
client = new DiscordRpcClient("525953831020920832");
if (!client.Initialize())
{
client.Dispose();
client = null;
}
}
}
else if (client != null)
{
client.Dispose();
client = null;
}
}
lock (m_Presence)
if (client != null)
{
m_Client.SetPresence(m_Presence);
client.Dispose();
}
}
else if (m_Client != null)
})
{
m_Client.Dispose();
m_Client = null;
}
IsBackground = true
};
m_Thread.Start();
}
public static void Exit()
{
var T = m_Thread;
m_Thread = null;
T.Interrupt();
T.Join();
}
public void SetActive(bool active)
@@ -45,16 +87,22 @@ namespace VRCX
public void SetText(string details, string state)
{
lock (m_Presence)
m_Lock.EnterWriteLock();
try
{
m_Presence.Details = details;
m_Presence.State = state;
}
finally
{
m_Lock.ExitWriteLock();
}
}
public void SetAssets(string largeKey, string largeText, string smallKey, string smallText)
{
lock (m_Presence)
m_Lock.EnterWriteLock();
try
{
if (string.IsNullOrEmpty(largeKey) &&
string.IsNullOrEmpty(smallKey))
@@ -73,6 +121,10 @@ namespace VRCX
m_Presence.Assets.SmallImageText = smallText;
}
}
finally
{
m_Lock.ExitWriteLock();
}
}
// JSB Sucks
@@ -83,7 +135,8 @@ namespace VRCX
public static void SetTimestamps(ulong startUnixMilliseconds, ulong endUnixMilliseconds)
{
lock (m_Presence)
m_Lock.EnterWriteLock();
try
{
if (startUnixMilliseconds == 0)
{
@@ -106,6 +159,10 @@ namespace VRCX
}
}
}
finally
{
m_Lock.ExitWriteLock();
}
}
}
}

View File

@@ -5,176 +5,194 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
namespace VRCX
{
public class VRCX_LogWatcher
public class LogWatcherFile
{
public long Length;
public long Position;
}
public class LogWatcher
{
private static readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim();
private static List<string[]> m_GameLog = new List<string[]>();
private static Thread m_Thread;
private static bool m_Reset;
public static void Start()
// NOTE
// FileSystemWatcher() is unreliable
public static void Init()
{
if (m_Thread == null)
m_Thread = new Thread(() =>
{
m_Thread = new Thread(() =>
var D = new Dictionary<string, LogWatcherFile>();
var di = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat");
while (m_Thread != null)
{
var lastPosition = 0L;
var firstLine = string.Empty;
while (m_Thread != null)
try
{
if (m_Reset)
Thread.Sleep(1000);
}
catch
{
// ThreadInterruptedException
}
if (m_Reset)
{
m_Reset = false;
D.Clear();
m_Lock.EnterWriteLock();
try
{
m_Reset = false;
firstLine = string.Empty;
lastPosition = 0;
m_GameLog.Clear();
}
var info = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat");
if (info != null &&
info.Exists)
finally
{
var files = info.GetFiles("output_log_*.txt", SearchOption.TopDirectoryOnly);
if (files != null &&
files.Length >= 1)
m_Lock.ExitWriteLock();
}
}
var S = new HashSet<string>(D.Keys);
di.Refresh();
if (di.Exists)
{
var files = di.GetFiles("output_log_*.txt", SearchOption.TopDirectoryOnly);
Array.Sort(files, (A, B) => A.CreationTime.CompareTo(B.CreationTime));
var bias = DateTime.Now.AddMinutes(-5d);
foreach (var fi in files)
{
if (bias.CompareTo(fi.LastWriteTime) <= 0)
{
Array.Sort(files, (A, B) => B.LastWriteTime.CompareTo(A.LastWriteTime));
if (firstLine == string.Empty)
fi.Refresh();
}
if (D.TryGetValue(fi.Name, out LogWatcherFile F))
{
S.Remove(fi.Name);
if (F.Length == fi.Length)
{
for (var i = files.Length - 1; i >= 1; --i)
continue;
}
}
else
{
F = new LogWatcherFile();
D.Add(fi.Name, F);
}
F.Length = fi.Length;
Parse(fi, ref F.Position);
}
}
foreach (var key in S)
{
D.Remove(key);
}
}
})
{
IsBackground = true
};
m_Thread.Start();
}
public static void Exit()
{
var T = m_Thread;
m_Thread = null;
T.Interrupt();
T.Join();
}
public static void Parse(FileInfo info, ref long position)
{
try
{
using (var stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
stream.Position = position;
var s = string.Empty;
while ((s = reader.ReadLine()) != null)
{
if (s.Length > 35)
{
var c = s[35];
if (c == 'R')
{
// 2019.07.31 22:26:24 Log - [RoomManager] Joining wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd:6974~private(usr_4f76a584-9d4b-46f6-8209-8305eb683661)~nonce(0000000000000000000000000000000000000000000000000000000000000000)
// 2019.07.31 22:26:24 Log - [RoomManager] Joining or Creating Room: VRChat Home
if (s.Length > 56 &&
string.Compare(s, 34, "[RoomManager] Joining ", 0, "[RoomManager] Joining ".Length, StringComparison.Ordinal) == 0 &&
string.Compare(s, 56, "or ", 0, "or ".Length, StringComparison.Ordinal) != 0)
{
var item = new[]
{
using (var stream = files[i].Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
var line = string.Empty;
while ((line = reader.ReadLine()) != null)
{
if (line.Length > 32 &&
line[31] == '-')
{
ParseLine(line);
}
}
}
ConvertLogTimeToISO8601(s),
"Location",
s.Substring(56)
};
m_Lock.EnterWriteLock();
try
{
m_GameLog.Add(item);
}
finally
{
m_Lock.ExitWriteLock();
}
}
using (var stream = files[0].Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var reader = new StreamReader(stream, Encoding.UTF8))
}
else if (c == 'N')
{
// 2019.07.31 22:41:18 Log - [NetworkManager] OnPlayerJoined pypy
if (s.Length > 66 &&
string.Compare(s, 34, "[NetworkManager] OnPlayerJoined ", 0, "[NetworkManager] OnPlayerJoined ".Length, StringComparison.Ordinal) == 0)
{
var line = reader.ReadLine();
if (line != null)
var item = new[]
{
if (string.Equals(firstLine, line))
{
stream.Position = lastPosition;
}
else
{
firstLine = line;
}
do
{
lastPosition = stream.Position;
ParseLine(line);
}
while ((line = reader.ReadLine()) != null);
ConvertLogTimeToISO8601(s),
"OnPlayerJoined",
s.Substring(66)
};
m_Lock.EnterWriteLock();
try
{
m_GameLog.Add(item);
}
finally
{
m_Lock.ExitWriteLock();
}
}
// 2019.07.31 22:29:31 Log - [NetworkManager] OnPlayerLeft pypy
else if (s.Length > 64 &&
string.Compare(s, 34, "[NetworkManager] OnPlayerLeft ", 0, "[NetworkManager] OnPlayerLeft ".Length, StringComparison.Ordinal) == 0)
{
var item = new[]
{
ConvertLogTimeToISO8601(s),
"OnPlayerLeft",
s.Substring(64)
};
m_Lock.EnterWriteLock();
try
{
m_GameLog.Add(item);
}
finally
{
m_Lock.ExitWriteLock();
}
}
}
}
try
{
Thread.Sleep(3000);
}
catch
{
// ThreadInterruptedException
}
}
});
m_Thread.Start();
}
}
public static void Stop()
{
var thread = m_Thread;
if (thread != null)
{
m_Thread = null;
try
{
thread.Interrupt();
thread.Join();
}
catch
{
}
}
}
private static string ConvertLogTimeToISO8601(string s)
{
// 2019.07.31 22:26:24
var dt = new DateTime(
int.Parse(s.Substring(0, 4)),
int.Parse(s.Substring(5, 2)),
int.Parse(s.Substring(8, 2)),
int.Parse(s.Substring(11, 2)),
int.Parse(s.Substring(14, 2)),
int.Parse(s.Substring(17, 2)),
DateTimeKind.Local
).ToUniversalTime();
return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'}";
}
private static void ParseLine(string line)
{
try
{
// 2019.07.31 22:26:24 Log - [RoomManager] Joining wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd:6974~private(usr_4f76a584-9d4b-46f6-8209-8305eb683661)~nonce(0000000000000000000000000000000000000000000000000000000000000000)
// 2019.07.31 22:26:24 Log - [RoomManager] Joining or Creating Room: VRChat Home
if (string.Compare(line, 34, "[RoomManager] Joining ", 0, "[RoomManager] Joining ".Length) == 0 &&
string.Compare(line, 56, "or ", 0, "or ".Length) != 0)
{
lock (m_GameLog)
{
m_GameLog.Add(new[]
{
ConvertLogTimeToISO8601(line),
"Location",
line.Substring(56)
});
}
}
// 2019.07.31 22:41:18 Log - [NetworkManager] OnPlayerJoined pypy
else if (string.Compare(line, 34, "[NetworkManager] OnPlayerJoined ", 0, "[NetworkManager] OnPlayerJoined ".Length) == 0)
{
lock (m_GameLog)
{
m_GameLog.Add(new[]
{
ConvertLogTimeToISO8601(line),
"OnPlayerJoined",
line.Substring(66)
});
}
}
// 2019.07.31 22:29:31 Log - [NetworkManager] OnPlayerLeft pypy
else if (string.Compare(line, 34, "[NetworkManager] OnPlayerLeft ", 0, "[NetworkManager] OnPlayerLeft ".Length) == 0)
{
lock (m_GameLog)
{
m_GameLog.Add(new[]
{
ConvertLogTimeToISO8601(line),
"OnPlayerLeft",
line.Substring(64)
});
}
position = stream.Position;
}
}
catch
@@ -182,31 +200,54 @@ namespace VRCX
}
}
private static string ConvertLogTimeToISO8601(string s)
{
// 2019.07.31 22:26:24
if (!DateTime.TryParseExact(s.Substring(0, 19),
"yyyy.MM.dd HH:mm:ss",
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal,
out DateTime dt))
{
dt = DateTime.UtcNow;
}
return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'}";
}
public void Reset()
{
lock (m_GameLog)
if (m_Thread != null)
{
m_Reset = true;
m_GameLog.Clear();
m_Thread.Interrupt();
}
m_Thread.Interrupt();
}
public string[][] GetLogs()
public string[][] Get()
{
lock (m_GameLog)
m_Lock.EnterUpgradeableReadLock();
try
{
if (m_Reset ||
m_GameLog.Count == 0)
{
return new string[][] { };
}
var array = m_GameLog.ToArray();
m_GameLog.Clear();
m_Lock.EnterWriteLock();
try
{
m_GameLog.Clear();
}
finally
{
m_Lock.ExitWriteLock();
}
return array;
}
}
public bool HasLog()
{
lock (m_GameLog)
finally
{
return m_GameLog.Count > 0;
m_Lock.ExitUpgradeableReadLock();
}
}
}

10
MainForm.Designer.cs generated
View File

@@ -33,16 +33,8 @@ namespace VRCX
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.timer = new System.Windows.Forms.Timer(this.components);
this.SuspendLayout();
//
// timer
//
this.timer.Enabled = true;
this.timer.Interval = 1000;
this.timer.Tick += new System.EventHandler(this.timer_Tick);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
@@ -56,7 +48,5 @@ namespace VRCX
}
#endregion
private System.Windows.Forms.Timer timer;
}
}

View File

@@ -43,7 +43,8 @@ namespace VRCX
};
Browser.JavascriptObjectRepository.Register("VRCX", new VRCX(), true, options);
Browser.JavascriptObjectRepository.Register("VRCXStorage", new VRCXStorage(), false, options);
Browser.JavascriptObjectRepository.Register("LogWatcher", new VRCX_LogWatcher(), true, options);
Browser.JavascriptObjectRepository.Register("SQLite", new SQLite(), true, options);
Browser.JavascriptObjectRepository.Register("LogWatcher", new LogWatcher(), true, options);
Browser.JavascriptObjectRepository.Register("Discord", new Discord(), true, options);
Browser.IsBrowserInitializedChanged += (A, B) =>
{
@@ -51,10 +52,5 @@ namespace VRCX
};
Controls.Add(Browser);
}
private void timer_Tick(object sender, System.EventArgs e)
{
Discord.Update();
}
}
}

View File

@@ -117,7 +117,4 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="timer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

View File

@@ -53,15 +53,19 @@ namespace VRCX
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
CpuMonitor.Start();
VRCXStorage.Load();
VRCXVR.Setup();
VRCX_LogWatcher.Start();
SQLite.Init();
CpuMonitor.Init();
Discord.Init();
LogWatcher.Init();
VRCXVR.Init();
Application.Run(new MainForm());
VRCX_LogWatcher.Stop();
VRCXVR.Stop();
VRCXVR.Exit();
LogWatcher.Exit();
Discord.Exit();
CpuMonitor.Exit();
SQLite.Exit();
VRCXStorage.Save();
CpuMonitor.Stop();
Cef.Shutdown();
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using CefSharp;
using CefSharp.Enums;
using CefSharp.OffScreen;
@@ -15,26 +16,32 @@ namespace VRCX
public class RenderHandler : IRenderHandler
{
private ChromiumWebBrowser m_Browser;
public readonly object BufferLock = new object();
private ReaderWriterLockSlim m_Lock;
public int BufferSize { get; private set; }
public GCHandle Buffer { get; private set; }
public int Width { get; private set; }
public int Height { get; private set; }
public RenderHandler(ChromiumWebBrowser browser)
public RenderHandler(ChromiumWebBrowser browser, ReaderWriterLockSlim @lock)
{
m_Browser = browser;
m_Lock = @lock;
}
public void Dispose()
{
lock (BufferLock)
m_Lock.EnterWriteLock();
try
{
if (Buffer.IsAllocated)
{
Buffer.Free();
}
}
finally
{
m_Lock.ExitWriteLock();
}
m_Browser = null;
}
@@ -64,23 +71,36 @@ namespace VRCX
{
if (type == PaintElementType.View)
{
lock (BufferLock)
m_Lock.EnterUpgradeableReadLock();
try
{
if (!Buffer.IsAllocated ||
width != Width ||
height != Height)
{
Width = width;
Height = height;
BufferSize = width * height * 4;
if (Buffer.IsAllocated)
m_Lock.EnterWriteLock();
try
{
Buffer.Free();
Width = width;
Height = height;
BufferSize = width * height * 4;
if (Buffer.IsAllocated)
{
Buffer.Free();
}
Buffer = GCHandle.Alloc(new byte[BufferSize], GCHandleType.Pinned);
}
finally
{
m_Lock.ExitWriteLock();
}
Buffer = GCHandle.Alloc(new byte[BufferSize], GCHandleType.Pinned);
}
WinApi.CopyMemory(Buffer.AddrOfPinnedObject(), buffer, (uint)BufferSize);
}
finally
{
m_Lock.ExitUpgradeableReadLock();
}
}
}

91
SQLite.cs Normal file
View File

@@ -0,0 +1,91 @@
using CefSharp;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLite;
using System.Threading;
using System.Windows.Forms;
namespace VRCX
{
public class SQLite
{
private static readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim();
private static SQLiteConnection m_Connection;
public static void Init()
{
m_Connection = new SQLiteConnection($"Data Source={Application.StartupPath}/VRCX.sqlite;Version=3");
m_Connection.Open();
}
public static void Exit()
{
m_Connection.Close();
m_Connection.Dispose();
}
public void Execute(string sql, IDictionary<string, object> param = null)
{
m_Lock.EnterWriteLock();
try
{
if (m_Connection.State != ConnectionState.Open)
{
m_Connection.Close();
m_Connection.Open();
}
using (var C = new SQLiteCommand(sql, m_Connection))
{
if (param != null)
{
foreach (var prop in param)
{
C.Parameters.Add(new SQLiteParameter("@" + prop.Key, prop.Value));
}
}
C.ExecuteNonQuery();
}
}
finally
{
m_Lock.ExitWriteLock();
}
}
public void ExecuteQuery(IJavascriptCallback callback, string sql, IDictionary<string, object> param = null)
{
m_Lock.EnterReadLock();
try
{
if (m_Connection.State != ConnectionState.Open)
{
m_Connection.Close();
m_Connection.Open();
}
using (var C = new SQLiteCommand(sql, m_Connection))
{
if (param != null)
{
foreach (var prop in param)
{
C.Parameters.Add(new SQLiteParameter("@" + prop.Key, prop.Value));
}
}
using (var R = C.ExecuteReader())
{
while (R.Read())
{
var row = new object[R.FieldCount];
R.GetValues(row);
callback.ExecuteAsync(row);
}
}
}
}
finally
{
m_Lock.ExitReadLock();
}
}
}
}

View File

@@ -69,12 +69,12 @@ namespace VRCX
public void StartVR()
{
VRCXVR.Start();
VRCXVR.SetActive(true);
}
public void StopVR()
{
VRCXVR.Stop();
VRCXVR.SetActive(false);
}
public void RefreshVR()

View File

@@ -110,6 +110,9 @@
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data.SQLite, Version=1.0.111.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=MSIL">
<HintPath>packages\System.Data.SQLite.Core.1.0.111.0\lib\net451\System.Data.SQLite.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
@@ -125,6 +128,7 @@
<Compile Include="CpuMonitor.cs" />
<Compile Include="Browser.cs" />
<Compile Include="RenderHandler.cs" />
<Compile Include="SQLite.cs" />
<Compile Include="VRForm.cs">
<SubType>Form</SubType>
</Compile>
@@ -204,8 +208,10 @@
<Error Condition="!Exists('packages\CefSharp.WinForms.73.1.130\build\CefSharp.WinForms.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.WinForms.73.1.130\build\CefSharp.WinForms.targets'))" />
<Error Condition="!Exists('packages\CefSharp.OffScreen.73.1.130\build\CefSharp.OffScreen.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.OffScreen.73.1.130\build\CefSharp.OffScreen.props'))" />
<Error Condition="!Exists('packages\CefSharp.OffScreen.73.1.130\build\CefSharp.OffScreen.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\CefSharp.OffScreen.73.1.130\build\CefSharp.OffScreen.targets'))" />
<Error Condition="!Exists('packages\System.Data.SQLite.Core.1.0.111.0\build\net451\System.Data.SQLite.Core.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\System.Data.SQLite.Core.1.0.111.0\build\net451\System.Data.SQLite.Core.targets'))" />
</Target>
<Import Project="packages\CefSharp.Common.73.1.130\build\CefSharp.Common.targets" Condition="Exists('packages\CefSharp.Common.73.1.130\build\CefSharp.Common.targets')" />
<Import Project="packages\CefSharp.WinForms.73.1.130\build\CefSharp.WinForms.targets" Condition="Exists('packages\CefSharp.WinForms.73.1.130\build\CefSharp.WinForms.targets')" />
<Import Project="packages\CefSharp.OffScreen.73.1.130\build\CefSharp.OffScreen.targets" Condition="Exists('packages\CefSharp.OffScreen.73.1.130\build\CefSharp.OffScreen.targets')" />
<Import Project="packages\System.Data.SQLite.Core.1.0.111.0\build\net451\System.Data.SQLite.Core.targets" Condition="Exists('packages\System.Data.SQLite.Core.1.0.111.0\build\net451\System.Data.SQLite.Core.targets')" />
</Project>

View File

@@ -4,71 +4,114 @@
// For a copy, see <https://opensource.org/licenses/MIT>.
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;
namespace VRCX
{
public class VRCXStorage
{
private static readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim();
private static Dictionary<string, string> m_Storage = new Dictionary<string, string>();
private static bool m_Dirty;
public static void Load()
{
JsonSerializer.Deserialize(Application.StartupPath + "/VRCX.json", ref m_Storage);
m_Lock.EnterWriteLock();
try
{
JsonSerializer.Deserialize(Application.StartupPath + "/VRCX.json", ref m_Storage);
m_Dirty = false;
}
finally
{
m_Lock.ExitWriteLock();
}
}
public static void Save()
{
JsonSerializer.Serialize(Application.StartupPath + "/VRCX.json", m_Storage);
}
public void Clear()
{
lock (m_Storage)
m_Lock.EnterReadLock();
try
{
m_Dirty = true;
m_Storage.Clear();
if (m_Dirty)
{
JsonSerializer.Serialize(Application.StartupPath + "/VRCX.json", m_Storage);
m_Dirty = false;
}
}
finally
{
m_Lock.ExitReadLock();
}
}
public void Flush()
{
lock (m_Storage)
Save();
}
public void Clear()
{
m_Lock.EnterWriteLock();
try
{
if (m_Dirty)
if (m_Storage.Count > 0)
{
m_Dirty = false;
Save();
m_Dirty = true;
}
m_Storage.Clear();
}
finally
{
m_Lock.ExitWriteLock();
}
}
public bool Remove(string key)
{
lock (m_Storage)
m_Lock.EnterWriteLock();
try
{
m_Dirty = true;
return m_Storage.Remove(key);
var result = m_Storage.Remove(key);
if (result)
{
m_Dirty = true;
}
return result;
}
finally
{
m_Lock.ExitWriteLock();
}
}
public string Get(string key)
{
lock (m_Storage)
m_Lock.EnterReadLock();
try
{
return m_Storage.TryGetValue(key, out string value)
? value
: string.Empty;
}
finally
{
m_Lock.ExitReadLock();
}
}
public void Set(string key, string value)
{
lock (m_Storage)
m_Lock.EnterWriteLock();
try
{
m_Dirty = true;
m_Storage[key] = value;
m_Dirty = true;
}
finally
{
m_Lock.ExitWriteLock();
}
}
}

415
VRCXVR.cs
View File

@@ -21,7 +21,7 @@ namespace VRCX
{
public static class VRCXVR
{
private static readonly object m_LockObject = new object();
private static readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim();
private static List<string[]> m_Devices = new List<string[]>();
private static Thread m_Thread;
private static Device m_Device;
@@ -29,6 +29,7 @@ namespace VRCX
private static Texture2D m_Texture2;
private static Browser m_Browser1;
private static Browser m_Browser2;
private static bool m_Active;
private static float[] m_Rotation = { 0f, 0f, 0f };
private static float[] m_Translation = { 0f, 0f, 0f };
private static float[] m_L_Translation = { -7f / 100f, -5f / 100f, 6f / 100f };
@@ -38,7 +39,7 @@ namespace VRCX
// NOTE
// 메모리 릭 때문에 미리 생성해놓고 계속 사용함
public static void Setup()
public static void Init()
{
m_Device = new Device(DriverType.Hardware, DeviceCreationFlags.SingleThreaded | DeviceCreationFlags.BgraSupport);
m_Texture1 = new Texture2D(m_Device, new Texture2DDescription()
@@ -67,38 +68,139 @@ namespace VRCX
});
m_Browser1 = new Browser(m_Texture1, Application.StartupPath + "/html/vr.html?1");
m_Browser2 = new Browser(m_Texture2, Application.StartupPath + "/html/vr.html?2");
}
public static void Start()
{
lock (m_LockObject)
m_Thread = new Thread(() =>
{
if (m_Thread == null)
var active = false;
var e = new VREvent_t();
var nextInit = DateTime.MinValue;
var nextDeviceUpdate = DateTime.MinValue;
var nextOverlay = DateTime.MinValue;
var overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
var overlayVisible1 = false;
var overlayVisible2 = false;
var dashboardHandle = 0UL;
var overlayHandle1 = 0UL;
var overlayHandle2 = 0UL;
while (m_Thread != null)
{
m_Thread = new Thread(ThreadProc);
m_Thread.Start();
}
}
}
public static void Stop()
{
lock (m_LockObject)
{
var thread = m_Thread;
if (thread != null)
{
m_Thread = null;
try
{
thread.Interrupt();
thread.Join();
Thread.Sleep(10);
}
catch
{
// ThreadInterruptedException
}
if (m_Active)
{
m_Browser1.Render();
m_Browser2.Render();
var system = OpenVR.System;
if (system == null)
{
if (DateTime.Now.CompareTo(nextInit) <= 0)
{
continue;
}
var _err = EVRInitError.None;
system = OpenVR.Init(ref _err, EVRApplicationType.VRApplication_Overlay);
nextInit = DateTime.Now.AddSeconds(5);
if (system == null)
{
continue;
}
active = true;
}
while (system.PollNextEvent(ref e, (uint)Marshal.SizeOf(e)))
{
var type = (EVREventType)e.eventType;
if (type == EVREventType.VREvent_Quit)
{
active = false;
OpenVR.Shutdown();
nextInit = DateTime.Now.AddSeconds(10);
system = null;
break;
}
}
if (system != null)
{
if (DateTime.Now.CompareTo(nextDeviceUpdate) >= 0)
{
overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
UpdateDevices(system, ref overlayIndex);
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
{
nextOverlay = DateTime.Now.AddSeconds(10);
}
nextDeviceUpdate = DateTime.Now.AddSeconds(0.1);
}
var overlay = OpenVR.Overlay;
if (overlay != null)
{
var dashboardVisible = overlay.IsDashboardVisible();
var err = ProcessDashboard(overlay, ref dashboardHandle, dashboardVisible);
if (err != EVROverlayError.None &&
dashboardHandle != 0)
{
overlay.DestroyOverlay(dashboardHandle);
dashboardHandle = 0;
}
err = ProcessOverlay1(overlay, ref overlayHandle1, ref overlayVisible1, dashboardVisible, overlayIndex, nextOverlay);
if (err != EVROverlayError.None &&
overlayHandle1 != 0)
{
overlay.DestroyOverlay(overlayHandle1);
overlayHandle1 = 0;
}
err = ProcessOverlay2(overlay, ref overlayHandle2, ref overlayVisible2, dashboardVisible);
if (err != EVROverlayError.None &&
overlayHandle2 != 0)
{
overlay.DestroyOverlay(overlayHandle2);
overlayHandle2 = 0;
}
}
}
}
else if (active)
{
active = false;
OpenVR.Shutdown();
m_Lock.EnterWriteLock();
try
{
m_Devices.Clear();
}
finally
{
m_Lock.ExitWriteLock();
}
}
}
}
})
{
IsBackground = true
};
m_Thread.Start();
}
public static void Exit()
{
var T = m_Thread;
m_Thread = null;
T.Interrupt();
T.Join();
m_Browser2.Dispose();
m_Browser1.Dispose();
m_Texture2.Dispose();
m_Texture1.Dispose();
m_Device.Dispose();
}
public static void SetActive(bool active)
{
m_Active = active;
}
public static void Refresh()
@@ -109,86 +211,105 @@ namespace VRCX
public static string[][] GetDevices()
{
lock (m_Devices)
m_Lock.EnterReadLock();
try
{
return m_Devices.ToArray();
}
finally
{
m_Lock.ExitReadLock();
}
}
private static void UpdateDevices(CVRSystem system, ref uint trackingIndex)
private static void UpdateDevices(CVRSystem system, ref uint overlayIndex)
{
lock (m_Devices)
m_Lock.EnterWriteLock();
try
{
m_Devices.Clear();
var sb = new StringBuilder(256);
var state = new VRControllerState_t();
for (var i = 0u; i < OpenVR.k_unMaxTrackedDeviceCount; ++i)
}
finally
{
m_Lock.ExitWriteLock();
}
var sb = new StringBuilder(256);
var state = new VRControllerState_t();
for (var i = 0u; i < OpenVR.k_unMaxTrackedDeviceCount; ++i)
{
var devClass = system.GetTrackedDeviceClass(i);
if (devClass == ETrackedDeviceClass.Controller ||
devClass == ETrackedDeviceClass.GenericTracker)
{
var devClass = system.GetTrackedDeviceClass(i);
if (devClass == ETrackedDeviceClass.Controller ||
devClass == ETrackedDeviceClass.GenericTracker)
var err = ETrackedPropertyError.TrackedProp_Success;
var batteryPercentage = system.GetFloatTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceBatteryPercentage_Float, ref err);
if (err != ETrackedPropertyError.TrackedProp_Success)
{
var err = ETrackedPropertyError.TrackedProp_Success;
var batteryPercentage = system.GetFloatTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceBatteryPercentage_Float, ref err);
if (err != ETrackedPropertyError.TrackedProp_Success)
{
batteryPercentage = 1f;
}
sb.Clear();
system.GetStringTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_TrackingSystemName_String, sb, (uint)sb.Capacity, ref err);
var isOculus = sb.ToString().IndexOf("oculus", StringComparison.OrdinalIgnoreCase) >= 0;
// Oculus : B/Y, Bit 1, Mask 2
// Oculus : A/X, Bit 7, Mask 128
// Vive : Menu, Bit 1, Mask 2,
// Vive : Grip, Bit 2, Mask 4
var role = system.GetControllerRoleForTrackedDeviceIndex(i);
if (role == ETrackedControllerRole.LeftHand ||
role == ETrackedControllerRole.RightHand)
{
if (system.GetControllerState(i, ref state, (uint)Marshal.SizeOf(state)) &&
(state.ulButtonPressed & (isOculus ? 2u : 4u)) != 0)
{
if (role == ETrackedControllerRole.LeftHand)
{
Array.Copy(m_L_Translation, m_Translation, 3);
Array.Copy(m_L_Rotation, m_Rotation, 3);
}
else
{
Array.Copy(m_R_Translation, m_Translation, 3);
Array.Copy(m_R_Rotation, m_Rotation, 3);
}
trackingIndex = i;
}
}
var type = string.Empty;
if (devClass == ETrackedDeviceClass.Controller)
batteryPercentage = 1f;
}
sb.Clear();
system.GetStringTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_TrackingSystemName_String, sb, (uint)sb.Capacity, ref err);
var isOculus = sb.ToString().IndexOf("oculus", StringComparison.OrdinalIgnoreCase) >= 0;
// Oculus : B/Y, Bit 1, Mask 2
// Oculus : A/X, Bit 7, Mask 128
// Vive : Menu, Bit 1, Mask 2,
// Vive : Grip, Bit 2, Mask 4
var role = system.GetControllerRoleForTrackedDeviceIndex(i);
if (role == ETrackedControllerRole.LeftHand ||
role == ETrackedControllerRole.RightHand)
{
if (system.GetControllerState(i, ref state, (uint)Marshal.SizeOf(state)) &&
(state.ulButtonPressed & (isOculus ? 2u : 4u)) != 0)
{
if (role == ETrackedControllerRole.LeftHand)
{
type = "leftController";
}
else if (role == ETrackedControllerRole.RightHand)
{
type = "rightController";
Array.Copy(m_L_Translation, m_Translation, 3);
Array.Copy(m_L_Rotation, m_Rotation, 3);
}
else
{
type = "controller";
Array.Copy(m_R_Translation, m_Translation, 3);
Array.Copy(m_R_Rotation, m_Rotation, 3);
}
overlayIndex = i;
}
else if (devClass == ETrackedDeviceClass.GenericTracker)
}
var type = string.Empty;
if (devClass == ETrackedDeviceClass.Controller)
{
if (role == ETrackedControllerRole.LeftHand)
{
type = "tracker";
type = "leftController";
}
m_Devices.Add(new[]
else if (role == ETrackedControllerRole.RightHand)
{
type,
system.IsTrackedDeviceConnected(i)
? "connected"
: "disconnected",
(batteryPercentage * 100).ToString()
});
type = "rightController";
}
else
{
type = "controller";
}
}
else if (devClass == ETrackedDeviceClass.GenericTracker)
{
type = "tracker";
}
var item = new[]
{
type,
system.IsTrackedDeviceConnected(i)
? "connected"
: "disconnected",
(batteryPercentage * 100).ToString()
};
m_Lock.EnterWriteLock();
try
{
m_Devices.Add(item);
}
finally
{
m_Lock.ExitWriteLock();
}
}
}
@@ -267,7 +388,7 @@ namespace VRCX
return err;
}
private static EVROverlayError ProcessOverlay1(CVROverlay overlay, ref ulong overlayHandle, ref bool overlayVisible, bool dashboardVisible, uint trackingIndex, DateTime nextRender)
private static EVROverlayError ProcessOverlay1(CVROverlay overlay, ref ulong overlayHandle, ref bool overlayVisible, bool dashboardVisible, uint overlayIndex, DateTime nextOverlay)
{
var err = EVROverlayError.None;
@@ -304,7 +425,7 @@ namespace VRCX
}
}
if (trackingIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
{
// http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices
// Scaling-Rotation-Translation
@@ -328,7 +449,7 @@ namespace VRCX
m10 = m.M33,
m11 = m.M43,
};
err = overlay.SetOverlayTransformTrackedDeviceRelative(overlayHandle, trackingIndex, ref hm34);
err = overlay.SetOverlayTransformTrackedDeviceRelative(overlayHandle, overlayIndex, ref hm34);
if (err != EVROverlayError.None)
{
return err;
@@ -336,7 +457,7 @@ namespace VRCX
}
if (!dashboardVisible &&
DateTime.Now.CompareTo(nextRender) <= 0)
DateTime.Now.CompareTo(nextOverlay) <= 0)
{
var texture = new Texture_t
{
@@ -462,123 +583,5 @@ namespace VRCX
return err;
}
private static void ThreadProc()
{
var e = new VREvent_t();
var nextOpenVRInit = DateTime.MinValue;
var nextDeviceInfoUpdate = DateTime.MinValue;
var nextRender = DateTime.MinValue;
var trackingIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
var overlayVisible1 = false;
var overlayVisible2 = false;
var dashboardHandle = 0UL;
var overlayHandle1 = 0UL;
var overlayHandle2 = 0UL;
while (m_Thread != null)
{
m_Browser1.Render();
m_Browser2.Render();
try
{
Thread.Sleep(10);
}
catch
{
// ThreadInterruptedException
}
var system = OpenVR.System;
if (system == null)
{
if (DateTime.Now.CompareTo(nextOpenVRInit) < 0)
{
continue;
}
var _err = EVRInitError.None;
system = OpenVR.Init(ref _err, EVRApplicationType.VRApplication_Overlay);
nextOpenVRInit = DateTime.Now.AddSeconds(5);
if (system == null)
{
continue;
}
}
while (system.PollNextEvent(ref e, (uint)Marshal.SizeOf(e)))
{
var type = (EVREventType)e.eventType;
if (type == EVREventType.VREvent_Quit)
{
OpenVR.Shutdown();
// VRChat이 실행 중일 때만 켜는 옵션이 생겨서 시간을 줄임
nextOpenVRInit = DateTime.Now.AddSeconds(10);
system = null;
break;
}
}
if (system == null)
{
continue;
}
if (DateTime.Now.CompareTo(nextDeviceInfoUpdate) >= 0)
{
trackingIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
UpdateDevices(system, ref trackingIndex);
if (trackingIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
{
nextRender = DateTime.Now.AddSeconds(10);
}
nextDeviceInfoUpdate = DateTime.Now.AddSeconds(0.1);
}
var overlay = OpenVR.Overlay;
if (overlay == null)
{
continue;
}
var dashboardVisible = overlay.IsDashboardVisible();
var err = ProcessDashboard(overlay, ref dashboardHandle, dashboardVisible);
if (err != EVROverlayError.None &&
dashboardHandle != 0)
{
overlay.DestroyOverlay(dashboardHandle);
dashboardHandle = 0;
}
err = ProcessOverlay1(overlay, ref overlayHandle1, ref overlayVisible1, dashboardVisible, trackingIndex, nextRender);
if (err != EVROverlayError.None &&
overlayHandle1 != 0)
{
overlay.DestroyOverlay(overlayHandle1);
overlayHandle1 = 0;
}
err = ProcessOverlay2(overlay, ref overlayHandle2, ref overlayVisible2, dashboardVisible);
if (err != EVROverlayError.None &&
overlayHandle2 != 0)
{
overlay.DestroyOverlay(overlayHandle2);
overlayHandle2 = 0;
}
}
lock (m_Devices)
{
m_Devices.Clear();
}
OpenVR.Shutdown();
}
}
}

View File

@@ -44,8 +44,10 @@ namespace VRCX
};
Browser1.JavascriptObjectRepository.Register("VRCX", new VRCX(), true, options);
Browser1.JavascriptObjectRepository.Register("VRCXStorage", new VRCXStorage(), false, options);
Browser1.JavascriptObjectRepository.Register("SQLite", new SQLite(), true, options);
Browser2.JavascriptObjectRepository.Register("VRCX", new VRCX(), true, options);
Browser2.JavascriptObjectRepository.Register("VRCXStorage", new VRCXStorage(), false, options);
Browser2.JavascriptObjectRepository.Register("SQLite", new SQLite(), true, options);
Browser1.IsBrowserInitializedChanged += (A, B) =>
{
// Browser1.ShowDevTools();
@@ -60,9 +62,9 @@ namespace VRCX
private void button_refresh_Click(object sender, System.EventArgs e)
{
VRCXVR.Refresh();
Browser1.ExecuteScriptAsync("location.reload()");
Browser2.ExecuteScriptAsync("location.reload()");
VRCXVR.Refresh();
}
private void button_devtools_Click(object sender, System.EventArgs e)

View File

@@ -9,6 +9,7 @@ module.exports = {
'CefSharp': 'readonly',
'VRCX': 'readonly',
'VRCXStorage': 'readonly',
'SQLite': 'readonly',
'LogWatcher': 'readonly',
'Discord': 'readonly',
'Noty': 'readonly',

View File

@@ -7,6 +7,7 @@ if (window.CefSharp) {
Promise.all([
CefSharp.BindObjectAsync('VRCX'),
CefSharp.BindObjectAsync('VRCXStorage'),
CefSharp.BindObjectAsync('SQLite'),
CefSharp.BindObjectAsync('LogWatcher'),
CefSharp.BindObjectAsync('Discord')
]).catch(() => {
@@ -3490,6 +3491,8 @@ if (window.CefSharp) {
var ref;
var i;
var j;
// FIXME
// 여러 개 켠다면 gameLogTable의 데이터가 시간순이 아닐 수도 있음
i = this.gameLogTable.data.length;
j = 0;
while (j < 25) {
@@ -4460,31 +4463,27 @@ if (window.CefSharp) {
};
$app.methods.refreshGameLog = function () {
LogWatcher.HasLog().then((result) => {
if (result) {
LogWatcher.GetLogs().then((logs) => {
logs.forEach((log) => {
var ctx = {
created_at: log[0],
type: log[1],
data: log[2]
};
this.gameLogTable.data.push(ctx);
if (ctx.type === 'Location') {
this.lastLocation = ctx.data;
}
});
this.sweepGameLog();
this.updateSharedFeed();
// sweepGameLog로 기록이 삭제되면
// 아무 것도 없는데 알림이 떠서 이상함
if (this.gameLogTable.length) {
this.notifyMenu('gameLog');
LogWatcher.Get().then((logs) => {
if (logs.length) {
logs.forEach((log) => {
var ctx = {
created_at: log[0],
type: log[1],
data: log[2]
};
this.gameLogTable.data.push(ctx);
if (ctx.type === 'Location') {
this.lastLocation = ctx.data;
}
});
} else {
this.updateSharedFeed();
this.sweepGameLog();
// sweepGameLog로 기록이 삭제되면
// 아무 것도 없는데 알림이 떠서 이상함
if (this.gameLogTable.length) {
this.notifyMenu('gameLog');
}
}
this.updateSharedFeed();
});
};

View File

@@ -6,7 +6,8 @@
if (window.CefSharp) {
Promise.all([
CefSharp.BindObjectAsync('VRCX'),
CefSharp.BindObjectAsync('VRCXStorage')
CefSharp.BindObjectAsync('VRCXStorage'),
CefSharp.BindObjectAsync('SQLite')
]).catch(() => {
location = 'https://github.com/pypy-vrc/vrcx';
}).then(() => {

View File

@@ -14,4 +14,5 @@
<package id="SharpDX.Direct3D11" version="4.2.0" targetFramework="net452" />
<package id="SharpDX.DXGI" version="4.2.0" targetFramework="net452" />
<package id="SharpDX.Mathematics" version="4.2.0" targetFramework="net452" />
<package id="System.Data.SQLite.Core" version="1.0.111.0" targetFramework="net452" />
</packages>