From 5ef1d60e2e0bef93d606f16647e54049cde1ef00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=BE=99=E8=85=BE=E7=8C=AB=E8=B7=83?= <1043137532@qq.com>
Date: Thu, 10 Nov 2022 20:22:47 +0800
Subject: [PATCH] =?UTF-8?q?=E7=94=B1=E4=BA=8E=E8=81=94=E6=9C=BA=E5=B7=B2?=
=?UTF-8?q?=E8=A2=AB=E7=A6=81=E7=94=A8=EF=BC=8C=E5=85=AC=E5=BC=80=E8=81=94?=
=?UTF-8?q?=E6=9C=BA=E9=83=A8=E5=88=86=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 3 -
licence.txt => LICENCE | 0
.../Pages/PageLink/PageLinkHiper.xaml.vb | 952 +++++++++++++
.../Pages/PageLink/PageLinkIoi.xaml.vb | 1262 +++++++++++++++++
4 files changed, 2214 insertions(+), 3 deletions(-)
rename licence.txt => LICENCE (100%)
create mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb
create mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb
diff --git a/.gitignore b/.gitignore
index c346e334..efe225f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -349,9 +349,6 @@ ModSetup.vb
PageOtherAbout.*
PageOtherTest.*
PageSetupUI.*
-# 公开联机代码可能导致安全风险
-Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb
-Plain Craft Launcher 2/Pages/PageLink/PageLinkHiPer.xaml.vb
# 开发者工具
ModDevelop.vb
# 不公开底层函数库,以免抄袭
diff --git a/licence.txt b/LICENCE
similarity index 100%
rename from licence.txt
rename to LICENCE
diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb
new file mode 100644
index 00000000..4c0ffa62
--- /dev/null
+++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb
@@ -0,0 +1,952 @@
+Public Class PageLinkHiper
+ Public Const RequestVersion As Char = "2"
+
+ '记录的启动情况
+ Public Shared IsServerSide As Boolean
+ Private Shared HostIp As String
+ Private Shared HostPort As Integer
+
+#Region "初始化"
+
+ '加载器初始化
+ Private Sub LoaderInit() Handles Me.Initialized
+ PageLoaderInit(Load, PanLoad, PanContent, PanAlways, InitLoader, AutoRun:=False)
+ '注册自定义的 OnStateChanged
+ AddHandler InitLoader.OnStateChangedUi, AddressOf OnLoadStateChanged
+ End Sub
+
+ Private IsLoad As Boolean = False
+ Private Sub OnLoaded() Handles Me.Loaded
+ If IsLoad Then Exit Sub
+ IsLoad = True
+ '启动监视线程
+ If Not IsWatcherStarted Then RunInNewThread(AddressOf WatcherThread, "Hiper Watcher")
+ '读取索引码
+ Try
+ Dim Time As String = Setup.Get("LinkHiperCertTime")
+ If Time = "" Then
+ Log("[HiPer] 没有缓存凭证")
+ ElseIf Date.Parse(Time) > Date.Now Then
+ TextCert.Text = Setup.Get("LinkHiperCertLast")
+ Log("[HiPer] 缓存凭证尚未过期:" & Time)
+ CurrentSubpage = Subpages.PanSelect
+ Else
+ Log("[HiPer] 缓存凭证已过期:" & Time)
+ LabCertTitle.Text = "输入索引码"
+ LabCertDesc.Text = "你的 HiPer 索引码已经过期,请输入新的索引码。" & vbCrLf & "如果实在没有索引码,可以在左侧选择 IOI 方式联机。"
+ End If
+ Catch ex As Exception
+ Log(ex, "读取缓存凭证失败")
+ Setup.Set("LinkHiperCertTime", "")
+ End Try
+ End Sub
+
+#End Region
+
+#Region "加载步骤"
+
+ Public Shared PathHiper As String = PathAppdata & "联机模块\"
+ Public Shared WithEvents InitLoader As New LoaderCombo(Of Integer)("HiPer 初始化", {
+ New LoaderTask(Of Integer, Integer)("网络环境:连通检测", AddressOf InitPingCheck) With {.Block = False, .ProgressWeight = 0.5},
+ New LoaderTask(Of Integer, Integer)("网络环境:IP 检测", AddressOf InitIpCheck) With {.Block = False, .ProgressWeight = 1},
+ New LoaderTask(Of Integer, Integer)("检查网络环境", AddressOf InitCheck) With {.ProgressWeight = 0.5},
+ New LoaderTask(Of Integer, List(Of NetFile))("获取所需文件", AddressOf InitGetFile) With {.ProgressWeight = 4},
+ New LoaderDownload("下载所需文件", New List(Of NetFile)) With {.ProgressWeight = 4},
+ New LoaderTask(Of Integer, Integer)("启动联机模块", AddressOf InitLaunch) With {.ProgressWeight = 7}
+ })
+
+ '检查网络状态
+ Private Shared Sub InitPingCheck(Task As LoaderTask(Of Integer, Integer))
+ PingTime = 0
+ Try
+ Log("[HiPer] 网络检测:连通检测开始")
+ Dim StartTime = Date.Now.Ticks
+ Dim Result As String = NetGetCodeByClient("https://www.baidu.com/duty/", Encoding.UTF8, 20000, "")
+ Dim DuringTime As Integer = (Date.Now.Ticks - StartTime) / 10000
+ If Result.Contains("百度") Then
+ Log("[HiPer] 网络检测:连通检测成功(" & DuringTime & "ms)")
+ PingTime = DuringTime
+ Else
+ Log("[HiPer] 网络检测:连通检测失败(获取的内容有误)")
+ PingTime = -1
+ End If
+ Catch ex As ThreadInterruptedException
+ Catch ex As Exception
+ Log(ex, "连通检测失败")
+ PingTime = -1
+ End Try
+ End Sub
+ Private Shared Sub InitIpCheck()
+ IpCheckStatus = LoadState.Loading
+ Try
+ Log("[HiPer] 网络检测:IP 地址检测开始")
+ Dim IpCheckResult As String = NetRequestOnce("https://ipinfo.io/json", "GET", "", "application/json", 10000)
+ Dim Country As String = GetJson(IpCheckResult)("country")
+ IpIsInChina = Country = "CN"
+ IpCheckStatus = LoadState.Finished
+ Log("[HiPer] 网络检测:IP 地址检测结果:" & Country)
+ Catch ex As ThreadInterruptedException
+ IpCheckStatus = LoadState.Aborted
+ Catch ex As Exception
+ Log(ex, "IP 地址检测失败")
+ IpCheckStatus = LoadState.Failed
+ IpIsInChina = True
+ End Try
+ End Sub
+ Private Shared PingTime As Integer, IpCheckStatus As LoadState = LoadState.Loading, IpIsInChina As Boolean
+ Private Shared Sub InitCheck(Task As LoaderTask(Of Integer, Integer))
+ '检查协议
+ If Not Setup.Get("LinkEula") Then
+Reopen:
+ Select Case MyMsgBox("PCL2 的联机服务由速聚授权提供。" & vbCrLf & "在使用前,你需要同意速聚的用户服务协议和隐私政策。", "协议授权", "同意", "拒绝", "查看用户服务协议和隐私政策")
+ Case 1
+ Setup.Set("LinkEula", True)
+ Case 2
+ Throw New Exception("$你拒绝了用户服务协议……")
+ Case 3
+ 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")
+ GoTo Reopen
+ End Select
+ End If
+ '等待网络环境检查加载器结束
+ SetLoadDesc("正在检查网络环境……", "检查网络环境")
+ Do While Task.State = LoadState.Loading AndAlso Not (PingTime <> 0 AndAlso IpCheckStatus <> LoadState.Loading)
+ Thread.Sleep(50)
+ Loop
+ '获取网络环境检查结果
+ If IpCheckStatus = LoadState.Finished AndAlso Not IpIsInChina Then
+ If MyMsgBox("检测到你的 IP 不在中国,这会导致联机变得非常不稳定。" & vbCrLf &
+ "如果你正开着加速器或者 VPN,请先关闭它们,然后再继续……", "警告", "继续", "取消", IsWarn:=True, ForceWait:=True) = 2 Then
+ Throw New Exception("$请在关闭加速器或者 VPN 后点击重试。")
+ End If
+ Task.Progress = 0.5
+ InitIpCheck() '重新进行检查
+ End If
+ '没有联网
+ If IpCheckStatus = LoadState.Failed AndAlso PingTime = -1 Then
+ Throw New Exception("$PCL2 没法连上网……" & vbCrLf & "如果你改变了网络环境,请重启 PCL2。")
+ End If
+ End Sub
+
+ '获取所需文件
+ Private Shared Sub InitGetFile(Task As LoaderTask(Of Integer, List(Of NetFile)))
+ '初始化
+ SetLoadDesc("正在初始化……", "初始化")
+ Directory.CreateDirectory(PathHiper)
+ WriteFile(PathHiper & "HiPer 联机模块.vbs", "createobject(""wscript.shell"").run """"""" & PathHiper & "HiPer 联机模块.exe"""" v"", 0", Encoding:=Encoding.GetEncoding("GB18030")) '准备启动脚本
+ '获取索引码文件
+ SetLoadDesc("正在获取索引码文件……", "获取索引码文件")
+ Dim Cert As String = RunInUiWait(Function() FrmLinkHiper.TextCert.Text)
+ Log("[Hiper] 联机索引码:" & Cert)
+ If Cert = Setup.Get("LinkHiperCertLast") AndAlso File.Exists(PathHiper & "cert.yml") Then
+ Log("[Hiper] 索引码与上次输入的一致,跳过获取步骤")
+ Else
+ Dim CertRaw As String
+ Try
+ CertRaw = NetRequestOnce("https://cert.mcer.cn/" & Cert & ".yml", "GET", "", "")
+ Catch ex As Exception
+ If GetString(ex).Contains("(404)") Then
+ Throw New CertOutdatedException '索引码无效或已过期
+ ElseIf GetString(ex).Contains("too many requests") Then
+ Throw New Exception("你的尝试太频繁了,请暂时啥都别点,等两分钟后再试……")
+ Else
+ Throw
+ End If
+ End Try
+ If Not CertRaw.Contains("WARNING <<< AUTO SYNC AREA") Then Throw New Exception("获取到的索引码文件内容有误!")
+ WriteFile(PathHiper & "cert.yml", CertRaw)
+ Setup.Set("LinkHiperCertLast", Cert)
+ Setup.Set("LinkHiperCertTime", "")
+ End If
+ Task.Progress = 0.25
+ '获取 CPU 架构
+ SetLoadDesc("正在获取 CPU 架构……", "获取 CPU 架构")
+ Dim Architecture As String
+ Select Case GetType(String).Assembly.GetName().ProcessorArchitecture
+ Case Reflection.ProcessorArchitecture.X86
+ Architecture = "386"
+ Case Reflection.ProcessorArchitecture.Amd64
+ Architecture = "amd64"
+ Case Reflection.ProcessorArchitecture.Arm
+ Architecture = "arm64"
+ Case Else
+ Architecture = "arm64"
+ Log("[Hiper] 当前 CPU 架构为 " & GetStringFromEnum(GetType(String).Assembly.GetName().ProcessorArchitecture) & ",没有最适合的项,这可能会导致联机模块无法启动!", LogLevel.Debug)
+ End Select
+ Log("[Hiper] CPU 架构:" & Architecture)
+ '检查更新:hiper.exe
+ SetLoadDesc("正在检查联机模块本体更新……", "检查联机模块本体更新")
+ Dim RequiredFiles As New List(Of NetFile)
+ Dim Checksums As String = Nothing, ChecksumHiper As String = Nothing
+ If File.Exists(PathHiper & "HiPer 联机模块.exe") Then
+ Try
+ Checksums = NetGetCodeByDownload({
+ "http://mirror.hiper.cn.s2.the.bb/packages.sha1",
+ "http://mirror.hiper.cn.s3.the.bb:175/packages.sha1",
+ "https://gitcode.net/to/hiper/-/raw/master/packages.sha1",
+ "https://cert.mcer.cn/mirror/packages.sha1"
+ })
+ Catch ex As Exception
+ Log(ex, "获取联机模块更新信息失败,将会强制启动联机模块", LogLevel.Hint)
+ GoTo FinishHiperFileCheck
+ End Try
+ ChecksumHiper = RegexSeek(Checksums, "[0-9a-f]{40}(?= windows-" & Architecture & "/hiper.exe)")
+ If ChecksumHiper Is Nothing Then
+ Log("[Hiper] 未找到联机模块更新信息,将会强制启动联机模块", LogLevel.Hint)
+ GoTo FinishHiperFileCheck
+ End If
+ Log("[Hiper] hiper.exe 的所需 SHA1:" & ChecksumHiper)
+ If ChecksumHiper = GetAuthSHA1(PathHiper & "HiPer 联机模块.exe") Then
+ Log("[Hiper] hiper.exe 文件校验通过,无需重新下载")
+ GoTo FinishHiperFileCheck
+ End If
+ End If
+ Log("[Hiper] 需要重新下载 hiper.exe")
+ RequiredFiles.Add(New NetFile({"http://mirror.hiper.cn.s2.the.bb/windows-" & Architecture & "/hiper.exe",
+ "http://mirror.hiper.cn.s3.the.bb:175/windows-" & Architecture & "/hiper.exe",
+ "https://gitcode.net/to/hiper/-/raw/master/windows-" & Architecture & "/hiper.exe?inline=false",
+ "https://cert.mcer.cn/mirror/windows-" & Architecture & "/hiper.exe"},
+ PathHiper & "HiPer 联机模块.exe",
+ New FileChecker(Hash:=ChecksumHiper, MinSize:=1024 * 200))) 'Hash 的默认值即为 Nothing,所以 Checksum 可以为 Nothing
+FinishHiperFileCheck:
+ Task.Progress = 0.5
+ '检查更新:wintun.dll
+ Dim ChecksumWintun As String = Nothing
+ If File.Exists(PathHiper & "wintun.dll") Then
+ Try
+ If Checksums Is Nothing Then
+ Checksums = NetGetCodeByDownload({
+ "http://mirror.hiper.cn.s2.the.bb/packages.sha1",
+ "http://mirror.hiper.cn.s3.the.bb:175/packages.sha1",
+ "https://gitcode.net/to/hiper/-/raw/master/packages.sha1",
+ "https://cert.mcer.cn/mirror/packages.sha1"
+ })
+ End If
+ Catch ex As Exception
+ Log(ex, "获取联机模块更新信息失败,将会强制启动联机模块", LogLevel.Hint)
+ GoTo FinishWintunFileCheck
+ End Try
+ ChecksumWintun = RegexSeek(Checksums, "[0-9a-f]{40}(?= windows-" & Architecture & "/wintun.dll)")
+ If ChecksumWintun Is Nothing Then
+ Log("[Hiper] 未找到联机模块更新信息,将会强制启动联机模块", LogLevel.Hint)
+ GoTo FinishWintunFileCheck
+ End If
+ Log("[Hiper] wintun.dll 的所需 SHA1:" & ChecksumWintun)
+ If ChecksumWintun = GetAuthSHA1(PathHiper & "wintun.dll") Then
+ Log("[Hiper] wintun.dll 文件校验通过,无需重新下载")
+ GoTo FinishWintunFileCheck
+ End If
+ End If
+ Log("[Hiper] 需要重新下载 wintun.dll")
+ RequiredFiles.Add(New NetFile({"http://mirror.hiper.cn.s2.the.bb/windows-" & Architecture & "/wintun.dll",
+ "http://mirror.hiper.cn.s3.the.bb:175/windows-" & Architecture & "/wintun.dll",
+ "https://gitcode.net/to/hiper/-/raw/master/windows-" & Architecture & "/wintun.dll?inline=false",
+ "https://cert.mcer.cn/mirror/windows-" & Architecture & "/wintun.dll"},
+ PathHiper & "wintun.dll",
+ New FileChecker(Hash:=ChecksumWintun, MinSize:=1024 * 200))) 'Hash 的默认值即为 Nothing,所以 Checksum 可以为 Nothing
+FinishWintunFileCheck:
+ Task.Progress = 0.75
+ '检查更新:MCB
+ If Not IsServerSide Then
+ SetLoadDesc("正在检查联机模块组件更新……", "检查联机模块组件更新")
+ Dim ChecksumMcb As String = Nothing
+ If File.Exists(PathHiper & "MCB 联机模块.exe") Then
+ Try
+ Checksums = NetGetCodeByDownload({
+ "http://mirror.hiper.cn.s2.the.bb/utils/minecraft-broadcast/packages.sha1",
+ "http://mirror.hiper.cn.s3.the.bb:175/utils/minecraft-broadcast/packages.sha1",
+ "https://gitcode.net/to/hiper/-/raw/master/utils/minecraft-broadcast/packages.sha1",
+ "https://cert.mcer.cn/mirror/utils/minecraft-broadcast/packages.sha1"
+ })
+ Catch ex As Exception
+ Log(ex, "获取联机模块组件更新信息失败,将会强制启动联机模块组件", LogLevel.Hint)
+ GoTo FinishMcbFileCheck
+ End Try
+ ChecksumMcb = RegexSeek(Checksums, "[0-9a-f]{40}(?= mcb-windows-" & Architecture & ".exe)")
+ If ChecksumMcb Is Nothing Then
+ Log("[Hiper] 未找到联机模块组件更新信息,将会强制启动联机模块组件", LogLevel.Hint)
+ GoTo FinishMcbFileCheck
+ End If
+ Log("[Hiper] mcb.exe 的所需 SHA1:" & ChecksumMcb)
+ If ChecksumMcb = GetAuthSHA1(PathHiper & "MCB 联机模块.exe") Then
+ Log("[Hiper] mcb.exe 文件校验通过,无需重新下载")
+ GoTo FinishMcbFileCheck
+ End If
+ End If
+ Log("[Hiper] 需要重新下载 mcb.exe")
+ RequiredFiles.Add(New NetFile({
+ "http://mirror.hiper.cn.s2.the.bb/utils/minecraft-broadcast/mcb-windows-" & Architecture & ".exe",
+ "http://mirror.hiper.cn.s3.the.bb:175/utils/minecraft-broadcast/mcb-windows-" & Architecture & ".exe",
+ "https://gitcode.net/to/hiper/-/raw/master/utils/minecraft-broadcast/mcb-windows-" & Architecture & ".exe?inline=false",
+ "https://cert.mcer.cn/mirror/utils/minecraft-broadcast/mcb-windows-" & Architecture & ".exe"},
+ PathHiper & "MCB 联机模块.exe",
+ New FileChecker(Hash:=ChecksumMcb, MinSize:=1024 * 200))) 'Hash 的默认值即为 Nothing,所以 Checksum 可以为 Nothing
+FinishMcbFileCheck:
+ End If
+ '添加 point.yml
+ RequiredFiles.Add(New NetFile({"https://cert.mcer.cn/point.yml"}, PathHiper & "point.yml", New FileChecker(MinSize:=200)))
+ '开始下载
+ SetLoadDesc("正在下载联机模块……", "下载联机模块")
+ Task.Output = RequiredFiles
+ End Sub
+ Public Class CertOutdatedException
+ Inherits Exception
+ End Class
+ '启动联机模块
+ Private Shared Sub InitLaunch(Task As LoaderTask(Of Integer, Integer))
+ '关闭运行中的 HiPer,然后再刷新凭证文件
+ SetLoadDesc("正在关闭运行中的联机模块……", "关闭运行中的联机模块")
+ HiperStop(True)
+ '准备凭证文件
+ SetLoadDesc("正在准备索引码文件……", "准备索引码文件")
+ Dim CertFile As String = ReadFile(PathHiper & "cert.yml")
+ '添加防火墙配置
+ If IsServerSide Then
+ CertFile += ("\n\ninbound:\n port: " & HostPort & "\n proto: tcp\n host: any").Replace("\n", vbCrLf)
+ Else
+ CertFile += ("\n\noutbound:\n port: " & HostPort & "\n proto: tcp\n host: " & HostIp).Replace("\n", vbCrLf)
+ End If
+ CertFile += "\nlogging:\n format: json".Replace("\n", vbCrLf)
+ '更新 point.yml 段
+ Const SyncAreaRegex As String = "(?<=#[ ]*WARNING >>> AUTO SYNC AREA)[\s\S]+?(?=#[ ]*WARNING <<< AUTO SYNC AREA)"
+ CertFile = CertFile.Replace(RegexSeek(CertFile, SyncAreaRegex), RegexSeek(ReadFile(PathHiper & "point.yml"), SyncAreaRegex))
+ WriteFile(PathHiper & "config.yml", CertFile)
+ AllNodes = RegexSearch(CertFile.Replace(vbLf, "cr"), "(?<="")[0-9]+.[0-9]+.[0-9]+.[0-9]+(?=""( )?:( )?cr - "")").Distinct.
+ Where(Function(l) Not (l = "6.6.1.1" OrElse l = "6.6.2.2" OrElse l = "6.6.3.3")).ToList
+ If AllNodes.Count < 2 Then Throw New Exception("$索引码文件格式有误,未找到节点 IP!")
+ Task.Progress = 0.05
+ '尝试启动
+ HiperStart(Task) '启动失败会直接抛出异常
+ Task.Progress = 0.2
+ '检查连接情况
+ SetLoadDesc("正在连接到联机节点 (1/2)……" & If(IpIsInChina, "", vbCrLf & "由于网络不在中国,可能要花费很长时间。"), "检查联机节点")
+ If Not IsServerSide AndAlso HostIp = HiperIp Then Throw New Exception("$还搁这自己连自己?!不是吧?!")
+ Dim PingNodeResults As New List(Of Integer)
+ Dim UnpingedNodes As New List(Of String)(AllNodes)
+ Do Until PingNodeResults.Count >= 2 OrElse Task.IsAborted
+ Dim SelectedNode = RandomOne(UnpingedNodes)
+ Dim PingNode = Ping(SelectedNode, 4000)
+ If PingNode = -1 Then
+ Task.Progress += (If(PingNodeResults.Count = 0, 0.58, 0.88) - Task.Progress) * 0.3
+ Else
+ SetLoadDesc("正在连接到联机节点 (2/2)……" & If(IpIsInChina, "", vbCrLf & "由于网络不在中国,可能要花费很长时间。"), "检查联机节点")
+ Task.Progress = 0.6
+ PingNodeResults.Add(PingNode)
+ UnpingedNodes.Remove(SelectedNode)
+ End If
+ Loop
+ PingNodes = PingNodeResults.Min
+ Task.Progress = 0.9
+ '与房主 Ping 一下看看
+ If Not IsServerSide Then
+ SetLoadDesc("正在连接到房主……", "检查房主")
+ HostPing = -1 '实际上不使用本次 Ping 的结果,因为它真的偏大……
+ Do
+ Dim PingHost = Ping(HostIp, 4000, False)
+ If PingHost = -1 Then
+ Thread.Sleep(500)
+ Else
+ Exit Do
+ End If
+ Loop Until Task.IsAborted
+ '在等待 2 秒后再次触发 Ping,以刷新显示的 Ping
+ RunInNewThread(Sub()
+ Thread.Sleep(2000)
+ TaskPingHost.Start(True, IsForceRestart:=True)
+ End Sub, "HiPer Delayed Ping")
+ End If
+ '刷新完成页面
+ SetLoadDesc("正在加载完成页面……", "加载完成页面")
+ RunInUiWait(AddressOf FrmLinkHiper.WatcherTimer1)
+ End Sub
+ Private Shared PingNodes As Integer, AllNodes As List(Of String)
+
+#End Region
+
+#Region "进程管理"
+
+ Private Shared _HiperState As LoadState = LoadState.Waiting
+ Public Shared Property HiperState As LoadState
+ Get
+ Return _HiperState
+ End Get
+ Set(value As LoadState)
+ _HiperState = value
+ RunInUi(Sub() If FrmLinkLeft IsNot Nothing Then CType(FrmLinkLeft.ItemHiper.Buttons(0), MyIconButton).Visibility = If(HiperState = LoadState.Finished OrElse HiperState = LoadState.Loading, Visibility.Visible, Visibility.Collapsed))
+ End Set
+ End Property
+ Public Shared Sub ModuleStopManually() '关闭联机模块按钮
+ HiperExit(False)
+ End Sub
+
+ Private Shared HiperIp As String = Nothing
+ Private Shared HiperProcessId As Integer = -1, McbProcessId As Integer = -1
+ Private Shared HiperCertTime As Date = Date.Now
+
+ '''
+ ''' 若程序正在运行,则结束程序进程,同时初始化状态数据。返回是否关闭了相关进程。
+ '''
+ Public Shared Function HiperStop(SleepWhenKilled As Boolean) As Boolean
+ HiperStop = False
+ '修改凭证
+ Dim ConfigContent As String = ReadFile(PathHiper & "config.yml")
+ If Not ConfigContent.Contains("enable: false") Then
+ WriteFile(PathHiper & "config.yml", ConfigContent & vbCrLf & "enable: false")
+ End If
+ '关闭所有进程
+ For Each ProcessObject In Process.GetProcesses
+ Dim IsHiper As Boolean = ProcessObject.ProcessName = "HiPer 联机模块"
+ Dim IsMcb As Boolean = ProcessObject.ProcessName = "MCB 联机模块"
+ If Not IsHiper AndAlso Not IsMcb Then Continue For
+ HiperStop = True
+ Try
+ If IsMcb Then
+ ProcessObject.Kill()
+ Log("[HiPer] 已结束进程 PID " & ProcessObject.Id & ":" & ProcessObject.ProcessName)
+ If HiperStop AndAlso SleepWhenKilled Then Thread.Sleep(1000) '等待 1 秒确认进程已退出
+ Else 'IsHiper
+ If SleepWhenKilled Then
+ Thread.Sleep(4000) '等待 4 秒确认进程已退出
+ If Not ProcessObject.HasExited Then
+ ProcessObject.Kill()
+ Log("[HiPer] 已结束进程 PID " & ProcessObject.Id & ":" & ProcessObject.ProcessName)
+ End If
+ End If
+ End If
+ Catch ex As Exception
+ Log(ex, "结束进程失败(" & ProcessObject.Id & "," & ProcessObject.ProcessName & ")")
+ End Try
+ Next
+ '初始化
+ HiperState = LoadState.Waiting
+ HiperProcessId = -1 : McbProcessId = -1
+ HiperCertTime = Nothing
+ End Function
+ '''
+ ''' 启动程序,并等待初始化完成后退出运行,同时更新 HiperIp。
+ ''' 若启动失败,则会直接抛出异常。
+ ''' 若程序正在运行,则会先停止其运行。
+ '''
+ Public Shared Sub HiperStart(Task As LoaderTask(Of Integer, Integer))
+ Try
+ SetLoadDesc("正在启动联机模块……", "启动联机模块")
+ PossibleFailReason = Nothing
+ '启动 Hiper
+ Log("[Hiper] 启动 Hiper 进程")
+ DeleteDirectory(PathHiper & "logs") '清理日志
+ Dim HiperInfo = New ProcessStartInfo With {
+ .FileName = "wscript",
+ .Arguments = """" & PathHiper & "HiPer 联机模块.vbs""",
+ .Verb = "runas" '需要管理员权限
+ }
+ Dim HiperVbsProcess As New Process() With {.StartInfo = HiperInfo}
+ HiperVbsProcess.Start()
+ '查找真正的 Hiper 进程
+ SetLoadDesc("联机模块正在启动……", "联机模块加载")
+ Do
+ Dim GotProcesses = Process.GetProcesses.
+ Where(Function(l) l.ProcessName = "HiPer 联机模块" AndAlso Math.Abs((l.StartTime - Date.Now).TotalMinutes) < 1)
+ If GotProcesses.Count > 0 Then
+ HiperProcessId = GotProcesses.First.Id
+ Log("[Hiper] 已发现 Hiper 进程,PID:" & HiperProcessId)
+ Exit Do
+ End If
+ Thread.Sleep(50)
+ Loop While Task.State = LoadState.Loading AndAlso Not Task.IsAborted
+ If HiperProcessId = -1 Then Throw New Exception("联机模块未能成功启动!")
+ HiperState = LoadState.Loading
+ Task.Progress = 0.15
+ '抓取日志
+ Dim LogLine As Integer = 0
+ Do
+ Thread.Sleep(100)
+ If Not File.Exists(PathHiper & "logs\hiper.log") Then Continue Do
+ Dim LogLines As String() = ReadFile(PathHiper & "logs\hiper.log").TrimEnd(vbLf).Split(vbLf)
+ For i = LogLine To LogLines.Count - 1
+ HiperLogLine(LogLines(i), Task)
+ Next
+ LogLine = LogLines.Count
+ Loop While HiperState = LoadState.Loading AndAlso Not Task.IsAborted AndAlso Process.GetProcesses.Any(Function(l) l.Id = HiperProcessId)
+ '输出
+ If HiperState = LoadState.Finished Then
+ Log("[Hiper] Hiper 启动完成")
+ ElseIf PossibleFailReason IsNot Nothing AndAlso PossibleFailReason.Contains("索引码已过期") Then
+ Throw New CertOutdatedException()
+ Else
+ Throw New Exception(If(PossibleFailReason, "联机模块因未知原因启动失败!"))
+ End If
+ '启动 MCB
+ Task.Progress = 0.25
+ If Not IsServerSide Then
+ Try
+ Log("[Hiper] 启动 MCB 进程")
+ Dim McbInfo = New ProcessStartInfo With {
+ .FileName = PathHiper & "MCB 联机模块.exe", .WorkingDirectory = PathHiper,
+ .UseShellExecute = False, .CreateNoWindow = True,
+ .RedirectStandardError = True, .RedirectStandardOutput = True,
+ .Arguments = "-addr " & HostIp & ":" & HostPort & " -motd ""PCL2 联机房间"""
+ }
+ Dim McbProcess As New Process() With {.StartInfo = McbInfo}
+ McbProcess.Start()
+ McbProcessId = McbProcess.Id
+ Catch ex As Exception
+ Throw New Exception("联机模块组件启动失败", ex)
+ End Try
+ End If
+ Catch ex As Exception
+ Try
+ HiperStop(True) '由于启动失败停止进程
+ Catch
+ End Try
+ HiperState = LoadState.Failed
+ Throw
+ End Try
+ End Sub
+
+ 'Hiper 日志
+ Private Shared Sub HiperLogLine(Content As String, Task As LoaderTask(Of Integer, Integer))
+
+ '检查报错
+ Dim ContentTest As String = Content.ToLower
+ Dim ErrorMessage As String = Nothing
+ Dim ContentJson As JObject = Nothing
+ If Content.StartsWith("{""") AndAlso Content.EndsWith("}") Then
+ ContentJson = GetJson(Content)
+ ContentTest = If(ContentJson("error"), ContentJson("msg")).ToString.ToLower
+ ErrorMessage = ContentJson("error")
+ End If
+ If ContentTest.Contains("hiper certificate for this host is expired") Then
+ PossibleFailReason = "$你的索引码已过期,请更换新的索引码!"
+ ElseIf ContentTest.Contains("error creating interface: access is denied") Then
+ PossibleFailReason = "$没有获得管理员权限,无法创建网络通道!"
+ ElseIf ContentTest.Contains("cannot create a file when that file already exists") Then
+ PossibleFailReason = "$请不要重复开启多个联机模块!"
+ ElseIf Content.Contains("failed to load config") Then
+ PossibleFailReason = "$索引码文件内容存在错误!"
+ ElseIf Content.Contains("system cannot find the file specified") Then
+ PossibleFailReason = "$创建网络通道失败!" & vbCrLf & "如果你曾启动过 VPN 软件,请先打开那个软件,然后关掉它,最后再重试。虽然不知道为啥,但这样大概管用……"
+ End If
+ If PossibleFailReason Is Nothing AndAlso ErrorMessage IsNot Nothing Then PossibleFailReason = ErrorMessage
+
+ '检查结束
+ If ContentJson IsNot Nothing Then
+ Dim Message As String = ContentJson("msg")
+ If Message = "HiPer interface is active" Then
+ HiperIp = ContentJson("network").ToString.Split("/").First
+ If IsServerSide Then HostIp = HiperIp
+ Log("[Hiper] Hiper 启动完成,IP:" & HiperIp)
+ HiperState = LoadState.Finished
+ ElseIf Message = "Validity of client certificate" Then
+ Dim VaildTime As String = ContentJson("valid")
+ Date.TryParseExact(VaildTime, "yyyy-MM-dd HH:mm:ss", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.None, HiperCertTime)
+ Log("[Hiper] 索引码到期时间:" & HiperCertTime.ToString)
+ Setup.Set("LinkHiperCertTime", HiperCertTime.ToString)
+ End If
+ End If
+
+ '写入日志
+ If ModeDebug OrElse Content.Contains("""error""") Then Log("[Hiper] " & Content)
+
+ End Sub
+ Private Shared PossibleFailReason As String = Nothing
+
+#End Region
+
+#Region "监视线程"
+
+ '主 Timer 线程
+ Private IsWatcherStarted As Boolean = False
+ Private Sub WatcherThread()
+ Dim Sec15 As Integer = 0
+ Do While True
+ Try
+ For i = 1 To 5
+ Thread.Sleep(200)
+ If InitLoader.State = LoadState.Loading Then
+ RunInUi(AddressOf UpdateProgress)
+ End If
+ Next
+ Thread.Sleep(1000)
+ Sec15 += 1
+ WatcherTimer1()
+ If Sec15 = 15 Then
+ Sec15 = 0
+ WatcherTimer15()
+ End If
+ Catch ex As Exception
+ Log(ex, "联机模块主时钟出错", LogLevel.Feedback)
+ Thread.Sleep(20000)
+ End Try
+ Loop
+ End Sub
+
+ '每 1 秒执行的 Timer
+ Private Sub WatcherTimer1()
+ If HiperState <> LoadState.Finished Then Exit Sub
+ RunInUi(Sub()
+ '索引码剩余时间
+ Dim Span As TimeSpan = HiperCertTime - Date.Now
+ If Span.TotalDays >= 30 Then
+ LabFinishTime.Text = "> 30 天"
+ ElseIf Span.TotalDays >= 4 Then
+ LabFinishTime.Text = Span.Days & " 天"
+ ElseIf Span.TotalDays >= 1 Then
+ LabFinishTime.Text = Span.Days & " 天" & If(Span.Hours > 0, " " & Span.Hours & " 小时", "")
+ ElseIf Span.TotalMinutes >= 10 Then
+ LabFinishTime.Text = Span.Hours & ":" & Span.Minutes.ToString.PadLeft(2, "0") & "'"
+ Else
+ LabFinishTime.Text = Span.Minutes & "'" & Span.Seconds.ToString.PadLeft(2, "0") & """"
+ End If
+ '提示索引码即将到期
+ If Span.TotalSeconds <= 5 * 60 AndAlso Span.TotalSeconds > 5 * 60 - 1 AndAlso Setup.Get("LinkHiperCertWarn") Then
+ MyMsgBox("你的索引码还有不到 5 分钟就要过期了!" & vbCrLf & "你可以在设置中关闭这个提示……", "索引码即将过期", "我知道了……")
+ ShowWindowToTop(Handle)
+ Beep()
+ End If
+ '检查索引码到期
+ If Span.TotalSeconds < 2 Then
+ LabCertTitle.Text = "索引码已过期"
+ LabCertDesc.Text = "你的 HiPer 索引码已经过期,请输入新的索引码。" & vbCrLf & "如果实在没有索引码,可以在左侧选择 IOI 方式联机。"
+ TextCert.Text = ""
+ HiperExit(True)
+ ShowWindowToTop(Handle)
+ Beep()
+ Exit Sub
+ End If
+ '网络质量
+ Dim QualityScore As Integer = If(IpIsInChina, 0, -2)
+ QualityScore -= Math.Ceiling((Math.Min(PingTime, 600) + Math.Min(PingNodes, 600)) / 80)
+ Select Case QualityScore
+ Case Is >= -1
+ LabFinishQuality.Text = "优秀"
+ Case Is >= -2
+ LabFinishQuality.Text = "优良"
+ Case Is >= -3
+ LabFinishQuality.Text = "良好"
+ Case Is >= -5
+ LabFinishQuality.Text = "一般"
+ Case Is >= -7
+ LabFinishQuality.Text = "较差"
+ Case Else
+ LabFinishQuality.Text = "很差"
+ End Select
+ 'Ping
+ If HostPing <> -1 Then
+ If FrmLinkHiper IsNot Nothing AndAlso FrmLinkHiper.LabFinishPing.IsLoaded Then
+ FrmLinkHiper.LabFinishPing.Text = HostPing & "ms"
+ End If
+ End If
+ End Sub)
+ End Sub
+ '每 15 秒执行的 Timer
+ Private Shared HostPing As Integer = -1
+ Private Sub WatcherTimer15()
+ If Not (HiperState = LoadState.Finished OrElse HiperState = LoadState.Loading) Then Exit Sub
+ '检查 HiPer 崩溃
+ Try
+ Process.GetProcessById(HiperProcessId)
+ Catch
+ Dim LogLines = ReadFile(PathHiper & "logs\hiper.log").TrimEnd(vbLf).Split(vbLf)
+ HiperExit(False)
+ MyMsgBox("由于联机模块异常退出,已退出联机房间。" & vbCrLf & vbCrLf &
+ "联机模块最后的日志:" & vbCrLf & Join(LogLines.Skip(LogLines.Count - 3).ToList, vbCrLf), "联机断开")
+ End Try
+ '下面的部分需要完全完成加载
+ If HiperState <> LoadState.Finished Then Exit Sub
+ '检查 MCB 崩溃
+ Try
+ If Not IsServerSide Then Process.GetProcessById(McbProcessId)
+ Catch
+ HiperExit(False)
+ MyMsgBox("由于 MCB 联机模块异常退出,已退出联机房间。", "联机断开")
+ End Try
+ '重新检查 Ping
+ If Not IsServerSide Then
+ Dim PingHostFailedCount As Integer = 0
+ Do
+ TaskPingHost.WaitForExit(True, IsForceRestart:=True)
+ If TaskPingHost.Output = -1 Then
+ PingHostFailedCount += 1
+ Log("[HiPer] Ping 房主失败(第 " & PingHostFailedCount & " 次)")
+ If PingHostFailedCount >= 3 Then
+ HiperExit(False) : MyMsgBox("与房主断开连接,已退出联机房间。", "联机断开") : Exit Sub
+ End If
+ Else
+ If PingHostFailedCount > 0 Then Log("[HiPer] Ping 房主已恢复成功(" & HostPing & "ms)")
+ Exit Do
+ End If
+ Loop While True
+ End If
+ End Sub
+
+#End Region
+
+#Region "PanCert | 索引码输入页面"
+
+ '检测输入
+ Private Sub TextCert_ValidateChanged(sender As Object, e As EventArgs) Handles TextCert.ValidateChanged
+ BtnCertDone.IsEnabled = TextCert.ValidateResult = ""
+ End Sub
+ Private Sub TextCert_KeyUp(sender As Object, e As KeyEventArgs) Handles TextCert.KeyUp
+ If e.Key = Key.Enter AndAlso BtnCertDone.IsEnabled Then BtnCertDone_Click() '允许回车确认
+ End Sub
+
+ '确认
+ Private Sub BtnCertDone_Click() Handles BtnCertDone.Click
+ CurrentSubpage = Subpages.PanSelect
+ End Sub
+
+#End Region
+
+#Region "PanSelect | 种类选择页面"
+
+ '返回
+ Private Sub BtnSelectReturn_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnSelectReturn.MouseLeftButtonUp
+ LabCertTitle.Text = "输入索引码"
+ LabCertDesc.Text = "你需要获取索引码才能使用 HiPer。" & vbCrLf & "如果实在没有索引码,可以在左侧选择 IOI 方式联机。"
+ CurrentSubpage = Subpages.PanCert
+ End Sub
+
+ '创建房间
+ Private Sub BtnSelectCreate_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnSelectCreate.MouseLeftButtonUp
+ '获取端口号
+ Dim PortInput As String = MyMsgBoxInput("", New ObjectModel.Collection(Of Validate) From {
+ New ValidateInteger(2, 65535),
+ New ValidateExceptSame({"55555", "55557"}, "端口不能为 %!")
+ }, "在 MC 的暂停画面选择【对局域网开放】", "输入端口号", "确定", "取消")
+ If PortInput Is Nothing Then Exit Sub
+ '开始
+ RoomCreate(Val(PortInput))
+ End Sub
+ Private Sub RoomCreate(Port As Integer)
+ '记录信息
+ HostIp = Nothing : HostPort = Port
+ IsServerSide = True
+ '启动
+ InitLoader.Start(IsForceRestart:=True)
+ End Sub
+
+ '加入房间
+ Private Sub BtnSelectJoin_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnSelectJoin.MouseLeftButtonUp
+ '获取信息
+ Dim Code As String = MyMsgBoxInput("", New ObjectModel.Collection(Of Validate) From {New ValidateLength(8, 99)}, "",
+ "输入联机码", "确定", "取消")
+ If Code Is Nothing Then Exit Sub
+ '记录信息
+ If Not Code.EndsWith(RequestVersion) OrElse Not Code.StartsWith("P") Then GoTo WrongCode
+ Dim Ip As String, Port As Integer
+ Try
+ Dim IpAndPort As Long = RadixConvert(Code.Substring(1, Code.Length - 2).ToUpper, 36, 10)
+ 'IpAndPort = HostPort + IpParts(0) * 65536 + IpParts(1) * 65536 * 256 + IpParts(2) * 65536 * 256 * 256 + IpParts(3) * 65536 * 256 * 256 * 256
+ Ip = Math.Floor(IpAndPort / (65536L * 256 * 256 * 256))
+ IpAndPort = IpAndPort Mod (65536L * 256 * 256 * 256)
+ Ip = Math.Floor(IpAndPort / (65536L * 256 * 256)) & "." & Ip
+ IpAndPort = IpAndPort Mod (65536L * 256 * 256)
+ Ip = Math.Floor(IpAndPort / (65536L * 256)) & "." & Ip
+ IpAndPort = IpAndPort Mod (65536L * 256)
+ Ip = Math.Floor(IpAndPort / (65536L)) & "." & Ip
+ Port = IpAndPort Mod 65536
+ Catch
+ GoTo WrongCode
+ End Try
+ '启动
+ RoomJoin(Ip, Port)
+ Exit Sub
+WrongCode:
+ If Not Code.StartsWith("P") AndAlso Code.Length >= 49 Then Hint("你输入的可能是 IOI 的联机码,请在左侧的联机方式中选择 IOI!", HintType.Critical) : Exit Sub
+ If Code.StartsWith("P") AndAlso Not Code.EndsWith(RequestVersion) Then Hint("你的 PCL2 版本与房主的 PCL2 版本不一致!", HintType.Critical) : Exit Sub
+ Hint("你输入的联机码无效!", HintType.Critical)
+ End Sub
+ Private Sub RoomJoin(Ip As String, Port As Integer)
+ '记录信息
+ HostIp = Ip : HostPort = Port
+ IsServerSide = False
+ '启动
+ InitLoader.Start(IsForceRestart:=True)
+ End Sub
+
+#End Region
+
+#Region "PanLoad | 加载中页面"
+
+ '承接状态切换的 UI 改变
+ Private Sub OnLoadStateChanged(Loader As LoaderBase, NewState As LoadState, OldState As LoadState)
+ Select Case NewState
+ Case LoadState.Loading
+ UpdateProgress(0)
+ If IsServerSide Then
+ LabLoadTitle.Text = "正在创建联机房间"
+ Log("[Hiper] 正在创建联机房间,端口 " & HostPort)
+ Else
+ LabLoadTitle.Text = "正在加入联机房间"
+ Log("[Hiper] 正在加入联机房间,目标 IP " & HostIp & ":" & HostPort)
+ End If
+ LabLoadDesc.Text = "正在初始化……"
+ LoadStep = "准备初始化"
+ Case LoadState.Failed
+ UpdateProgress(1)
+ If IsServerSide Then
+ LabLoadTitle.Text = "创建联机房间失败"
+ Else
+ LabLoadTitle.Text = "加入联机房间失败"
+ End If
+ Dim RealException As Exception = If(Loader.Error.InnerException, Loader.Error)
+ If TypeOf RealException Is CertOutdatedException Then
+ LabCertTitle.Text = "索引码无效"
+ LabCertDesc.Text = "你的 HiPer 索引码无效或者已经过期!" & vbCrLf & "请重新输入索引码。"
+ HiperExit(True)
+ ElseIf RealException.Message.StartsWith("$") Then
+ LabLoadDesc.Text = RealException.Message.TrimStart("$") & vbCrLf &
+ "点击镐子重试,或者点击灰色的 × 取消。"
+ Else
+ LabLoadDesc.Text = LoadStep & "失败:" & GetString(RealException) & vbCrLf &
+ "点击镐子重试,或者点击灰色的 × 取消。"
+ End If
+ Log(Loader.Error, "HiPer 联机尝试失败")
+ Case LoadState.Finished
+ UpdateProgress(1)
+ CurrentSubpage = Subpages.PanFinish
+ BtnFinishPing.Visibility = If(IsServerSide, Visibility.Collapsed, Visibility.Visible)
+ LineFinishPing.Visibility = If(IsServerSide, Visibility.Collapsed, Visibility.Visible)
+ BtnFinishCopy.Visibility = If(IsServerSide, Visibility.Visible, Visibility.Collapsed)
+ If IsServerSide Then
+ LabFinishTitle.Text = "已创建联机房间"
+ LabFinishDesc.Text = "已在端口 " & HostPort & " 创建了联机房间。" & vbCrLf & "点击下方的复制联机码按钮,然后把联机码发给朋友吧!"
+ Else
+ LabFinishTitle.Text = "已加入联机房间"
+ LabFinishDesc.Text = "启动游戏,进入多人游戏页面后稍等片刻,房间就会出现在服务器列表的最下方。" & vbCrLf & "如果提示无效会话,不要退出联机房间,重新启动游戏即可!"
+ End If
+ LabFinishIp.Text = HostIp & ":" & HostPort
+ Log("[Hiper] 已完成连接")
+ End Select
+ End Sub
+ Private Shared LoadStep As String = "准备初始化"
+ Private Shared Sub SetLoadDesc(Intro As String, [Step] As String)
+ Log("[Hiper] 连接步骤:" & Intro)
+ LoadStep = [Step]
+ RunInUiWait(Sub()
+ If FrmLinkHiper Is Nothing OrElse Not FrmLinkHiper.LabLoadDesc.IsLoaded Then Exit Sub
+ FrmLinkHiper.LabLoadDesc.Text = Intro
+ FrmLinkHiper.UpdateProgress()
+ End Sub)
+ End Sub
+
+ '承接重试
+ Private Sub CardLoad_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles CardLoad.MouseLeftButtonUp
+ If Not InitLoader.State = LoadState.Failed Then Exit Sub
+ InitLoader.Start(IsForceRestart:=True)
+ End Sub
+
+ '取消加载
+ Private Sub CancelLoad() Handles BtnLoadCancel.Click
+ If InitLoader.State = LoadState.Loading Then
+ CurrentSubpage = Subpages.PanSelect
+ InitLoader.Abort()
+ Else
+ InitLoader.State = LoadState.Waiting
+ End If
+ HiperStop(False)
+ End Sub
+
+ '进度改变
+ Private Sub UpdateProgress(Optional Value As Double = -1)
+ If Value = -1 Then Value = InitLoader.Progress
+ Dim DisplayingProgress As Double = ColumnProgressA.Width.Value
+ If Math.Round(Value - DisplayingProgress, 3) = 0 Then Exit Sub
+ If DisplayingProgress > Value Then
+ ColumnProgressA.Width = New GridLength(Value, GridUnitType.Star)
+ ColumnProgressB.Width = New GridLength(1 - Value, GridUnitType.Star)
+ AniStop("Hiper Progress")
+ Else
+ Dim NewProgress As Double = If(Value = 1, 1, (Value - DisplayingProgress) * 0.2 + DisplayingProgress)
+ AniStart({
+ AaGridLengthWidth(ColumnProgressA, NewProgress - ColumnProgressA.Width.Value, 300, Ease:=New AniEaseOutFluent),
+ AaGridLengthWidth(ColumnProgressB, (1 - NewProgress) - ColumnProgressB.Width.Value, 300, Ease:=New AniEaseOutFluent)
+ }, "Hiper Progress")
+ End If
+ End Sub
+ Private Sub CardResized() Handles CardLoad.SizeChanged
+ RectProgressClip.Rect = New Rect(0, 0, CardLoad.ActualWidth, 12)
+ End Sub
+
+#End Region
+
+#Region "PanFinish | 加载完成页面"
+
+ '复制 IP
+ Private Sub BtnFinishIp_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishIp.MouseLeftButtonUp
+ ClipboardSet(LabFinishIp.Text)
+ End Sub
+
+ '退出
+ Private Sub BtnFinishExit_Click(sender As Object, e As EventArgs) Handles BtnFinishExit.Click
+ If IsServerSide AndAlso MyMsgBox("你确定要关闭联机房间吗?", "确认退出", "确定", "取消", IsWarn:=True) = 2 Then Exit Sub
+ HiperExit(False)
+ End Sub
+
+ '复制联机码
+ Private Sub BtnFinishCopy_Click(sender As Object, e As EventArgs) Handles BtnFinishCopy.Click
+ Dim IpParts = HostIp.Split(".").Select(Function(l) Val(l)).ToList
+ Dim IpAndPort As Long = HostPort + IpParts(0) * 65536 + IpParts(1) * 65536 * 256 + IpParts(2) * 65536 * 256 * 256 + IpParts(3) * 65536 * 256 * 256 * 256
+ ClipboardSet("P" & RadixConvert(IpAndPort, 10, 36) & RequestVersion, False)
+ Hint("已复制联机码!", HintType.Finish)
+ End Sub
+
+ 'Ping 房主
+ Private Sub BtnFinishPing_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishPing.MouseLeftButtonUp
+ LabFinishPing.Text = "检测中"
+ If TaskPingHost.State = LoadState.Loading Then Exit Sub
+ TaskPingHost.Start(True, IsForceRestart:=True)
+ End Sub
+ Private Shared TaskPingHost As New LoaderTask(Of Boolean, Integer)("HiPer Ping Host",
+ Sub(Task As LoaderTask(Of Boolean, Integer))
+ HostPing = -1
+ HostPing = Ping(HostIp, 5000, Task.Input)
+ End Sub)
+
+#End Region
+
+#Region "子页面管理"
+
+ Public Enum Subpages
+ PanCert
+ PanSelect
+ PanFinish
+ End Enum
+ Private _CurrentSubpage As Subpages = Subpages.PanCert
+ Public Property CurrentSubpage As Subpages
+ Get
+ Return _CurrentSubpage
+ End Get
+ Set(value As Subpages)
+ If _CurrentSubpage = value Then Exit Property
+ _CurrentSubpage = value
+ Log("[Hiper] 子页面更改为 " & GetStringFromEnum(value))
+ PageOnContentExit()
+ If value = Subpages.PanSelect Then
+ LabSelectCode.Text = "(" & TextCert.Text.Substring(0, Math.Min(TextCert.Text.Length, 3)) & "…)"
+ End If
+ End Set
+ End Property
+
+ Private Sub PageLinkHiper_OnPageEnter() Handles Me.PageEnter
+ FrmLinkHiper.PanCert.Visibility = If(CurrentSubpage = Subpages.PanCert, Visibility.Visible, Visibility.Collapsed)
+ FrmLinkHiper.PanSelect.Visibility = If(CurrentSubpage = Subpages.PanSelect, Visibility.Visible, Visibility.Collapsed)
+ FrmLinkHiper.PanFinish.Visibility = If(CurrentSubpage = Subpages.PanFinish, Visibility.Visible, Visibility.Collapsed)
+ End Sub
+
+ Private Shared Sub HiperExit(ExitToCertPage As Boolean)
+ Log("[Hiper] 要求退出 Hiper(当前加载器状态为 " & GetStringFromEnum(InitLoader.State) & ")")
+ HiperStop(False)
+ If InitLoader.State = LoadState.Loading Then InitLoader.Abort()
+ If InitLoader.State = LoadState.Failed Then InitLoader.State = LoadState.Waiting
+ RunInUi(Sub()
+ If FrmLinkHiper Is Nothing OrElse Not FrmLinkHiper.IsLoaded Then Exit Sub
+ FrmLinkHiper.CurrentSubpage = If(ExitToCertPage, Subpages.PanCert, Subpages.PanSelect)
+ FrmLinkHiper.PageOnContentExit()
+ End Sub)
+ End Sub
+
+#End Region
+
+End Class
diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb
new file mode 100644
index 00000000..f95e6a31
--- /dev/null
+++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb
@@ -0,0 +1,1262 @@
+Imports System.Net
+Imports System.Net.Sockets
+
+Public Class PageLinkIoi
+ Public Const RequestVersion As Integer = 4
+ Public Const IoiVersion As Integer = 10 '由于已关闭更新渠道,在提升 IoiVersion 时必须提升 RequestVersion
+ Public Shared PathIoi As String = PathAppdata & "联机模块\IOI 联机模块.exe"
+
+#Region "初始化"
+
+ '页面初始化
+ Private IsLoad As Boolean = False
+ Private Sub MeLoaded() Handles Me.Loaded
+
+ '重复加载部分
+ PanBack.ScrollToHome()
+ RefreshUi()
+
+ '非重复加载部分
+ If IsLoad Then Exit Sub
+ IsLoad = True
+
+ '更新线程
+ RunInNewThread(Sub()
+ Do While True
+ Thread.Sleep(200)
+ If FrmMain.PageCurrent.Page = FormMain.PageType.Link AndAlso FrmMain.PageCurrentSub = FormMain.PageSubType.LinkIoi Then RunInUiWait(Sub() FrmLinkIoi.RefreshUi())
+ RefreshWorker()
+ Loop
+ End Sub, "Link Timer")
+
+ End Sub
+
+ '加载器初始化与左边栏处理
+ Private Sub LoaderInit() Handles Me.Initialized
+ PageLoaderInit(Load, PanLoad, PanBack, PanAlways, InitLoader, Sub()
+ End Sub)
+ End Sub
+
+ '初始化加载器与步骤
+ Public Shared WithEvents InitLoader As New LoaderCombo(Of Integer)("联机模块初始化", {
+ New LoaderTask(Of Integer, Integer)("端口检查", AddressOf InitPortCheck) With {.ProgressWeight = 1},
+ New LoaderTask(Of Integer, Integer)("启动请求核心", AddressOf StartSocketListener) With {.ProgressWeight = 1},
+ New LoaderTask(Of Integer, List(Of NetFile))("初次启动尝试", AddressOf InitFirst) With {.ProgressWeight = 4},
+ New LoaderDownload("下载更新文件", New List(Of NetFile)) With {.ProgressWeight = 6},
+ New LoaderTask(Of List(Of NetFile), Boolean)("二次启动尝试", AddressOf InitSecond) With {.ProgressWeight = 3},
+ New LoaderTask(Of Boolean, Boolean)("创建请求核心房间", AddressOf InitRequest) With {.ProgressWeight = 2}
+ })
+ '判断端口是否被占用
+ Private Shared Sub InitPortCheck()
+ '检查协议
+ If Not Setup.Get("LinkEula") Then
+Reopen:
+ Select Case MyMsgBox("PCL2 的联机服务由速聚授权提供。" & vbCrLf & "在使用前,你需要同意速聚的用户服务协议和隐私政策。", "协议授权", "同意", "拒绝", "查看用户服务协议和隐私政策")
+ Case 1
+ Setup.Set("LinkEula", True)
+ Case 2
+ Throw New Exception("你拒绝了用户服务协议……")
+ Case 3
+ 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")
+ GoTo Reopen
+ End Select
+ End If
+ '先掐死
+ IoiStop(True)
+ '检查端口
+ Dim HasConflict As Boolean = False
+ For Each Port In Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners()
+ If Port.Port <> 55555 AndAlso Port.Port <> 55557 Then Continue For
+ Log("[IOI] 发现端口 " & Port.Port & " 被占用")
+ HasConflict = True
+ Next
+ If Not HasConflict Then Exit Sub
+ '对应的报错
+ For Each Line As String In ShellAndGetOutput("netstat", "-ano", 30000).Split({vbCrLf}, StringSplitOptions.RemoveEmptyEntries)
+ If Not (Line.Contains("127.0.0.1:55555") OrElse Line.Contains("127.0.0.1:55557")) Then Continue For
+ Dim ProcessName As String
+ Try
+ ProcessName = Process.GetProcessById(Line.Split(" ").Last).ProcessName
+ Catch ex As Exception
+ Log(ex, "获取占用端口的进程信息失败,假定进程已结束")
+ Continue For
+ End Try
+ If ProcessName = "联机模块" Then
+ Throw New Exception("由于一个已知问题,请在重启电脑后再尝试使用联机功能")
+ ElseIf ProcessName = "Idle" Then
+ '不知道为啥反正又没占用了
+ Log("[IOI] 未发现占用此端口的程序,继续执行")
+ Else
+ Throw New Exception("端口被程序 " & ProcessName & " 占用,无法启动联机模块,请在任务管理器关闭此程序后再试")
+ End If
+ Next
+ End Sub
+ '下载更新前尝试启动
+ Private Shared Sub InitFirst(Task As LoaderTask(Of Integer, List(Of NetFile)))
+ '初次启动尝试
+ If IoiVersion <> Setup.Get("LinkIoiVersion") Then
+ Log("[IOI] 设置版本强制要求联机模块更新")
+ Setup.Set("LinkIoiVersion", IoiVersion)
+ ElseIf File.Exists(PathIoi) Then
+ Task.Progress = 0.2
+ CheckFirewall()
+ If IoiStart() Then
+ '已完成启动与初始化
+ Task.Output = New List(Of NetFile)
+ Exit Sub
+ Else
+ '需要更新
+ IoiStop(True)
+ File.Delete(PathIoi)
+ GoTo StartDownload
+ End If
+ '如果抛出异常则直接使加载器失败
+ End If
+ '开始下载
+StartDownload:
+ Task.Progress = 0.8
+ Log("[IOI] 需要下载联机模块")
+ If File.Exists(PathTemp & "联机模块.zip") Then File.Delete(PathTemp & "联机模块.zip")
+ Task.Output = New List(Of NetFile) From {New NetFile(
+ {"https://gitcode.net/to/cato_bin/-/raw/master/ioi_v2_x" & If(Is32BitSystem, 32, 64) & ".zip",
+ "http://mirror.hiper.cn.s2.the.bb/ioi_v2_x" & If(Is32BitSystem, 32, 64) & ".zip",
+ "http://mirror.hiper.cn.s3.the.bb:175/ioi_v2_x" & If(Is32BitSystem, 32, 64) & ".zip",
+ "https://pcl2-server-1253424809.file.myqcloud.com/link/ioi_v2_x" & If(Is32BitSystem, 32, 64) & ".zip{CDN}"},
+ PathTemp & "联机模块.zip")}
+ End Sub
+ '下载更新后尝试启动
+ Private Shared Sub InitSecond(Task As LoaderTask(Of List(Of NetFile), Boolean))
+ '若首次尝试已经成功,则直接跳过
+ If IoiState = LoadState.Finished Then Exit Sub
+ '解压更新包
+ Log("[IOI] 解压联机模块以完成下载")
+ If File.Exists(PathTemp & "ioi.exe") Then File.Delete(PathTemp & "ioi.exe")
+ If File.Exists(PathIoi) Then File.Delete(PathIoi)
+ Compression.ZipFile.ExtractToDirectory(PathTemp & "联机模块.zip", PathTemp)
+ File.Delete(PathTemp & "联机模块.zip")
+ CopyFile(PathTemp & "ioi.exe", PathIoi)
+ File.Delete(PathTemp & "ioi.exe")
+ Task.Progress = 0.4
+ '再次尝试启动
+ CheckFirewall()
+ If IoiStart() Then Exit Sub
+ IoiStop(True)
+ Throw New Exception("联机模块初始化失败")
+ End Sub
+ '在 IOI 启动后建立 55557 房间
+ Private Shared Sub InitRequest()
+ Try
+ Dim Result As JObject = GetJson(NetRequestOnce("http://127.0.0.1:55555/api/port?proto=tcp&port=55557" & "&password=" & IoiPassword, "PUT", "", "", 100000))
+ If Result("msg") IsNot Nothing Then Throw New InvalidOperationException(Result("msg").ToString)
+ Catch ex As InvalidOperationException
+ 'API 返回的错误
+ Log("创建请求核心房间失败:" & ex.Message, LogLevel.Msgbox)
+ Catch ex As Exception
+ '常规错误
+ Log(ex, "创建请求核心房间失败", LogLevel.Msgbox)
+ End Try
+ End Sub
+
+ '检查防火墙权限,并添加对应的权限
+ Private Shared Sub CheckFirewall()
+ Try
+ Log("[IOI] Windows 防火墙:检测开始")
+ If Not PageLinkLeft.FirewallIsBlock(PathIoi) Then Exit Try
+ If PageLinkLeft.FirewallPolicy.CurrentProfile.ExceptionsNotAllowed Then
+ '禁止白名单
+ MyMsgBox("由于 Windows 防火墙阻止了所有传入连接,PCL2 无法获取防火墙通行权限。" & vbCrLf &
+ "联机会有很大概率失败,就算连上了,延迟也会变高……" & vbCrLf & vbCrLf &
+ "请先关闭 Windows 防火墙中的 " & vbLQ & "阻止所有传入连接" & vbRQ & " 选项,然后重启 PCL2。", "遭到防火墙拦截")
+ ElseIf IsAdmin() Then
+ '有管理员权限
+ Log("[IOI] Windows 防火墙:尝试添加防火墙通行权限")
+ Try
+ PageLinkLeft.FirewallAddAuthorized("Plain Craft Launcher 启动器(IOI 联机模块)", PathIoi)
+ Catch ex As Exception
+ Log(ex, "无法将联机模块添加至防火墙白名单,可能导致联机失败", LogLevel.Msgbox)
+ End Try
+ Else
+ '无管理员权限
+ If MyMsgBox("由于你开启了 Windows 防火墙,PCL2 需要获取防火墙通行权限。" & vbCrLf & vbCrLf &
+ "若继续,PCL2 将尝试以管理员权限重新启动。" & vbCrLf &
+ "若拒绝,联机模块可能会被防火墙拦截,联机会有很大概率失败。", "需要管理员权限", "继续", "拒绝") = 1 Then
+ Log("[IOI] Windows 防火墙:尝试提升权限")
+ If RerunAsAdmin("--link ioi") Then
+ FrmMain.EndProgram(False) '已重新运行
+ Else
+ Hint("获取管理员权限失败,请尝试右键 PCL2,选择 " & vbLQ & "以管理员身份运行" & vbRQ & ",然后再进入联机页面!", HintType.Critical)
+ End If
+ Else
+ Hint("在没有防火墙权限的情况下尝试联机,很可能会导致联机失败!", HintType.Critical)
+ End If
+ End If
+ Log("[IOI] Windows 防火墙:无防火墙通行权限")
+ Catch ex As Exception
+ Log(ex, "无法检测防火墙状态,可能导致联机失败", LogLevel.Msgbox)
+ End Try
+ End Sub
+
+#End Region
+
+#Region "进程管理"
+
+ Private Shared IoiId As String, IoiPassword As String
+ Private Shared IoiProcess As Process = Nothing
+ Private Shared IoiState As LoadState = LoadState.Waiting
+
+ '''
+ ''' 若 Ioi 正在运行,则结束 Ioi 进程,同时初始化状态数据。返回是否关闭了对应进程。
+ '''
+ Public Shared Function IoiStop(SleepWhenKilled As Boolean) As Boolean
+ IoiStop = False
+ '发送断开信息
+ Try
+ If IoiProcess IsNot Nothing AndAlso Not IoiProcess.HasExited Then
+ For i = 0 To UserList.Count - 1
+ If i > UserList.Count - 1 Then Exit For
+ Dim User = UserList.Values(i)
+ SendDisconnectRequest(User)
+ Next
+ End If
+ Catch ex As Exception
+ Log(ex, "结束 IOI 进程时发送 Disconnect 信息失败")
+ End Try
+ '关闭所有 Ioi 进程
+ For Each ProcessObject In Process.GetProcesses
+ If ProcessObject.ProcessName <> "IOI 联机模块" Then Continue For
+ IoiStop = True
+ Try
+ ProcessObject.Kill()
+ Log("[IOI] 已关闭联机模块:" & ProcessObject.Id)
+ If SleepWhenKilled Then Thread.Sleep(3000) '等待 3 秒确认进程已退出
+ Catch ex As Exception
+ Log(ex, "关闭联机模块失败(" & ProcessObject.Id & ")")
+ End Try
+ Next
+ '初始化
+ IoiProcess = Nothing
+ IoiId = Nothing
+ IoiPassword = Nothing
+ IoiState = LoadState.Waiting
+ UserList = New Dictionary(Of String, LinkUserIoi)
+ RoomListForMe = New List(Of RoomEntry)
+ End Function
+ '''
+ ''' 启动 Ioi,并等待初始化完成后退出运行,同时更新 IoiId 与 IoiPassword。
+ ''' 正常初始化返回 True,需要更新返回 False,其余情况抛出异常。
+ ''' 若 Ioi 正在运行,则会先停止其运行。
+ '''
+ Public Shared Function IoiStart() As Boolean
+ IoiStop(True)
+ Log("[IOI] 启动联机模块进程")
+ Dim Info = New ProcessStartInfo With {
+ .FileName = PathIoi,
+ .UseShellExecute = False,
+ .CreateNoWindow = True,
+ .RedirectStandardError = True,
+ .RedirectStandardOutput = True,
+ .WorkingDirectory = PathTemp
+ }
+ IoiProcess = New Process() With {.StartInfo = Info}
+ Using outputWaitHandle As New AutoResetEvent(False)
+ Using errorWaitHandle As New AutoResetEvent(False)
+ AddHandler IoiProcess.OutputDataReceived, Function(sender, e)
+ Try
+ If e.Data Is Nothing Then
+ outputWaitHandle.[Set]()
+ Else
+ IoiLogLine(e.Data)
+ End If
+ Catch ex As ObjectDisposedException
+ Catch ex As Exception
+ Log(ex, "读取联机模块信息失败")
+ IoiState = LoadState.Failed
+ End Try
+ Return Nothing
+ End Function
+ AddHandler IoiProcess.ErrorDataReceived, Function(sender, e)
+ Try
+ If e.Data Is Nothing Then
+ errorWaitHandle.[Set]()
+ Else
+ IoiLogLine(e.Data)
+ End If
+ Catch ex As ObjectDisposedException
+ Catch ex As Exception
+ Log(ex, "读取联机模块错误信息失败")
+ IoiState = LoadState.Failed
+ End Try
+ Return Nothing
+ End Function
+ IoiProcess.Start()
+ IoiState = LoadState.Loading
+ IoiProcess.BeginOutputReadLine()
+ IoiProcess.BeginErrorReadLine()
+ '等待
+ Do Until IoiProcess.HasExited OrElse IoiState <> LoadState.Loading
+ Thread.Sleep(10)
+ Loop
+ '输出
+ If IoiState = LoadState.Finished Then
+ Log("[IOI] 联机模块启动成功")
+ Return True
+ Else
+ Throw New Exception("联机模块启动失败")
+ End If
+ 'Select Case IoiState
+ ' Case LoadState.Finished
+ ' Log("[IOI] 联机模块启动成功")
+ ' Return True
+ ' Case LoadState.Aborted
+ ' Log("[IOI] 联机模块要求更新")
+ ' Return False
+ ' Case LoadState.Failed
+ ' Log("[IOI] 联机模块启动出现异常")
+ ' Return False
+ ' Case Else 'LoadState.Loading
+ ' Throw New Exception("联机模块启动失败,请检查你的网络连接")
+ 'End Select
+ End Using
+ End Using
+ End Function
+
+ 'Ioi 日志
+ Private Shared Sub IoiLogLine(Content As String)
+
+ '初始化
+ If Content.Contains(" > http://127.0.0.1:55555/") Then Exit Sub
+ If Content.Contains("All done") Then
+ If IoiId IsNot Nothing AndAlso IoiPassword IsNot Nothing Then
+ IoiState = LoadState.Finished
+ Else
+ Log("[IOI] 联机模块汇报初始化完成,但未提供账户信息")
+ IoiState = LoadState.Failed
+ End If
+ Exit Sub
+ End If
+ If Content.Contains("Password :: ") Then
+ IoiPassword = Content.Split({"Password :: "}, StringSplitOptions.None)(1)
+ Exit Sub '为保证安全不记录密码到 Log
+ End If
+
+ '初始化
+ If Content.Contains("ID :: ") Then IoiId = Content.Split({"ID :: "}, StringSplitOptions.None)(1)
+ If Content.Contains("Initialization failed") OrElse Content.Contains("The version is ") Then IoiState = LoadState.Aborted
+
+ '系统回应
+ If Content.Contains("'portssub' from ") Then LastPortsId = Content.Split(" ").Last
+ If Content.Contains("Listening tcp ") Then
+ If UserList.ContainsKey(LastPortsId) Then
+ DictionaryAdd(UserList(LastPortsId).Ports, Content.Split(" ").Last, RegexSeek(Content, "(?<=Listening tcp )[^:]+"))
+ Else
+ Log("[IOI] 未在列表中的用户出现意料外的连接信息")
+ NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & LastPortsId & "&password=" & IoiPassword, "DELETE", "", "", 500)
+ End If
+ End If
+
+ '断开连接
+ If Content.Contains("Closed tcp connection from ") AndAlso Content.Contains(":55557") Then
+ For i = 0 To UserList.Count - 1
+ Dim User = UserList.Values(i)
+ If Not Content.Contains(User.Ports(55557)) OrElse User.IsDisposed Then Continue For
+ Log("[IOI] 检测到 55557 端口断开(" & User.DisplayName & ")")
+ UserRemove(User, True)
+ Exit For
+ Next
+ End If
+
+ '写入日志
+ If Not Content.Contains("read /dev/stdin: The handle is invalid.") AndAlso LogLinesCount < If(ModeDebug, 10000, 1000) Then
+ LogLinesCount += 1
+ Log("[IOI] " & Content)
+ End If
+
+ End Sub
+ Private Shared LogLinesCount As Integer = 0
+ Private Shared LastPortsId As String = "" '上一个收到 portssub 的 ID,用于记录端口
+
+#End Region
+
+#Region "时钟"
+
+ 'UI 线程刷新
+ Private Shared UserListIdentifyCache As String = ""
+ Private Shared RoomListIdentifyCache As String = ""
+ Public Sub RefreshUi()
+ Try
+ '确认用户列表缓存
+ Dim UserListIdentify As String = Join(UserList, vbCrLf)
+ If UserListIdentify <> UserListIdentifyCache Then
+ UserListIdentifyCache = UserListIdentify
+ '刷新列表项
+ PanUserList.Children.Clear()
+ For i = 0 To UserList.Count - 1
+ If i > UserList.Count - 1 Then Exit For
+ PanUserList.Children.Add(UserList.Values(i).ToListItem)
+ Next
+ '刷新列表标题
+ CardUser.Title = "已连接的玩家 (" & PanUserList.Children.Count & ")"
+ End If
+ '刷新用户列表项
+ For Each UserItem As MyListItem In PanUserList.Children
+ CType(UserItem.Tag, LinkUserIoi).RefreshUi(UserItem)
+ Next
+ '确认房间列表缓存
+ Dim RoomListIdentify As String = Join(GetRoomList(), vbCrLf)
+ If RoomListIdentify <> RoomListIdentifyCache Then
+ RoomListIdentifyCache = RoomListIdentify
+ '刷新列表项
+ PanRoom.Children.Clear()
+ For Each Room In GetRoomList()
+ PanRoom.Children.Add(Room.ToListItem)
+ Next
+ End If
+ '刷新房间列表项
+ For Each RoomItem As MyListItem In PanRoom.Children
+ CType(RoomItem.Tag, RoomEntry).RefreshUi(RoomItem)
+ Next
+ '刷新操作提示
+ If UserList.Count = 0 Then
+ If RoomListForMe.Count = 0 Then
+ LabHint.Text = "若想创建房间,请点击创建房间按钮,并按说明进行操作。" & vbCrLf & "若想加入他人的房间,请点击建立连接,然后输入对方的联机码。"
+ Else
+ LabHint.Text = "若想让其他人加入你的房间,请点击复制联机码,然后让你的朋友输入你的联机码以建立连接。"
+ End If
+ Else
+ If GetRoomList.Count = 0 Then
+ LabHint.Text = "若想创建房间,请点击创建房间按钮,然后输入对局域网开放后 MC 显示的端口号,或本地服务端的端口号。"
+ ElseIf RoomListForMe.Count = 0 Then
+ '已有连接,本人没有房间,对方有房间
+ LabHint.Text = "若想加入某个房间,直接点击该房间即可获取说明。"
+ Else
+ '已有连接,且本人有房间
+ LabHint.Text = "指向你所创建的房间,能在右侧找到修改房间名称、关闭房间等选项。" &
+ If(RoomListForMe.Count <> GetRoomList.Count, vbCrLf & "若想加入其他人的房间,直接点击该房间即可获取说明。", "")
+ End If
+ End If
+ Catch ex As Exception
+ Log(ex, "联机模块 UI 时钟运行失败", LogLevel.Feedback)
+ End Try
+ End Sub
+
+ '工作线程刷新
+ Public Sub RefreshWorker()
+ Try
+ '检测退出
+ If InitLoader.State = LoadState.Finished AndAlso IoiProcess.HasExited Then
+ Log("[IOI] 联机模块出现异常!", LogLevel.Hint)
+ ModuleStopManually()
+ End If
+ '检测用户
+ For i = 0 To UserList.Values.Count - 1
+ If i > UserList.Values.Count - 1 Then Exit For
+ Dim User = UserList.Values(i)
+ If User.Progress < 1 Then Continue For '跳过连接中的用户
+ '发送心跳包
+ If Date.Now - User.LastSend > New TimeSpan(0, 0, RandomInteger(50, 30)) Then
+ RunInNewThread(Sub()
+ Try
+ SendUpdateRequest(User, 1)
+ Catch ex As Exception
+ Log(ex, "心跳包发送失败(" & User.DisplayName & ")", LogLevel.Normal)
+ End Try
+ End Sub, "Link Heartbeat " & User.Id)
+ End If
+ '检测被动离线
+ If Date.Now - User.LastReceive > New TimeSpan(0, 2, 0) Then
+ SendDisconnectRequest(User)
+ Hint("与 " & User.DisplayName & " 的连接已中断!", HintType.Critical)
+ End If
+ Next
+ Catch ex As Exception
+ Log(ex, "联机模块工作时钟运行失败", LogLevel.Feedback)
+ End Try
+ End Sub
+
+#End Region
+
+#Region "发送请求"
+
+ '''
+ ''' 发送 Portsub 请求并等待获取控制台端口。进度将从 0 变化至 80%。
+ '''
+ Private Shared Sub SendPortsubRequest(User As LinkUserIoi)
+ Log("[IOI] 尝试建立连接:" & User.Id)
+ '向对方发送 portsub 请求
+ Dim Retry As Integer = 0
+RetryLink:
+ Dim Result As JObject = GetJson(NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "PUT", "", "", 100000))
+ '失败与重试
+ If Result("msg") IsNot Nothing Then
+ Dim ErrorMessage As String = Result("msg").ToString
+ Select Case ErrorMessage
+ Case "failed to find any peer in table"
+ Retry += 1
+ ErrorMessage = "我方网络环境不佳,连接失败。" '初始化对等机列表超时
+ Log("[IOI] 尝试建立连接结果:未找到对等机,第 " & Retry & " 级重试")
+ Case "routing: not found"
+ Retry += 4
+ ErrorMessage = "我方或对方网络环境不佳,或对方已关闭联机模块,未找到路由。" '未知情况
+ Log("[IOI] 尝试建立连接结果:无法连接到路由,第 " & Retry & " 级重试")
+ Case "you are already connected to specified host"
+ If User.IsDisposed Then Throw New ThreadInterruptedException("用户对象已被释放")
+ Log("[IOI] 尝试建立连接结果:已与对方连接")
+ GoTo Done
+ 'Retry += 1
+ 'NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 100000)
+ 'ErrorMessage = "已与对方连接。" '双向连接偶尔导致
+ 'Log("[IOI] 已与对方连接,尝试中断现有连接,第 " & Retry & " 级重试")
+ Case "dial backoff"
+ Retry += 20
+ ErrorMessage = "对方网络环境不佳,或对方已关闭联机模块。请尝试让对方主动连接你,而不是你去连接对方。" 'NAT2 连 NAT4 导致
+ Log("[IOI] 尝试建立连接结果:NAT 异常,第 " & Retry & " 级重试")
+ Case Else
+ If ErrorMessage.Contains("all dials failed") Then
+ Retry += 8
+ ErrorMessage = "我方或对方网络环境不佳,或对方已关闭联机模块,连接失败。" '网络不佳,或对方已关闭模块
+ Log("[IOI] 尝试建立连接结果:连接失败,第 " & Retry & " 级重试")
+ Else
+ Retry += 8
+ Log("[IOI] 尝试建立连接结果:未知错误(" & ErrorMessage & "),第 " & Retry & " 级重试")
+ End If
+ End Select
+ If User.IsDisposed Then Throw New ThreadInterruptedException("用户对象已被释放")
+ If Retry <= 64 Then
+ User.Progress = Retry * 0.01 + 0.05
+ Thread.Sleep(3000)
+ GoTo RetryLink
+ Else
+ Throw New InvalidOperationException(ErrorMessage)
+ End If
+ End If
+ Log("[IOI] 尝试建立连接结果:成功")
+ '等待获取对方端口
+Done:
+ Dim WaitCount As Integer = 0
+ Do Until User.Ports.ContainsKey(55555) AndAlso User.Ports.ContainsKey(55557)
+ WaitCount += 1
+ User.Progress = 0.7 + WaitCount / 200 * 0.1 '70% ~ 80%
+ If WaitCount = 100 Then Throw New Exception("连接超时,请尝试重新连接(未收到端口回报)!")
+ Thread.Sleep(150)
+ Loop
+ User.Progress = 0.8
+ End Sub
+
+ '''
+ ''' 向控制台发送 Connect 请求。
+ '''
+ Private Shared Sub SendConnectRequest(User As LinkUserIoi)
+ Dim RawJson As New JObject()
+ RawJson("version") = RequestVersion
+ RawJson("name") = GetPlayerName()
+ RawJson("id") = IoiId
+ RawJson("type") = "connect"
+ User.Send(RawJson)
+ End Sub
+
+ '''
+ ''' 向控制台发送 Update 请求。
+ '''
+ Private Shared Sub SendUpdateRequest(User As LinkUserIoi, Stage As Integer, Optional Unique As Long = -1)
+ If Unique = -1 Then Unique = GetTimeTick()
+ Dim RawJson As New JObject
+ RawJson("name") = GetPlayerName()
+ RawJson("id") = IoiId
+ RawJson("type") = "update"
+ RawJson("stage") = Stage
+ RawJson("unique") = Unique
+ If Stage < 3 Then
+ Dim Rooms As New JArray
+ For Each Room In RoomListForMe
+ Dim RoomObject As New JObject
+ RoomObject("name") = Room.DisplayName
+ RoomObject("port") = Room.Port
+ Rooms.Add(RoomObject)
+ Next
+ RawJson("rooms") = Rooms
+ DictionaryAdd(User.PingPending, Unique, Date.Now)
+ End If
+ User.Send(RawJson)
+ End Sub
+
+ '''
+ ''' 尝试发送断开请求,并将其从用户列表中移除。
+ '''
+ Private Shared Sub SendDisconnectRequest(User As LinkUserIoi, Optional Message As String = Nothing, Optional IsError As Boolean = False)
+ Dim RawJson As New JObject()
+ RawJson("id") = IoiId
+ RawJson("type") = "disconnect"
+ If Message IsNot Nothing Then
+ RawJson("message") = Message
+ RawJson("isError") = IsError
+ End If
+ Try
+ User.Send(RawJson)
+ Thread.Sleep(50)
+ Catch
+ End Try
+ NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
+ User.Dispose()
+ End Sub
+
+#End Region
+
+#Region "左边栏操作"
+
+ '刷新连接
+ Private Shared Sub BtnListRefresh_Click(sender As MyIconButton, e As EventArgs)
+ Dim User As LinkUserIoi = sender.Tag
+ User.PingRecord.Clear()
+ FrmLinkIoi.RefreshUi()
+ RunInThread(Sub()
+ Try
+ SendUpdateRequest(User, 1)
+ Catch ex As Exception
+ If InitLoader.State = LoadState.Finished Then Log(ex, "刷新与 " & User.DisplayName & " 的连接失败", LogLevel.Hint)
+ End Try
+ End Sub)
+ End Sub
+ '断开连接
+ Private Shared Sub BtnListDisconnect_Click(sender As MyIconButton, e As EventArgs)
+ Dim User As LinkUserIoi = sender.Tag
+ sender.IsEnabled = False
+ RunInThread(Sub()
+ If User.Progress < 1 AndAlso User.RelativeThread IsNot Nothing AndAlso User.RelativeThread.IsAlive Then
+ NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
+ User.Dispose()
+ Else
+ SendDisconnectRequest(User, GetPlayerName() & " 主动断开了连接!")
+ End If
+ End Sub)
+ End Sub
+ '复制联机码
+ Public Shared Sub BtnLeftCopy_Click() Handles BtnLeftCopy.Click
+ ClipboardSet(IoiId.Substring(4) & SecretEncrypt(GetPlayerName), False)
+ Hint("已复制联机码!", HintType.Finish)
+ End Sub
+
+#End Region
+
+#Region "玩家名"
+
+ '''
+ ''' 获取当前的玩家名。
+ '''
+ Public Shared Function GetPlayerName() As String
+ '自动生成玩家名
+ If AutogenPlayerName Is Nothing Then
+ If IsPlayerNameValid(McLoginName) Then
+ AutogenPlayerName = McLoginName()
+ Else
+ AutogenPlayerName = "玩家 " & CType(GetHash(If(UniqueAddress, "")) Mod 1048576, Integer).ToString("x5").ToUpper
+ End If
+ End If
+ '获取玩家自定义的名称
+ Dim CustomName As String = Setup.Get("LinkName").ToString.Trim()
+ If CustomName <> "" Then
+ If IsPlayerNameValid(CustomName) Then
+ Return CustomName.Trim
+ Else
+ Hint("你所设置的玩家名存在异常,已被重置!", HintType.Critical)
+ Setup.Set("LinkName", "")
+ End If
+ End If
+ '使用自动生成的玩家名
+ Return AutogenPlayerName
+ End Function
+ Private Shared AutogenPlayerName As String = Nothing '并非由玩家自定义,而是自动生成的玩家名
+ '''
+ ''' 检查某个玩家名是否合法。
+ '''
+ Private Shared Function IsPlayerNameValid(Name As String) As Boolean
+ For Each ValidateRule As Validate In {New ValidateNullOrWhiteSpace, New ValidateLength(0, 20), New ValidateFilter}
+ If Not String.IsNullOrEmpty(ValidateRule.Validate(Name)) Then Return False
+ Next
+ Return True
+ End Function
+
+#End Region
+
+#Region "请求核心"
+
+ Private Shared Listener As Socket = Nothing
+ '''
+ ''' 启动 Socket 监听核心。
+ '''
+ Public Shared Sub StartSocketListener()
+ If Listener IsNot Nothing Then Exit Sub '已经启动过了
+ Listener = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) With {.ReceiveTimeout = 1000000000, .SendTimeout = 1000000000}
+ Listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, True)
+ Try
+ Listener.Bind(New IPEndPoint(IPAddress.Any, 55557))
+ Catch ex As Exception
+ Log(ex, "初次启动 Socket 监听核心失败,开始重试")
+ Thread.Sleep(1000)
+ Listener.Bind(New IPEndPoint(IPAddress.Any, 55557))
+ End Try
+ Listener.Listen(100)
+ Log("[IOI] 已启动 Socket 监听核心")
+ RunInNewThread(Sub()
+ While True
+ Try
+ '获取新的 Socket
+ Dim bytes(1024) As Byte
+ Dim ClientSocket As Socket = Listener.Accept()
+ '获取初始输入信息
+ Dim RequestInput As String = Encoding.UTF8.GetString(bytes, 0, ClientSocket.Receive(bytes))
+ If Not RequestInput.StartsWith("PCL - ") Then Exit Sub
+ Dim RawData As String
+ Try
+ RawData = SecretDecrypt(WebUtility.UrlDecode(RequestInput.Substring(6)))
+ Log("[IOI] 接收到新的 Socket:" & RawData)
+ PageLinkIoi.ReceiveJson(GetJson(RawData), ClientSocket)
+ Catch ex As Exception
+ Log(ex, "新的 Socket 处理出错(内容:" & RequestInput & ")")
+ End Try
+ Catch ex As Exception
+ Log(ex, "Socket 监听出错")
+ Thread.Sleep(3000) '防止死循环出错
+ End Try
+ End While
+ End Sub, "Socket Listener")
+ End Sub
+
+#End Region
+
+#Region "用户核心"
+
+ '用户基类
+ Public MustInherit Class LinkUserBase
+ Implements IDisposable
+
+ '基础数据
+ Public Uuid As Integer = GetUuid()
+ Public Id As String
+ Public DisplayName As String
+
+ '请求管理
+ Public Socket As Socket = Nothing
+ Public Sub Send(Request As JObject)
+ If Socket Is Nothing Then Throw New Exception("该用户尚未绑定 Socket")
+ Dim Data As String = Request.ToString(Newtonsoft.Json.Formatting.None)
+ If ModeDebug Then Log("[IOI] 发送联机数据包(" & DisplayName & "):" & Data)
+ Data = "PCL - " & WebUtility.UrlEncode(SecretEncrypt(Data))
+ Me.LastSend = Date.Now
+ Socket.Send(Encoding.UTF8.GetBytes(Data))
+ End Sub
+ Public ListenerThread As Thread = Nothing
+ Public Sub StartListener()
+ If ListenerThread IsNot Nothing Then Exit Sub
+ '启动监听
+ ListenerThread = RunInNewThread(Sub()
+ Try
+ Dim bytes(1024) As Byte
+ While PageLinkIoi.UserList.ContainsValue(Me)
+ Dim RequestInput As String = Encoding.UTF8.GetString(bytes, 0, Socket.Receive(bytes))
+ If Not RequestInput.StartsWith("PCL - ") Then Exit Sub
+ Dim RawData As String = SecretDecrypt(WebUtility.UrlDecode(RequestInput.Substring(6)))
+ If ModeDebug Then Log("[IOI] 接收联机数据包(" & DisplayName & "):" & RawData)
+ PageLinkIoi.ReceiveJson(GetJson(RawData)) '调用去实际的联机模块
+ End While
+ Catch ex As ThreadInterruptedException
+ Log("[IOI] 用户监听已中断(" & DisplayName & ")")
+ Catch ex As Exception
+ If IsDisposed Then Exit Sub
+ If ex.GetType.Equals(GetType(SocketException)) AndAlso CType(ex, SocketException).SocketErrorCode = 10053 Then
+ Log("[IOI] 客户端已关闭(" & DisplayName & ")")
+ PageLinkIoi.UserRemove(CType(Me, PageLinkIoi.LinkUserIoi), True)
+ Exit Sub
+ End If
+ Log(ex, "用户监听出错(" & DisplayName & ")")
+ PageLinkIoi.UserRemove(CType(Me, PageLinkIoi.LinkUserIoi), True)
+ End Try
+ End Sub, "Link Listener " & DisplayName)
+ End Sub
+ Public Sub BindSocket(Socket As Socket)
+ If Me.Socket IsNot Nothing Then Throw New Exception("该用户已经绑定了 Socket")
+ Me.Socket = Socket
+ StartListener()
+ End Sub
+
+ 'Ping
+ '0:与 Ping 计算无关,不回应
+ '1:A to B,2:B to A,3:A to B
+ Public PingPending As New Dictionary(Of Long, Date)
+ Public PingRecord As New Queue(Of Integer)
+
+ '心跳包
+ Public LastSend As Date = Date.Now
+ Public LastReceive As Date = Date.Now
+
+ '类型转换
+ Public Sub New(Id As String, DisplayName As String)
+ Me.Id = Id
+ Me.DisplayName = DisplayName
+ Log("[IOI] 无通信包的新用户对象:" & ToString())
+ End Sub
+ Public Sub New(Id As String, DisplayName As String, Socket As Socket)
+ Me.Id = Id
+ Me.DisplayName = DisplayName
+ Me.Socket = Socket
+ Log("[IOI] 新用户对象:" & ToString())
+ StartListener()
+ End Sub
+ Public Overrides Function ToString() As String
+ Return DisplayName & " @ " & Id & " #" & Uuid
+ End Function
+ Public Shared Widening Operator CType(User As LinkUserBase) As String
+ Return User.ToString
+ End Operator
+
+ '释放资源
+ Public IsDisposed As Boolean = False
+ Protected Overridable Sub Dispose(IsDisposing As Boolean)
+ If Socket IsNot Nothing Then Socket.Dispose()
+ If ListenerThread IsNot Nothing AndAlso ListenerThread.IsAlive Then ListenerThread.Interrupt()
+ End Sub
+ Public Sub Dispose() Implements IDisposable.Dispose
+ If Not IsDisposed Then
+ IsDisposed = True
+ Dispose(True)
+ End If
+ GC.SuppressFinalize(Me)
+ End Sub
+ End Class
+
+ '用户对象
+ Public Shared UserList As New Dictionary(Of String, LinkUserIoi)
+ Public Class LinkUserIoi
+ Inherits LinkUserBase
+ Public Sub New(Id As String, DisplayName As String, Socket As Socket)
+ MyBase.New(Id, DisplayName, Socket)
+ End Sub
+ Public Sub New(Id As String, DisplayName As String)
+ MyBase.New(Id, DisplayName)
+ End Sub
+
+ '基础数据
+ Public Ports As New Dictionary(Of Integer, String)
+ Public Rooms As New List(Of RoomEntry)
+
+ '进度与 UI
+ Public Progress As Double = 0
+ Public RelativeThread As Thread = Nothing
+
+ Public Function GetDescription() As String
+ Return If(Progress < 1,
+ "正在连接," & Math.Round(Progress * 100) & "%",
+ "已连接," & If(PingRecord.Count = 0, "检查延迟中", Math.Round(PingRecord.Average) & "ms"))
+ End Function
+ Public Function ToListItem() As MyListItem
+ Dim Item As New MyListItem With {
+ .Title = DisplayName, .Height = 42, .Tag = Me, .Type = MyListItem.CheckType.None,
+ .PaddingRight = 60,
+ .Logo = "pack://application:,,,/images/Blocks/Grass.png"}
+ '绑定图标按钮
+ Dim BtnRefresh As New MyIconButton With {.Logo = Logo.IconButtonRefresh, .LogoScale = 0.85, .ToolTip = "刷新", .Tag = Me}
+ AddHandler BtnRefresh.Click, AddressOf BtnListRefresh_Click
+ ToolTipService.SetPlacement(BtnRefresh, Primitives.PlacementMode.Bottom)
+ ToolTipService.SetHorizontalOffset(BtnRefresh, -10)
+ ToolTipService.SetVerticalOffset(BtnRefresh, 5)
+ ToolTipService.SetShowDuration(BtnRefresh, 2333333)
+ ToolTipService.SetInitialShowDelay(BtnRefresh, 200)
+ Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "断开", .Tag = Me}
+ AddHandler BtnClose.Click, AddressOf BtnListDisconnect_Click
+ ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
+ ToolTipService.SetHorizontalOffset(BtnClose, -10)
+ ToolTipService.SetVerticalOffset(BtnClose, 5)
+ ToolTipService.SetShowDuration(BtnClose, 2333333)
+ ToolTipService.SetInitialShowDelay(BtnClose, 200)
+ Item.Buttons = {BtnRefresh, BtnClose}
+ '刷新并返回
+ RefreshUi(Item)
+ Return Item
+ End Function
+ Public Sub RefreshUi(RelatedListItem As MyListItem)
+ RelatedListItem.Title = DisplayName
+ RelatedListItem.Info = GetDescription()
+ RelatedListItem.Buttons(0).Visibility = If(Progress = 1, Visibility.Visible, Visibility.Collapsed)
+ End Sub
+
+ '释放
+ Protected Overrides Sub Dispose(IsDisposing As Boolean)
+ Log("[IOI] 用户资源释放(IOI, " & DisplayName & ")")
+ If RelativeThread IsNot Nothing AndAlso RelativeThread.IsAlive Then RelativeThread.Interrupt()
+ UserList.Remove(Id)
+ MyBase.Dispose(IsDisposing)
+ End Sub
+ End Class
+
+ '房间对象
+ Private Shared RoomListForMe As New List(Of RoomEntry)
+
+ Private Function GetRoomList() As List(Of RoomEntry)
+ Dim RoomList As New List(Of RoomEntry)(RoomListForMe)
+ For i = 0 To UserList.Count - 1
+ If i > UserList.Count - 1 Then Exit For
+ RoomList.AddRange(UserList.Values(i).Rooms)
+ Next
+ Return RoomList
+ End Function
+ Public Class RoomEntry
+
+ '基础数据
+ Public Port As Integer
+ Public DisplayName As String
+ Public User As LinkUserIoi = Nothing '若 IsOwner = True,则此项为 Nothing
+ Public IsOwner As Boolean
+ Public ReadOnly Property Ip As String
+ Get
+ If IsOwner Then
+ Return "localhost:" & Port
+ Else
+ Return User.Ports(Port) & ":" & Port
+ End If
+ End Get
+ End Property
+
+ '类型转换
+ Public Sub New(Port As Integer, DisplayName As String, Optional User As LinkUserIoi = Nothing)
+ Me.IsOwner = User Is Nothing
+ Me.User = User
+ Me.DisplayName = DisplayName
+ Me.Port = Port
+ End Sub
+ Public Overrides Function ToString() As String
+ Return DisplayName & " - " & Port & " - " & IsOwner
+ End Function
+ Public Shared Widening Operator CType(Room As RoomEntry) As String
+ Return Room.ToString
+ End Operator
+ Public Shared Function SelectPort(Room As RoomEntry) As Integer
+ Return Room.Port
+ End Function
+
+ 'UI
+ Public Function GetDescription() As String
+ If IsOwner Then
+ Return "由我创建,端口 " & Port
+ Else
+ Return "由 " & User.DisplayName & " 创建,端口 " & Port
+ End If
+ End Function
+ Public Function ToListItem() As MyListItem
+ Dim Item As New MyListItem With {
+ .Title = DisplayName, .Height = 42, .Info = GetDescription(), .Tag = Me, .PaddingRight = If(IsOwner, 60, 0),
+ .Type = If(IsOwner, MyListItem.CheckType.None, MyListItem.CheckType.Clickable),
+ .Logo = "pack://application:,,,/images/Blocks/" & If(IsOwner, "GrassPath", "Grass") & ".png"}
+ If IsOwner Then
+ '绑定图标按钮
+ Dim BtnEdit As New MyIconButton With {.Logo = Logo.IconButtonEdit, .LogoScale = 1, .ToolTip = "修改名称", .Tag = Me}
+ AddHandler BtnEdit.Click, AddressOf BtnRoomEdit_Click
+ ToolTipService.SetPlacement(BtnEdit, Primitives.PlacementMode.Bottom)
+ ToolTipService.SetHorizontalOffset(BtnEdit, -22)
+ ToolTipService.SetVerticalOffset(BtnEdit, 5)
+ ToolTipService.SetShowDuration(BtnEdit, 2333333)
+ ToolTipService.SetInitialShowDelay(BtnEdit, 200)
+ Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "关闭", .Tag = Me}
+ AddHandler BtnClose.Click, AddressOf BtnRoomClose_Click
+ ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
+ ToolTipService.SetHorizontalOffset(BtnClose, -10)
+ ToolTipService.SetVerticalOffset(BtnClose, 5)
+ ToolTipService.SetShowDuration(BtnClose, 2333333)
+ ToolTipService.SetInitialShowDelay(BtnClose, 200)
+ Item.Buttons = {BtnEdit, BtnClose}
+ Else
+ '绑定点击事件
+ AddHandler Item.Click, AddressOf BtnRoom_Click
+ End If
+ Return Item
+ End Function
+ Public Sub RefreshUi(RelatedListItem As MyListItem)
+ RelatedListItem.Title = DisplayName
+ RelatedListItem.Info = GetDescription()
+ End Sub
+
+ End Class
+
+#End Region
+
+ '正向与反向连接
+ Public Shared Sub BtnLeftCreate_Click() Handles BtnLeftCreate.Click
+ '获取信息
+ Dim Code As String = MyMsgBoxInput("", New ObjectModel.Collection(Of Validate) From {New ValidateLength(9, 99999)}, "", "输入对方的联机码", "确定", "取消")
+ If Code Is Nothing Then Exit Sub
+ '检查
+ If Code.StartsWith("P") AndAlso Code.Length < 48 Then
+ Hint("你输入的可能是 HiPer 的联机码,请在左侧的联机方式中选择 HiPer!", HintType.Critical) : Exit Sub
+ End If
+ Dim Id As String, DisplayName As String
+ Try '解密失败检查
+ Id = "12D3" & Code.Substring(0, 48)
+ DisplayName = SecretDecrypt(Code.Substring(48))
+ Catch
+ Hint("你输入的联机码有误!", HintType.Critical)
+ Exit Sub
+ End Try
+ If Id = IoiId Then '自我连接检查
+ Hint("我连我自己?搁这卡 Bug 呢?", HintType.Critical)
+ Exit Sub
+ End If
+ '开始
+ Dim User As New LinkUserIoi(Id, DisplayName)
+ User.RelativeThread = RunInNewThread(Sub()
+ Dim WaitCount As Integer = 0
+ Try
+ '加入列表
+ If UserList.ContainsKey(Id) Then
+ Hint(UserList(Id).DisplayName & " 已在列表中,无需再次连接!")
+ Exit Sub
+ Else
+ UserList.Add(Id, User)
+ End If
+ '发送 portsub 请求(0% -> 80%)
+ SendPortsubRequest(User)
+ '构建 Socket(81% -> 82%)
+ Dim ClientSocket As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) With {.ReceiveTimeout = 1000000000, .SendTimeout = 1000000000}
+ ClientSocket.Connect(New IPEndPoint(IPAddress.Parse(User.Ports(55557)), 55557))
+ User.BindSocket(ClientSocket)
+ User.Progress = 0.82
+ '发送 connect 请求(83% -> 85%)
+ SendConnectRequest(User)
+ User.Progress = 0.85
+ '等待对方向自己请求
+ Log("[IOI] 加入成功,等待反向请求")
+ Do While User.Progress < 0.9999
+ User.Progress += 0.0002
+ If User.Progress > 0.98 AndAlso User.Progress < 0.9999 Then Throw New Exception("对方未回应连接请求!")
+ Thread.Sleep(100)
+ Loop
+ Hint("已连接到 " & User.DisplayName & "!", HintType.Finish)
+ Catch ex As ThreadInterruptedException
+ Log("[IOI] 已中断主动发起的连接(" & User.DisplayName & ")")
+ Catch ex As InvalidOperationException
+ 'API 返回的错误
+ NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
+ User.Dispose()
+ If InitLoader.State = LoadState.Finished Then Log("与 " & DisplayName & " 建立连接失败:" & ex.Message, LogLevel.Msgbox)
+ Catch ex As Exception
+ '常规错误
+ NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
+ User.Dispose()
+ If ex.InnerException IsNot Nothing AndAlso TypeOf ex.InnerException Is ThreadInterruptedException Then Exit Sub
+ If InitLoader.State = LoadState.Finished Then Log(ex, "与 " & DisplayName & " 建立连接失败", LogLevel.Msgbox)
+ End Try
+ End Sub, "Link Create " & DisplayName)
+ End Sub
+ Private Shared Sub SendPortsubBack(User As LinkUserIoi, TargetVersion As Integer)
+ Try
+ SendPortsubRequest(User)
+ User.Progress = 0.9
+ If TargetVersion > RequestVersion Then
+ SendDisconnectRequest(User, "无法连接到 " & GetPlayerName() & ":对方的 PCL2 版本过低!", True)
+ Throw New InvalidOperationException("你的 PCL2 版本过低!")
+ ElseIf TargetVersion < RequestVersion Then
+ SendDisconnectRequest(User, "无法连接到 " & GetPlayerName() & ":你的 PCL2 版本过低!", True)
+ Throw New InvalidOperationException("对方的 PCL2 版本过低!")
+ Else
+ SendUpdateRequest(User, 1)
+ User.Progress = 1
+ Hint(User.DisplayName & " 已与你建立连接!")
+ End If
+ Catch ex As ThreadInterruptedException
+ Log("[IOI] 已中断被动建立的连接(" & User.DisplayName & ")")
+ Catch ex As InvalidOperationException
+ 'API 返回的错误
+ NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
+ User.Dispose()
+ If InitLoader.State = LoadState.Finished Then Log("与 " & User.DisplayName & " 建立连接失败:" & ex.Message, LogLevel.Hint)
+ Catch ex As Exception
+ '常规错误
+ NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
+ User.Dispose()
+ If InitLoader.State = LoadState.Finished Then Log(ex, "与 " & User.DisplayName & " 建立连接失败", LogLevel.Hint)
+ End Try
+ End Sub
+
+ '创建房间
+ Private Sub LinkCreate() Handles BtnCreate.Click
+ If MyMsgBox("请先进入 MC 并暂停游戏,在暂停页面选择对局域网开放,然后在下一个窗口输入 MC 显示的端口号。" & vbCrLf & "若使用服务端开服,则直接在下一个窗口输入服务器配置中的端口号即可。", "提示", "继续", "取消") = 2 Then Exit Sub
+ '获取端口号
+ Dim Port As String = MyMsgBoxInput("", New ObjectModel.Collection(Of Validate) From {
+ New ValidateInteger(0, 65535),
+ New ValidateExceptSame({"55555", "55557"}, "端口不能为 %!"),
+ New ValidateExceptSame(RoomListForMe.Select(AddressOf RoomEntry.SelectPort), "端口 % 已创建过房间,请在删除该房间后继续!")
+ }, "", "输入端口号", "确定", "取消")
+ If Port Is Nothing Then Exit Sub
+ '获取显示名称
+ Dim DisplayName As String = MyMsgBoxInput(GetPlayerName() & " 的房间 - " & Port, New ObjectModel.Collection(Of Validate) From {
+ New ValidateNullOrWhiteSpace(), New ValidateLength(1, 40), New ValidateFilter()
+ }, "", "输入房间名(建议包含游戏版本等信息)", "确定", "取消")
+ If DisplayName Is Nothing Then Exit Sub
+ DisplayName = DisplayName.Trim
+ '开始
+ RunInThread(Sub()
+ Try
+ '请求
+ Dim Result As JObject = GetJson(NetRequestOnce("http://127.0.0.1:55555/api/port?proto=tcp&port=" & Port & "&password=" & IoiPassword, "PUT", "", "", 100000))
+ If Result("msg") IsNot Nothing Then Throw New InvalidOperationException(Result("msg").ToString)
+ '成功
+ RoomListForMe.Add(New RoomEntry(Port, DisplayName))
+ Hint("房间 " & DisplayName & " 已创建!", HintType.Finish)
+ SendUpdateRequestToAllUsers()
+ Catch ex As InvalidOperationException
+ 'API 返回的错误
+ Log("创建房间失败:" & ex.Message, LogLevel.Msgbox)
+ Catch ex As Exception
+ '常规错误
+ Log(ex, "创建房间失败", LogLevel.Msgbox)
+ End Try
+ End Sub)
+ End Sub
+ Private Shared Sub SendUpdateRequestToAllUsers()
+ For i = 0 To UserList.Count - 1
+ If i > UserList.Count - 1 Then Exit For
+ Dim User = UserList.Values(i)
+ If User.Progress < 1 Then Continue For
+ Try
+ SendUpdateRequest(User, 1) '不需要使用多线程,发送实际会瞬间完成
+ Catch ex As Exception
+ Log(ex, "发送全局刷新请求失败(" & User.DisplayName & ")")
+ End Try
+ Next
+ End Sub
+ '修改房间名称
+ Private Shared Sub BtnRoomEdit_Click(sender As MyIconButton, e As EventArgs)
+ Dim Room As RoomEntry = sender.Tag
+ '获取房间名
+ Dim DisplayName As String = MyMsgBoxInput(Room.DisplayName, New ObjectModel.Collection(Of Validate) From {
+ New ValidateNullOrWhiteSpace(),
+ New ValidateLength(1, 40)
+ }, "", "输入房间名(建议包含游戏版本等信息)", "确定", "取消")
+ If DisplayName Is Nothing Then Exit Sub
+ DisplayName = DisplayName.Trim
+ '修改
+ Room.DisplayName = DisplayName
+ FrmLinkIoi.RefreshUi()
+ SendUpdateRequestToAllUsers()
+ End Sub
+ '加入房间
+ Private Shared Sub BtnRoom_Click(sender As MyListItem, e As EventArgs)
+ Dim Room As RoomEntry = sender.Tag
+ If MyMsgBox("请在多人游戏页面点击直接连接,输入 " & Room.Ip & " 以进入服务器!", "加入房间", "复制地址", "确定") = 1 Then
+ ClipboardSet(Room.Ip)
+ End If
+ End Sub
+ '关闭房间
+ Private Shared Sub BtnRoomClose_Click(sender As MyIconButton, e As EventArgs)
+ Dim Room As RoomEntry = sender.Tag
+ RunInThread(Sub()
+ Try
+ '远程移除
+ Dim Result As JObject = GetJson(NetRequestOnce("http://127.0.0.1:55555/api/port?proto=tcp&port=" & Room.Port & "&password=" & IoiPassword, "DELETE", "", "", 100000))
+ If Result("msg") IsNot Nothing Then Throw New InvalidOperationException(Result("msg").ToString)
+ '本地移除
+ RoomListForMe.Remove(Room)
+ '成功
+ RunInUi(Sub() FrmLinkIoi.RefreshUi())
+ SendUpdateRequestToAllUsers()
+ Catch ex As InvalidOperationException
+ 'API 返回的错误
+ If InitLoader.State = LoadState.Finished Then Log("移除房间失败:" & ex.Message, LogLevel.Msgbox)
+ Catch ex As Exception
+ '常规错误
+ If InitLoader.State = LoadState.Finished Then Log(ex, "移除房间失败", LogLevel.Msgbox)
+ End Try
+ End Sub)
+ End Sub
+
+ '获取数据包
+ Public Shared Sub ReceiveJson(JsonData As JObject, Optional NewSocket As Socket = Nothing)
+ '获取数据
+ Dim Id As String = JsonData("id"), Type As String = JsonData("type")
+ Select Case Type
+ Case "connect"
+ Dim DisplayName As String = JsonData("name")
+ Dim User As New LinkUserIoi(Id, DisplayName, NewSocket)
+ '如果发生了双向连接
+ If UserList.ContainsKey(Id) Then
+ If Id > IoiId Then
+ Log("[IOI] 双向连接,应当抛弃当前用户(" & DisplayName & ")")
+ For Each Pair In UserList(User.Id).Ports.ToList
+ DictionaryAdd(User.Ports, Pair.Key, Pair.Value)
+ Next
+ UserList(Id).Dispose()
+ Else
+ '应当保留当前用户
+ Log("[IOI] 双向连接,应当保留当前用户(" & DisplayName & ")")
+ NewSocket.Dispose()
+ Exit Sub
+ End If
+ End If
+ '加入列表
+ UserList.Add(Id, User)
+ '返回请求
+ User.RelativeThread = RunInNewThread(Sub()
+ SendPortsubBack(User, JsonData("version").ToObject(Of Integer))
+ End Sub, "Link Connect " & DisplayName)
+ '更新时间
+ User.LastReceive = Date.Now
+ Case "update"
+ If Not UserList.ContainsKey(Id) Then Throw New Exception("未在列表中的用户发送了更新请求:" & Id)
+ Dim User = UserList(Id)
+ '拉满进度(该请求也作为反向连接回应出现,用于向正向连接方传达连接已完成信号)
+ User.Progress = 1
+ '更新名称
+ Dim DisplayName As String = JsonData("name")
+ User.DisplayName = DisplayName
+ '更新房间列表
+ If JsonData("rooms") IsNot Nothing Then
+ User.Rooms = New List(Of RoomEntry)
+ For Each RoomObject In JsonData("rooms")
+ User.Rooms.Add(New RoomEntry(RoomObject("port"), RoomObject("name"), User))
+ Next
+ End If
+ '更新 Ping
+ Dim Stage As Integer = JsonData("stage"), Unique As Long = JsonData("unique")
+ If Stage > 1 Then
+ User.PingRecord.Enqueue((Date.Now - User.PingPending(Unique)).TotalMilliseconds / 2)
+ If User.PingRecord.Count > 5 Then User.PingRecord.Dequeue()
+ User.PingPending.Remove(Unique)
+ End If
+ '返回请求
+ If Stage > 0 AndAlso Stage < 3 Then RunInNewThread(Sub()
+ Try
+ SendUpdateRequest(User, Stage + 1, Unique)
+ Catch ex As Exception
+ Log(ex, "发送回程请求失败")
+ End Try
+ End Sub, "Link Update " & DisplayName)
+ '更新时间
+ User.LastReceive = Date.Now
+ Case "disconnect"
+ '断开连接
+ If Not UserList.ContainsKey(Id) Then Exit Sub
+ UserRemove(UserList(Id), ShowLeaveMessage:=JsonData("message") Is Nothing)
+ If JsonData("message") IsNot Nothing Then Hint(JsonData("message").ToString, If(JsonData("isError").ToObject(Of Boolean), HintType.Critical, HintType.Info))
+ Case Else
+ Throw New Exception("未知的操作种类:" & Type)
+ End Select
+ End Sub
+ '''
+ ''' 从用户列表中移除一位用户。提示信息视作该用户主动离开。
+ '''
+ Public Shared Sub UserRemove(User As LinkUserIoi, ShowLeaveMessage As Boolean)
+ If Not UserList.ContainsKey(User.Id) Then Exit Sub
+ If ShowLeaveMessage Then Hint(User.DisplayName & " 已离开!")
+ NetRequestOnce("http://127.0.0.1:55555/api/link?id=" & User.Id & "&password=" & IoiPassword, "DELETE", "", "", 500)
+ User.Dispose()
+ End Sub
+
+ '关闭联机模块按钮
+ Private Shared Sub ModuleStateChanged(Loader As LoaderBase, NewState As LoadState, OldState As LoadState) Handles InitLoader.OnStateChangedUi
+ If FrmLinkLeft IsNot Nothing Then CType(FrmLinkLeft.ItemIoi.Buttons(0), MyIconButton).Visibility = If(NewState = LoadState.Finished, Visibility.Visible, Visibility.Collapsed)
+ End Sub
+ Public Shared Sub ModuleStopManually()
+ IoiStop(False)
+ InitLoader.Error = New Exception("联机模块已关闭,点击以重新启动")
+ InitLoader.State = LoadState.Failed
+ IoiState = LoadState.Failed
+ End Sub
+
+End Class