| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- Imports System.Net.Sockets
- Public Class PageLinkIoi
- Public Const RequestVersion As Integer = 4
- Public Const IoiVersion As Integer = 10 '由于已关闭更新渠道,在提升 IoiVersion 时必须提升 RequestVersion
- Public Shared PathIoi As String = PathAppdata & "联机模块\IOI 联机模块.exe"
- #Region "进程管理"
- Private Shared IoiId As String, IoiPassword As String
- Private Shared IoiProcess As Process = Nothing
- Private Shared IoiState As LoadState = LoadState.Waiting
- ''' <summary>
- ''' 若 Ioi 正在运行,则结束 Ioi 进程,同时初始化状态数据。返回是否关闭了对应进程。
- ''' </summary>
- Public Shared Function IoiStop(SleepWhenKilled As Boolean) As Boolean
- Return False
- End Function
- ''' <summary>
- ''' 启动 Ioi,并等待初始化完成后退出运行,同时更新 IoiId 与 IoiPassword。
- ''' 正常初始化返回 True,需要更新返回 False,其余情况抛出异常。
- ''' 若 Ioi 正在运行,则会先停止其运行。
- ''' </summary>
- Public Shared Function IoiStart() As Boolean
- Return False
- End Function
- 'Ioi 日志
- Private Shared Sub IoiLogLine(Content As String)
- End Sub
- Private Shared LogLinesCount As Integer = 0
- Private Shared LastPortsId As String = "" '上一个收到 portssub 的 ID,用于记录端口
- #End Region
- #Region "时钟"
- 'UI 线程刷新
- Private Shared UserListIdentifyCache As String = ""
- Private Shared RoomListIdentifyCache As String = ""
- Public Sub RefreshUi()
- End Sub
- '工作线程刷新
- Public Sub RefreshWorker()
- End Sub
- #End Region
- #Region "发送请求"
- ''' <summary>
- ''' 发送 Portsub 请求并等待获取控制台端口。进度将从 0 变化至 80%。
- ''' </summary>
- Private Shared Sub SendPortsubRequest(User As LinkUserIoi)
- End Sub
- ''' <summary>
- ''' 向控制台发送 Connect 请求。
- ''' </summary>
- Private Shared Sub SendConnectRequest(User As LinkUserIoi)
- Dim RawJson As New JObject()
- RawJson("version") = RequestVersion
- RawJson("name") = GetPlayerName()
- RawJson("id") = IoiId
- RawJson("type") = "connect"
- User.Send(RawJson)
- End Sub
- ''' <summary>
- ''' 向控制台发送 Update 请求。
- ''' </summary>
- Private Shared Sub SendUpdateRequest(User As LinkUserIoi, Stage As Integer, Optional Unique As Long = -1)
- If Unique = -1 Then Unique = GetTimeTick()
- Dim RawJson As New JObject
- RawJson("name") = GetPlayerName()
- RawJson("id") = IoiId
- RawJson("type") = "update"
- RawJson("stage") = Stage
- RawJson("unique") = Unique
- If Stage < 3 Then
- Dim Rooms As New JArray
- For Each Room In RoomListForMe
- Dim RoomObject As New JObject
- RoomObject("name") = Room.DisplayName
- RoomObject("port") = Room.Port
- Rooms.Add(RoomObject)
- Next
- RawJson("rooms") = Rooms
- User.PingPending(Unique) = Date.Now
- End If
- User.Send(RawJson)
- End Sub
- ''' <summary>
- ''' 尝试发送断开请求,并将其从用户列表中移除。
- ''' </summary>
- Private Shared Sub SendDisconnectRequest(User As LinkUserIoi, Optional Message As String = Nothing, Optional IsError As Boolean = False)
- End Sub
- #End Region
- #Region "左边栏操作"
- '刷新连接
- Private Shared Sub BtnListRefresh_Click(sender As MyIconButton, e As EventArgs)
- End Sub
- '断开连接
- Private Shared Sub BtnListDisconnect_Click(sender As MyIconButton, e As EventArgs)
- End Sub
- '复制联机码
- Public Shared Sub BtnLeftCopy_Click() Handles BtnLeftCopy.Click
- ClipboardSet(IoiId.Substring(4) & SecretEncrypt(GetPlayerName), False)
- Hint("已复制联机码!", HintType.Finish)
- End Sub
- #End Region
- #Region "玩家名"
- ''' <summary>
- ''' 获取当前的玩家名。
- ''' </summary>
- Public Shared Function GetPlayerName() As String
- '自动生成玩家名
- If AutogenPlayerName Is Nothing Then
- If IsPlayerNameValid(McLoginName) Then
- AutogenPlayerName = McLoginName()
- Else
- AutogenPlayerName = "玩家 " & CType(GetHash(If(UniqueAddress, "")) Mod 1048576, Integer).ToString("x5").ToUpper
- End If
- End If
- '获取玩家自定义的名称
- Dim CustomName As String = Setup.Get("LinkName").ToString.Trim()
- If CustomName <> "" Then
- If IsPlayerNameValid(CustomName) Then
- Return CustomName.Trim
- Else
- Hint("你所设置的玩家名存在异常,已被重置!", HintType.Critical)
- Setup.Set("LinkName", "")
- End If
- End If
- '使用自动生成的玩家名
- Return AutogenPlayerName
- End Function
- Private Shared AutogenPlayerName As String = Nothing '并非由玩家自定义,而是自动生成的玩家名
- ''' <summary>
- ''' 检查某个玩家名是否合法。
- ''' </summary>
- Private Shared Function IsPlayerNameValid(Name As String) As Boolean
- For Each ValidateRule As Validate In {New ValidateNullOrWhiteSpace, New ValidateLength(0, 20), New ValidateFilter}
- If Not String.IsNullOrEmpty(ValidateRule.Validate(Name)) Then Return False
- Next
- Return True
- End Function
- #End Region
- #Region "请求核心"
- ''' <summary>
- ''' 启动 Socket 监听核心。
- ''' </summary>
- Public Shared Sub StartSocketListener()
- End Sub
- #End Region
- #Region "用户核心"
- '用户基类
- Public MustInherit Class LinkUserBase
- Implements IDisposable
- '基础数据
- Public Uuid As Integer = GetUuid()
- Public Id As String
- Public DisplayName As String
- '请求管理
- Public Socket As Socket = Nothing
- Public Sub Send(Request As JObject)
- End Sub
- Public ListenerThread As Thread = Nothing
- Public Sub StartListener()
- End Sub
- Public Sub BindSocket(Socket As Socket)
- If Me.Socket IsNot Nothing Then Throw New Exception("该用户已经绑定了 Socket")
- Me.Socket = Socket
- StartListener()
- End Sub
- 'Ping
- '0:与 Ping 计算无关,不回应
- '1:A to B,2:B to A,3:A to B
- Public PingPending As New Dictionary(Of Long, Date)
- Public PingRecord As New Queue(Of Integer)
- '心跳包
- Public LastSend As Date = Date.Now
- Public LastReceive As Date = Date.Now
- '类型转换
- Public Sub New(Id As String, DisplayName As String)
- Me.Id = Id
- Me.DisplayName = DisplayName
- Log("[IOI] 无通信包的新用户对象:" & ToString())
- End Sub
- Public Sub New(Id As String, DisplayName As String, Socket As Socket)
- Me.Id = Id
- Me.DisplayName = DisplayName
- Me.Socket = Socket
- Log("[IOI] 新用户对象:" & ToString())
- StartListener()
- End Sub
- Public Overrides Function ToString() As String
- Return DisplayName & " @ " & Id & " #" & Uuid
- End Function
- Public Shared Widening Operator CType(User As LinkUserBase) As String
- Return User.ToString
- End Operator
- '释放资源
- Public IsDisposed As Boolean = False
- Protected Overridable Sub Dispose(IsDisposing As Boolean)
- If Socket IsNot Nothing Then Socket.Dispose()
- If ListenerThread IsNot Nothing AndAlso ListenerThread.IsAlive Then ListenerThread.Interrupt()
- End Sub
- Public Sub Dispose() Implements IDisposable.Dispose
- If Not IsDisposed Then
- IsDisposed = True
- Dispose(True)
- End If
- GC.SuppressFinalize(Me)
- End Sub
- End Class
- '用户对象
- Public Shared UserList As New Dictionary(Of String, LinkUserIoi)
- Public Class LinkUserIoi
- Inherits LinkUserBase
- Public Sub New(Id As String, DisplayName As String, Socket As Socket)
- MyBase.New(Id, DisplayName, Socket)
- End Sub
- Public Sub New(Id As String, DisplayName As String)
- MyBase.New(Id, DisplayName)
- End Sub
- '基础数据
- Public Ports As New Dictionary(Of Integer, String)
- Public Rooms As New List(Of RoomEntry)
- '进度与 UI
- Public Progress As Double = 0
- Public RelativeThread As Thread = Nothing
- Public Function GetDescription() As String
- Return If(Progress < 1,
- "正在连接," & Math.Round(Progress * 100) & "%",
- "已连接," & If(PingRecord.Count = 0, "检查延迟中", Math.Round(PingRecord.Average) & "ms"))
- End Function
- Public Function ToListItem() As MyListItem
- Dim Item As New MyListItem With {
- .Title = DisplayName, .Height = 42, .Tag = Me, .Type = MyListItem.CheckType.None,
- .Logo = "pack://application:,,,/images/Blocks/Grass.png"}
- '绑定图标按钮
- Dim BtnRefresh As New MyIconButton With {.Logo = Logo.IconButtonRefresh, .LogoScale = 0.85, .ToolTip = "刷新", .Tag = Me}
- AddHandler BtnRefresh.Click, AddressOf BtnListRefresh_Click
- ToolTipService.SetPlacement(BtnRefresh, Primitives.PlacementMode.Bottom)
- ToolTipService.SetHorizontalOffset(BtnRefresh, -10)
- ToolTipService.SetVerticalOffset(BtnRefresh, 5)
- ToolTipService.SetInitialShowDelay(BtnRefresh, 200)
- Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "断开", .Tag = Me}
- AddHandler BtnClose.Click, AddressOf BtnListDisconnect_Click
- ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
- ToolTipService.SetHorizontalOffset(BtnClose, -10)
- ToolTipService.SetVerticalOffset(BtnClose, 5)
- ToolTipService.SetInitialShowDelay(BtnClose, 200)
- Item.Buttons = {BtnRefresh, BtnClose}
- '刷新并返回
- RefreshUi(Item)
- Return Item
- End Function
- Public Sub RefreshUi(RelatedListItem As MyListItem)
- RelatedListItem.Title = DisplayName
- RelatedListItem.Info = GetDescription()
- RelatedListItem.Buttons(0).Visibility = If(Progress = 1, Visibility.Visible, Visibility.Collapsed)
- End Sub
- '释放
- Protected Overrides Sub Dispose(IsDisposing As Boolean)
- Log("[IOI] 用户资源释放(IOI, " & DisplayName & ")")
- If RelativeThread IsNot Nothing AndAlso RelativeThread.IsAlive Then RelativeThread.Interrupt()
- UserList.Remove(Id)
- MyBase.Dispose(IsDisposing)
- End Sub
- End Class
- '房间对象
- Private Shared RoomListForMe As New List(Of RoomEntry)
- Private Function GetRoomList() As List(Of RoomEntry)
- Dim RoomList As New List(Of RoomEntry)(RoomListForMe)
- For i = 0 To UserList.Count - 1
- If i > UserList.Count - 1 Then Exit For
- RoomList.AddRange(UserList.Values(i).Rooms)
- Next
- Return RoomList
- End Function
- Public Class RoomEntry
- '基础数据
- Public Port As Integer
- Public DisplayName As String
- Public User As LinkUserIoi = Nothing '若 IsOwner = True,则此项为 Nothing
- Public IsOwner As Boolean
- Public ReadOnly Property Ip As String
- Get
- If IsOwner Then
- Return "localhost:" & Port
- Else
- Return User.Ports(Port) & ":" & Port
- End If
- End Get
- End Property
- '类型转换
- Public Sub New(Port As Integer, DisplayName As String, Optional User As LinkUserIoi = Nothing)
- Me.IsOwner = User Is Nothing
- Me.User = User
- Me.DisplayName = DisplayName
- Me.Port = Port
- End Sub
- Public Overrides Function ToString() As String
- Return DisplayName & " - " & Port & " - " & IsOwner
- End Function
- Public Shared Widening Operator CType(Room As RoomEntry) As String
- Return Room.ToString
- End Operator
- Public Shared Function SelectPort(Room As RoomEntry) As Integer
- Return Room.Port
- End Function
- 'UI
- Public Function GetDescription() As String
- If IsOwner Then
- Return "由我创建,端口 " & Port
- Else
- Return "由 " & User.DisplayName & " 创建,端口 " & Port
- End If
- End Function
- Public Function ToListItem() As MyListItem
- Dim Item As New MyListItem With {
- .Title = DisplayName, .Height = 42, .Info = GetDescription(), .Tag = Me,
- .Type = If(IsOwner, MyListItem.CheckType.None, MyListItem.CheckType.Clickable),
- .Logo = "pack://application:,,,/images/Blocks/" & If(IsOwner, "GrassPath", "Grass") & ".png"}
- If IsOwner Then
- '绑定图标按钮
- Dim BtnEdit As New MyIconButton With {.Logo = Logo.IconButtonEdit, .LogoScale = 1, .ToolTip = "修改名称", .Tag = Me}
- AddHandler BtnEdit.Click, AddressOf BtnRoomEdit_Click
- ToolTipService.SetPlacement(BtnEdit, Primitives.PlacementMode.Bottom)
- ToolTipService.SetHorizontalOffset(BtnEdit, -22)
- ToolTipService.SetVerticalOffset(BtnEdit, 5)
- ToolTipService.SetInitialShowDelay(BtnEdit, 200)
- Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "关闭", .Tag = Me}
- AddHandler BtnClose.Click, AddressOf BtnRoomClose_Click
- ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
- ToolTipService.SetHorizontalOffset(BtnClose, -10)
- ToolTipService.SetVerticalOffset(BtnClose, 5)
- ToolTipService.SetInitialShowDelay(BtnClose, 200)
- Item.Buttons = {BtnEdit, BtnClose}
- Else
- '绑定点击事件
- AddHandler Item.Click, AddressOf BtnRoom_Click
- End If
- Return Item
- End Function
- Public Sub RefreshUi(RelatedListItem As MyListItem)
- RelatedListItem.Title = DisplayName
- RelatedListItem.Info = GetDescription()
- End Sub
- End Class
- #End Region
- '正向与反向连接
- Public Shared Sub BtnLeftCreate_Click() Handles BtnLeftCreate.Click
- End Sub
- Private Shared Sub SendPortsubBack(User As LinkUserIoi, TargetVersion As Integer)
- End Sub
- '创建房间
- Private Sub LinkCreate() Handles BtnCreate.Click
- End Sub
- Private Shared Sub SendUpdateRequestToAllUsers()
- For i = 0 To UserList.Count - 1
- If i > UserList.Count - 1 Then Exit For
- Dim User = UserList.Values(i)
- If User.Progress < 1 Then Continue For
- Try
- SendUpdateRequest(User, 1) '不需要使用多线程,发送实际会瞬间完成
- Catch ex As Exception
- Log(ex, "发送全局刷新请求失败(" & User.DisplayName & ")")
- End Try
- Next
- End Sub
- '修改房间名称
- Private Shared Sub BtnRoomEdit_Click(sender As MyIconButton, e As EventArgs)
- End Sub
- '加入房间
- Private Shared Sub BtnRoom_Click(sender As MyListItem, e As EventArgs)
- Dim Room As RoomEntry = sender.Tag
- If MyMsgBox("请在多人游戏页面点击直接连接,输入 " & Room.Ip & " 以进入服务器!", "加入房间", "复制地址", "确定") = 1 Then
- ClipboardSet(Room.Ip)
- End If
- End Sub
- '关闭房间
- Private Shared Sub BtnRoomClose_Click(sender As MyIconButton, e As EventArgs)
- End Sub
- '获取数据包
- Public Shared Sub ReceiveJson(JsonData As JObject, Optional NewSocket As Socket = Nothing)
- '获取数据
- Dim Id As String = JsonData("id"), Type As String = JsonData("type")
- Select Case Type
- Case "connect"
- Dim DisplayName As String = JsonData("name")
- Dim User As New LinkUserIoi(Id, DisplayName, NewSocket)
- '如果发生了双向连接
- If UserList.ContainsKey(Id) Then
- If Id > IoiId Then
- Log("[IOI] 双向连接,应当抛弃当前用户(" & DisplayName & ")")
- For Each Pair In UserList(User.Id).Ports.ToList
- User.Ports(Pair.Key) = Pair.Value
- Next
- UserList(Id).Dispose()
- Else
- '应当保留当前用户
- Log("[IOI] 双向连接,应当保留当前用户(" & DisplayName & ")")
- NewSocket.Dispose()
- Exit Sub
- End If
- End If
- '加入列表
- UserList.Add(Id, User)
- '返回请求
- User.RelativeThread = RunInNewThread(Sub()
- SendPortsubBack(User, JsonData("version").ToObject(Of Integer))
- End Sub, "Link Connect " & DisplayName)
- '更新时间
- User.LastReceive = Date.Now
- Case "update"
- If Not UserList.ContainsKey(Id) Then Throw New Exception("未在列表中的用户发送了更新请求:" & Id)
- Dim User = UserList(Id)
- '拉满进度(该请求也作为反向连接回应出现,用于向正向连接方传达连接已完成信号)
- User.Progress = 1
- '更新名称
- Dim DisplayName As String = JsonData("name")
- User.DisplayName = DisplayName
- '更新房间列表
- If JsonData("rooms") IsNot Nothing Then
- User.Rooms = New List(Of RoomEntry)
- For Each RoomObject In JsonData("rooms")
- User.Rooms.Add(New RoomEntry(RoomObject("port"), RoomObject("name"), User))
- Next
- End If
- '更新 Ping
- Dim Stage As Integer = JsonData("stage"), Unique As Long = JsonData("unique")
- If Stage > 1 Then
- User.PingRecord.Enqueue((Date.Now - User.PingPending(Unique)).TotalMilliseconds / 2)
- If User.PingRecord.Count > 5 Then User.PingRecord.Dequeue()
- User.PingPending.Remove(Unique)
- End If
- '返回请求
- If Stage > 0 AndAlso Stage < 3 Then RunInNewThread(Sub()
- Try
- SendUpdateRequest(User, Stage + 1, Unique)
- Catch ex As Exception
- Log(ex, "发送回程请求失败")
- End Try
- End Sub, "Link Update " & DisplayName)
- '更新时间
- User.LastReceive = Date.Now
- Case "disconnect"
- '断开连接
- If Not UserList.ContainsKey(Id) Then Exit Sub
- UserRemove(UserList(Id), ShowLeaveMessage:=JsonData("message") Is Nothing)
- If JsonData("message") IsNot Nothing Then Hint(JsonData("message").ToString, If(JsonData("isError").ToObject(Of Boolean), HintType.Critical, HintType.Info))
- Case Else
- Throw New Exception("未知的操作种类:" & Type)
- End Select
- End Sub
- ''' <summary>
- ''' 从用户列表中移除一位用户。提示信息视作该用户主动离开。
- ''' </summary>
- Public Shared Sub UserRemove(User As LinkUserIoi, ShowLeaveMessage As Boolean)
- End Sub
- Public Shared Sub ModuleStopManually()
- End Sub
- End Class
|