PageLinkIoi.xaml.vb 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259
  1. Imports System.Net
  2. Imports System.Net.Sockets
  3. Public Class PageLinkIoi
  4. Public Const RequestVersion As Integer = 4
  5. Public Const IoiVersion As Integer = 10 '由于已关闭更新渠道,在提升 IoiVersion 时必须提升 RequestVersion
  6. Public Shared PathIoi As String = PathAppdata & "联机模块\IOI 联机模块.exe"
  7. #Region "初始化"
  8. '页面初始化
  9. Private IsLoad As Boolean = False
  10. Private Sub MeLoaded() Handles Me.Loaded
  11. FormMain.EndProgramForce(Result.Aborted)
  12. '重复加载部分
  13. PanBack.ScrollToHome()
  14. RefreshUi()
  15. '非重复加载部分
  16. If IsLoad Then Exit Sub
  17. IsLoad = True
  18. '更新线程
  19. RunInNewThread(Sub()
  20. Do While True
  21. Thread.Sleep(200)
  22. If FrmMain.PageCurrent.Page = FormMain.PageType.Link AndAlso FrmMain.PageCurrentSub = FormMain.PageSubType.LinkIoi Then RunInUiWait(Sub() FrmLinkIoi.RefreshUi())
  23. RefreshWorker()
  24. Loop
  25. End Sub, "Link Timer")
  26. End Sub
  27. '加载器初始化与左边栏处理
  28. Private Sub LoaderInit() Handles Me.Initialized
  29. PageLoaderInit(Load, PanLoad, PanBack, PanAlways, InitLoader, Sub()
  30. End Sub)
  31. End Sub
  32. '初始化加载器与步骤
  33. Public Shared WithEvents InitLoader As New LoaderCombo(Of Integer)("联机模块初始化", {
  34. New LoaderTask(Of Integer, Integer)("端口检查", AddressOf InitPortCheck) With {.ProgressWeight = 1},
  35. New LoaderTask(Of Integer, Integer)("启动请求核心", AddressOf StartSocketListener) With {.ProgressWeight = 1},
  36. New LoaderTask(Of Integer, List(Of NetFile))("初次启动尝试", AddressOf InitFirst) With {.ProgressWeight = 4},
  37. New LoaderDownload("下载更新文件", New List(Of NetFile)) With {.ProgressWeight = 6},
  38. New LoaderTask(Of List(Of NetFile), Boolean)("二次启动尝试", AddressOf InitSecond) With {.ProgressWeight = 3},
  39. New LoaderTask(Of Boolean, Boolean)("创建请求核心房间", AddressOf InitRequest) With {.ProgressWeight = 2}
  40. })
  41. '判断端口是否被占用
  42. Private Shared Sub InitPortCheck()
  43. '检查协议
  44. If Not Setup.Get("LinkEula") Then
  45. Reopen:
  46. Select Case MyMsgBox("PCL 的联机服务由速聚授权提供。" & vbCrLf & "在使用前,你需要同意速聚的用户服务协议和隐私政策。", "协议授权", "同意", "拒绝", "查看用户服务协议和隐私政策")
  47. Case 1
  48. Setup.Set("LinkEula", True)
  49. Case 2
  50. Throw New Exception("你拒绝了用户服务协议……")
  51. Case 3
  52. OpenWebsite("https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzkxMTMyODk3Mg==&action=getalbum&album_id=2585385685407514625&scene=173&from_msgid=2247483720&from_itemidx=1&count=3&nolastread=1#wechat_redirect")
  53. GoTo Reopen
  54. End Select
  55. End If
  56. '先掐死
  57. IoiStop(True)
  58. '检查端口
  59. Dim HasConflict As Boolean = False
  60. For Each Port In Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners()
  61. If Port.Port <> 55555 AndAlso Port.Port <> 55557 Then Continue For
  62. Log("[IOI] 发现端口 " & Port.Port & " 被占用")
  63. HasConflict = True
  64. Next
  65. If Not HasConflict Then Exit Sub
  66. '对应的报错
  67. For Each Line As String In ShellAndGetOutput("netstat", "-ano", 30000).Split({vbCrLf}, StringSplitOptions.RemoveEmptyEntries)
  68. If Not (Line.Contains("127.0.0.1:55555") OrElse Line.Contains("127.0.0.1:55557")) Then Continue For
  69. Dim ProcessName As String
  70. Try
  71. ProcessName = Process.GetProcessById(Line.Split(" ").Last).ProcessName
  72. Catch ex As Exception
  73. Log(ex, "获取占用端口的进程信息失败,假定进程已结束")
  74. Continue For
  75. End Try
  76. If ProcessName = "联机模块" Then
  77. Throw New Exception("由于一个已知问题,请在重启电脑后再尝试使用联机功能")
  78. ElseIf ProcessName = "Idle" Then
  79. '不知道为啥反正又没占用了
  80. Log("[IOI] 未发现占用此端口的程序,继续执行")
  81. Else
  82. Throw New Exception("端口被程序 " & ProcessName & " 占用,无法启动联机模块,请在任务管理器关闭此程序后再试")
  83. End If
  84. Next
  85. End Sub
  86. '下载更新前尝试启动
  87. Private Shared Sub InitFirst(Task As LoaderTask(Of Integer, List(Of NetFile)))
  88. '初次启动尝试
  89. If IoiVersion <> Setup.Get("LinkIoiVersion") Then
  90. Log("[IOI] 设置版本强制要求联机模块更新")
  91. Setup.Set("LinkIoiVersion", IoiVersion)
  92. ElseIf File.Exists(PathIoi) Then
  93. Task.Progress = 0.2
  94. CheckFirewall()
  95. If IoiStart() Then
  96. '已完成启动与初始化
  97. Task.Output = New List(Of NetFile)
  98. Exit Sub
  99. Else
  100. '需要更新
  101. IoiStop(True)
  102. File.Delete(PathIoi)
  103. GoTo StartDownload
  104. End If
  105. '如果抛出异常则直接使加载器失败
  106. End If
  107. '开始下载
  108. StartDownload:
  109. Task.Progress = 0.8
  110. Log("[IOI] 需要下载联机模块")
  111. If File.Exists(PathTemp & "联机模块.zip") Then File.Delete(PathTemp & "联机模块.zip")
  112. Task.Output = New List(Of NetFile) From {New NetFile(
  113. {"https://gitcode.net/to/cato_bin/-/raw/master/ioi_v2_x" & If(Is32BitSystem, 32, 64) & ".zip",
  114. "http://mirror.hiper.cn.s2.the.bb/ioi_v2_x" & If(Is32BitSystem, 32, 64) & ".zip",
  115. "http://mirror.hiper.cn.s3.the.bb:175/ioi_v2_x" & If(Is32BitSystem, 32, 64) & ".zip",
  116. "https://pcl2-server-1253424809.file.myqcloud.com/link/ioi_v2_x" & If(Is32BitSystem, 32, 64) & ".zip{CDN}"},
  117. PathTemp & "联机模块.zip")}
  118. End Sub
  119. '下载更新后尝试启动
  120. Private Shared Sub InitSecond(Task As LoaderTask(Of List(Of NetFile), Boolean))
  121. '若首次尝试已经成功,则直接跳过
  122. If IoiState = LoadState.Finished Then Exit Sub
  123. '解压更新包
  124. Log("[IOI] 解压联机模块以完成下载")
  125. If File.Exists(PathTemp & "ioi.exe") Then File.Delete(PathTemp & "ioi.exe")
  126. If File.Exists(PathIoi) Then File.Delete(PathIoi)
  127. Compression.ZipFile.ExtractToDirectory(PathTemp & "联机模块.zip", PathTemp)
  128. File.Delete(PathTemp & "联机模块.zip")
  129. CopyFile(PathTemp & "ioi.exe", PathIoi)
  130. File.Delete(PathTemp & "ioi.exe")
  131. Task.Progress = 0.4
  132. '再次尝试启动
  133. CheckFirewall()
  134. If IoiStart() Then Exit Sub
  135. IoiStop(True)
  136. Throw New Exception("联机模块初始化失败")
  137. End Sub
  138. '在 IOI 启动后建立 55557 房间
  139. Private Shared Sub InitRequest()
  140. Try
  141. Dim Result As JObject = GetJson(NetRequestOnce("http://127.0.0.1:55555/api/port?proto=tcp&port=55557" & "&password=" & IoiPassword, "PUT", "", "", 100000))
  142. If Result("msg") IsNot Nothing Then Throw New InvalidOperationException(Result("msg").ToString)
  143. Catch ex As InvalidOperationException
  144. 'API 返回的错误
  145. Log("创建请求核心房间失败:" & ex.Message, LogLevel.Msgbox)
  146. Catch ex As Exception
  147. '常规错误
  148. Log(ex, "创建请求核心房间失败", LogLevel.Msgbox)
  149. End Try
  150. End Sub
  151. '检查防火墙权限,并添加对应的权限
  152. Private Shared Sub CheckFirewall()
  153. Try
  154. Log("[IOI] Windows 防火墙:检测开始")
  155. If Not PageLinkLeft.FirewallIsBlock(PathIoi) Then Exit Try
  156. If PageLinkLeft.FirewallPolicy.CurrentProfile.ExceptionsNotAllowed Then
  157. '禁止白名单
  158. MyMsgBox("由于 Windows 防火墙阻止了所有传入连接,PCL 无法获取防火墙通行权限。" & vbCrLf &
  159. "联机会有很大概率失败,就算连上了,延迟也会变高……" & vbCrLf & vbCrLf &
  160. "请先关闭 Windows 防火墙中的 " & vbLQ & "阻止所有传入连接" & vbRQ & " 选项,然后重启 PCL。", "遭到防火墙拦截")
  161. ElseIf IsAdmin() Then
  162. '有管理员权限
  163. Log("[IOI] Windows 防火墙:尝试添加防火墙通行权限")
  164. Try
  165. PageLinkLeft.FirewallAddAuthorized("Plain Craft Launcher 启动器(IOI 联机模块)", PathIoi)
  166. Catch ex As Exception
  167. Log(ex, "无法将联机模块添加至防火墙白名单,可能导致联机失败", LogLevel.Msgbox)
  168. End Try
  169. Else
  170. '无管理员权限
  171. If MyMsgBox("由于你开启了 Windows 防火墙,PCL 需要获取防火墙通行权限。" & vbCrLf & vbCrLf &
  172. "若继续,PCL 将尝试以管理员权限重新启动。" & vbCrLf &
  173. "若拒绝,联机模块可能会被防火墙拦截,联机会有很大概率失败。", "需要管理员权限", "继续", "拒绝") = 1 Then
  174. Log("[IOI] Windows 防火墙:尝试提升权限")
  175. If RunAsAdmin("--link ioi") Then
  176. FrmMain.EndProgram(False) '已重新运行
  177. Else
  178. Hint("获取管理员权限失败,请尝试右键 PCL,选择 " & vbLQ & "以管理员身份运行" & vbRQ & ",然后再进入联机页面!", HintType.Critical)
  179. End If
  180. Else
  181. Hint("在没有防火墙权限的情况下尝试联机,很可能会导致联机失败!", HintType.Critical)
  182. End If
  183. End If
  184. Log("[IOI] Windows 防火墙:无防火墙通行权限")
  185. Catch ex As Exception
  186. Log(ex, "无法检测防火墙状态,可能导致联机失败", LogLevel.Msgbox)
  187. End Try
  188. End Sub
  189. #End Region
  190. #Region "进程管理"
  191. Private Shared IoiId As String, IoiPassword As String
  192. Private Shared IoiProcess As Process = Nothing
  193. Private Shared IoiState As LoadState = LoadState.Waiting
  194. ''' <summary>
  195. ''' 若 Ioi 正在运行,则结束 Ioi 进程,同时初始化状态数据。返回是否关闭了对应进程。
  196. ''' </summary>
  197. Public Shared Function IoiStop(SleepWhenKilled As Boolean) As Boolean
  198. IoiStop = False
  199. '发送断开信息
  200. Try
  201. If IoiProcess IsNot Nothing AndAlso Not IoiProcess.HasExited Then
  202. For i = 0 To UserList.Count - 1
  203. If i > UserList.Count - 1 Then Exit For
  204. Dim User = UserList.Values(i)
  205. SendDisconnectRequest(User)
  206. Next
  207. End If
  208. Catch ex As Exception
  209. Log(ex, "结束 IOI 进程时发送 Disconnect 信息失败")
  210. End Try
  211. '关闭所有 Ioi 进程
  212. For Each ProcessObject In Process.GetProcesses
  213. If ProcessObject.ProcessName <> "IOI 联机模块" Then Continue For
  214. IoiStop = True
  215. Try
  216. ProcessObject.Kill()
  217. Log("[IOI] 已关闭联机模块:" & ProcessObject.Id)
  218. If SleepWhenKilled Then Thread.Sleep(3000) '等待 3 秒确认进程已退出
  219. Catch ex As Exception
  220. Log(ex, "关闭联机模块失败(" & ProcessObject.Id & ")")
  221. End Try
  222. Next
  223. '初始化
  224. IoiProcess = Nothing
  225. IoiId = Nothing
  226. IoiPassword = Nothing
  227. IoiState = LoadState.Waiting
  228. UserList = New Dictionary(Of String, LinkUserIoi)
  229. RoomListForMe = New List(Of RoomEntry)
  230. End Function
  231. ''' <summary>
  232. ''' 启动 Ioi,并等待初始化完成后退出运行,同时更新 IoiId 与 IoiPassword。
  233. ''' 正常初始化返回 True,需要更新返回 False,其余情况抛出异常。
  234. ''' 若 Ioi 正在运行,则会先停止其运行。
  235. ''' </summary>
  236. Public Shared Function IoiStart() As Boolean
  237. IoiStop(True)
  238. Log("[IOI] 启动联机模块进程")
  239. Dim Info = New ProcessStartInfo With {
  240. .FileName = PathIoi,
  241. .UseShellExecute = False,
  242. .CreateNoWindow = True,
  243. .RedirectStandardError = True,
  244. .RedirectStandardOutput = True,
  245. .WorkingDirectory = PathTemp
  246. }
  247. IoiProcess = New Process() With {.StartInfo = Info}
  248. Using outputWaitHandle As New AutoResetEvent(False)
  249. Using errorWaitHandle As New AutoResetEvent(False)
  250. AddHandler IoiProcess.OutputDataReceived, Function(sender, e)
  251. Try
  252. If e.Data Is Nothing Then
  253. outputWaitHandle.[Set]()
  254. Else
  255. IoiLogLine(e.Data)
  256. End If
  257. Catch ex As ObjectDisposedException
  258. Catch ex As Exception
  259. Log(ex, "读取联机模块信息失败")
  260. IoiState = LoadState.Failed
  261. End Try
  262. Return Nothing
  263. End Function
  264. AddHandler IoiProcess.ErrorDataReceived, Function(sender, e)
  265. Try
  266. If e.Data Is Nothing Then
  267. errorWaitHandle.[Set]()
  268. Else
  269. IoiLogLine(e.Data)
  270. End If
  271. Catch ex As ObjectDisposedException
  272. Catch ex As Exception
  273. Log(ex, "读取联机模块错误信息失败")
  274. IoiState = LoadState.Failed
  275. End Try
  276. Return Nothing
  277. End Function
  278. IoiProcess.Start()
  279. IoiState = LoadState.Loading
  280. IoiProcess.BeginOutputReadLine()
  281. IoiProcess.BeginErrorReadLine()
  282. '等待
  283. Do Until IoiProcess.HasExited OrElse IoiState <> LoadState.Loading
  284. Thread.Sleep(10)
  285. Loop
  286. '输出
  287. If IoiState = LoadState.Finished Then
  288. Log("[IOI] 联机模块启动成功")
  289. Return True
  290. Else
  291. Throw New Exception("联机模块启动失败")
  292. End If
  293. 'Select Case IoiState
  294. ' Case LoadState.Finished
  295. ' Log("[IOI] 联机模块启动成功")
  296. ' Return True
  297. ' Case LoadState.Aborted
  298. ' Log("[IOI] 联机模块要求更新")
  299. ' Return False
  300. ' Case LoadState.Failed
  301. ' Log("[IOI] 联机模块启动出现异常")
  302. ' Return False
  303. ' Case Else 'LoadState.Loading
  304. ' Throw New Exception("联机模块启动失败,请检查你的网络连接")
  305. 'End Select
  306. End Using
  307. End Using
  308. End Function
  309. 'Ioi 日志
  310. Private Shared Sub IoiLogLine(Content As String)
  311. '初始化
  312. If Content.Contains(" > http://127.0.0.1:55555/") Then Exit Sub
  313. If Content.Contains("All done") Then
  314. If IoiId IsNot Nothing AndAlso IoiPassword IsNot Nothing Then
  315. IoiState = LoadState.Finished
  316. Else
  317. Log("[IOI] 联机模块汇报初始化完成,但未提供账户信息")
  318. IoiState = LoadState.Failed
  319. End If
  320. Exit Sub
  321. End If
  322. If Content.Contains("Password :: ") Then
  323. IoiPassword = Content.Split({"Password :: "}, StringSplitOptions.None)(1)
  324. Exit Sub '为保证安全不记录密码到 Log
  325. End If
  326. '初始化
  327. If Content.Contains("ID :: ") Then IoiId = Content.Split({"ID :: "}, StringSplitOptions.None)(1)
  328. If Content.Contains("Initialization failed") OrElse Content.Contains("The version is ") Then IoiState = LoadState.Aborted
  329. '系统回应
  330. If Content.Contains("'portssub' from ") Then LastPortsId = Content.Split(" ").Last
  331. If Content.Contains("Listening tcp ") Then
  332. If UserList.ContainsKey(LastPortsId) Then
  333. UserList(LastPortsId).Ports(Content.Split(" ").Last) = RegexSeek(Content, "(?<=Listening tcp )[^:]+")
  334. Else
  335. Log("[IOI] 未在列表中的用户出现意料外的连接信息")
  336. NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & LastPortsId & "&password=" & IoiPassword, "DELETE", "", "", 500)
  337. End If
  338. End If
  339. '断开连接
  340. If Content.Contains("Closed tcp connection from ") AndAlso Content.Contains(":55557") Then
  341. For i = 0 To UserList.Count - 1
  342. Dim User = UserList.Values(i)
  343. If Not Content.Contains(User.Ports(55557)) OrElse User.IsDisposed Then Continue For
  344. Log("[IOI] 检测到 55557 端口断开(" & User.DisplayName & ")")
  345. UserRemove(User, True)
  346. Exit For
  347. Next
  348. End If
  349. '写入日志
  350. If Not Content.Contains("read /dev/stdin: The handle is invalid.") AndAlso LogLinesCount < If(ModeDebug, 10000, 1000) Then
  351. LogLinesCount += 1
  352. Log("[IOI] " & Content)
  353. End If
  354. End Sub
  355. Private Shared LogLinesCount As Integer = 0
  356. Private Shared LastPortsId As String = "" '上一个收到 portssub 的 ID,用于记录端口
  357. #End Region
  358. #Region "时钟"
  359. 'UI 线程刷新
  360. Private Shared UserListIdentifyCache As String = ""
  361. Private Shared RoomListIdentifyCache As String = ""
  362. Public Sub RefreshUi()
  363. Try
  364. '确认用户列表缓存
  365. Dim UserListIdentify As String = Join(UserList, vbCrLf)
  366. If UserListIdentify <> UserListIdentifyCache Then
  367. UserListIdentifyCache = UserListIdentify
  368. '刷新列表项
  369. PanUserList.Children.Clear()
  370. For i = 0 To UserList.Count - 1
  371. If i > UserList.Count - 1 Then Exit For
  372. PanUserList.Children.Add(UserList.Values(i).ToListItem)
  373. Next
  374. '刷新列表标题
  375. CardUser.Title = "已连接的玩家 (" & PanUserList.Children.Count & ")"
  376. End If
  377. '刷新用户列表项
  378. For Each UserItem As MyListItem In PanUserList.Children
  379. CType(UserItem.Tag, LinkUserIoi).RefreshUi(UserItem)
  380. Next
  381. '确认房间列表缓存
  382. Dim RoomListIdentify As String = Join(GetRoomList(), vbCrLf)
  383. If RoomListIdentify <> RoomListIdentifyCache Then
  384. RoomListIdentifyCache = RoomListIdentify
  385. '刷新列表项
  386. PanRoom.Children.Clear()
  387. For Each Room In GetRoomList()
  388. PanRoom.Children.Add(Room.ToListItem)
  389. Next
  390. End If
  391. '刷新房间列表项
  392. For Each RoomItem As MyListItem In PanRoom.Children
  393. CType(RoomItem.Tag, RoomEntry).RefreshUi(RoomItem)
  394. Next
  395. '刷新操作提示
  396. If UserList.Count = 0 Then
  397. If RoomListForMe.Count = 0 Then
  398. LabHint.Text = "若想创建房间,请点击创建房间按钮,并按说明进行操作。" & vbCrLf & "若想加入他人的房间,请点击建立连接,然后输入对方的联机码。"
  399. Else
  400. LabHint.Text = "若想让其他人加入你的房间,请点击复制联机码,然后让你的朋友输入你的联机码以建立连接。"
  401. End If
  402. Else
  403. If GetRoomList.Count = 0 Then
  404. LabHint.Text = "若想创建房间,请点击创建房间按钮,然后输入对局域网开放后 MC 显示的端口号,或本地服务端的端口号。"
  405. ElseIf RoomListForMe.Count = 0 Then
  406. '已有连接,本人没有房间,对方有房间
  407. LabHint.Text = "若想加入某个房间,直接点击该房间即可获取说明。"
  408. Else
  409. '已有连接,且本人有房间
  410. LabHint.Text = "指向你所创建的房间,能在右侧找到修改房间名称、关闭房间等选项。" &
  411. If(RoomListForMe.Count <> GetRoomList.Count, vbCrLf & "若想加入其他人的房间,直接点击该房间即可获取说明。", "")
  412. End If
  413. End If
  414. Catch ex As Exception
  415. Log(ex, "联机模块 UI 时钟运行失败", LogLevel.Feedback)
  416. End Try
  417. End Sub
  418. '工作线程刷新
  419. Public Sub RefreshWorker()
  420. Try
  421. '检测退出
  422. If InitLoader.State = LoadState.Finished AndAlso IoiProcess.HasExited Then
  423. Log("[IOI] 联机模块出现异常!", LogLevel.Hint)
  424. ModuleStopManually()
  425. End If
  426. '检测用户
  427. For i = 0 To UserList.Values.Count - 1
  428. If i > UserList.Values.Count - 1 Then Exit For
  429. Dim User = UserList.Values(i)
  430. If User.Progress < 1 Then Continue For '跳过连接中的用户
  431. '发送心跳包
  432. If Date.Now - User.LastSend > New TimeSpan(0, 0, RandomInteger(50, 30)) Then
  433. RunInNewThread(Sub()
  434. Try
  435. SendUpdateRequest(User, 1)
  436. Catch ex As Exception
  437. Log(ex, "心跳包发送失败(" & User.DisplayName & ")", LogLevel.Normal)
  438. End Try
  439. End Sub, "Link Heartbeat " & User.Id)
  440. End If
  441. '检测被动离线
  442. If Date.Now - User.LastReceive > New TimeSpan(0, 2, 0) Then
  443. SendDisconnectRequest(User)
  444. Hint("与 " & User.DisplayName & " 的连接已中断!", HintType.Critical)
  445. End If
  446. Next
  447. Catch ex As Exception
  448. Log(ex, "联机模块工作时钟运行失败", LogLevel.Feedback)
  449. End Try
  450. End Sub
  451. #End Region
  452. #Region "发送请求"
  453. ''' <summary>
  454. ''' 发送 Portsub 请求并等待获取控制台端口。进度将从 0 变化至 80%。
  455. ''' </summary>
  456. Private Shared Sub SendPortsubRequest(User As LinkUserIoi)
  457. Log("[IOI] 尝试建立连接:" & User.Id)
  458. '向对方发送 portsub 请求
  459. Dim Retry As Integer = 0
  460. RetryLink:
  461. Dim Result As JObject = GetJson(NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "PUT", "", "", 100000))
  462. '失败与重试
  463. If Result("msg") IsNot Nothing Then
  464. Dim ErrorMessage As String = Result("msg").ToString
  465. Select Case ErrorMessage
  466. Case "failed to find any peer in table"
  467. Retry += 1
  468. ErrorMessage = "我方网络环境不佳,连接失败。" '初始化对等机列表超时
  469. Log("[IOI] 尝试建立连接结果:未找到对等机,第 " & Retry & " 级重试")
  470. Case "routing: not found"
  471. Retry += 4
  472. ErrorMessage = "我方或对方网络环境不佳,或对方已关闭联机模块,未找到路由。" '未知情况
  473. Log("[IOI] 尝试建立连接结果:无法连接到路由,第 " & Retry & " 级重试")
  474. Case "you are already connected to specified host"
  475. If User.IsDisposed Then Throw New ThreadInterruptedException("用户对象已被释放")
  476. Log("[IOI] 尝试建立连接结果:已与对方连接")
  477. GoTo Done
  478. 'Retry += 1
  479. 'NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 100000)
  480. 'ErrorMessage = "已与对方连接。" '双向连接偶尔导致
  481. 'Log("[IOI] 已与对方连接,尝试中断现有连接,第 " & Retry & " 级重试")
  482. Case "dial backoff"
  483. Retry += 20
  484. ErrorMessage = "对方网络环境不佳,或对方已关闭联机模块。请尝试让对方主动连接你,而不是你去连接对方。" 'NAT2 连 NAT4 导致
  485. Log("[IOI] 尝试建立连接结果:NAT 异常,第 " & Retry & " 级重试")
  486. Case Else
  487. If ErrorMessage.Contains("all dials failed") Then
  488. Retry += 8
  489. ErrorMessage = "我方或对方网络环境不佳,或对方已关闭联机模块,连接失败。" '网络不佳,或对方已关闭模块
  490. Log("[IOI] 尝试建立连接结果:连接失败,第 " & Retry & " 级重试")
  491. Else
  492. Retry += 8
  493. Log("[IOI] 尝试建立连接结果:未知错误(" & ErrorMessage & "),第 " & Retry & " 级重试")
  494. End If
  495. End Select
  496. If User.IsDisposed Then Throw New ThreadInterruptedException("用户对象已被释放")
  497. If Retry <= 64 Then
  498. User.Progress = Retry * 0.01 + 0.05
  499. Thread.Sleep(3000)
  500. GoTo RetryLink
  501. Else
  502. Throw New InvalidOperationException(ErrorMessage)
  503. End If
  504. End If
  505. Log("[IOI] 尝试建立连接结果:成功")
  506. '等待获取对方端口
  507. Done:
  508. Dim WaitCount As Integer = 0
  509. Do Until User.Ports.ContainsKey(55555) AndAlso User.Ports.ContainsKey(55557)
  510. WaitCount += 1
  511. User.Progress = 0.7 + WaitCount / 200 * 0.1 '70% ~ 80%
  512. If WaitCount = 100 Then Throw New Exception("连接超时,请尝试重新连接(未收到端口回报)!")
  513. Thread.Sleep(150)
  514. Loop
  515. User.Progress = 0.8
  516. End Sub
  517. ''' <summary>
  518. ''' 向控制台发送 Connect 请求。
  519. ''' </summary>
  520. Private Shared Sub SendConnectRequest(User As LinkUserIoi)
  521. Dim RawJson As New JObject()
  522. RawJson("version") = RequestVersion
  523. RawJson("name") = GetPlayerName()
  524. RawJson("id") = IoiId
  525. RawJson("type") = "connect"
  526. User.Send(RawJson)
  527. End Sub
  528. ''' <summary>
  529. ''' 向控制台发送 Update 请求。
  530. ''' </summary>
  531. Private Shared Sub SendUpdateRequest(User As LinkUserIoi, Stage As Integer, Optional Unique As Long = -1)
  532. If Unique = -1 Then Unique = GetTimeTick()
  533. Dim RawJson As New JObject
  534. RawJson("name") = GetPlayerName()
  535. RawJson("id") = IoiId
  536. RawJson("type") = "update"
  537. RawJson("stage") = Stage
  538. RawJson("unique") = Unique
  539. If Stage < 3 Then
  540. Dim Rooms As New JArray
  541. For Each Room In RoomListForMe
  542. Dim RoomObject As New JObject
  543. RoomObject("name") = Room.DisplayName
  544. RoomObject("port") = Room.Port
  545. Rooms.Add(RoomObject)
  546. Next
  547. RawJson("rooms") = Rooms
  548. User.PingPending(Unique) = Date.Now
  549. End If
  550. User.Send(RawJson)
  551. End Sub
  552. ''' <summary>
  553. ''' 尝试发送断开请求,并将其从用户列表中移除。
  554. ''' </summary>
  555. Private Shared Sub SendDisconnectRequest(User As LinkUserIoi, Optional Message As String = Nothing, Optional IsError As Boolean = False)
  556. Dim RawJson As New JObject()
  557. RawJson("id") = IoiId
  558. RawJson("type") = "disconnect"
  559. If Message IsNot Nothing Then
  560. RawJson("message") = Message
  561. RawJson("isError") = IsError
  562. End If
  563. Try
  564. User.Send(RawJson)
  565. Thread.Sleep(50)
  566. Catch
  567. End Try
  568. NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
  569. User.Dispose()
  570. End Sub
  571. #End Region
  572. #Region "左边栏操作"
  573. '刷新连接
  574. Private Shared Sub BtnListRefresh_Click(sender As MyIconButton, e As EventArgs)
  575. Dim User As LinkUserIoi = sender.Tag
  576. User.PingRecord.Clear()
  577. FrmLinkIoi.RefreshUi()
  578. RunInThread(Sub()
  579. Try
  580. SendUpdateRequest(User, 1)
  581. Catch ex As Exception
  582. If InitLoader.State = LoadState.Finished Then Log(ex, "刷新与 " & User.DisplayName & " 的连接失败", LogLevel.Hint)
  583. End Try
  584. End Sub)
  585. End Sub
  586. '断开连接
  587. Private Shared Sub BtnListDisconnect_Click(sender As MyIconButton, e As EventArgs)
  588. Dim User As LinkUserIoi = sender.Tag
  589. sender.IsEnabled = False
  590. RunInThread(Sub()
  591. If User.Progress < 1 AndAlso User.RelativeThread IsNot Nothing AndAlso User.RelativeThread.IsAlive Then
  592. NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
  593. User.Dispose()
  594. Else
  595. SendDisconnectRequest(User, GetPlayerName() & " 主动断开了连接!")
  596. End If
  597. End Sub)
  598. End Sub
  599. '复制联机码
  600. Public Shared Sub BtnLeftCopy_Click() Handles BtnLeftCopy.Click
  601. ClipboardSet(IoiId.Substring(4) & SecretEncrypt(GetPlayerName), False)
  602. Hint("已复制联机码!", HintType.Finish)
  603. End Sub
  604. #End Region
  605. #Region "玩家名"
  606. ''' <summary>
  607. ''' 获取当前的玩家名。
  608. ''' </summary>
  609. Public Shared Function GetPlayerName() As String
  610. '自动生成玩家名
  611. If AutogenPlayerName Is Nothing Then
  612. If IsPlayerNameValid(McLoginName) Then
  613. AutogenPlayerName = McLoginName()
  614. Else
  615. AutogenPlayerName = "玩家 " & CType(GetHash(If(UniqueAddress, "")) Mod 1048576, Integer).ToString("x5").ToUpper
  616. End If
  617. End If
  618. '获取玩家自定义的名称
  619. Dim CustomName As String = Setup.Get("LinkName").ToString.Trim()
  620. If CustomName <> "" Then
  621. If IsPlayerNameValid(CustomName) Then
  622. Return CustomName.Trim
  623. Else
  624. Hint("你所设置的玩家名存在异常,已被重置!", HintType.Critical)
  625. Setup.Set("LinkName", "")
  626. End If
  627. End If
  628. '使用自动生成的玩家名
  629. Return AutogenPlayerName
  630. End Function
  631. Private Shared AutogenPlayerName As String = Nothing '并非由玩家自定义,而是自动生成的玩家名
  632. ''' <summary>
  633. ''' 检查某个玩家名是否合法。
  634. ''' </summary>
  635. Private Shared Function IsPlayerNameValid(Name As String) As Boolean
  636. For Each ValidateRule As Validate In {New ValidateNullOrWhiteSpace, New ValidateLength(0, 20), New ValidateFilter}
  637. If Not String.IsNullOrEmpty(ValidateRule.Validate(Name)) Then Return False
  638. Next
  639. Return True
  640. End Function
  641. #End Region
  642. #Region "请求核心"
  643. Private Shared Listener As Socket = Nothing
  644. ''' <summary>
  645. ''' 启动 Socket 监听核心。
  646. ''' </summary>
  647. Public Shared Sub StartSocketListener()
  648. If Listener IsNot Nothing Then Exit Sub '已经启动过了
  649. Listener = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) With {.ReceiveTimeout = 1000000000, .SendTimeout = 1000000000}
  650. Listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, True)
  651. Try
  652. Listener.Bind(New IPEndPoint(IPAddress.Any, 55557))
  653. Catch ex As Exception
  654. Log(ex, "初次启动 Socket 监听核心失败,开始重试")
  655. Thread.Sleep(1000)
  656. Listener.Bind(New IPEndPoint(IPAddress.Any, 55557))
  657. End Try
  658. Listener.Listen(100)
  659. Log("[IOI] 已启动 Socket 监听核心")
  660. RunInNewThread(Sub()
  661. While True
  662. Try
  663. '获取新的 Socket
  664. Dim bytes(1024) As Byte
  665. Dim ClientSocket As Socket = Listener.Accept()
  666. '获取初始输入信息
  667. Dim RequestInput As String = Encoding.UTF8.GetString(bytes, 0, ClientSocket.Receive(bytes))
  668. If Not RequestInput.StartsWith("PCL - ") Then Exit Sub
  669. Dim RawData As String
  670. Try
  671. RawData = SecretDecrypt(WebUtility.UrlDecode(RequestInput.Substring(6)))
  672. Log("[IOI] 接收到新的 Socket:" & RawData)
  673. PageLinkIoi.ReceiveJson(GetJson(RawData), ClientSocket)
  674. Catch ex As Exception
  675. Log(ex, "新的 Socket 处理出错(内容:" & RequestInput & ")")
  676. End Try
  677. Catch ex As Exception
  678. Log(ex, "Socket 监听出错")
  679. Thread.Sleep(3000) '防止死循环出错
  680. End Try
  681. End While
  682. End Sub, "Socket Listener")
  683. End Sub
  684. #End Region
  685. #Region "用户核心"
  686. '用户基类
  687. Public MustInherit Class LinkUserBase
  688. Implements IDisposable
  689. '基础数据
  690. Public Uuid As Integer = GetUuid()
  691. Public Id As String
  692. Public DisplayName As String
  693. '请求管理
  694. Public Socket As Socket = Nothing
  695. Public Sub Send(Request As JObject)
  696. If Socket Is Nothing Then Throw New Exception("该用户尚未绑定 Socket")
  697. Dim Data As String = Request.ToString(Newtonsoft.Json.Formatting.None)
  698. If ModeDebug Then Log("[IOI] 发送联机数据包(" & DisplayName & "):" & Data)
  699. Data = "PCL - " & WebUtility.UrlEncode(SecretEncrypt(Data))
  700. Me.LastSend = Date.Now
  701. Socket.Send(Encoding.UTF8.GetBytes(Data))
  702. End Sub
  703. Public ListenerThread As Thread = Nothing
  704. Public Sub StartListener()
  705. If ListenerThread IsNot Nothing Then Exit Sub
  706. '启动监听
  707. ListenerThread = RunInNewThread(Sub()
  708. Try
  709. Dim bytes(1024) As Byte
  710. While PageLinkIoi.UserList.ContainsValue(Me)
  711. Dim RequestInput As String = Encoding.UTF8.GetString(bytes, 0, Socket.Receive(bytes))
  712. If Not RequestInput.StartsWith("PCL - ") Then Exit Sub
  713. Dim RawData As String = SecretDecrypt(WebUtility.UrlDecode(RequestInput.Substring(6)))
  714. If ModeDebug Then Log("[IOI] 接收联机数据包(" & DisplayName & "):" & RawData)
  715. PageLinkIoi.ReceiveJson(GetJson(RawData)) '调用去实际的联机模块
  716. End While
  717. Catch ex As ThreadInterruptedException
  718. Log("[IOI] 用户监听已中断(" & DisplayName & ")")
  719. Catch ex As Exception
  720. If IsDisposed Then Exit Sub
  721. If ex.GetType.Equals(GetType(SocketException)) AndAlso CType(ex, SocketException).SocketErrorCode = 10053 Then
  722. Log("[IOI] 客户端已关闭(" & DisplayName & ")")
  723. PageLinkIoi.UserRemove(CType(Me, PageLinkIoi.LinkUserIoi), True)
  724. Exit Sub
  725. End If
  726. Log(ex, "用户监听出错(" & DisplayName & ")")
  727. PageLinkIoi.UserRemove(CType(Me, PageLinkIoi.LinkUserIoi), True)
  728. End Try
  729. End Sub, "Link Listener " & DisplayName)
  730. End Sub
  731. Public Sub BindSocket(Socket As Socket)
  732. If Me.Socket IsNot Nothing Then Throw New Exception("该用户已经绑定了 Socket")
  733. Me.Socket = Socket
  734. StartListener()
  735. End Sub
  736. 'Ping
  737. '0:与 Ping 计算无关,不回应
  738. '1:A to B,2:B to A,3:A to B
  739. Public PingPending As New Dictionary(Of Long, Date)
  740. Public PingRecord As New Queue(Of Integer)
  741. '心跳包
  742. Public LastSend As Date = Date.Now
  743. Public LastReceive As Date = Date.Now
  744. '类型转换
  745. Public Sub New(Id As String, DisplayName As String)
  746. Me.Id = Id
  747. Me.DisplayName = DisplayName
  748. Log("[IOI] 无通信包的新用户对象:" & ToString())
  749. End Sub
  750. Public Sub New(Id As String, DisplayName As String, Socket As Socket)
  751. Me.Id = Id
  752. Me.DisplayName = DisplayName
  753. Me.Socket = Socket
  754. Log("[IOI] 新用户对象:" & ToString())
  755. StartListener()
  756. End Sub
  757. Public Overrides Function ToString() As String
  758. Return DisplayName & " @ " & Id & " #" & Uuid
  759. End Function
  760. Public Shared Widening Operator CType(User As LinkUserBase) As String
  761. Return User.ToString
  762. End Operator
  763. '释放资源
  764. Public IsDisposed As Boolean = False
  765. Protected Overridable Sub Dispose(IsDisposing As Boolean)
  766. If Socket IsNot Nothing Then Socket.Dispose()
  767. If ListenerThread IsNot Nothing AndAlso ListenerThread.IsAlive Then ListenerThread.Interrupt()
  768. End Sub
  769. Public Sub Dispose() Implements IDisposable.Dispose
  770. If Not IsDisposed Then
  771. IsDisposed = True
  772. Dispose(True)
  773. End If
  774. GC.SuppressFinalize(Me)
  775. End Sub
  776. End Class
  777. '用户对象
  778. Public Shared UserList As New Dictionary(Of String, LinkUserIoi)
  779. Public Class LinkUserIoi
  780. Inherits LinkUserBase
  781. Public Sub New(Id As String, DisplayName As String, Socket As Socket)
  782. MyBase.New(Id, DisplayName, Socket)
  783. End Sub
  784. Public Sub New(Id As String, DisplayName As String)
  785. MyBase.New(Id, DisplayName)
  786. End Sub
  787. '基础数据
  788. Public Ports As New Dictionary(Of Integer, String)
  789. Public Rooms As New List(Of RoomEntry)
  790. '进度与 UI
  791. Public Progress As Double = 0
  792. Public RelativeThread As Thread = Nothing
  793. Public Function GetDescription() As String
  794. Return If(Progress < 1,
  795. "正在连接," & Math.Round(Progress * 100) & "%",
  796. "已连接," & If(PingRecord.Count = 0, "检查延迟中", Math.Round(PingRecord.Average) & "ms"))
  797. End Function
  798. Public Function ToListItem() As MyListItem
  799. Dim Item As New MyListItem With {
  800. .Title = DisplayName, .Height = 42, .Tag = Me, .Type = MyListItem.CheckType.None,
  801. .PaddingRight = 60,
  802. .Logo = "pack://application:,,,/images/Blocks/Grass.png"}
  803. '绑定图标按钮
  804. Dim BtnRefresh As New MyIconButton With {.Logo = Logo.IconButtonRefresh, .LogoScale = 0.85, .ToolTip = "刷新", .Tag = Me}
  805. AddHandler BtnRefresh.Click, AddressOf BtnListRefresh_Click
  806. ToolTipService.SetPlacement(BtnRefresh, Primitives.PlacementMode.Bottom)
  807. ToolTipService.SetHorizontalOffset(BtnRefresh, -10)
  808. ToolTipService.SetVerticalOffset(BtnRefresh, 5)
  809. ToolTipService.SetInitialShowDelay(BtnRefresh, 200)
  810. Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "断开", .Tag = Me}
  811. AddHandler BtnClose.Click, AddressOf BtnListDisconnect_Click
  812. ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
  813. ToolTipService.SetHorizontalOffset(BtnClose, -10)
  814. ToolTipService.SetVerticalOffset(BtnClose, 5)
  815. ToolTipService.SetInitialShowDelay(BtnClose, 200)
  816. Item.Buttons = {BtnRefresh, BtnClose}
  817. '刷新并返回
  818. RefreshUi(Item)
  819. Return Item
  820. End Function
  821. Public Sub RefreshUi(RelatedListItem As MyListItem)
  822. RelatedListItem.Title = DisplayName
  823. RelatedListItem.Info = GetDescription()
  824. RelatedListItem.Buttons(0).Visibility = If(Progress = 1, Visibility.Visible, Visibility.Collapsed)
  825. End Sub
  826. '释放
  827. Protected Overrides Sub Dispose(IsDisposing As Boolean)
  828. Log("[IOI] 用户资源释放(IOI, " & DisplayName & ")")
  829. If RelativeThread IsNot Nothing AndAlso RelativeThread.IsAlive Then RelativeThread.Interrupt()
  830. UserList.Remove(Id)
  831. MyBase.Dispose(IsDisposing)
  832. End Sub
  833. End Class
  834. '房间对象
  835. Private Shared RoomListForMe As New List(Of RoomEntry)
  836. Private Function GetRoomList() As List(Of RoomEntry)
  837. Dim RoomList As New List(Of RoomEntry)(RoomListForMe)
  838. For i = 0 To UserList.Count - 1
  839. If i > UserList.Count - 1 Then Exit For
  840. RoomList.AddRange(UserList.Values(i).Rooms)
  841. Next
  842. Return RoomList
  843. End Function
  844. Public Class RoomEntry
  845. '基础数据
  846. Public Port As Integer
  847. Public DisplayName As String
  848. Public User As LinkUserIoi = Nothing '若 IsOwner = True,则此项为 Nothing
  849. Public IsOwner As Boolean
  850. Public ReadOnly Property Ip As String
  851. Get
  852. If IsOwner Then
  853. Return "localhost:" & Port
  854. Else
  855. Return User.Ports(Port) & ":" & Port
  856. End If
  857. End Get
  858. End Property
  859. '类型转换
  860. Public Sub New(Port As Integer, DisplayName As String, Optional User As LinkUserIoi = Nothing)
  861. Me.IsOwner = User Is Nothing
  862. Me.User = User
  863. Me.DisplayName = DisplayName
  864. Me.Port = Port
  865. End Sub
  866. Public Overrides Function ToString() As String
  867. Return DisplayName & " - " & Port & " - " & IsOwner
  868. End Function
  869. Public Shared Widening Operator CType(Room As RoomEntry) As String
  870. Return Room.ToString
  871. End Operator
  872. Public Shared Function SelectPort(Room As RoomEntry) As Integer
  873. Return Room.Port
  874. End Function
  875. 'UI
  876. Public Function GetDescription() As String
  877. If IsOwner Then
  878. Return "由我创建,端口 " & Port
  879. Else
  880. Return "由 " & User.DisplayName & " 创建,端口 " & Port
  881. End If
  882. End Function
  883. Public Function ToListItem() As MyListItem
  884. Dim Item As New MyListItem With {
  885. .Title = DisplayName, .Height = 42, .Info = GetDescription(), .Tag = Me, .PaddingRight = If(IsOwner, 60, 0),
  886. .Type = If(IsOwner, MyListItem.CheckType.None, MyListItem.CheckType.Clickable),
  887. .Logo = "pack://application:,,,/images/Blocks/" & If(IsOwner, "GrassPath", "Grass") & ".png"}
  888. If IsOwner Then
  889. '绑定图标按钮
  890. Dim BtnEdit As New MyIconButton With {.Logo = Logo.IconButtonEdit, .LogoScale = 1, .ToolTip = "修改名称", .Tag = Me}
  891. AddHandler BtnEdit.Click, AddressOf BtnRoomEdit_Click
  892. ToolTipService.SetPlacement(BtnEdit, Primitives.PlacementMode.Bottom)
  893. ToolTipService.SetHorizontalOffset(BtnEdit, -22)
  894. ToolTipService.SetVerticalOffset(BtnEdit, 5)
  895. ToolTipService.SetInitialShowDelay(BtnEdit, 200)
  896. Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "关闭", .Tag = Me}
  897. AddHandler BtnClose.Click, AddressOf BtnRoomClose_Click
  898. ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
  899. ToolTipService.SetHorizontalOffset(BtnClose, -10)
  900. ToolTipService.SetVerticalOffset(BtnClose, 5)
  901. ToolTipService.SetInitialShowDelay(BtnClose, 200)
  902. Item.Buttons = {BtnEdit, BtnClose}
  903. Else
  904. '绑定点击事件
  905. AddHandler Item.Click, AddressOf BtnRoom_Click
  906. End If
  907. Return Item
  908. End Function
  909. Public Sub RefreshUi(RelatedListItem As MyListItem)
  910. RelatedListItem.Title = DisplayName
  911. RelatedListItem.Info = GetDescription()
  912. End Sub
  913. End Class
  914. #End Region
  915. '正向与反向连接
  916. Public Shared Sub BtnLeftCreate_Click() Handles BtnLeftCreate.Click
  917. ''获取信息
  918. 'Dim Code As String = MyMsgBoxInput("", New ObjectModel.Collection(Of Validate) From {New ValidateLength(9, 99999)}, "", "输入对方的联机码", "确定", "取消")
  919. 'If Code Is Nothing Then Exit Sub
  920. ''检查
  921. 'If Code.StartsWith("P") AndAlso Code.Length < 48 Then
  922. ' Hint("你输入的可能是 HiPer 的联机码,请在左侧的联机方式中选择 HiPer!", HintType.Critical) : Exit Sub
  923. 'End If
  924. 'Dim Id As String, DisplayName As String
  925. 'Try '解密失败检查
  926. ' Id = "12D3" & Code.Substring(0, 48)
  927. ' DisplayName = SecretDecrypt(Code.Substring(48))
  928. 'Catch
  929. ' Hint("你输入的联机码有误!", HintType.Critical)
  930. ' Exit Sub
  931. 'End Try
  932. 'If Id = IoiId Then '自我连接检查
  933. ' Hint("我连我自己?搁这卡 Bug 呢?", HintType.Critical)
  934. ' Exit Sub
  935. 'End If
  936. ''开始
  937. 'Dim User As New LinkUserIoi(Id, DisplayName)
  938. 'User.RelativeThread = RunInNewThread(Sub()
  939. ' Dim WaitCount As Integer = 0
  940. ' Try
  941. ' '加入列表
  942. ' If UserList.ContainsKey(Id) Then
  943. ' Hint(UserList(Id).DisplayName & " 已在列表中,无需再次连接!")
  944. ' Exit Sub
  945. ' Else
  946. ' UserList.Add(Id, User)
  947. ' End If
  948. ' '发送 portsub 请求(0% -> 80%)
  949. ' SendPortsubRequest(User)
  950. ' '构建 Socket(81% -> 82%)
  951. ' Dim ClientSocket As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) With {.ReceiveTimeout = 1000000000, .SendTimeout = 1000000000}
  952. ' ClientSocket.Connect(New IPEndPoint(IPAddress.Parse(User.Ports(55557)), 55557))
  953. ' User.BindSocket(ClientSocket)
  954. ' User.Progress = 0.82
  955. ' '发送 connect 请求(83% -> 85%)
  956. ' SendConnectRequest(User)
  957. ' User.Progress = 0.85
  958. ' '等待对方向自己请求
  959. ' Log("[IOI] 加入成功,等待反向请求")
  960. ' Do While User.Progress < 0.9999
  961. ' User.Progress += 0.0002
  962. ' If User.Progress > 0.98 AndAlso User.Progress < 0.9999 Then Throw New Exception("对方未回应连接请求!")
  963. ' Thread.Sleep(100)
  964. ' Loop
  965. ' Hint("已连接到 " & User.DisplayName & "!", HintType.Finish)
  966. ' Catch ex As ThreadInterruptedException
  967. ' Log("[IOI] 已中断主动发起的连接(" & User.DisplayName & ")")
  968. ' Catch ex As InvalidOperationException
  969. ' 'API 返回的错误
  970. ' NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
  971. ' User.Dispose()
  972. ' If InitLoader.State = LoadState.Finished Then Log("与 " & DisplayName & " 建立连接失败:" & ex.Message, LogLevel.Msgbox)
  973. ' Catch ex As Exception
  974. ' '常规错误
  975. ' NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
  976. ' User.Dispose()
  977. ' If ex.InnerException IsNot Nothing AndAlso TypeOf ex.InnerException Is ThreadInterruptedException Then Exit Sub
  978. ' If InitLoader.State = LoadState.Finished Then Log(ex, "与 " & DisplayName & " 建立连接失败", LogLevel.Msgbox)
  979. ' End Try
  980. ' End Sub, "Link Create " & DisplayName)
  981. End Sub
  982. Private Shared Sub SendPortsubBack(User As LinkUserIoi, TargetVersion As Integer)
  983. Try
  984. SendPortsubRequest(User)
  985. User.Progress = 0.9
  986. If TargetVersion > RequestVersion Then
  987. SendDisconnectRequest(User, "无法连接到 " & GetPlayerName() & ":对方的 PCL 版本过低!", True)
  988. Throw New InvalidOperationException("你的 PCL 版本过低!")
  989. ElseIf TargetVersion < RequestVersion Then
  990. SendDisconnectRequest(User, "无法连接到 " & GetPlayerName() & ":你的 PCL 版本过低!", True)
  991. Throw New InvalidOperationException("对方的 PCL 版本过低!")
  992. Else
  993. SendUpdateRequest(User, 1)
  994. User.Progress = 1
  995. Hint(User.DisplayName & " 已与你建立连接!")
  996. End If
  997. Catch ex As ThreadInterruptedException
  998. Log("[IOI] 已中断被动建立的连接(" & User.DisplayName & ")")
  999. Catch ex As InvalidOperationException
  1000. 'API 返回的错误
  1001. NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
  1002. User.Dispose()
  1003. If InitLoader.State = LoadState.Finished Then Log("与 " & User.DisplayName & " 建立连接失败:" & ex.Message, LogLevel.Hint)
  1004. Catch ex As Exception
  1005. '常规错误
  1006. NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
  1007. User.Dispose()
  1008. If InitLoader.State = LoadState.Finished Then Log(ex, "与 " & User.DisplayName & " 建立连接失败", LogLevel.Hint)
  1009. End Try
  1010. End Sub
  1011. '创建房间
  1012. Private Sub LinkCreate() Handles BtnCreate.Click
  1013. 'If MyMsgBox("请先进入 MC 并暂停游戏,在暂停页面选择对局域网开放,然后在下一个窗口输入 MC 显示的端口号。" & vbCrLf & "若使用服务端开服,则直接在下一个窗口输入服务器配置中的端口号即可。", "提示", "继续", "取消") = 2 Then Exit Sub
  1014. ''获取端口号
  1015. 'Dim Port As String = MyMsgBoxInput("", New ObjectModel.Collection(Of Validate) From {
  1016. ' New ValidateInteger(0, 65535),
  1017. ' New ValidateExceptSame({"55555", "55557"}, "端口不能为 %!"),
  1018. ' New ValidateExceptSame(RoomListForMe.Select(AddressOf RoomEntry.SelectPort), "端口 % 已创建过房间,请在删除该房间后继续!")
  1019. ' }, "", "输入端口号", "确定", "取消")
  1020. 'If Port Is Nothing Then Exit Sub
  1021. ''获取显示名称
  1022. 'Dim DisplayName As String = MyMsgBoxInput(GetPlayerName() & " 的房间 - " & Port, New ObjectModel.Collection(Of Validate) From {
  1023. ' New ValidateNullOrWhiteSpace(), New ValidateLength(1, 40), New ValidateFilter()
  1024. ' }, "", "输入房间名(建议包含游戏版本等信息)", "确定", "取消")
  1025. 'If DisplayName Is Nothing Then Exit Sub
  1026. 'DisplayName = DisplayName.Trim
  1027. ''开始
  1028. 'RunInThread(Sub()
  1029. ' Try
  1030. ' '请求
  1031. ' Dim Result As JObject = GetJson(NetRequestOnce("http://127.0.0.1:55555/api/port?proto=tcp&port=" & Port & "&password=" & IoiPassword, "PUT", "", "", 100000))
  1032. ' If Result("msg") IsNot Nothing Then Throw New InvalidOperationException(Result("msg").ToString)
  1033. ' '成功
  1034. ' RoomListForMe.Add(New RoomEntry(Port, DisplayName))
  1035. ' Hint("房间 " & DisplayName & " 已创建!", HintType.Finish)
  1036. ' SendUpdateRequestToAllUsers()
  1037. ' Catch ex As InvalidOperationException
  1038. ' 'API 返回的错误
  1039. ' Log("创建房间失败:" & ex.Message, LogLevel.Msgbox)
  1040. ' Catch ex As Exception
  1041. ' '常规错误
  1042. ' Log(ex, "创建房间失败", LogLevel.Msgbox)
  1043. ' End Try
  1044. ' End Sub)
  1045. End Sub
  1046. Private Shared Sub SendUpdateRequestToAllUsers()
  1047. For i = 0 To UserList.Count - 1
  1048. If i > UserList.Count - 1 Then Exit For
  1049. Dim User = UserList.Values(i)
  1050. If User.Progress < 1 Then Continue For
  1051. Try
  1052. SendUpdateRequest(User, 1) '不需要使用多线程,发送实际会瞬间完成
  1053. Catch ex As Exception
  1054. Log(ex, "发送全局刷新请求失败(" & User.DisplayName & ")")
  1055. End Try
  1056. Next
  1057. End Sub
  1058. '修改房间名称
  1059. Private Shared Sub BtnRoomEdit_Click(sender As MyIconButton, e As EventArgs)
  1060. 'Dim Room As RoomEntry = sender.Tag
  1061. ''获取房间名
  1062. 'Dim DisplayName As String = MyMsgBoxInput(Room.DisplayName, New ObjectModel.Collection(Of Validate) From {
  1063. ' New ValidateNullOrWhiteSpace(),
  1064. ' New ValidateLength(1, 40)
  1065. ' }, "", "输入房间名(建议包含游戏版本等信息)", "确定", "取消")
  1066. 'If DisplayName Is Nothing Then Exit Sub
  1067. 'DisplayName = DisplayName.Trim
  1068. ''修改
  1069. 'Room.DisplayName = DisplayName
  1070. 'FrmLinkIoi.RefreshUi()
  1071. 'SendUpdateRequestToAllUsers()
  1072. End Sub
  1073. '加入房间
  1074. Private Shared Sub BtnRoom_Click(sender As MyListItem, e As EventArgs)
  1075. Dim Room As RoomEntry = sender.Tag
  1076. If MyMsgBox("请在多人游戏页面点击直接连接,输入 " & Room.Ip & " 以进入服务器!", "加入房间", "复制地址", "确定") = 1 Then
  1077. ClipboardSet(Room.Ip)
  1078. End If
  1079. End Sub
  1080. '关闭房间
  1081. Private Shared Sub BtnRoomClose_Click(sender As MyIconButton, e As EventArgs)
  1082. Dim Room As RoomEntry = sender.Tag
  1083. RunInThread(Sub()
  1084. Try
  1085. '远程移除
  1086. Dim Result As JObject = GetJson(NetRequestOnce("http://127.0.0.1:55555/api/port?proto=tcp&port=" & Room.Port & "&password=" & IoiPassword, "DELETE", "", "", 100000))
  1087. If Result("msg") IsNot Nothing Then Throw New InvalidOperationException(Result("msg").ToString)
  1088. '本地移除
  1089. RoomListForMe.Remove(Room)
  1090. '成功
  1091. RunInUi(Sub() FrmLinkIoi.RefreshUi())
  1092. SendUpdateRequestToAllUsers()
  1093. Catch ex As InvalidOperationException
  1094. 'API 返回的错误
  1095. If InitLoader.State = LoadState.Finished Then Log("移除房间失败:" & ex.Message, LogLevel.Msgbox)
  1096. Catch ex As Exception
  1097. '常规错误
  1098. If InitLoader.State = LoadState.Finished Then Log(ex, "移除房间失败", LogLevel.Msgbox)
  1099. End Try
  1100. End Sub)
  1101. End Sub
  1102. '获取数据包
  1103. Public Shared Sub ReceiveJson(JsonData As JObject, Optional NewSocket As Socket = Nothing)
  1104. '获取数据
  1105. Dim Id As String = JsonData("id"), Type As String = JsonData("type")
  1106. Select Case Type
  1107. Case "connect"
  1108. Dim DisplayName As String = JsonData("name")
  1109. Dim User As New LinkUserIoi(Id, DisplayName, NewSocket)
  1110. '如果发生了双向连接
  1111. If UserList.ContainsKey(Id) Then
  1112. If Id > IoiId Then
  1113. Log("[IOI] 双向连接,应当抛弃当前用户(" & DisplayName & ")")
  1114. For Each Pair In UserList(User.Id).Ports.ToList
  1115. User.Ports(Pair.Key) = Pair.Value
  1116. Next
  1117. UserList(Id).Dispose()
  1118. Else
  1119. '应当保留当前用户
  1120. Log("[IOI] 双向连接,应当保留当前用户(" & DisplayName & ")")
  1121. NewSocket.Dispose()
  1122. Exit Sub
  1123. End If
  1124. End If
  1125. '加入列表
  1126. UserList.Add(Id, User)
  1127. '返回请求
  1128. User.RelativeThread = RunInNewThread(Sub()
  1129. SendPortsubBack(User, JsonData("version").ToObject(Of Integer))
  1130. End Sub, "Link Connect " & DisplayName)
  1131. '更新时间
  1132. User.LastReceive = Date.Now
  1133. Case "update"
  1134. If Not UserList.ContainsKey(Id) Then Throw New Exception("未在列表中的用户发送了更新请求:" & Id)
  1135. Dim User = UserList(Id)
  1136. '拉满进度(该请求也作为反向连接回应出现,用于向正向连接方传达连接已完成信号)
  1137. User.Progress = 1
  1138. '更新名称
  1139. Dim DisplayName As String = JsonData("name")
  1140. User.DisplayName = DisplayName
  1141. '更新房间列表
  1142. If JsonData("rooms") IsNot Nothing Then
  1143. User.Rooms = New List(Of RoomEntry)
  1144. For Each RoomObject In JsonData("rooms")
  1145. User.Rooms.Add(New RoomEntry(RoomObject("port"), RoomObject("name"), User))
  1146. Next
  1147. End If
  1148. '更新 Ping
  1149. Dim Stage As Integer = JsonData("stage"), Unique As Long = JsonData("unique")
  1150. If Stage > 1 Then
  1151. User.PingRecord.Enqueue((Date.Now - User.PingPending(Unique)).TotalMilliseconds / 2)
  1152. If User.PingRecord.Count > 5 Then User.PingRecord.Dequeue()
  1153. User.PingPending.Remove(Unique)
  1154. End If
  1155. '返回请求
  1156. If Stage > 0 AndAlso Stage < 3 Then RunInNewThread(Sub()
  1157. Try
  1158. SendUpdateRequest(User, Stage + 1, Unique)
  1159. Catch ex As Exception
  1160. Log(ex, "发送回程请求失败")
  1161. End Try
  1162. End Sub, "Link Update " & DisplayName)
  1163. '更新时间
  1164. User.LastReceive = Date.Now
  1165. Case "disconnect"
  1166. '断开连接
  1167. If Not UserList.ContainsKey(Id) Then Exit Sub
  1168. UserRemove(UserList(Id), ShowLeaveMessage:=JsonData("message") Is Nothing)
  1169. If JsonData("message") IsNot Nothing Then Hint(JsonData("message").ToString, If(JsonData("isError").ToObject(Of Boolean), HintType.Critical, HintType.Info))
  1170. Case Else
  1171. Throw New Exception("未知的操作种类:" & Type)
  1172. End Select
  1173. End Sub
  1174. ''' <summary>
  1175. ''' 从用户列表中移除一位用户。提示信息视作该用户主动离开。
  1176. ''' </summary>
  1177. Public Shared Sub UserRemove(User As LinkUserIoi, ShowLeaveMessage As Boolean)
  1178. If Not UserList.ContainsKey(User.Id) Then Exit Sub
  1179. If ShowLeaveMessage Then Hint(User.DisplayName & " 已离开!")
  1180. NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
  1181. User.Dispose()
  1182. End Sub
  1183. '关闭联机模块按钮
  1184. Private Shared Sub ModuleStateChanged(Loader As LoaderBase, NewState As LoadState, OldState As LoadState) Handles InitLoader.OnStateChangedUi
  1185. If FrmLinkLeft IsNot Nothing Then CType(FrmLinkLeft.ItemIoi.Buttons(0), MyIconButton).Visibility = If(NewState = LoadState.Finished, Visibility.Visible, Visibility.Collapsed)
  1186. End Sub
  1187. Public Shared Sub ModuleStopManually()
  1188. IoiStop(False)
  1189. InitLoader.Error = New Exception("联机模块已关闭,点击以重新启动")
  1190. InitLoader.State = LoadState.Failed
  1191. IoiState = LoadState.Failed
  1192. End Sub
  1193. End Class