PageLinkIoi.xaml.vb 64 KB

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