PageLinkIoi.xaml.vb 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. Imports System.Net.Sockets
  2. Public Class PageLinkIoi
  3. Public Const RequestVersion As Integer = 4
  4. Public Const IoiVersion As Integer = 10 '由于已关闭更新渠道,在提升 IoiVersion 时必须提升 RequestVersion
  5. Public Shared PathIoi As String = PathAppdata & "联机模块\IOI 联机模块.exe"
  6. #Region "进程管理"
  7. Private Shared IoiId As String, IoiPassword As String
  8. Private Shared IoiProcess As Process = Nothing
  9. Private Shared IoiState As LoadState = LoadState.Waiting
  10. ''' <summary>
  11. ''' 若 Ioi 正在运行,则结束 Ioi 进程,同时初始化状态数据。返回是否关闭了对应进程。
  12. ''' </summary>
  13. Public Shared Function IoiStop(SleepWhenKilled As Boolean) As Boolean
  14. Return False
  15. End Function
  16. ''' <summary>
  17. ''' 启动 Ioi,并等待初始化完成后退出运行,同时更新 IoiId 与 IoiPassword。
  18. ''' 正常初始化返回 True,需要更新返回 False,其余情况抛出异常。
  19. ''' 若 Ioi 正在运行,则会先停止其运行。
  20. ''' </summary>
  21. Public Shared Function IoiStart() As Boolean
  22. Return False
  23. End Function
  24. 'Ioi 日志
  25. Private Shared Sub IoiLogLine(Content As String)
  26. End Sub
  27. Private Shared LogLinesCount As Integer = 0
  28. Private Shared LastPortsId As String = "" '上一个收到 portssub 的 ID,用于记录端口
  29. #End Region
  30. #Region "时钟"
  31. 'UI 线程刷新
  32. Private Shared UserListIdentifyCache As String = ""
  33. Private Shared RoomListIdentifyCache As String = ""
  34. Public Sub RefreshUi()
  35. End Sub
  36. '工作线程刷新
  37. Public Sub RefreshWorker()
  38. End Sub
  39. #End Region
  40. #Region "发送请求"
  41. ''' <summary>
  42. ''' 发送 Portsub 请求并等待获取控制台端口。进度将从 0 变化至 80%。
  43. ''' </summary>
  44. Private Shared Sub SendPortsubRequest(User As LinkUserIoi)
  45. End Sub
  46. ''' <summary>
  47. ''' 向控制台发送 Connect 请求。
  48. ''' </summary>
  49. Private Shared Sub SendConnectRequest(User As LinkUserIoi)
  50. Dim RawJson As New JObject()
  51. RawJson("version") = RequestVersion
  52. RawJson("name") = GetPlayerName()
  53. RawJson("id") = IoiId
  54. RawJson("type") = "connect"
  55. User.Send(RawJson)
  56. End Sub
  57. ''' <summary>
  58. ''' 向控制台发送 Update 请求。
  59. ''' </summary>
  60. Private Shared Sub SendUpdateRequest(User As LinkUserIoi, Stage As Integer, Optional Unique As Long = -1)
  61. If Unique = -1 Then Unique = GetTimeTick()
  62. Dim RawJson As New JObject
  63. RawJson("name") = GetPlayerName()
  64. RawJson("id") = IoiId
  65. RawJson("type") = "update"
  66. RawJson("stage") = Stage
  67. RawJson("unique") = Unique
  68. If Stage < 3 Then
  69. Dim Rooms As New JArray
  70. For Each Room In RoomListForMe
  71. Dim RoomObject As New JObject
  72. RoomObject("name") = Room.DisplayName
  73. RoomObject("port") = Room.Port
  74. Rooms.Add(RoomObject)
  75. Next
  76. RawJson("rooms") = Rooms
  77. User.PingPending(Unique) = Date.Now
  78. End If
  79. User.Send(RawJson)
  80. End Sub
  81. ''' <summary>
  82. ''' 尝试发送断开请求,并将其从用户列表中移除。
  83. ''' </summary>
  84. Private Shared Sub SendDisconnectRequest(User As LinkUserIoi, Optional Message As String = Nothing, Optional IsError As Boolean = False)
  85. End Sub
  86. #End Region
  87. #Region "左边栏操作"
  88. '刷新连接
  89. Private Shared Sub BtnListRefresh_Click(sender As MyIconButton, e As EventArgs)
  90. End Sub
  91. '断开连接
  92. Private Shared Sub BtnListDisconnect_Click(sender As MyIconButton, e As EventArgs)
  93. End Sub
  94. '复制联机码
  95. Public Shared Sub BtnLeftCopy_Click() Handles BtnLeftCopy.Click
  96. ClipboardSet(IoiId.Substring(4) & SecretEncrypt(GetPlayerName), False)
  97. Hint("已复制联机码!", HintType.Finish)
  98. End Sub
  99. #End Region
  100. #Region "玩家名"
  101. ''' <summary>
  102. ''' 获取当前的玩家名。
  103. ''' </summary>
  104. Public Shared Function GetPlayerName() As String
  105. '自动生成玩家名
  106. If AutogenPlayerName Is Nothing Then
  107. If IsPlayerNameValid(McLoginName) Then
  108. AutogenPlayerName = McLoginName()
  109. Else
  110. AutogenPlayerName = "玩家 " & CType(GetHash(If(UniqueAddress, "")) Mod 1048576, Integer).ToString("x5").ToUpper
  111. End If
  112. End If
  113. '获取玩家自定义的名称
  114. Dim CustomName As String = Setup.Get("LinkName").ToString.Trim()
  115. If CustomName <> "" Then
  116. If IsPlayerNameValid(CustomName) Then
  117. Return CustomName.Trim
  118. Else
  119. Hint("你所设置的玩家名存在异常,已被重置!", HintType.Critical)
  120. Setup.Set("LinkName", "")
  121. End If
  122. End If
  123. '使用自动生成的玩家名
  124. Return AutogenPlayerName
  125. End Function
  126. Private Shared AutogenPlayerName As String = Nothing '并非由玩家自定义,而是自动生成的玩家名
  127. ''' <summary>
  128. ''' 检查某个玩家名是否合法。
  129. ''' </summary>
  130. Private Shared Function IsPlayerNameValid(Name As String) As Boolean
  131. For Each ValidateRule As Validate In {New ValidateNullOrWhiteSpace, New ValidateLength(0, 20), New ValidateFilter}
  132. If Not String.IsNullOrEmpty(ValidateRule.Validate(Name)) Then Return False
  133. Next
  134. Return True
  135. End Function
  136. #End Region
  137. #Region "请求核心"
  138. ''' <summary>
  139. ''' 启动 Socket 监听核心。
  140. ''' </summary>
  141. Public Shared Sub StartSocketListener()
  142. End Sub
  143. #End Region
  144. #Region "用户核心"
  145. '用户基类
  146. Public MustInherit Class LinkUserBase
  147. Implements IDisposable
  148. '基础数据
  149. Public Uuid As Integer = GetUuid()
  150. Public Id As String
  151. Public DisplayName As String
  152. '请求管理
  153. Public Socket As Socket = Nothing
  154. Public Sub Send(Request As JObject)
  155. End Sub
  156. Public ListenerThread As Thread = Nothing
  157. Public Sub StartListener()
  158. End Sub
  159. Public Sub BindSocket(Socket As Socket)
  160. If Me.Socket IsNot Nothing Then Throw New Exception("该用户已经绑定了 Socket")
  161. Me.Socket = Socket
  162. StartListener()
  163. End Sub
  164. 'Ping
  165. '0:与 Ping 计算无关,不回应
  166. '1:A to B,2:B to A,3:A to B
  167. Public PingPending As New Dictionary(Of Long, Date)
  168. Public PingRecord As New Queue(Of Integer)
  169. '心跳包
  170. Public LastSend As Date = Date.Now
  171. Public LastReceive As Date = Date.Now
  172. '类型转换
  173. Public Sub New(Id As String, DisplayName As String)
  174. Me.Id = Id
  175. Me.DisplayName = DisplayName
  176. Log("[IOI] 无通信包的新用户对象:" & ToString())
  177. End Sub
  178. Public Sub New(Id As String, DisplayName As String, Socket As Socket)
  179. Me.Id = Id
  180. Me.DisplayName = DisplayName
  181. Me.Socket = Socket
  182. Log("[IOI] 新用户对象:" & ToString())
  183. StartListener()
  184. End Sub
  185. Public Overrides Function ToString() As String
  186. Return DisplayName & " @ " & Id & " #" & Uuid
  187. End Function
  188. Public Shared Widening Operator CType(User As LinkUserBase) As String
  189. Return User.ToString
  190. End Operator
  191. '释放资源
  192. Public IsDisposed As Boolean = False
  193. Protected Overridable Sub Dispose(IsDisposing As Boolean)
  194. If Socket IsNot Nothing Then Socket.Dispose()
  195. If ListenerThread IsNot Nothing AndAlso ListenerThread.IsAlive Then ListenerThread.Interrupt()
  196. End Sub
  197. Public Sub Dispose() Implements IDisposable.Dispose
  198. If Not IsDisposed Then
  199. IsDisposed = True
  200. Dispose(True)
  201. End If
  202. GC.SuppressFinalize(Me)
  203. End Sub
  204. End Class
  205. '用户对象
  206. Public Shared UserList As New Dictionary(Of String, LinkUserIoi)
  207. Public Class LinkUserIoi
  208. Inherits LinkUserBase
  209. Public Sub New(Id As String, DisplayName As String, Socket As Socket)
  210. MyBase.New(Id, DisplayName, Socket)
  211. End Sub
  212. Public Sub New(Id As String, DisplayName As String)
  213. MyBase.New(Id, DisplayName)
  214. End Sub
  215. '基础数据
  216. Public Ports As New Dictionary(Of Integer, String)
  217. Public Rooms As New List(Of RoomEntry)
  218. '进度与 UI
  219. Public Progress As Double = 0
  220. Public RelativeThread As Thread = Nothing
  221. Public Function GetDescription() As String
  222. Return If(Progress < 1,
  223. "正在连接," & Math.Round(Progress * 100) & "%",
  224. "已连接," & If(PingRecord.Count = 0, "检查延迟中", Math.Round(PingRecord.Average) & "ms"))
  225. End Function
  226. Public Function ToListItem() As MyListItem
  227. Dim Item As New MyListItem With {
  228. .Title = DisplayName, .Height = 42, .Tag = Me, .Type = MyListItem.CheckType.None,
  229. .Logo = "pack://application:,,,/images/Blocks/Grass.png"}
  230. '绑定图标按钮
  231. Dim BtnRefresh As New MyIconButton With {.Logo = Logo.IconButtonRefresh, .LogoScale = 0.85, .ToolTip = "刷新", .Tag = Me}
  232. AddHandler BtnRefresh.Click, AddressOf BtnListRefresh_Click
  233. ToolTipService.SetPlacement(BtnRefresh, Primitives.PlacementMode.Bottom)
  234. ToolTipService.SetHorizontalOffset(BtnRefresh, -10)
  235. ToolTipService.SetVerticalOffset(BtnRefresh, 5)
  236. ToolTipService.SetInitialShowDelay(BtnRefresh, 200)
  237. Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "断开", .Tag = Me}
  238. AddHandler BtnClose.Click, AddressOf BtnListDisconnect_Click
  239. ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
  240. ToolTipService.SetHorizontalOffset(BtnClose, -10)
  241. ToolTipService.SetVerticalOffset(BtnClose, 5)
  242. ToolTipService.SetInitialShowDelay(BtnClose, 200)
  243. Item.Buttons = {BtnRefresh, BtnClose}
  244. '刷新并返回
  245. RefreshUi(Item)
  246. Return Item
  247. End Function
  248. Public Sub RefreshUi(RelatedListItem As MyListItem)
  249. RelatedListItem.Title = DisplayName
  250. RelatedListItem.Info = GetDescription()
  251. RelatedListItem.Buttons(0).Visibility = If(Progress = 1, Visibility.Visible, Visibility.Collapsed)
  252. End Sub
  253. '释放
  254. Protected Overrides Sub Dispose(IsDisposing As Boolean)
  255. Log("[IOI] 用户资源释放(IOI, " & DisplayName & ")")
  256. If RelativeThread IsNot Nothing AndAlso RelativeThread.IsAlive Then RelativeThread.Interrupt()
  257. UserList.Remove(Id)
  258. MyBase.Dispose(IsDisposing)
  259. End Sub
  260. End Class
  261. '房间对象
  262. Private Shared RoomListForMe As New List(Of RoomEntry)
  263. Private Function GetRoomList() As List(Of RoomEntry)
  264. Dim RoomList As New List(Of RoomEntry)(RoomListForMe)
  265. For i = 0 To UserList.Count - 1
  266. If i > UserList.Count - 1 Then Exit For
  267. RoomList.AddRange(UserList.Values(i).Rooms)
  268. Next
  269. Return RoomList
  270. End Function
  271. Public Class RoomEntry
  272. '基础数据
  273. Public Port As Integer
  274. Public DisplayName As String
  275. Public User As LinkUserIoi = Nothing '若 IsOwner = True,则此项为 Nothing
  276. Public IsOwner As Boolean
  277. Public ReadOnly Property Ip As String
  278. Get
  279. If IsOwner Then
  280. Return "localhost:" & Port
  281. Else
  282. Return User.Ports(Port) & ":" & Port
  283. End If
  284. End Get
  285. End Property
  286. '类型转换
  287. Public Sub New(Port As Integer, DisplayName As String, Optional User As LinkUserIoi = Nothing)
  288. Me.IsOwner = User Is Nothing
  289. Me.User = User
  290. Me.DisplayName = DisplayName
  291. Me.Port = Port
  292. End Sub
  293. Public Overrides Function ToString() As String
  294. Return DisplayName & " - " & Port & " - " & IsOwner
  295. End Function
  296. Public Shared Widening Operator CType(Room As RoomEntry) As String
  297. Return Room.ToString
  298. End Operator
  299. Public Shared Function SelectPort(Room As RoomEntry) As Integer
  300. Return Room.Port
  301. End Function
  302. 'UI
  303. Public Function GetDescription() As String
  304. If IsOwner Then
  305. Return "由我创建,端口 " & Port
  306. Else
  307. Return "由 " & User.DisplayName & " 创建,端口 " & Port
  308. End If
  309. End Function
  310. Public Function ToListItem() As MyListItem
  311. Dim Item As New MyListItem With {
  312. .Title = DisplayName, .Height = 42, .Info = GetDescription(), .Tag = Me,
  313. .Type = If(IsOwner, MyListItem.CheckType.None, MyListItem.CheckType.Clickable),
  314. .Logo = "pack://application:,,,/images/Blocks/" & If(IsOwner, "GrassPath", "Grass") & ".png"}
  315. If IsOwner Then
  316. '绑定图标按钮
  317. Dim BtnEdit As New MyIconButton With {.Logo = Logo.IconButtonEdit, .LogoScale = 1, .ToolTip = "修改名称", .Tag = Me}
  318. AddHandler BtnEdit.Click, AddressOf BtnRoomEdit_Click
  319. ToolTipService.SetPlacement(BtnEdit, Primitives.PlacementMode.Bottom)
  320. ToolTipService.SetHorizontalOffset(BtnEdit, -22)
  321. ToolTipService.SetVerticalOffset(BtnEdit, 5)
  322. ToolTipService.SetInitialShowDelay(BtnEdit, 200)
  323. Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "关闭", .Tag = Me}
  324. AddHandler BtnClose.Click, AddressOf BtnRoomClose_Click
  325. ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
  326. ToolTipService.SetHorizontalOffset(BtnClose, -10)
  327. ToolTipService.SetVerticalOffset(BtnClose, 5)
  328. ToolTipService.SetInitialShowDelay(BtnClose, 200)
  329. Item.Buttons = {BtnEdit, BtnClose}
  330. Else
  331. '绑定点击事件
  332. AddHandler Item.Click, AddressOf BtnRoom_Click
  333. End If
  334. Return Item
  335. End Function
  336. Public Sub RefreshUi(RelatedListItem As MyListItem)
  337. RelatedListItem.Title = DisplayName
  338. RelatedListItem.Info = GetDescription()
  339. End Sub
  340. End Class
  341. #End Region
  342. '正向与反向连接
  343. Public Shared Sub BtnLeftCreate_Click() Handles BtnLeftCreate.Click
  344. End Sub
  345. Private Shared Sub SendPortsubBack(User As LinkUserIoi, TargetVersion As Integer)
  346. End Sub
  347. '创建房间
  348. Private Sub LinkCreate() Handles BtnCreate.Click
  349. End Sub
  350. Private Shared Sub SendUpdateRequestToAllUsers()
  351. For i = 0 To UserList.Count - 1
  352. If i > UserList.Count - 1 Then Exit For
  353. Dim User = UserList.Values(i)
  354. If User.Progress < 1 Then Continue For
  355. Try
  356. SendUpdateRequest(User, 1) '不需要使用多线程,发送实际会瞬间完成
  357. Catch ex As Exception
  358. Log(ex, "发送全局刷新请求失败(" & User.DisplayName & ")")
  359. End Try
  360. Next
  361. End Sub
  362. '修改房间名称
  363. Private Shared Sub BtnRoomEdit_Click(sender As MyIconButton, e As EventArgs)
  364. End Sub
  365. '加入房间
  366. Private Shared Sub BtnRoom_Click(sender As MyListItem, e As EventArgs)
  367. Dim Room As RoomEntry = sender.Tag
  368. If MyMsgBox("请在多人游戏页面点击直接连接,输入 " & Room.Ip & " 以进入服务器!", "加入房间", "复制地址", "确定") = 1 Then
  369. ClipboardSet(Room.Ip)
  370. End If
  371. End Sub
  372. '关闭房间
  373. Private Shared Sub BtnRoomClose_Click(sender As MyIconButton, e As EventArgs)
  374. End Sub
  375. '获取数据包
  376. Public Shared Sub ReceiveJson(JsonData As JObject, Optional NewSocket As Socket = Nothing)
  377. '获取数据
  378. Dim Id As String = JsonData("id"), Type As String = JsonData("type")
  379. Select Case Type
  380. Case "connect"
  381. Dim DisplayName As String = JsonData("name")
  382. Dim User As New LinkUserIoi(Id, DisplayName, NewSocket)
  383. '如果发生了双向连接
  384. If UserList.ContainsKey(Id) Then
  385. If Id > IoiId Then
  386. Log("[IOI] 双向连接,应当抛弃当前用户(" & DisplayName & ")")
  387. For Each Pair In UserList(User.Id).Ports.ToList
  388. User.Ports(Pair.Key) = Pair.Value
  389. Next
  390. UserList(Id).Dispose()
  391. Else
  392. '应当保留当前用户
  393. Log("[IOI] 双向连接,应当保留当前用户(" & DisplayName & ")")
  394. NewSocket.Dispose()
  395. Exit Sub
  396. End If
  397. End If
  398. '加入列表
  399. UserList.Add(Id, User)
  400. '返回请求
  401. User.RelativeThread = RunInNewThread(Sub()
  402. SendPortsubBack(User, JsonData("version").ToObject(Of Integer))
  403. End Sub, "Link Connect " & DisplayName)
  404. '更新时间
  405. User.LastReceive = Date.Now
  406. Case "update"
  407. If Not UserList.ContainsKey(Id) Then Throw New Exception("未在列表中的用户发送了更新请求:" & Id)
  408. Dim User = UserList(Id)
  409. '拉满进度(该请求也作为反向连接回应出现,用于向正向连接方传达连接已完成信号)
  410. User.Progress = 1
  411. '更新名称
  412. Dim DisplayName As String = JsonData("name")
  413. User.DisplayName = DisplayName
  414. '更新房间列表
  415. If JsonData("rooms") IsNot Nothing Then
  416. User.Rooms = New List(Of RoomEntry)
  417. For Each RoomObject In JsonData("rooms")
  418. User.Rooms.Add(New RoomEntry(RoomObject("port"), RoomObject("name"), User))
  419. Next
  420. End If
  421. '更新 Ping
  422. Dim Stage As Integer = JsonData("stage"), Unique As Long = JsonData("unique")
  423. If Stage > 1 Then
  424. User.PingRecord.Enqueue((Date.Now - User.PingPending(Unique)).TotalMilliseconds / 2)
  425. If User.PingRecord.Count > 5 Then User.PingRecord.Dequeue()
  426. User.PingPending.Remove(Unique)
  427. End If
  428. '返回请求
  429. If Stage > 0 AndAlso Stage < 3 Then RunInNewThread(Sub()
  430. Try
  431. SendUpdateRequest(User, Stage + 1, Unique)
  432. Catch ex As Exception
  433. Log(ex, "发送回程请求失败")
  434. End Try
  435. End Sub, "Link Update " & DisplayName)
  436. '更新时间
  437. User.LastReceive = Date.Now
  438. Case "disconnect"
  439. '断开连接
  440. If Not UserList.ContainsKey(Id) Then Exit Sub
  441. UserRemove(UserList(Id), ShowLeaveMessage:=JsonData("message") Is Nothing)
  442. If JsonData("message") IsNot Nothing Then Hint(JsonData("message").ToString, If(JsonData("isError").ToObject(Of Boolean), HintType.Critical, HintType.Info))
  443. Case Else
  444. Throw New Exception("未知的操作种类:" & Type)
  445. End Select
  446. End Sub
  447. ''' <summary>
  448. ''' 从用户列表中移除一位用户。提示信息视作该用户主动离开。
  449. ''' </summary>
  450. Public Shared Sub UserRemove(User As LinkUserIoi, ShowLeaveMessage As Boolean)
  451. End Sub
  452. Public Shared Sub ModuleStopManually()
  453. End Sub
  454. End Class