diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 997554ce..9260d1c1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,7 +1,7 @@
name: build
on: [push, pull_request]
jobs:
- dotnet-build:
+ dotnet-build-logic:
if: true
runs-on: windows-latest
steps:
@@ -14,11 +14,41 @@ jobs:
- name: Build Logic
run: dotnet build "./logic/logic.sln" -c Release
+ dotnet-build-install:
+ if: true
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 7.0.x
+
- name: Build Installer
- run: dotnet build "./installer/installer.sln" -c Release
+ run: dotnet build "./installer/installer.sln" -c Release -f net7.0-windows10.0.19041.0
+ dotnet-build-launcher:
+ if: true
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 6.0.x
+
- name: Build Launcher
run: dotnet build "./launcher/launcher.sln" -c Release
-
+
+ dotnet-build-playback:
+ if: true
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 6.0.x
+
- name: Build Playback
run: dotnet build "./playback/playback.sln" -c Release
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index c7eeb3a0..33f924e5 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -13,7 +13,7 @@ jobs:
exclude: './players'
inplace: False
- dotnet-format-checking:
+ dotnet-format-checking-logic:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
@@ -26,17 +26,44 @@ jobs:
run: |
dotnet restore "./logic/logic.sln"
dotnet format "./logic/logic.sln" --severity error --no-restore --verify-no-changes
-
+
+ dotnet-format-checking-installer:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 7.0.x
+
- name: Check Installer
run: |
dotnet restore "./installer/installer.sln"
dotnet format "./installer/installer.sln" --severity error --no-restore --verify-no-changes
-
+
+ dotnet-format-checking-launcher:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 6.0.x
+
- name: Check Launcher
run: |
dotnet restore "./launcher/launcher.sln"
dotnet format "./launcher/launcher.sln" --severity error --no-restore --verify-no-changes
+ dotnet-format-checking-playback:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 6.0.x
+
- name: Check Playback
run: |
dotnet restore "./playback/playback.sln"
diff --git a/installer/.gitignore b/installer/.gitignore
index ec116cbf..154e1272 100644
--- a/installer/.gitignore
+++ b/installer/.gitignore
@@ -57,11 +57,14 @@ dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
-# .NET Core
+# .NET
project.lock.json
project.fragment.lock.json
artifacts/
+# Tye
+.tye/
+
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
@@ -397,5 +400,78 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
-#THUAI playback file
-*.thuaipb
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
diff --git a/installer/App.xaml b/installer/App.xaml
new file mode 100644
index 00000000..1589d582
--- /dev/null
+++ b/installer/App.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/App.xaml.cs b/installer/App.xaml.cs
new file mode 100644
index 00000000..88ea998b
--- /dev/null
+++ b/installer/App.xaml.cs
@@ -0,0 +1,12 @@
+namespace installer
+{
+ public partial class App : Application
+ {
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+ }
+}
\ No newline at end of file
diff --git a/installer/AppShell.xaml b/installer/AppShell.xaml
new file mode 100644
index 00000000..7bd085d9
--- /dev/null
+++ b/installer/AppShell.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/AppShell.xaml.cs b/installer/AppShell.xaml.cs
new file mode 100644
index 00000000..1ad5cc6f
--- /dev/null
+++ b/installer/AppShell.xaml.cs
@@ -0,0 +1,10 @@
+namespace installer
+{
+ public partial class AppShell : Shell
+ {
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/installer/MauiProgram.cs b/installer/MauiProgram.cs
new file mode 100644
index 00000000..fd4751b6
--- /dev/null
+++ b/installer/MauiProgram.cs
@@ -0,0 +1,25 @@
+using Microsoft.Extensions.Logging;
+
+namespace installer
+{
+ public static class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ });
+
+#if DEBUG
+ builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/installer/Model/Downloader.cs b/installer/Model/Downloader.cs
new file mode 100644
index 00000000..b4e34d24
--- /dev/null
+++ b/installer/Model/Downloader.cs
@@ -0,0 +1,246 @@
+using COSXML.CosException;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace installer.Model
+{
+ public struct UpdateInfo // 更新信息,包括新版本版本号、更改文件数和新文件数
+ {
+ public string status;
+ public int changedFileCount;
+ public int newFileCount;
+ }
+
+ public class Downloader
+ {
+ #region 属性区
+ public class UserInfo
+ {
+ public string _id = "";
+ public string email = "";
+ }
+ public string ProgramName = "THUAI6"; // 要运行或下载的程序名称
+ public string StartName = "maintest.exe"; // 启动的程序名
+ private Local_Data Data;
+ private Tencent_Cos Cloud;
+
+ private HttpClient Client = new HttpClient();
+ private EEsast Web = new EEsast();
+
+ public enum UpdateStatus
+ {
+ success, unarchieving, downloading, hash_computing, error
+ } //{ newUser, menu, move, working, initializing, disconnected, error, successful, login, web, launch };
+ public UpdateStatus Status;
+
+ ConcurrentQueue downloadFile = new ConcurrentQueue(); // 需要下载的文件名
+ ConcurrentQueue downloadFailed = new ConcurrentQueue(); //更新失败的文件名
+ public List UpdateFailed
+ {
+ get { return downloadFailed.ToList(); }
+ }
+ public bool UpdatePlanned
+ {
+ get; set;
+ }
+
+ public void ResetDownloadFailedInfo()
+ {
+ downloadFailed.Clear();
+ }
+
+ private int filenum = 0; // 总文件个数
+
+ public string Route { get; set; }
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public string UserId { get => Web.ID; }
+ public string UserEmail { get => Web.Email; }
+ public string CodeRoute { get; set; } = string.Empty;
+ public string? Language { get; set; } = null;
+ public string PlayerNum { get; set; } = "nSelect";
+ public enum LaunchLanguage { cpp, python };
+ public LaunchLanguage launchLanguage { get; set; } = LaunchLanguage.cpp;
+ public enum UsingOS { Win, Linux, OSX };
+ public UsingOS usingOS { get; set; }
+ public class Updater
+ {
+ public string Message;
+ public bool Working { get; set; }
+ public bool CombatCompleted { get => false; }
+ public bool UploadReady { get; set; } = false;
+ public bool ProfileAvailable { get; set; }
+ }
+ public bool LoginFailed { get; set; } = false;
+ public bool RememberMe { get; set; }
+
+ #endregion
+
+ #region 方法区
+ public Downloader()
+ {
+ Data = new Local_Data();
+ Route = Data.InstallPath;
+ Cloud = new Tencent_Cos("1314234950", "ap-beijing", "thuai6");
+ Web.Token_Changed += SaveToken;
+ string temp;
+ if (Data.Config.TryGetValue("Remembered", out temp))
+ {
+ if (Convert.ToBoolean(temp))
+ {
+ if (Data.Config.TryGetValue("Username", out temp))
+ Username = temp;
+ if (Data.Config.TryGetValue("Password", out temp))
+ Password = temp;
+ }
+ }
+ }
+
+ public void UpdateMD5()
+ {
+ if (File.Exists(Data.MD5DataPath))
+ File.Delete(Data.MD5DataPath);
+ Status = UpdateStatus.downloading;
+ Cloud.DownloadFileAsync(Data.MD5DataPath, "hash.json").Wait();
+ if (Cloud.Exceptions.Count > 0)
+ {
+ Status = UpdateStatus.error;
+ return;
+ }
+ Data.ReadMD5Data();
+ }
+
+ ///
+ /// 全新安装
+ ///
+ public void Install()
+ {
+ UpdateMD5();
+ if (Status == UpdateStatus.error) return;
+
+ if (Directory.Exists(Data.InstallPath))
+ Directory.Delete(Data.InstallPath, true);
+
+ Data.Installed = false;
+ string zp = Path.Combine(Data.InstallPath, "THUAI7.tar.gz");
+ Status = UpdateStatus.downloading;
+ Cloud.DownloadFileAsync(zp, "THUAI7.tar.gz").Wait();
+ Status = UpdateStatus.unarchieving;
+ Cloud.ArchieveUnzip(zp, Data.InstallPath);
+ File.Delete(zp);
+
+ Data.ResetInstallPath(Data.InstallPath);
+ Status = UpdateStatus.hash_computing;
+ Data.ScanDir();
+ if (Data.MD5Update.Count != 0)
+ {
+ // TO DO: 下载文件与hash校验值不匹配修复
+ Status = UpdateStatus.error;
+ Update();
+ }
+ else
+ {
+ Status = UpdateStatus.success;
+ }
+ }
+
+ ///
+ /// 检测是否需要进行更新
+ /// 返回真时则表明需要更新
+ ///
+ ///
+ public bool CheckUpdate()
+ {
+ UpdateMD5();
+ Data.MD5Update.Clear();
+ Status = UpdateStatus.hash_computing;
+ Data.ScanDir();
+ return Data.MD5Update.Count != 0;
+ }
+
+ ///
+ /// 更新文件
+ ///
+ public void Update()
+ {
+ if (CheckUpdate())
+ {
+ Status = UpdateStatus.downloading;
+ Cloud.DownloadQueueAsync(new ConcurrentQueue(Data.MD5Update), downloadFailed).Wait();
+ if (downloadFailed.Count == 0)
+ {
+ Data.MD5Update.Clear();
+ Status = UpdateStatus.hash_computing;
+ Data.ScanDir();
+ if (Data.MD5Update.Count == 0)
+ {
+ Status = UpdateStatus.success;
+ return;
+ }
+ }
+ }
+ else
+ {
+ Status = UpdateStatus.success;
+ return;
+ }
+ Status = UpdateStatus.error;
+ }
+
+ public async Task Login()
+ {
+ await Web.LoginToEEsast(Client, Username, Password);
+ }
+
+ public void SaveToken(object? sender, EventArgs args) // 保存token
+ {
+ if (Data.Config.ContainsKey("Token"))
+ Data.Config["Token"] = Web.Token;
+ else
+ Data.Config.Add("Token", Web.Token);
+ Data.SaveConfig();
+ }
+
+
+ public void RememberUser()
+ {
+ if (Data.Config.ContainsKey("Username"))
+ Data.Config["Username"] = Username;
+ else
+ Data.Config.Add("Username", Username);
+
+ if (Data.Config.ContainsKey("Password"))
+ Data.Config["Password"] = Password;
+ else
+ Data.Config.Add("Password", Password);
+
+ if (Data.Config.ContainsKey("Remembered"))
+ Data.Config["Remembered"] = "true";
+ else
+ Data.Config.Add("Remembered", "true");
+
+ Data.SaveConfig();
+ }
+ public void ForgetUser()
+ {
+ if (Data.Config.ContainsKey("Remembered"))
+ Data.Config["Remembered"] = "false";
+
+ if (Data.Config.ContainsKey("Username"))
+ Data.Config["Username"] = string.Empty;
+
+ if (Data.Config.ContainsKey("Password"))
+ Data.Config["Password"] = string.Empty;
+
+ Data.SaveConfig();
+ }
+ #endregion
+ }
+
+
+}
diff --git a/installer/Model/EEsast.cs b/installer/Model/EEsast.cs
new file mode 100644
index 00000000..03cc0782
--- /dev/null
+++ b/installer/Model/EEsast.cs
@@ -0,0 +1,226 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Json;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using COSXML.Auth;
+
+namespace installer.Model
+{
+ [Serializable]
+ record LoginResponse
+ {
+ // Map `Token` to `token` when serializing
+
+ public string Token { get; set; } = "";
+ }
+
+ class EEsast
+ {
+ public enum language { cpp, py };
+ private string token = string.Empty;
+ public string Token
+ {
+ get => token; protected set
+ {
+ if (token != value)
+ Token_Changed.Invoke(this, new EventArgs());
+ token = value;
+ }
+ }
+ public event EventHandler Token_Changed;
+ public string ID { get; protected set; }
+ public string Email { get; protected set; }
+
+ public ConcurrentQueue Exceptions = new ConcurrentQueue();
+ public enum WebStatus
+ {
+ disconnected, offline, logined
+ }
+ public WebStatus Status = WebStatus.disconnected;
+ public Tencent_Cos EEsast_Cos { get; protected set; }
+ public async Task LoginToEEsast(HttpClient client, string useremail, string userpassword)
+ {
+ EEsast_Cos = new Tencent_Cos("1255334966", "ap-beijing", "eesast");
+ try
+ {
+ using (var response = await client.PostAsync("https://api.eesast.com/users/login", JsonContent.Create(new
+ {
+ email = useremail,
+ password = userpassword,
+ })))
+ {
+ switch (response.StatusCode)
+ {
+ case System.Net.HttpStatusCode.OK:
+ var info = Helper.DeserializeJson1>(await response.Content.ReadAsStringAsync());
+ ID = info.Keys.Contains("_id") ? info["_id"] : string.Empty;
+ Email = info.Keys.Contains("email") ? info["email"] : string.Empty;
+ Token = info.Keys.Contains("token") ? info["token"] : string.Empty;
+ Status = WebStatus.logined;
+ break;
+ default:
+ int code = ((int)response.StatusCode);
+ if (code == 401)
+ {
+ Exceptions.Enqueue(new Exception("邮箱或密码错误!"));
+ }
+ else
+ {
+ Exceptions.Enqueue(new Exception($"HTTP错误,错误码:{code}"));
+ }
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Exceptions.Enqueue(ex);
+ }
+ }
+
+ ///
+ ///
+ ///
+ /// http client
+ /// 代码源位置
+ /// 编程语言,格式为"cpp"或"python"
+ /// 第x位玩家,格式为"player_x"
+ /// -1:tokenFail;-2:FileNotExist;-3:CosFail;-4:loginTimeout;-5:Fail;-6:ReadFileFail;-7:networkError
+ async public Task UploadFiles(HttpClient client, string tarfile, string type, string plr) //用来上传文件
+ {
+ if (Status != WebStatus.logined) //
+ {
+ Exceptions.Append(new UnauthorizedAccessException("用户未登录"));
+ return -1;
+ }
+ try
+ {
+ string content;
+ client.DefaultRequestHeaders.Authorization = new("Bearer", Token);
+ if (!File.Exists(tarfile))
+ {
+ Exceptions.Append(new IOException("用户不存在"));
+ return -2;
+ }
+ using FileStream fs = new FileStream(tarfile, FileMode.Open, FileAccess.Read);
+ using StreamReader sr = new StreamReader(fs);
+ content = sr.ReadToEnd();
+ string targetUrl = $"https://api.eesast.com/static/player?team_id={await GetTeamId()}";
+ using (var response = await client.GetAsync(targetUrl))
+ {
+ switch (response.StatusCode)
+ {
+ case System.Net.HttpStatusCode.OK:
+ var res = Helper.DeserializeJson1>(await response.Content.ReadAsStringAsync());
+ string tmpSecretId = res["TmpSecretId"]; //"临时密钥 SecretId";
+ string tmpSecretKey = res["TmpSecretKey"]; //"临时密钥 SecretKey";
+ string tmpToken = res["SecurityToken"]; //"临时密钥 token";
+ long tmpExpiredTime = Convert.ToInt64(res["ExpiredTime"]); //临时密钥有效截止时间,精确到秒
+ QCloudCredentialProvider cosCredentialProvider = new DefaultSessionQCloudCredentialProvider(
+ tmpSecretId, tmpSecretKey, tmpExpiredTime, tmpToken
+ );
+ EEsast_Cos.UpdateSecret(cosCredentialProvider);
+
+ string cosPath = $"/THUAI7/{GetTeamId()}/{type}/{plr}"; //对象在存储桶中的位置标识符,即称对象键
+ string srcPath = tarfile;//本地文件绝对路径
+ EEsast_Cos.UploadFileAsync(srcPath, cosPath).Wait();
+
+ break;
+ case System.Net.HttpStatusCode.Unauthorized:
+ //Console.WriteLine("您未登录或登录过期,请先登录");
+ return -4;
+ default:
+ //Console.WriteLine("上传失败!");
+ return -5;
+ }
+ }
+ }
+ catch (IOException)
+ {
+ //Console.WriteLine("文件读取错误!请检查文件是否被其它应用占用!");
+ return -6;
+ }
+ catch
+ {
+ //Console.WriteLine("请求错误!请检查网络连接!");
+ return -7;
+ }
+ return 0;
+ }
+
+ async public Task UserDetails(HttpClient client) // 用来测试访问网站
+ {
+ if (Status != WebStatus.logined) // 读取token失败
+ {
+ Exceptions.Append(new UnauthorizedAccessException("用户未登录"));
+ return;
+ }
+ try
+ {
+ client.DefaultRequestHeaders.Authorization = new("Bearer", Token);
+ Console.WriteLine(Token);
+ using (var response = await client.GetAsync("https://api.eesast.com/application/info"))
+ {
+ switch (response.StatusCode)
+ {
+ case System.Net.HttpStatusCode.OK:
+ Console.WriteLine("Require OK");
+ Console.WriteLine(await response.Content.ReadAsStringAsync());
+ break;
+ default:
+ int code = ((int)response.StatusCode);
+ if (code == 401)
+ {
+ Console.WriteLine("您未登录或登录过期,请先登录");
+ }
+ return;
+ }
+ }
+ }
+ catch
+ {
+ Console.WriteLine("请求错误!请检查网络连接!");
+ }
+ }
+
+ async public Task GetTeamId()
+ {
+ var client = new HttpClient();
+ var request = new HttpRequestMessage(HttpMethod.Post, "https://api.eesast.com/dev/v1/graphql");
+ request.Headers.Add("x-hasura-admin-secret", "hasuraDevAdminSecret");
+ //var content = new StringContent($@"
+ // {{
+ // ""query"": ""query MyQuery {{contest_team_member(where: {{user_id: {{_eq: \""{Downloader.UserInfo._id}\""}}}}) {{ team_id }}}}"",
+ // ""variables"": {{}},
+ // }}", null, "application/json");
+ var content = new StringContent("{\"query\":\"query MyQuery {\\r\\n contest_team_member(where: {user_id: {_eq: \\\"" + ID + "\\\"}}) {\\r\\n team_id\\r\\n }\\r\\n}\",\"variables\":{}}", null, "application/json");
+ request.Content = content;
+ var response = await client.SendAsync(request);
+ response.EnsureSuccessStatusCode();
+ var info = await response.Content.ReadAsStringAsync();
+ var s1 = Helper.DeserializeJson1>(info)["data"];
+ var s2 = Helper.DeserializeJson1>>(s1.ToString() ?? "")["contest_team_member"];
+ var sres = Helper.DeserializeJson1>(s2[0].ToString() ?? "")["team_id"];
+ return sres;
+ }
+
+ public async Task GetUserId(string learnNumber)
+ {
+ var client = new HttpClient();
+ var request = new HttpRequestMessage(HttpMethod.Post, "https://api.eesast.com/dev/v1/graphql");
+ request.Headers.Add("x-hasura-admin-secret", "hasuraDevAdminSecret");
+ var content = new StringContent("{\"query\":\"query MyQuery {\r\n user(where: {id: {_eq: \""
+ + learnNumber + "\"}}) {\r\n _id\r\n }\r\n}\r\n\",\"variables\":{}}", null, "application/json");
+ request.Content = content;
+ var response = await client.SendAsync(request);
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStringAsync();
+ }
+
+
+ }
+}
diff --git a/installer/Model/Helper.cs b/installer/Model/Helper.cs
new file mode 100644
index 00000000..b0e8137f
--- /dev/null
+++ b/installer/Model/Helper.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace installer.Model
+{
+ internal static class Helper
+ {
+ public static T DeserializeJson1(string json)
+ where T : notnull
+ {
+ return JsonConvert.DeserializeObject(json)
+ ?? throw new Exception("Failed to deserialize json.");
+ }
+
+ public static T? TryDeserializeJson(string json)
+ where T : notnull
+ {
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public static string GetFileMd5Hash(string strFileFullPath)
+ {
+ FileStream? fst = null;
+ try
+ {
+ fst = new FileStream(strFileFullPath, FileMode.Open, FileAccess.Read);
+ byte[] data = MD5.Create().ComputeHash(fst);
+
+ StringBuilder sBuilder = new StringBuilder();
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ sBuilder.Append(data[i].ToString("x2"));
+ }
+
+ fst.Close();
+ return sBuilder.ToString().ToLower();
+ }
+ catch (Exception)
+ {
+ if (fst != null)
+ fst.Close();
+ if (File.Exists(strFileFullPath))
+ return "conflict";
+ return "";
+ }
+ finally
+ {
+ }
+ }
+
+ public static string ConvertAbsToRel(string basePath, string fullPath)
+ {
+ if (fullPath.StartsWith(basePath))
+ {
+ fullPath.Replace(basePath, ".");
+ }
+ return fullPath;
+ }
+ }
+}
diff --git a/installer/Model/Local_Data.cs b/installer/Model/Local_Data.cs
new file mode 100644
index 00000000..e987b841
--- /dev/null
+++ b/installer/Model/Local_Data.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Maui.Platform;
+using Newtonsoft.Json;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace installer.Model
+{
+ class Local_Data
+ {
+ public string ConfigPath; // 标记路径记录文件THUAI7.json的路径
+ public string MD5DataPath; // 标记MD5本地文件缓存值
+ public Dictionary Config
+ {
+ get; protected set;
+ }
+ public Dictionary MD5Data
+ {
+ get; protected set;
+ }// 路径为尽可能相对路径
+ public ConcurrentBag MD5Update
+ {
+ get; set;
+ }// 路径为绝对路径
+ public string InstallPath = ""; // 最后一级为THUAI7文件夹所在目录
+ public bool Installed = false; // 项目是否安装
+ public Local_Data()
+ {
+ MD5Update = new ConcurrentBag();
+ ConfigPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+ "THUAI7.json");
+ if (File.Exists(ConfigPath))
+ {
+ ReadConfig();
+ if (Config.ContainsKey("InstallPath") && Directory.Exists(Config["InstallPath"]))
+ {
+ InstallPath = Config["InstallPath"].Replace('\\', '/');
+ if (Config.ContainsKey("MD5DataPath"))
+ {
+ MD5DataPath = Config["MD5DataPath"].StartsWith('.') ?
+ Path.Combine(InstallPath, Config["MD5DataPath"]) :
+ Config["MD5DataPath"];
+ ReadMD5Data();
+ }
+ else
+ {
+ MD5DataPath = Path.Combine(InstallPath, "./hash.json");
+ Config["MD5DataPath"] = "./hash.json";
+ SaveMD5Data();
+ SaveConfig();
+ }
+ Installed = true;
+ }
+ else
+ {
+ MD5DataPath = Path.Combine(InstallPath, "./hash.json");
+ Config["MD5DataPath"] = "./hash.json";
+ SaveMD5Data();
+ SaveConfig();
+ }
+ }
+ else
+ {
+ Config = new Dictionary
+ {
+ { "THUAI7", "2024" },
+ { "MD5DataPath", "./hash.json" }
+ };
+ MD5DataPath = Path.Combine(InstallPath, "./hash.json");
+ SaveMD5Data();
+ SaveConfig();
+ }
+ }
+
+ ~Local_Data()
+ {
+ SaveConfig();
+ }
+
+ public void ResetInstallPath(string newPath)
+ {
+ if (!Directory.Exists(Path.GetDirectoryName(newPath)))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(newPath));
+ }
+ if (Installed)
+ {
+ // 移动已有文件夹至新位置
+ Directory.Move(newPath, InstallPath);
+ }
+ InstallPath = newPath.Replace('\\', '/');
+ if (Config.ContainsKey("InstallPath"))
+ Config["InstallPath"] = InstallPath;
+ else
+ Config.Add("InstallPath", InstallPath);
+ SaveConfig();
+ }
+
+ public static bool IsUserFile(string filename)
+ {
+ if (filename.Substring(filename.Length - 3, 3).Equals(".sh") || filename.Substring(filename.Length - 4, 4).Equals(".cmd"))
+ return true;
+ if (filename.Equals("AI.cpp") || filename.Equals("AI.py"))
+ return true;
+ return false;
+ }
+
+ public void ReadConfig()
+ {
+ using (StreamReader r = new StreamReader(ConfigPath))
+ {
+ string json = r.ReadToEnd();
+ if (json == null || json == "")
+ {
+ json += @"{""THUAI6""" + ":" + @"""2023""}";
+ }
+ Config = Helper.TryDeserializeJson>(json) ?? new Dictionary();
+ }
+ }
+
+ public void SaveConfig()
+ {
+ using FileStream fs = new FileStream(ConfigPath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
+ using StreamWriter sw = new StreamWriter(fs);
+ fs.SetLength(0);
+ sw.Write(JsonConvert.SerializeObject(Config));
+ sw.Flush();
+ }
+
+ public void ReadMD5Data()
+ {
+ var newMD5Data = new Dictionary();
+ using (StreamReader r = new StreamReader(MD5DataPath))
+ {
+ string json = r.ReadToEnd();
+ if (json == null || json == "")
+ {
+ newMD5Data = new Dictionary();
+ }
+ else
+ {
+ newMD5Data = Helper.TryDeserializeJson>(json) ?? new Dictionary();
+ }
+ }
+ foreach (var item in newMD5Data)
+ {
+ if (MD5Data.ContainsKey(item.Key))
+ {
+ if (MD5Data[item.Key] != newMD5Data[item.Value])
+ {
+ MD5Data[item.Key] = newMD5Data[item.Value];
+ MD5Update.Add(Path.Combine(InstallPath, item.Key));
+ }
+ }
+ else
+ {
+ MD5Data.Add(item.Key, item.Value);
+ MD5Update.Add(Path.Combine(InstallPath, item.Key));
+ }
+ }
+ }
+
+ public void SaveMD5Data()
+ {
+ using FileStream fs = new FileStream(MD5DataPath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
+ using StreamWriter sw = new StreamWriter(fs);
+ fs.SetLength(0);
+ sw.Write(JsonConvert.SerializeObject(MD5Data));
+ sw.Flush();
+ }
+
+ public void ScanDir() => ScanDir(InstallPath);
+
+ public void ScanDir(string dir)
+ {
+ var d = new DirectoryInfo(dir);
+ foreach (var file in d.GetFiles())
+ {
+ var relFile = Helper.ConvertAbsToRel(InstallPath, file.FullName);
+ // 用户自己的文件不会被计入更新hash数据中
+ if (IsUserFile(file.Name))
+ continue;
+ var hash = Helper.GetFileMd5Hash(file.FullName);
+ if (MD5Data.Keys.Contains(relFile))
+ {
+ if (MD5Data[relFile] != hash)
+ {
+ MD5Data[relFile] = hash;
+ MD5Update.Add(file.FullName);
+ }
+ }
+ else
+ {
+ MD5Data.Add(relFile, hash);
+ MD5Update.Add(file.FullName);
+ }
+ }
+ foreach (var d1 in d.GetDirectories()) { ScanDir(d1.FullName); }
+ }
+ }
+}
diff --git a/installer/Model/Run_Program.cs b/installer/Model/Run_Program.cs
new file mode 100644
index 00000000..f63a30c9
--- /dev/null
+++ b/installer/Model/Run_Program.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace installer.Model
+{
+ class Run_Program
+ {
+ }
+}
diff --git a/installer/Model/Tencent_Cos.cs b/installer/Model/Tencent_Cos.cs
new file mode 100644
index 00000000..359d5784
--- /dev/null
+++ b/installer/Model/Tencent_Cos.cs
@@ -0,0 +1,172 @@
+using COSXML;
+using COSXML.Auth;
+using COSXML.CosException;
+using COSXML.Model.Object;
+using ICSharpCode.SharpZipLib;
+using ICSharpCode.SharpZipLib.Tar;
+using ICSharpCode.SharpZipLib.GZip;
+using Newtonsoft.Json;
+using System.Collections.Concurrent;
+using COSXML.Common;
+using COSXML.Transfer;
+
+namespace installer.Model
+{
+ public class Tencent_Cos
+ {
+ public string Appid { get; init; } // 设置腾讯云账户的账户标识(APPID)
+ public string Region { get; init; } // 设置一个默认的存储桶地域
+ public string BucketName { get; set; }
+ public ConcurrentStack Exceptions { get; set; }
+
+ private string secretId = "***"; //"云 API 密钥 SecretId";
+ private string secretKey = "***"; //"云 API 密钥 SecretKey";
+ protected CosXmlServer cosXml;
+
+ public Tencent_Cos(string appid, string region, string bucketName)
+ {
+ Appid = appid; Region = region; BucketName = bucketName;
+ Exceptions = new ConcurrentStack();
+ // 初始化CosXmlConfig(提供配置SDK接口)
+ var config = new CosXmlConfig.Builder()
+ .IsHttps(true) // 设置默认 HTTPS 请求
+ .SetAppid(Appid) // 设置腾讯云账户的账户标识 APPID
+ .SetRegion(Region) // 设置一个默认的存储桶地域
+ .SetDebugLog(true) // 显示日志
+ .Build(); // 创建 CosXmlConfig 对象
+ long durationSecond = 1000; // 每次请求签名有效时长,单位为秒
+ QCloudCredentialProvider cosCredentialProvider = new DefaultQCloudCredentialProvider(secretId, secretKey, durationSecond);
+ // 初始化 CosXmlServer
+ cosXml = new CosXmlServer(config, cosCredentialProvider);
+ }
+
+ public void UpdateSecret(QCloudCredentialProvider credential)
+ {
+ var config = new CosXmlConfig.Builder()
+ .IsHttps(true) // 设置默认 HTTPS 请求
+ .SetAppid(Appid) // 设置腾讯云账户的账户标识 APPID
+ .SetRegion(Region) // 设置一个默认的存储桶地域
+ .SetDebugLog(true) // 显示日志
+ .Build(); // 创建 CosXmlConfig 对象
+ cosXml = new CosXmlServer(config, credential);
+ }
+
+ public async Task DownloadFileAsync(string savePath, string remotePath = null)
+ {
+ // download_dir标记根文件夹路径,key为相对根文件夹的路径(不带./)
+ // 创建存储桶
+ try
+ {
+ // 覆盖对应文件,如果无法覆盖则报错
+ if (File.Exists(savePath))
+ File.Delete(savePath);
+ string bucket = $"{BucketName}-{Appid}"; // 格式:BucketName-APPID
+ string localDir = Path.GetDirectoryName(savePath) // 本地文件夹
+ ?? throw new Exception("本地文件夹路径获取失败");
+ string localFileName = Path.GetFileName(savePath); // 指定本地保存的文件名
+ GetObjectRequest request = new GetObjectRequest(bucket, remotePath ?? localFileName, localDir, localFileName);
+
+ Dictionary test = request.GetRequestHeaders();
+ request.SetCosProgressCallback(delegate (long completed, long total)
+ {
+ //Console.WriteLine(String.Format("progress = {0:##.##}%", completed * 100.0 / total));
+ });
+ // 执行请求
+ GetObjectResult result = cosXml.GetObject(request);
+ // 请求成功
+ }
+ catch (Exception ex)
+ {
+ Exceptions.Push(ex);
+ throw;
+ //MessageBox.Show($"下载{download_dir}时出现未知问题,请反馈");
+ }
+ }
+
+ public async Task DownloadQueueAsync(ConcurrentQueue queue, ConcurrentQueue downloadFailed)
+ {
+ ThreadPool.SetMaxThreads(20, 20);
+ for (int i = 0; i < queue.Count; i++)
+ {
+ string item;
+ queue.TryDequeue(out item);
+ ThreadPool.QueueUserWorkItem(async _ =>
+ {
+ try
+ {
+ await DownloadFileAsync(item);
+ }
+ catch (Exception ex)
+ {
+ downloadFailed.Enqueue(item);
+ }
+ });
+ }
+ }
+
+ public void ArchieveUnzip(string zipPath, string targetDir)
+ {
+ Stream? inStream = null;
+ Stream? gzipStream = null;
+ TarArchive? tarArchive = null;
+ try
+ {
+ using (inStream = File.OpenRead(zipPath))
+ {
+ using (gzipStream = new GZipInputStream(inStream))
+ {
+ tarArchive = TarArchive.CreateInputTarArchive(gzipStream);
+ tarArchive.ExtractContents(targetDir);
+ tarArchive.Close();
+ }
+ }
+ }
+ catch
+ {
+ //出错
+ }
+ finally
+ {
+ if (tarArchive != null) tarArchive.Close();
+ if (gzipStream != null) gzipStream.Close();
+ if (inStream != null) inStream.Close();
+ }
+ }
+
+ public async Task UploadFileAsync(string localPath, string targetPath)
+ {
+ // 初始化 TransferConfig
+ TransferConfig transferConfig = new TransferConfig();
+
+ // 初始化 TransferManager
+ TransferManager transferManager = new TransferManager(cosXml, transferConfig);
+
+ string bucket = $"{BucketName}-{Appid}";
+
+ COSXMLUploadTask uploadTask = new COSXMLUploadTask(bucket, targetPath);
+
+ uploadTask.SetSrcPath(localPath);
+
+ uploadTask.progressCallback = delegate (long completed, long total)
+ {
+ //Console.WriteLine(string.Format("progress = {0:##.##}%", completed * 100.0 / total));
+ };
+
+ COSXMLUploadTask.UploadTaskResult result = await transferManager.UploadAsync(uploadTask);
+
+ try
+ {
+ COSXMLUploadTask.UploadTaskResult r = await transferManager.UploadAsync(uploadTask);
+ //Console.WriteLine(result.GetResultInfo());
+ string eTag = r.eTag;
+ //到这里应该是成功了,但是因为我没有试过,也不知道具体情况,可能还要根据result的内容判断
+ }
+ catch (Exception ex)
+ {
+ Exceptions.Push(ex);
+ throw;
+ }
+
+ }
+ }
+}
diff --git a/installer/Page/CompetitionPage.xaml b/installer/Page/CompetitionPage.xaml
new file mode 100644
index 00000000..51d1ffde
--- /dev/null
+++ b/installer/Page/CompetitionPage.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/installer/Page/CompetitionPage.xaml.cs b/installer/Page/CompetitionPage.xaml.cs
new file mode 100644
index 00000000..788e0249
--- /dev/null
+++ b/installer/Page/CompetitionPage.xaml.cs
@@ -0,0 +1,14 @@
+namespace installer;
+
+public partial class CompetitionPage : ContentPage
+{
+ public CompetitionPage()
+ {
+ InitializeComponent();
+ }
+
+ private async void JumpBtn_Clicked(object sender, EventArgs e)
+ {
+ await Navigation.PopToRootAsync();
+ }
+}
\ No newline at end of file
diff --git a/installer/Page/LoginPage.xaml b/installer/Page/LoginPage.xaml
new file mode 100644
index 00000000..c19911af
--- /dev/null
+++ b/installer/Page/LoginPage.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/installer/Page/LoginPage.xaml.cs b/installer/Page/LoginPage.xaml.cs
new file mode 100644
index 00000000..e0a7d2dd
--- /dev/null
+++ b/installer/Page/LoginPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace installer;
+
+public partial class LoginPage : ContentPage
+{
+ public LoginPage()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/installer/Page/MainPage.xaml b/installer/Page/MainPage.xaml
new file mode 100644
index 00000000..edaf02fe
--- /dev/null
+++ b/installer/Page/MainPage.xaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/Page/MainPage.xaml.cs b/installer/Page/MainPage.xaml.cs
new file mode 100644
index 00000000..937e032b
--- /dev/null
+++ b/installer/Page/MainPage.xaml.cs
@@ -0,0 +1,17 @@
+namespace installer
+{
+ public partial class MainPage : ContentPage
+ {
+ int count = 0;
+
+ public MainPage()
+ {
+ InitializeComponent();
+ }
+
+ private void OnSliderChanged(object sender, ValueChangedEventArgs e)
+ {
+ //Txt_Slider1.Text = e.NewValue.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/installer/Platforms/Android/AndroidManifest.xml b/installer/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 00000000..e9937ad7
--- /dev/null
+++ b/installer/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/installer/Platforms/Android/MainActivity.cs b/installer/Platforms/Android/MainActivity.cs
new file mode 100644
index 00000000..7f96f837
--- /dev/null
+++ b/installer/Platforms/Android/MainActivity.cs
@@ -0,0 +1,11 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace installer
+{
+ [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+ public class MainActivity : MauiAppCompatActivity
+ {
+ }
+}
\ No newline at end of file
diff --git a/installer/Platforms/Android/MainApplication.cs b/installer/Platforms/Android/MainApplication.cs
new file mode 100644
index 00000000..195e9855
--- /dev/null
+++ b/installer/Platforms/Android/MainApplication.cs
@@ -0,0 +1,16 @@
+using Android.App;
+using Android.Runtime;
+
+namespace installer
+{
+ [Application]
+ public class MainApplication : MauiApplication
+ {
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+ }
+}
\ No newline at end of file
diff --git a/installer/Platforms/Android/Resources/values/colors.xml b/installer/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 00000000..c04d7492
--- /dev/null
+++ b/installer/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
\ No newline at end of file
diff --git a/installer/Platforms/MacCatalyst/AppDelegate.cs b/installer/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 00000000..a12db161
--- /dev/null
+++ b/installer/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace installer
+{
+ [Register("AppDelegate")]
+ public class AppDelegate : MauiUIApplicationDelegate
+ {
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+ }
+}
\ No newline at end of file
diff --git a/installer/Platforms/MacCatalyst/Info.plist b/installer/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 00000000..c96dd0a2
--- /dev/null
+++ b/installer/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,30 @@
+
+
+
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/installer/Platforms/MacCatalyst/Program.cs b/installer/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 00000000..03718df0
--- /dev/null
+++ b/installer/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace installer
+{
+ public class Program
+ {
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+ }
+}
\ No newline at end of file
diff --git a/installer/Platforms/Tizen/Main.cs b/installer/Platforms/Tizen/Main.cs
new file mode 100644
index 00000000..c58e96ee
--- /dev/null
+++ b/installer/Platforms/Tizen/Main.cs
@@ -0,0 +1,17 @@
+using Microsoft.Maui;
+using Microsoft.Maui.Hosting;
+using System;
+
+namespace installer
+{
+ internal class Program : MauiApplication
+ {
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+
+ static void Main(string[] args)
+ {
+ var app = new Program();
+ app.Run(args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/installer/Platforms/Tizen/tizen-manifest.xml b/installer/Platforms/Tizen/tizen-manifest.xml
new file mode 100644
index 00000000..e9242175
--- /dev/null
+++ b/installer/Platforms/Tizen/tizen-manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ maui-appicon-placeholder
+
+
+
+
+ http://tizen.org/privilege/internet
+
+
+
+
\ No newline at end of file
diff --git a/installer/Platforms/Windows/App.xaml b/installer/Platforms/Windows/App.xaml
new file mode 100644
index 00000000..29e06881
--- /dev/null
+++ b/installer/Platforms/Windows/App.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/installer/Platforms/Windows/App.xaml.cs b/installer/Platforms/Windows/App.xaml.cs
new file mode 100644
index 00000000..4877ab06
--- /dev/null
+++ b/installer/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,24 @@
+using Microsoft.UI.Xaml;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace installer.WinUI
+{
+ ///
+ /// Provides application-specific behavior to supplement the default Application class.
+ ///
+ public partial class App : MauiWinUIApplication
+ {
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+ }
+}
\ No newline at end of file
diff --git a/installer/Platforms/Windows/Package.appxmanifest b/installer/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 00000000..217ac0ac
--- /dev/null
+++ b/installer/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/Platforms/Windows/app.manifest b/installer/Platforms/Windows/app.manifest
new file mode 100644
index 00000000..5d03b3e3
--- /dev/null
+++ b/installer/Platforms/Windows/app.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
diff --git a/installer/Platforms/iOS/AppDelegate.cs b/installer/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 00000000..a12db161
--- /dev/null
+++ b/installer/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace installer
+{
+ [Register("AppDelegate")]
+ public class AppDelegate : MauiUIApplicationDelegate
+ {
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+ }
+}
\ No newline at end of file
diff --git a/installer/Platforms/iOS/Info.plist b/installer/Platforms/iOS/Info.plist
new file mode 100644
index 00000000..0004a4fd
--- /dev/null
+++ b/installer/Platforms/iOS/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/installer/Platforms/iOS/Program.cs b/installer/Platforms/iOS/Program.cs
new file mode 100644
index 00000000..03718df0
--- /dev/null
+++ b/installer/Platforms/iOS/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace installer
+{
+ public class Program
+ {
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+ }
+}
\ No newline at end of file
diff --git a/installer/Program.cs b/installer/Program.cs
deleted file mode 100644
index 1bc52a60..00000000
--- a/installer/Program.cs
+++ /dev/null
@@ -1 +0,0 @@
-Console.WriteLine("Hello, World!");
diff --git a/installer/Properties/launchSettings.json b/installer/Properties/launchSettings.json
new file mode 100644
index 00000000..edf8aadc
--- /dev/null
+++ b/installer/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/installer/README.md b/installer/README.md
index 3b77a62c..aaf9643a 100644
--- a/installer/README.md
+++ b/installer/README.md
@@ -1,27 +1,191 @@
-# Installer
+# THUAI7
+清华大学第七届人工智能挑战赛电子系赛道(原电子系第 25 届队式程序设计大赛 teamstyle25)
-## 简介
+## 赛题背景
-THUAI7 选手包下载器
+待定
-## 目标
+## 比赛规则
-### 基本目标
+待定
-- 为 Windows 编写桌面应用程序,推荐使用WPF,目标平台 .NET 6,需要实现以下功能:
- - 选手包下载
- - 检查选手包更新情况
+## 软件架构
-## 重要目标
+![structure](resource/structure.png)
-- 设计美观的UI,便于选手操作
-- 使用 MVVM 或其他设计模式
-- 为下载器添加 "暂停与断点续传" 的功能
+## 仓库说明
-## 提高目标
+### 配置说明
-- 与网站组对接,支持账号密码登录
+本仓库使用 git 进行版本控制,为所有开发工作共用仓库,请勿上传不必要的文件。主目录文件结构非必要请勿修改,且主目录中已配置的 `.gitignore`、`.gitattributes` 文件非必要请勿修改;各子目录已预先包含使用 `Visual Studio` 开发的 `.gitignore` 模板,可以根据自身需要增加忽略规则;如有必要,可在子目录下自定义 `.gitattributes` 文件。
-## 开发人员
+### 目录分配
-- 张琨、叶芊屿
\ No newline at end of file
+| 子目录 | 说明 | 主要开发组 |
+| :--------: | :--------------------------------------------------------: | :----------------------------: |
+| .github | CI,用于选手包同步到服务器上供选手下载 | 运维组 |
+| CAPI | 选手接口,生成可执行文件 | 通信组 |
+| docs | 比赛的说明文档 | 逻辑组、通信组、运维组、界面组 |
+| dependency | 项目依赖文件,如 proto、dll、lib、dockerfile、shell 脚本等 | 逻辑组、通信组、运维组、界面组 |
+| installer | 下载器,用于选手包的下载与更新,生成可执行文件 | 运维组 |
+| interface | Unity 界面 | 界面组 |
+| launcher | 游戏启动器,用于快速启动游戏,生成可执行文件 | 运维组 |
+| logic | 游戏逻辑、Server,生成可执行文件 | 通信组、逻辑组 |
+| playback | 游戏回放组件,生成类库 | 逻辑组 |
+| resource | 资源文件目录,用于存储主目录下 README 所用图片 | 端茶倒水 |
+
+### 分支管理
+
+- main:代码较稳定版本或阶段性成果,需要 2 reviewers,但由总负责人(端茶倒水)维护
+- dev:各开发者开发工作的最新进展,需要 1 reviewer
+
+## 开发规则
+
+### 关于社区开发者
+
+- 社区开发者开发工作请遵循 [THUAI7社区开发者贡献指南](./CONTRIBUTING.md)
+- 社区开发者贡献的代码请遵循 [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md)
+
+### 开发流程
+
+THUAI7 开发组成员与其他贡献者应当遵循以下流程:
+
+1. 将 `eesast/THUAI7` fork 到自己的仓库中
+2. 基于 `dev` 分支建立一个新的功能分支
+3. 在新的分支上进行修改与开发
+4. 向 `eesast/THUAI7` 的 `dev` 分支提出 pull request
+5. 等待其他开发组成员 review 与 merge
+
+### 使用 Git 与 Github 时的注意事项
+
+- 非必要请勿上传大文件到 Github
+
+- commit 提交信息请遵循 Semantic Commit 规范,即:`type: content `格式;**推荐使用 VS Code 提供的 Conventional Commits 插件进行格式化提交**。
+
+ 常用的 commit message type 包括:
+
+ | Type | Explanation |
+ | -------- | ---------------------------------------------- |
+ | chore | 日常代码开发;改变构建流程;增加依赖库、工具等 |
+ | fix | 修复bug |
+ | refactor | 改变代码结构,但没有增加新功能 |
+ | docs | 修改文档,如 README、CONTRIBUTE 等 |
+ | revert | 版本回退 |
+ | ... | ... |
+
+- 鼓励开发组成员之间互相 review 并 merge 代码到 dev 分支上。merge 前建议简单检查其 pull request 是否符合上述规范
+- 一般情况下,不要向 main 分支提出 pull request,更不要 merge 到 main 分支上
+- 一般情况下,不允许 merge 无法通过 CI 的 pull request
+
+### 开发约定
+
++ 统一使用空格缩进而非制表符缩进
++ 统一使用 4 个空格进行缩进而非 2 个
++ 统一使用 UTF-8 字符编码
+
+## 代码风格
+
+本仓库严格规定了 `C++` 与 `CSharp` 代码风格,具体配置请参见 [.clang-format](.clang-format)
+
+### 风格说明
+
+几项重要规定如下:
+
+- 需严格按照要求缩进
+
+ ~~~csharp
+ namespace Exp
+ {
+ public class Program
+ {
+ public static void Main()
+ {
+ }
+ }
+ }
+ ~~~
+
+- `if`、`while`等关键字后须加空格
+
+ ~~~csharp
+ while (1)
+ {
+ // code
+ }
+ ~~~
+
+- 大括号须换行书写
+
+ ~~~csharp
+ // Allowed
+ if (...)
+ {
+ // ...
+ }
+
+ // Forbidden !!!
+ if (...) {
+ // ...
+ }
+ ~~~
+
+### 规范代码风格
+
+- C++ 语言请按照配置文件 [.clang-format](.clang-format) 对代码进行规范,C# 语言请按照 dotnet format 进行代码规范;可以使用脚本 [dependency/shell/format.sh](./dependency/shell/format.sh) 规范代码风格
+
+## 文档风格
+
+仓库的文档使用 Markdown 语法,具体语法可以参照 [Markdown 语法文档](https://docs.github.com/zh/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax)。
+
+中文文档的书写须严格遵循:[中文技术文档规范](https://github.com/ruanyf/document-style-guide)、[中文文案排版指北](https://mazhuang.org/wiki/chinese-copywriting-guidelines/),以写出更美观的中文文档。例如:[中文文字与西文文字间空格](https://github.com/ruanyf/document-style-guide/blob/master/docs/text.md#%E5%AD%97%E9%97%B4%E8%B7%9D)、[全角标点符号的正确使用](https://github.com/ruanyf/document-style-guide/blob/master/docs/marks.md),等等。
+
+西文文档的书写须遵循:
+
++ 在 [中文技术文档规范](https://github.com/ruanyf/document-style-guide) 和 [中文文案排版指北](https://mazhuang.org/wiki/chinese-copywriting-guidelines/) 中规定的数字和西文的规范
++ 半角空格的标点符号的使用:
+ + 半角的逗号 `,`、分号 `;`、句号 `.`、叹号 `!`、问号 `?` 等标点,在不位于行尾时,后需加空格,除非其后紧跟全角标点符号。例如:`"Hello, world",是一句学习编程语言常用的句子`
+ + 半角的括号 `()`、`[]`、`{}`、`<>` 等在左半括号前加空格、右半括号后加空格,除非其空格处紧跟全角标点符号或位于行首或行尾。例如:`Tsinghua University (THU) and Peking University (PKU)(我是全角括号)`
+
+## 其他注意事项
+
++ 文件的字符编码格式须统一使用 UTF-8 编码,并用 4 空格缩进,尤其是 C/C++:Visual Studio 创建 cpp 文件时默认使用 GB2312 编码、TAB 缩进,因此每创建一个文件都需要注意手动设置字符编码(当代码文件中出现中文时)和缩进方式
+
++ 使用等宽字体进行编程,例如 Source Code Pro、Consolas 等,便于对齐
+
++ 注意代码的整洁性与可读性
+
+ + 代码风格尽量统一。书写不要过于紧凑,善于使用空格、缩进与换行来让代码看起来更整洁
+
+ + 命名风格尽量统一。相同类别的命名规则要相同,例如类名统一使用大驼峰命名法或其他常用的命名法,但是不要混用(非必要不使用匈牙利命名法)
+
+ + 命名应当通俗易懂,让阅读代码者能够通过命名理解变量、函数等的意义。除循环变量等可以使用 `i`、`j`、`k` 等单字母外,其他的命名应当明白如话,且谨慎使用缩写。尽量使用众人皆知的缩写,不要自创缩写。如果连自己都不知道的缩写或根本没有众人皆知的缩写,则应当坚持使用全称,命名可以适当加长。
+
+ 常用的缩写有:
+
+
+
+ > address-addr、answer-ans、application-app、arguments-args、array-arr、assemble-asm、(a)synchronize-(a)sync、attribute-attr、begin-beg、bitmap-bmp、buffer-buf、button-btn、clock-clk、command-cmd、coordinates-coord、copy-cpy、count-cnt、current-cur、database-db、decrease-dec、default/define-def、delete-del、dependency-dep、destination-dest、device-dev、dialog-dlg、dictionary-dict、display-disp、divide-div、document-doc、educate-edu、equal-eq、error-err、extend-ext、format-fmt、frequency-freq、function-func、horizon-horz、image-img、implement-impl、increment-inc、index-idx、information-info、initialize-init、instance-inst、iterator-itr、length-len、list-lst、memory-mem、message-msg、middle-mid、number-num、object-obj、package-pkg、parameter-param、password-pwd、pointer-ptr、position-pos、previous-prev、receive-recv、reference-ref、resource/result-res、return-ret、second-sec、select-sel、semaphore-sem、signal-sig、source-src、stack-stk、standard-std、statistic/state-stat、string-str、system-sys、temporary-temp/tmp、text-txt、user-usr、value-val、variable-var、version-ver、vertical-vert、window-win
+ >
+ >
+
+- 命名允许较长,但不应过于啰嗦冗余,能完整表明意图即可。
+- 代码应保证良好的可读性;**禁止**中学 OI 竞赛的各种“卡常”奇技淫巧!!!效率并非总是最重要的,良好的可读性与可维护性往往更加重要。
+- 熟练运用面向对象编程的思想,设计架构时尽可能降低模块与模块的耦合性,保证代码的可维护性
+ - 慎用全局变量、全局函数等
+ - 尽可能将各功能模块化,便于日后复用;尽可能降低类与类的耦合,善用继承与多态
+ - 适当设计单元测试,保证代码的正确运行
+
+- 注意跨平台问题,代码需保证同时支持 Windows 与 Linux,避免直接的系统调用带来的跨平台问题
+- 注意维护开发文档,便于后来者快速了解本仓库代码结构
+- 小组内合理分工,避免个人任务量过重或过轻
+- 做好部会记录,及时完成工作任务,避免拖延到ddl
+- 各组间多交流,相互了解各自的开发进度,加强协作,遇到困难互相帮助。
+- 加油,奥里给
+
+## 开发组成员
+
+- 逻辑组:潘徐成、顾朗哲、顾又嘉
+- 通信组:肖钦帆、郭雯齐
+- 界面组:陈宇瀚、韩志峰
+- 运维组:张琨、叶芊屿
+- 端茶倒水:李羿璇
\ No newline at end of file
diff --git a/installer/Resources/AppIcon/appicon.svg b/installer/Resources/AppIcon/appicon.svg
new file mode 100644
index 00000000..9d63b651
--- /dev/null
+++ b/installer/Resources/AppIcon/appicon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/installer/Resources/AppIcon/appiconfg.svg b/installer/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 00000000..21dfb25f
--- /dev/null
+++ b/installer/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/installer/Resources/Fonts/OpenSans-Regular.ttf b/installer/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 00000000..1e57f677
Binary files /dev/null and b/installer/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/installer/Resources/Fonts/OpenSans-Semibold.ttf b/installer/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 00000000..7689b813
Binary files /dev/null and b/installer/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/installer/Resources/Images/dotnet_bot.svg b/installer/Resources/Images/dotnet_bot.svg
new file mode 100644
index 00000000..abfaff26
--- /dev/null
+++ b/installer/Resources/Images/dotnet_bot.svg
@@ -0,0 +1,93 @@
+
diff --git a/installer/Resources/Raw/AboutAssets.txt b/installer/Resources/Raw/AboutAssets.txt
new file mode 100644
index 00000000..15d62448
--- /dev/null
+++ b/installer/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,15 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with you package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
diff --git a/installer/Resources/Splash/splash.svg b/installer/Resources/Splash/splash.svg
new file mode 100644
index 00000000..21dfb25f
--- /dev/null
+++ b/installer/Resources/Splash/splash.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/installer/Resources/Styles/Colors.xaml b/installer/Resources/Styles/Colors.xaml
new file mode 100644
index 00000000..245758ba
--- /dev/null
+++ b/installer/Resources/Styles/Colors.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+ #512BD4
+ #DFD8F7
+ #2B0B98
+ White
+ Black
+ #E1E1E1
+ #C8C8C8
+ #ACACAC
+ #919191
+ #6E6E6E
+ #404040
+ #212121
+ #141414
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #F7B548
+ #FFD590
+ #FFE5B9
+ #28C2D1
+ #7BDDEF
+ #C3F2F4
+ #3E8EED
+ #72ACF1
+ #A7CBF6
+
+
\ No newline at end of file
diff --git a/installer/Resources/Styles/Styles.xaml b/installer/Resources/Styles/Styles.xaml
new file mode 100644
index 00000000..dc4a0347
--- /dev/null
+++ b/installer/Resources/Styles/Styles.xaml
@@ -0,0 +1,405 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/ViewModel/Common.cs b/installer/ViewModel/Common.cs
new file mode 100644
index 00000000..fd1d5f80
--- /dev/null
+++ b/installer/ViewModel/Common.cs
@@ -0,0 +1,164 @@
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+using System.ComponentModel;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Globalization;
+using System.Windows.Input;
+using System.Runtime.CompilerServices;
+
+namespace installer.ViewModel
+{
+ public abstract class NotificationObject : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler? PropertyChanged;
+ ///< summary>
+ /// announce notification
+ ///
+ ///< param name="propertyName">property name
+ public void OnPropertyChanged([CallerMemberName] string propertyName = "")
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+ ///< summary>
+ /// BaseCommand
+ ///
+ public class BaseCommand : ICommand
+ {
+ private Func