diff --git a/DaS-PC-MPChan/Config.vb b/DaS-PC-MPChan/Config.vb index b3fc51e..63ae251 100644 --- a/DaS-PC-MPChan/Config.vb +++ b/DaS-PC-MPChan/Config.vb @@ -1,4 +1,10 @@ Module Config + 'How frequently should we update the node list from the central server? + Public Const UpdateNetNodesInterval = 120 * 1000 + + 'How frequently should we publish our nodes to the central server? + Public Const PublishNodesInterval = 60 * 1000 + 'How frequently should we connect to node from IRC? Public Const IRCNodeConnectInterval = 10 * 1000 diff --git a/DaS-PC-MPChan/DSCM.vbproj b/DaS-PC-MPChan/DSCM.vbproj index 965cb81..70748c6 100644 --- a/DaS-PC-MPChan/DSCM.vbproj +++ b/DaS-PC-MPChan/DSCM.vbproj @@ -81,6 +81,8 @@ + + @@ -111,7 +113,7 @@ Form - + True @@ -192,7 +194,9 @@ - + + + $(SolutionDir)\packages\CommonMark.NET.0.12.0\tools\cmark.exe "$(SolutionDir)\Readme.md" --out $(ProjectDir)Resources\Readme_compiled.html diff --git a/DaS-PC-MPChan/DSNode.vb b/DaS-PC-MPChan/DSNode.vb index 913002b..0c9272d 100644 --- a/DaS-PC-MPChan/DSNode.vb +++ b/DaS-PC-MPChan/DSNode.vb @@ -196,4 +196,4 @@ Public Class DSNode Return (SoulLevel - (50 + SoulLevel * 0.2) < other.SoulLevel And other.SoulLevel < SoulLevel + (10 + SoulLevel * 0.1)) End Function -End Class +End Class \ No newline at end of file diff --git a/DaS-PC-MPChan/IRCClient.vb b/DaS-PC-MPChan/IRCClient.vb deleted file mode 100644 index bea1b60..0000000 --- a/DaS-PC-MPChan/IRCClient.vb +++ /dev/null @@ -1,242 +0,0 @@ -Imports System.Threading -Imports System.IO -Imports System.Net.Sockets -Imports System.Collections.Concurrent -Imports System.Text.RegularExpressions - -Class IRCConnectionError - Inherits System.ApplicationException - - Sub New(message As String) - MyBase.New(message) - End Sub -End Class - -Public Class IRCClient - Private mainWindow As MainWindow - - Private _thread As Thread - Private tcpClient As System.Net.Sockets.TcpClient - Private stream As NetworkStream - Private _streamWriter As StreamWriter = Nothing - Private _streamReader As StreamReader = Nothing - - Private shouldQuit As Boolean = False - - Private localNodesLock As New Object() - Private localNodes As New List(Of DSNode) - Private selfNode As DSNode - Public ircNodes As New ConcurrentDictionary(Of String, Tuple(Of DSNode, Date)) - - Public Sub New(mainWindow As MainWindow) - Me.mainWindow = mainWindow - _thread = New Thread(AddressOf main) - _thread.IsBackground = True - _thread.Start() - End Sub - - Public Sub setLocalNodes(self As DSNode, nodes As IEnumerable(Of DSNode)) - SyncLock localNodesLock - localNodes = nodes.ToList() - selfNode = self - End SyncLock - End Sub - - Public Sub Shutdown() - shouldQuit = True - End Sub - - Private Sub main(args As String()) - Try - While Not shouldQuit - Try - setStatus("Initiating connection ...") - connectToServer() - setStatus("Connected.") - messageLoop() - Catch ex As IRCConnectionError - If Not shouldQuit Then - setStatus("Error: " & ex.Message & " – retrying in 10 seconds") - Thread.Sleep(10000) - End If - End Try - If tcpClient IsNot Nothing Then - tcpClient.Close() - tcpClient = Nothing - End If - End While - setStatus("Disconnected.") - Catch ex As Exception - setStatus("DSCMNet crashed with: " & ex.Message) - End Try - End Sub - - Private Sub connectToServer() - Dim nick As String = Config.IRCNick & "[" & mainWindow.Version.Replace(".", "-") & "]" & Guid.NewGuid.ToString().Split("-")(0) - - tcpClient = New System.Net.Sockets.TcpClient() - tcpClient.Connect(Config.IRCHost, Config.IRCPort) - If Not tcpClient.Connected Then - Throw New IRCConnectionError("Failed to connect") - End If - stream = tcpClient.GetStream() - _streamReader = New StreamReader(stream) - _streamWriter = New StreamWriter(stream) - _streamWriter.AutoFlush = True - - _streamWriter.Write("USER " & nick & " 0 * :" & Config.IRCOwner & vbCr & vbLf) - _streamWriter.Write("NICK " & nick & vbCr & vbLf) - _streamWriter.Write("MODE " & nick & " +B" & vbCr & vbLf) - - 'Join channel on start - While True - If isConnectionDropped() Then - Throw New IRCConnectionError("Connection was dropped during init") - End If - Dim buf As String = _streamReader.ReadLine() - If buf IsNot Nothing Then - If buf.StartsWith("PING ") Then - _streamWriter.Write(buf.Replace("PING", "PONG") & vbCr & vbLf) - ElseIf buf.Contains(":MOTD") Then - _streamWriter.Write("JOIN " & Config.IRCChannel & vbCr & vbLf) - Exit While - End If - End If - End While - End Sub - Private Sub messageLoop() - Dim lastPublish As Date = DateTime.UtcNow - While True - If shouldQuit Then - _streamWriter.Write("QUIT" & vbCr & vbLf) - Exit While - ElseIf isConnectionDropped() Then - Throw New IRCConnectionError("Connection was dropped") - ElseIf (DateTime.UtcNow - lastPublish).TotalSeconds >= Config.IRCPublishInterval Then - lastPublish = DateTime.UtcNow - publishLocalNodes() - expireIrcNodes() - ElseIf stream.DataAvailable Then - handleIRCLine(_streamReader.ReadLine()) - Else - Thread.Sleep(50) - End If - End While - End Sub - Private Function isConnectionDropped() As Boolean - Return tcpClient.Client.Poll(0, SelectMode.SelectRead) AndAlso tcpClient.Client.Available = 0 - End Function - Private Sub handleIRCLine(line As String) - Dim prefix As String = Nothing - If line.StartsWith(":") Then - prefix = line.Split({" "c}, 2)(0).Substring(1) - line = line.Split({" "c}, 2)(1) - End If - Dim parts = line.Split({" "c}, 2) - Dim command As String = parts(0) - Dim rest As String = parts.ElementAtOrDefault(1) - - If command = "PING" Then - _streamWriter.Write("PONG " & rest & vbCr & vbLf) - ElseIf command = "PRIVMSG" Then - Dim msg As String = rest.Split({" "c}, 2)(1).Substring(1) - If msg.StartsWith("REPORT|") Or msg.StartsWith("REPORTSELF|") Then - Dim inNode As DSNode - Try - inNode = parseNodeReport(msg.Split("|")(1)) - Catch ex As Exception -#If DEBUG Then - Dim senderNick As String = Regex.Match(prefix, "^([^!@]+)").Groups.Item(1).Value - setStatus("Invalid: " & senderNick & " " & msg) -#End If - Return - End Try - If (ircNodes.ContainsKey(inNode.SteamId) AndAlso - Not inNode.HasExtendedInfo AndAlso - ircNodes(inNode.SteamId).Item1.HasExtendedInfo) Then - inNode.Covenant = ircNodes(inNode.SteamId).Item1.Covenant - inNode.Indictments = ircNodes(inNode.SteamId).Item1.Indictments - End If - ircNodes(inNode.SteamId) = Tuple.Create(inNode, DateTime.UtcNow) - End If - End If - End Sub - - Private Function parseNodeReport(text) As DSNode - Dim node As New DSNode() - Dim tmpFields() - - tmpFields = text.Split(",") - - node.CharacterName = tmpFields(0) - node.SteamId = tmpFields(1) - node.SoulLevel = tmpFields(2) - node.PhantomType = tmpFields(3) - node.MPZone = tmpFields(4) - node.World = tmpFields(5) - - If tmpFields.Count > 6 Then - node.Covenant = tmpFields(6) - node.Indictments = tmpFields(7) - End If - - Return node - End Function - Private Sub publishLocalNodes() - 'Report your active node status - Try - SyncLock localNodesLock - For Each node In localNodes - 'Check if node was already reported in the last 3 minutes - Dim networkKnowsNode = ( - ircNodes.ContainsKey(node.SteamId) AndAlso - (DateTime.UtcNow - ircNodes(node.SteamId).Item2).TotalSeconds <= IRCNodePublishInterval) - If networkKnowsNode Then - 'Publish anyways if the information in the network is incorrect - Dim networkInfoIsStale = ( - Not ircNodes(node.SteamId).Item1.BasicEquals(node) AndAlso - (DateTime.UtcNow - ircNodes(node.SteamId).Item2).TotalSeconds >= IRCNodeUpdateInterval) - If Not networkInfoIsStale Then Continue For - End If - - Dim ircName As String = node.CharacterName - ircName = ircName.Replace(",", "") - ircName = ircName.Replace("|", "") - Dim reportData As String = ( - ircName & "," & node.SteamId & "," & node.SoulLevel & "," & - node.PhantomType & "," & node.MPZone & "," & node.World) - _streamWriter.Write("PRIVMSG #DSCM-Main REPORT|" & reportData & vbCr & vbLf) - Next - If selfNode IsNot Nothing Then - Dim ircName As String = selfNode.CharacterName - ircName = ircName.Replace(",", "") - ircName = ircName.Replace("|", "") - Dim reportData As String = ( - ircName & "," & selfNode.SteamId & "," & selfNode.SoulLevel & "," & - selfNode.PhantomType & "," & selfNode.MPZone & "," & selfNode.World & "," & - selfNode.Covenant & "," & selfNode.Indictments) - _streamWriter.Write("PRIVMSG #DSCM-Main REPORTSELF|" & reportData & vbCr & vbLf) - Console.WriteLine(reportData) - End If - End SyncLock - Catch ex As Exception - setStatus("Error publishing local nodes: " & ex.Message) - End Try - End Sub - Private Sub expireIrcNodes() - Dim now As Date = DateTime.UtcNow - For Each t In ircNodes.Values.ToList() - If (now - t.Item2).TotalSeconds >= Config.IRCNodeTTL Then - ircNodes.TryRemove(t.Item1.SteamId, t) - End If - Next - End Sub - Private Sub setStatus(status As String) - If mainWindow.InvokeRequired Then - mainWindow.Invoke(New setStatusDelegate(AddressOf setStatus), {status}) - Else - mainWindow.txtIRCDebug.Text = status - End If - End Sub - Private Delegate Sub setStatusDelegate(status As String) -End Class \ No newline at end of file diff --git a/DaS-PC-MPChan/MainWindow.vb b/DaS-PC-MPChan/MainWindow.vb index 2e41547..9046bef 100644 --- a/DaS-PC-MPChan/MainWindow.vb +++ b/DaS-PC-MPChan/MainWindow.vb @@ -9,8 +9,10 @@ Public Class MainWindow 'Timers Private WithEvents updateActiveNodesTimer As New System.Windows.Forms.Timer() Private WithEvents updateUITimer As New System.Windows.Forms.Timer() + Private WithEvents updateNetNodesTimer As New System.Windows.Forms.Timer() Private WithEvents updateOnlineStateTimer As New System.Windows.Forms.Timer() - Private WithEvents ircNodeConnectTimer As New System.Windows.Forms.Timer() + Private WithEvents netNodeConnectTimer As New System.Windows.Forms.Timer() + Private WithEvents publishNodesTimer As New System.Windows.Forms.Timer() Private WithEvents dsAttachmentTimer As New System.Windows.Forms.Timer() Private WithEvents hotkeyTimer As New System.Windows.Forms.Timer() @@ -25,7 +27,7 @@ Public Class MainWindow Public Version As String Private dsProcess As DarkSoulsProcess = Nothing - Private _ircClient As IRCClient = Nothing + Private _netClient As NetClient = Nothing Private ircDisplayList As New DSNodeBindingList() Private activeNodesDisplayList As New DSNodeBindingList() Private connectedNodes As New Dictionary(Of String, ConnectedNode) @@ -79,7 +81,9 @@ Public Class MainWindow dsAttachmentTimer.Start() updateOnlineStateTimer.Interval = Config.OnlineCheckInterval updateOnlineStateTimer.Start() - ircNodeConnectTimer.Interval = Config.IRCNodeConnectInterval + updateNetNodesTimer.Interval = Config.UpdateNetNodesInterval + netNodeConnectTimer.Interval = Config.IRCNodeConnectInterval + publishNodesTimer.Interval = Config.PublishNodesInterval attachDSProcess() @@ -356,8 +360,8 @@ Public Class MainWindow Me.Close() End If End Sub - Private Sub connectToIRCNode() Handles ircNodeConnectTimer.Tick - If (_ircClient Is Nothing OrElse + Private Sub connectToIRCNode() Handles netNodeConnectTimer.Tick + If (_netClient Is Nothing OrElse dsProcess Is Nothing OrElse dsProcess.SelfSteamId = "" OrElse dsProcess.SelfNode.CharacterName = "" OrElse @@ -384,8 +388,7 @@ Public Class MainWindow Next Dim candidates As New List(Of DSNode) - For Each t In _ircClient.ircNodes.Values - Dim node As DSNode = t.Item1 + For Each node In _netClient.netNodes.Values If blackSet.Contains(node.SteamId) Then Continue For candidates.Add(node) Next @@ -492,7 +495,7 @@ Public Class MainWindow Return 2 End Function Private Sub handleDisconnects() - If _ircClient Is Nothing Or dsProcess Is Nothing Then Return + If _netClient Is Nothing Or dsProcess Is Nothing Then Return If dsProcess.SelfNode.PhantomType = -1 Then Return Dim now As Date = Date.UtcNow @@ -565,14 +568,22 @@ Public Class MainWindow txtCurrNodes.Text = dsProcess.NodeCount End If - If _ircClient IsNot Nothing Then - ircDisplayList.SyncWithDict(_ircClient.ircNodes, Function(x) x.Item1, dgvDSCMNet) - End If If Not tabDSCMNet.Text = "DSCM-Net (" & dgvDSCMNet.Rows.Count & ")" Then tabDSCMNet.Text = "DSCM-Net (" & dgvDSCMNet.Rows.Count & ")" End If End Sub + Private Async Sub updateNetNodes() Handles updateNetNodesTimer.Tick + If _netClient IsNot Nothing Then + Await _netClient.loadNodes() + ircDisplayList.SyncWithDict(_netClient.netNodes, dgvDSCMNet) + End If + End Sub + Private Async Sub publishNodes() Handles publishNodesTimer.Tick + If _netClient IsNot Nothing AndAlso dsProcess IsNot Nothing AndAlso dsProcess.SelfNode.SteamId IsNot Nothing Then + Await _netClient.publishLocalNodes(dsProcess.SelfNode, dsProcess.ConnectedNodes.Values()) + End If + End Sub Private Shared Sub hotkeyTimer_Tick() Handles hotkeyTimer.Tick Dim ctrlkey As Boolean Dim oneKey As Boolean 'Toggle Node Display @@ -642,10 +653,7 @@ Public Class MainWindow selfNode = dsProcess.SelfNode.Clone() Else connectedNodes.Clear() - End If - If _ircClient IsNot Nothing Then - _ircClient.setLocalNodes(selfNode, connectedNodes.Select(Function(kv, i) kv.Value.node)) End If Dim activeNodes = connectedNodes.ToDictionary(Function(kv) kv.Key, Function(kv) kv.Value.node) @@ -884,14 +892,17 @@ Public Class MainWindow key.SetValue("JoinDSCM-Net", chkDSCMNet.Checked) If chkDSCMNet.Checked Then - _ircClient = New IRCClient(Me) - ircNodeConnectTimer.Start() + _netClient = New NetClient(Me) + netNodeConnectTimer.Start() + updateNetNodesTimer.Start() + publishNodesTimer.Start() + updateNetNodes() Else - If _ircClient IsNot Nothing Then - ircNodeConnectTimer.Stop() - _ircClient.Shutdown() - _ircClient = Nothing - ircDisplayList.Clear() + If _netClient IsNot Nothing Then + updateNetNodesTimer.Stop() + netNodeConnectTimer.Stop() + publishNodesTimer.Stop() + _netClient = Nothing End If End If End Sub diff --git a/DaS-PC-MPChan/NetClient.vb b/DaS-PC-MPChan/NetClient.vb new file mode 100644 index 0000000..f61f28c --- /dev/null +++ b/DaS-PC-MPChan/NetClient.vb @@ -0,0 +1,111 @@ +Imports System.Threading +Imports System.IO +Imports System.Net.Sockets +Imports System.Text.RegularExpressions +Imports System.Web.Script.Serialization +Imports System.Net.Http + +Public Class NetClient + Private mainWindow As MainWindow + Public netNodes As New Dictionary(Of String, DSNode) + Private client As HttpClient + + Public Sub New(mainWindow As MainWindow) + Dim handler = New HttpClientHandler() + handler.AutomaticDecompression = Net.DecompressionMethods.GZip Or Net.DecompressionMethods.Deflate + client = New HttpClient(handler) + Me.mainWindow = mainWindow + End Sub + Private Function JSONContent(data As Object) As StringContent + Dim serializer As New JavaScriptSerializer() + serializer.RegisterConverters({New DSNodeSerializer()}) + Dim str As String = serializer.Serialize(data) + Dim content As New StringContent(str, New System.Text.UTF8Encoding()) + content.Headers.ContentType = New Headers.MediaTypeHeaderValue("application/json") + Return content + End Function + Public Async Function publishLocalNodes(self As DSNode, nodes As IEnumerable(Of DSNode)) As Task + Dim data As New Dictionary(Of String, Object)() From { + {"self", self}, + {"nodes", nodes} + } + Dim content = JSONContent(data) + Try + Dim response As HttpResponseMessage = Await client.PostAsync("http://dscm-net.chronial.de:8811/store", content) + response.EnsureSuccessStatusCode() + Catch ex As Exception + setStatus("Error publishing local nodes: " & ex.Message) + End Try + End Function + Public Async Function loadNodes() As Task(Of IEnumerable(Of DSNode)) + Try + Dim response As HttpResponseMessage = Await client.GetAsync("http://dscm-net.chronial.de:8811/list") + response.EnsureSuccessStatusCode() + Dim content = Await response.Content.ReadAsStringAsync() + Dim serializer As New JavaScriptSerializer() + Dim dsNodeSer As New DSNodeSerializer() + Dim data As IDictionary(Of String, Object) = serializer.Deserialize(Of Dictionary(Of String, Object))(content) + Dim nodes As IEnumerable = data("nodes") + netNodes = New Dictionary(Of String, DSNode) + For Each n As Dictionary(Of String, Object) In nodes + Dim node As DSNode = dsNodeSer.NodeFromDict(n) + netNodes(node.SteamId) = node + Next + Catch ex As Exception + setStatus("Error loading node list: " & ex.Message) + End Try + Return Nothing + End Function + Private Sub setStatus(status As String) + If mainWindow.InvokeRequired Then + mainWindow.Invoke(New setStatusDelegate(AddressOf setStatus), {status}) + Else + mainWindow.txtIRCDebug.Text = status + End If + End Sub + Private Delegate Sub setStatusDelegate(status As String) +End Class + + + +Public Class DSNodeSerializer + Inherits JavaScriptConverter + + Public Function NodeFromDict(dict As IDictionary(Of String, Object)) As DSNode + Dim node As New DSNode() + node.SteamId = dict("steamid") + node.CharacterName = dict("name") + node.SoulLevel = dict("sl") + node.PhantomType = dict("phantom_type") + node.MPZone = dict("mp_zone") + node.World = dict("world") + If dict.ContainsKey("covenant") Then + node.Covenant = dict("covenant") + node.Indictments = dict("indictments") + End If + Return node + End Function + Public Overrides Function Deserialize(dict As IDictionary(Of String, Object), type As Type, serializer As JavaScriptSerializer) As Object + Return NodeFromDict(dict) + End Function + Public Overrides Function Serialize(obj As Object, serializer As JavaScriptSerializer) As IDictionary(Of String, Object) + Dim node As DSNode = CType(obj, DSNode) + Dim out As New Dictionary(Of String, Object) + out("steamid") = node.SteamId + out("name") = node.CharacterName + out("sl") = node.SoulLevel + out("phantom_type") = node.PhantomType + out("mp_zone") = node.MPZone + out("world") = node.World + If node.HasExtendedInfo Then + out("covenant") = node.Covenant + out("indictments") = node.Indictments + End If + Return out + End Function + Public Overrides ReadOnly Property SupportedTypes As IEnumerable(Of Type) + Get + Return New List(Of Type)({GetType(DSNode)}) + End Get + End Property +End Class \ No newline at end of file