├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── Andreal.Core ├── Andreal.Core.csproj ├── Common │ ├── BackgroundTask.cs │ ├── CommandPrefixAttribute.cs │ ├── ConfigJson.cs │ ├── ConfigWatcher.cs │ ├── ExceptionLogger.cs │ ├── External.cs │ ├── GlobalConfig.cs │ ├── MessageInfo.cs │ ├── MessageInfoType.cs │ ├── Path.cs │ └── RobotReply.cs ├── Data │ ├── Api │ │ ├── OtherApi.cs │ │ └── UnofficialArcaeaAPI.cs │ ├── Json │ │ ├── Arcaea │ │ │ ├── ArcaeaUnlimited │ │ │ │ ├── AccountInfo.cs │ │ │ │ ├── ArcSongdata.cs │ │ │ │ ├── ResponseRoot.cs │ │ │ │ ├── SessionQueryingContent.cs │ │ │ │ ├── SongListContent.cs │ │ │ │ ├── UserBestContent.cs │ │ │ │ ├── UserBestsContent.cs │ │ │ │ └── UserInfoContent.cs │ │ │ └── PartnerPosInfoBase │ │ │ │ ├── PartnerPosInfoBase.cs │ │ │ │ └── PosInfoItem.cs │ │ ├── Bandori │ │ │ ├── BandoriRoomInfo.cs │ │ │ ├── ResponseItem.cs │ │ │ └── SourceInfo.cs │ │ ├── Hitokoto │ │ │ └── Hitokoto.cs │ │ └── Ycm │ │ │ ├── CarsItem.cs │ │ │ └── YcmResponse.cs │ └── Sqlite │ │ ├── BotUserInfo.cs │ │ └── ImgVersion.cs ├── Executor │ ├── ArcExecutor.cs │ ├── ExecutorBase.cs │ ├── OtherExecutor.cs │ └── PermissionExecutor.cs ├── Message │ ├── IMessage.cs │ ├── ImageMessage.cs │ ├── MessageChain.cs │ ├── ReplyMessage.cs │ └── TextMessage.cs ├── Model │ └── Arcaea │ │ ├── ArcaeaChart.cs │ │ ├── ArcaeaCharts.cs │ │ ├── ArcaeaSong.cs │ │ ├── Best30Data.cs │ │ ├── Best40Data.cs │ │ ├── DifficultyInfo.cs │ │ ├── IBest30Data.cs │ │ ├── IBest40Data.cs │ │ ├── PlayerInfo.cs │ │ ├── RecordData.cs │ │ ├── RecordInfo.cs │ │ └── Side.cs ├── UI │ ├── BackGround.cs │ ├── Color.cs │ ├── Font.cs │ ├── Image.cs │ ├── ImageGenerator │ │ ├── ArcBackgroundGenerator.cs │ │ ├── ArcBest30ImageGenerator.cs │ │ ├── ArcBest40ImageGenerator.cs │ │ ├── ArcRecord5ImageGenerator.cs │ │ ├── ArcRecordImageGenerator.cs │ │ └── ArcSongLevelListImageGenerator.cs │ ├── Model │ │ ├── IGraphicsModel.cs │ │ ├── ImageModel.cs │ │ ├── LineModel.cs │ │ ├── PartnerModel.cs │ │ ├── PolygonModel.cs │ │ ├── PotentitalModel.cs │ │ ├── RectangleModel.cs │ │ ├── TextOnlyModel.cs │ │ ├── TextWithShadowModel.cs │ │ └── TextWithStrokeModel.cs │ └── StackBlur.cs └── Utils │ ├── AbbreviationHelper.cs │ ├── ArcaeaHelper.cs │ ├── BotStatementHelper.cs │ ├── ConcurrentDictionaryExtend.cs │ ├── DateTimeHelper.cs │ ├── LastExceptionHelper.cs │ ├── RandStringHelper.cs │ ├── RandomHelper.cs │ ├── SqliteHelper.cs │ ├── StringHelper.cs │ └── SystemHelper.cs ├── Andreal.Test ├── AIRandomSongTest.cs ├── Andreal.Test.csproj └── Usings.cs ├── Andreal.Window ├── Andreal.Window.csproj ├── Assets │ ├── Fonts │ │ └── Roboto-Light.ttf │ ├── chevron-down.png │ ├── close.png │ ├── favicon.ico │ ├── img_contact.png │ ├── img_map.png │ ├── img_message.png │ ├── img_setting.png │ └── min.png ├── Common │ ├── AssemblyInfo.cs │ ├── DelegateCommand.cs │ ├── EnumDescriptionConverter.cs │ ├── ExceptionLog.cs │ ├── NotifyIconViewModel.cs │ ├── Program.cs │ └── TimeConverter.cs └── UI │ ├── App.xaml │ ├── App.xaml.cs │ ├── Login.xaml │ ├── Login.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── SliderSubmit.xaml │ ├── SliderSubmit.xaml.cs │ ├── SliderVerify.xaml │ ├── SliderVerify.xaml.cs │ ├── SmsCodeVerify.xaml │ ├── SmsCodeVerify.xaml.cs │ ├── SourceDownloader.xaml │ ├── SourceDownloader.xaml.cs │ └── UserControl │ ├── Accounts.xaml │ ├── Accounts.xaml.cs │ ├── ExceptionLog.xaml │ ├── ExceptionLog.xaml.cs │ ├── MessageLog.xaml │ ├── MessageLog.xaml.cs │ ├── ReplySetting.xaml │ ├── ReplySetting.xaml.cs │ ├── Setting.xaml │ └── Setting.xaml.cs ├── Andreal.sln ├── Andreal.sln.DotSettings.user ├── LICENSE ├── README.md └── UpdateLog.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://afdian.net/@Awbugl'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.dll 3 | *.xml 4 | *.cache 5 | *.pdb 6 | *.txt 7 | *.nupkg 8 | *.p7s 9 | *._ 10 | 11 | [Dd]ebug/ 12 | [Dd]ebugPublic/ 13 | [Rr]elease/ 14 | [Rr]eleases/ 15 | x64/ 16 | x86/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | [Ll]og/ 21 | -------------------------------------------------------------------------------- /Andreal.Core/Andreal.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | true 8 | embedded 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Andreal.Core/Common/BackgroundTask.cs: -------------------------------------------------------------------------------- 1 | using System.Timers; 2 | using Timer = System.Timers.Timer; 3 | 4 | namespace Andreal.Core.Common; 5 | 6 | internal static class BackgroundTask 7 | { 8 | internal static void Init() 9 | { 10 | Timer timer = new(240000); 11 | timer.Elapsed += Clean; 12 | timer.AutoReset = true; 13 | timer.Enabled = true; 14 | } 15 | 16 | private static void Clean(object? source, ElapsedEventArgs e) 17 | { 18 | var time = DateTime.Now.AddMinutes(-2); 19 | 20 | foreach (var j in new DirectoryInfo(Path.TempImageRoot).GetFiles().Where(j => time > j.LastWriteTime)) j.Delete(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Andreal.Core/Common/CommandPrefixAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Common; 2 | 3 | [Serializable] 4 | [AttributeUsage(AttributeTargets.Method)] 5 | internal class CommandPrefixAttribute : Attribute 6 | { 7 | internal CommandPrefixAttribute(params string[] prefixs) 8 | { 9 | Prefixs = prefixs; 10 | } 11 | 12 | internal string[] Prefixs { get; } 13 | } 14 | -------------------------------------------------------------------------------- /Andreal.Core/Common/ConfigJson.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Konata.Core.Common; 3 | using Newtonsoft.Json; 4 | 5 | namespace Andreal.Core.Common; 6 | 7 | public class ConfigJson 8 | { 9 | [JsonProperty("protocol")] 10 | public OicqProtocol Protocol { get; set; } 11 | 12 | [JsonProperty("keystore")] 13 | public BotKeyStore? KeyStore { get; set; } 14 | 15 | [JsonProperty("device")] 16 | public BotDevice? Device { get; set; } 17 | } 18 | 19 | public class AccountInfo 20 | { 21 | [JsonProperty("uid")] 22 | public uint Account { get; set; } 23 | 24 | [JsonProperty("password")] 25 | public string Password { get; set; } = ""; 26 | } 27 | 28 | public class ApiConfig 29 | { 30 | [JsonProperty("url")] 31 | public string Url { get; set; } = ""; 32 | 33 | [JsonProperty("token")] 34 | public string Token { get; set; } = ""; 35 | } 36 | 37 | public class AndrealSettings 38 | { 39 | [JsonProperty("friend_add")] 40 | public bool FriendAdd { get; set; } 41 | 42 | [JsonProperty("group_add")] 43 | public bool GroupAdd { get; set; } 44 | 45 | [JsonProperty("group_inviter_whitelist")] 46 | public List GroupInviterWhitelist { get; set; } = new(); 47 | } 48 | 49 | public class AndrealConfig 50 | { 51 | [JsonProperty("master")] 52 | public uint Master { get; set; } 53 | 54 | [JsonProperty("protocol")] 55 | public OicqProtocol Protocol { get; set; } 56 | 57 | [JsonProperty("slider")] 58 | public SliderType SliderType { get; set; } 59 | 60 | [JsonProperty("enable_handle_message")] 61 | public bool EnableHandleMessage { get; set; } = true; 62 | 63 | [JsonProperty("accounts")] 64 | public List Accounts { get; set; } = new(); 65 | 66 | [JsonProperty("api")] 67 | public Dictionary Api { get; set; } = new(); 68 | 69 | [JsonProperty("approve_settings")] 70 | public AndrealSettings Settings { get; set; } = new(); 71 | } 72 | 73 | public enum SliderType 74 | { 75 | [Description("滑块验证助手")] 76 | Helper, 77 | 78 | [Description("浏览器模拟滑块(不建议)")] 79 | Browser 80 | } 81 | -------------------------------------------------------------------------------- /Andreal.Core/Common/ConfigWatcher.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Common; 2 | 3 | internal static class ConfigWatcher 4 | { 5 | private const int TimeoutMillis = 2000; 6 | private static readonly FileSystemWatcher Watcher = new(Path.AndreaConfigRoot); 7 | private static readonly List TmpFiles = new(); 8 | 9 | internal static void Init() 10 | { 11 | Watcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite; 12 | Watcher.Filter = "*.json"; 13 | Watcher.EnableRaisingEvents = true; 14 | 15 | Timer timer = new(OnTimer, null, Timeout.Infinite, Timeout.Infinite); 16 | Watcher.Changed += (_, e) => 17 | { 18 | TmpFiles.Add(e.Name!); 19 | timer.Change(TimeoutMillis, Timeout.Infinite); 20 | }; 21 | } 22 | 23 | private static void OnTimer(object? _) 24 | { 25 | lock (TmpFiles) 26 | { 27 | foreach (var file in TmpFiles) GlobalConfig.Init(file); 28 | TmpFiles.Clear(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Andreal.Core/Common/ExceptionLogger.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Utils; 2 | using static Andreal.Core.Utils.BotStatementHelper; 3 | 4 | namespace Andreal.Core.Common; 5 | 6 | public static class ExceptionLogger 7 | { 8 | public delegate void ExceptionEventHandler(Exception e); 9 | 10 | private static readonly object SyncObj = new(); 11 | 12 | public static event ExceptionEventHandler? OnExceptionRecorded; 13 | 14 | public static void Log(Exception? ex) 15 | { 16 | if (ex is null) return; 17 | LastExceptionHelper.Set(ex); 18 | 19 | switch (ex) 20 | { 21 | case HttpRequestException or TaskCanceledException: 22 | ++WebExceptionCount; 23 | return; 24 | 25 | case InvalidCastException when ex.Message.Contains("Konata.Core.Events.ProtocolEvent"): 26 | return; 27 | 28 | default: 29 | ++ExceptionCount; 30 | WriteException(ex); 31 | OnExceptionRecorded?.Invoke(ex); 32 | return; 33 | } 34 | } 35 | 36 | private static void WriteException(Exception ex) 37 | { 38 | lock (SyncObj) 39 | { 40 | File.AppendAllText(Path.ExceptionReport, $"{DateTime.Now}\n{ex}\n\n"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Andreal.Core/Common/External.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Utils; 2 | using Konata.Core; 3 | using Konata.Core.Message; 4 | 5 | namespace Andreal.Core.Common; 6 | 7 | [Serializable] 8 | public static class External 9 | { 10 | public static void Process( 11 | Bot bot, 12 | int type, 13 | uint fromGroup, 14 | uint fromQq, 15 | MessageStruct message) 16 | => MessageInfo.Process(bot, type, fromGroup, fromQq, message); 17 | 18 | public static void Initialize(AndrealConfig andrealConfig) => SystemHelper.Init(andrealConfig); 19 | } 20 | -------------------------------------------------------------------------------- /Andreal.Core/Common/GlobalConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Andreal.Core.Data.Json.Arcaea.PartnerPosInfoBase; 3 | using Newtonsoft.Json; 4 | 5 | namespace Andreal.Core.Common; 6 | 7 | public static class GlobalConfig 8 | { 9 | internal static ConcurrentDictionary> Locations 10 | = new(JsonConvert.DeserializeObject>>(File.ReadAllText(Path.PartnerConfig)) !); 11 | 12 | public static RobotReply RobotReply = JsonConvert.DeserializeObject(File.ReadAllText(Path.RobotReply))!; 13 | 14 | internal static List RandomReply = JsonConvert.DeserializeObject>(File.ReadAllText(Path.RandomReply))!; 15 | 16 | internal static void Init(string file) 17 | { 18 | switch (file) 19 | { 20 | case "positioninfo.json": 21 | Locations = new(JsonConvert.DeserializeObject>>(File.ReadAllText(Path.PartnerConfig))!); 22 | return; 23 | 24 | case "replytemplate.json": 25 | RobotReply = JsonConvert.DeserializeObject(File.ReadAllText(Path.RobotReply))!; 26 | return; 27 | 28 | case "randomtemplate.json": 29 | RandomReply = JsonConvert.DeserializeObject>(File.ReadAllText(Path.RandomReply))!; 30 | return; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Andreal.Core/Common/MessageInfoType.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Common; 2 | 3 | // ReSharper disable UnusedMember.Global 4 | [Serializable] 5 | internal enum MessageInfoType 6 | { 7 | Friend = 0, 8 | Group = 1, 9 | Temp = 2 10 | } 11 | -------------------------------------------------------------------------------- /Andreal.Core/Common/RobotReply.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Message; 2 | using Andreal.Core.Utils; 3 | using Newtonsoft.Json; 4 | 5 | namespace Andreal.Core.Common; 6 | 7 | public class RobotReply 8 | { 9 | [JsonProperty("NotBind")] 10 | public string NotBind { get; set; } = ""; 11 | 12 | [JsonProperty("NotBindArc")] 13 | public string NotBindArc { get; set; } = ""; 14 | 15 | [JsonProperty("ParameterLengthError")] 16 | public string ParameterLengthError { get; set; } = ""; 17 | 18 | [JsonProperty("ParameterError")] 19 | public string ParameterError { get; set; } = ""; 20 | 21 | [JsonProperty("ConfigNotFound")] 22 | public string ConfigNotFound { get; set; } = ""; 23 | 24 | [JsonProperty("NotPlayedTheSong")] 25 | public string NotPlayedTheSong { get; set; } = ""; 26 | 27 | [JsonProperty("ArcUidNotFound")] 28 | public string ArcUidNotFound { get; set; } = ""; 29 | 30 | [JsonProperty("TooManyArcUid")] 31 | public string TooManyArcUid { get; set; } = ""; 32 | 33 | [JsonProperty("NoSongFound")] 34 | public string NoSongFound { get; set; } = ""; 35 | 36 | [JsonProperty("NoBydChart")] 37 | public string NoBydChart { get; set; } = ""; 38 | 39 | [JsonProperty("NotPlayedTheChart")] 40 | public string NotPlayedTheChart { get; set; } = ""; 41 | 42 | [JsonProperty("UnBindSuccess")] 43 | public string UnBindSuccess { get; set; } = ""; 44 | 45 | [JsonProperty("GotShadowBanned")] 46 | public string GotShadowBanned { get; set; } = ""; 47 | 48 | [JsonProperty("APIQueryFailed")] 49 | public string APIQueryFailed { get; set; } = ""; 50 | 51 | [JsonProperty("TooManySongFound")] 52 | public string TooManySongFound { get; set; } = ""; 53 | 54 | [JsonProperty("JrrpResult")] 55 | public string JrrpResult { get; set; } = ""; 56 | 57 | [JsonProperty("SendMessageFailed")] 58 | public string SendMessageFailed { get; set; } = ""; 59 | 60 | [JsonProperty("BindSuccess")] 61 | public string BindSuccess { get; set; } = ""; 62 | 63 | [JsonProperty("HelpMessage")] 64 | public string HelpMessage { get; set; } = ""; 65 | 66 | [JsonProperty("GroupLeave")] 67 | public string GroupLeave { get; set; } = ""; 68 | 69 | [JsonProperty("BelowTheThreshold")] 70 | public string BelowTheThreshold { get; set; } = ""; 71 | 72 | [JsonProperty("NeedUpdateAUA")] 73 | public string NeedUpdateAUA { get; set; } = ""; 74 | 75 | [JsonProperty("InvalidSessionInfo")] 76 | public string InvalidSessionInfo { get; set; } = ""; 77 | 78 | [JsonProperty("SessionExpired")] 79 | public string SessionExpired { get; set; } = ""; 80 | 81 | [JsonProperty("SessionQuerying")] 82 | public string SessionQuerying { get; set; } = ""; 83 | 84 | [JsonProperty("SessionWaitingForAccount")] 85 | public string SessionWaitingForAccount { get; set; } = ""; 86 | 87 | [JsonProperty("DuplicateBestsRequests")] 88 | public string DuplicateBestsRequests { get; set; } = ""; 89 | 90 | [JsonProperty("UserBestsSession")] 91 | public string UserBestsSession { get; set; } = ""; 92 | 93 | [JsonProperty("ExceptionOccured")] 94 | public string ExceptionOccured { get; set; } = ""; 95 | 96 | internal TextMessage OnExceptionOccured(Exception ex) => ExceptionOccured.Replace("$exception$", ex.Message); 97 | internal TextMessage OnAPIQueryFailed(Exception ex) => APIQueryFailed.Replace("$exception$", ex.Message); 98 | 99 | internal TextMessage OnAPIQueryFailed(int status, string message) => APIQueryFailed.Replace("$exception$", $"{status}: {message}"); 100 | 101 | internal TextMessage OnJrrpResult(string value) => JrrpResult.Replace("$jrrp$", value); 102 | internal TextMessage OnBindSuccess(string value) => BindSuccess.Replace("$info$", value); 103 | 104 | internal TextMessage OnUserBestsSession(string value) => UserBestsSession.Replace("$session$", value); 105 | 106 | internal TextMessage OnSessionQuerying(string value) => SessionQuerying.Replace("$count$", value); 107 | 108 | internal TextMessage OnSessionWaitingForAccount(string value) => SessionWaitingForAccount.Replace("$count$", value); 109 | 110 | internal string GetRandomAIReply(string songname, string artist) 111 | => GlobalConfig.RandomReply.GetRandomItem().Replace("$songname$", songname).Replace("$artist$", artist); 112 | } 113 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Api/OtherApi.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Andreal.Core.Data.Json.Bandori; 3 | using Andreal.Core.Data.Json.Hitokoto; 4 | using Andreal.Core.Data.Json.Ycm; 5 | using Newtonsoft.Json; 6 | 7 | namespace Andreal.Core.Data.Api; 8 | 9 | internal static class OtherApi 10 | { 11 | [NonSerialized] 12 | private static readonly HttpClient Client; 13 | 14 | static OtherApi() 15 | { 16 | Client = new(); 17 | } 18 | 19 | private static async Task GetString(string url) => await Client.GetStringAsync(url); 20 | 21 | internal static async Task HitokotoApi() 22 | { 23 | var data = JsonConvert.DeserializeObject(await GetString("https://v1.hitokoto.cn")); 24 | return $"{data?.Content}\n ----「{data?.From}」"; 25 | } 26 | 27 | internal static async Task JrrpApi(long uin) 28 | { 29 | Task msg = Client.PostAsync("http://api.kokona.tech:5555/jrrp", 30 | new StringContent($"QQ=2967373629&v=20190114&QueryQQ={uin}", Encoding.UTF8, 31 | new("application/x-www-form-urlencoded"))); 32 | 33 | return await (await msg).Content.ReadAsStringAsync(); 34 | } 35 | 36 | internal static async Task YcmApi(string carType) 37 | => JsonConvert.DeserializeObject(await 38 | GetString($"https://ycm.chinosk6.cn/get_car?car_type={carType}&time_limit=900&token=L4ETRgHBPUt8KSkvcO")); 39 | 40 | internal static async Task AddCarApi( 41 | string carType, 42 | string roomid, 43 | string description, 44 | long uin) 45 | => JsonConvert.DeserializeObject(await 46 | GetString($"https://ycm.chinosk6.cn/add_car?car_type={carType}&room_id={roomid}&description={description}&creator_id={uin}&data_from=AndreaBot&token=L4ETRgHBPUt8KSkvcO")); 47 | 48 | internal static async Task BandoriYcmApi() 49 | { 50 | var jsonData = JsonConvert.DeserializeObject(await GetString("https://api.bandoristation.com/?function=query_room_number")); 51 | 52 | return jsonData?.Response.Count == 0 53 | ? "myc" 54 | : jsonData?.Response.Aggregate("", 55 | (current, i) => current + 56 | $"\n\n{((DateTime.UtcNow - DateTime.UnixEpoch).TotalMilliseconds - i.Time) / 1000:##.000}秒前\n来自 {i.SourceInfo.Name} 的车牌:\n{i.RawMessage}"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Api/UnofficialArcaeaAPI.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Common; 2 | using Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 3 | using Andreal.Core.Message; 4 | using Newtonsoft.Json; 5 | using Path = Andreal.Core.Common.Path; 6 | 7 | namespace Andreal.Core.Data.Api; 8 | 9 | public static class UnofficialArcaeaAPI 10 | { 11 | private static HttpClient? _client; 12 | 13 | public static void Init(AndrealConfig config) 14 | { 15 | _client = new(); 16 | if (config.Api.TryGetValue("unlimited", out var apiConfig)) 17 | { 18 | _client.BaseAddress = new(apiConfig.Url); 19 | _client.DefaultRequestHeaders.Authorization = new("Bearer", apiConfig.Token); 20 | } 21 | } 22 | 23 | private static async Task GetString(string url) 24 | => JsonConvert.DeserializeObject(await (await _client!.SendAsync(new(HttpMethod.Get, url))).Content.ReadAsStringAsync()); 25 | 26 | private static async Task GetImage(string url, Path filename) 27 | { 28 | FileStream? fileStream = null; 29 | var message = await _client!.GetAsync(url); 30 | 31 | if (message.Content.Headers.ContentType?.MediaType?.StartsWith("image/") != true) 32 | throw new ArgumentException(JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync())!.Message); 33 | 34 | var exflag = false; 35 | 36 | try 37 | { 38 | fileStream = new(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); 39 | await message.Content.CopyToAsync(fileStream); 40 | } 41 | catch 42 | { 43 | exflag = true; 44 | throw; 45 | } 46 | finally 47 | { 48 | try 49 | { 50 | if (fileStream is not null) 51 | { 52 | fileStream.Close(); 53 | await fileStream.DisposeAsync(); 54 | } 55 | } 56 | catch 57 | { 58 | // ignore 59 | } 60 | 61 | try 62 | { 63 | if (exflag && File.Exists(filename)) File.Delete(filename); 64 | } 65 | catch 66 | { 67 | // ignore 68 | } 69 | } 70 | } 71 | 72 | internal static async Task UserInfo(long ucode) => await GetString($"user/info?user_code={ucode:D9}"); 73 | 74 | internal static async Task UserInfo(string uname) => await GetString($"user/info?user_name={uname}"); 75 | 76 | internal static async Task UserBest(long ucode, string song, object dif) 77 | => await GetString($"user/best?usercode={ucode:D9}&song_id={song}&difficulty={dif}"); 78 | 79 | internal static async Task UserBestsSession(long ucode) => await GetString($"user/bests/session?user_code={ucode:D9}"); 80 | 81 | internal static async Task UserBestsResult(string session) 82 | => await GetString($"user/bests/result?session_info={session}&overflow=9"); 83 | 84 | internal static async Task SongList() => await GetString("song/list"); 85 | 86 | internal static async Task SongAssets(string sid, int difficulty, Path pth) 87 | => await GetImage($"assets/song?song_id={sid}&difficulty={difficulty}", pth); 88 | 89 | internal static async Task CharAssets(int partner, bool awakened, Path pth) 90 | => await GetImage($"assets/char?partner={partner}&awakened={(awakened ? "true" : "false")}", pth); 91 | 92 | internal static async Task IconAssets(int partner, bool awakened, Path pth) 93 | => await GetImage($"assets/icon?partner={partner}&awakened={(awakened ? "true" : "false")}", pth); 94 | 95 | internal static async Task PreviewAssets(string sid, int difficulty, Path pth) 96 | => await GetImage($"assets/preview?song_id={sid}&difficulty={difficulty}", pth); 97 | 98 | internal static TextMessage GetErrorMessage(RobotReply info, int status, string message) 99 | => status switch 100 | { 101 | -1 or -2 or -3 or -13 => info.ArcUidNotFound, 102 | -4 => info.TooManyArcUid, 103 | -14 => info.NoBydChart, 104 | -15 => info.NotPlayedTheChart, 105 | -16 => info.GotShadowBanned, 106 | -23 => info.BelowTheThreshold, 107 | -24 => info.NeedUpdateAUA, 108 | -29 => info.InvalidSessionInfo, 109 | -30 => info.SessionExpired, 110 | _ => info.OnAPIQueryFailed(status, message) 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/ArcaeaUnlimited/AccountInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 6 | 7 | public class AccountInfo 8 | { 9 | [JsonProperty("code")] 10 | public int Code { get; set; } 11 | 12 | [JsonProperty("name")] 13 | public string Name { get; set; } 14 | 15 | [JsonProperty("user_id")] 16 | public int UserID { get; set; } 17 | 18 | [JsonProperty("is_mutual")] 19 | public bool IsMutual { get; set; } 20 | 21 | [JsonProperty("is_char_uncapped_override")] 22 | public bool IsCharUncappedOverride { get; set; } 23 | 24 | [JsonProperty("is_char_uncapped")] 25 | public bool IsCharUncapped { get; set; } 26 | 27 | [JsonProperty("is_skill_sealed")] 28 | public bool IsSkillSealed { get; set; } 29 | 30 | [JsonProperty("rating")] 31 | public short Rating { get; set; } 32 | 33 | [JsonProperty("character")] 34 | public int Character { get; set; } 35 | } 36 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/ArcaeaUnlimited/ArcSongdata.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 6 | 7 | public class ArcSongdata 8 | { 9 | [JsonProperty("song_id")] 10 | public string SongID { get; set; } 11 | 12 | [JsonProperty("difficulty")] 13 | public sbyte Difficulty { get; set; } 14 | 15 | [JsonProperty("score")] 16 | public int Score { get; set; } 17 | 18 | [JsonProperty("shiny_perfect_count")] 19 | public string MaxPure { get; set; } 20 | 21 | [JsonProperty("perfect_count")] 22 | public string Pure { get; set; } 23 | 24 | [JsonProperty("near_count")] 25 | public string Far { get; set; } 26 | 27 | [JsonProperty("miss_count")] 28 | public string Lost { get; set; } 29 | 30 | [JsonProperty("time_played")] 31 | public long TimePlayed { get; set; } 32 | 33 | [JsonProperty("clear_type")] 34 | public sbyte ClearType { get; set; } 35 | 36 | [JsonProperty("rating")] 37 | public double Rating { get; set; } 38 | } 39 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/ArcaeaUnlimited/ResponseRoot.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 6 | 7 | public class ResponseRoot 8 | { 9 | [JsonProperty("status")] 10 | public int Status { get; set; } 11 | 12 | [JsonProperty("message")] 13 | public string Message { get; set; } 14 | 15 | [JsonProperty("content")] 16 | public dynamic Content { get; set; } 17 | 18 | internal T DeserializeContent() => JsonConvert.DeserializeObject(Content.ToString()); 19 | } 20 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/ArcaeaUnlimited/SessionQueryingContent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 4 | 5 | #pragma warning disable CS8618 6 | public class SessionQueryingContent 7 | { 8 | [JsonProperty("queried_charts")] 9 | public string QueriedCharts { get; set; } = "0"; 10 | 11 | [JsonProperty("current_account")] 12 | public string CurrentAccount { get; set; } = "0"; 13 | } 14 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/ArcaeaUnlimited/SongListContent.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Model.Arcaea; 2 | using Newtonsoft.Json; 3 | 4 | #pragma warning disable CS8618 5 | 6 | namespace Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 7 | 8 | public class SongListContent 9 | { 10 | [JsonProperty("songs")] 11 | public List Songs { get; set; } 12 | } 13 | 14 | public class SongsItem 15 | { 16 | [JsonProperty("song_id")] 17 | public string SongID { get; set; } 18 | 19 | [JsonProperty("difficulties")] 20 | public ArcaeaSong Difficulties { get; set; } 21 | 22 | [JsonProperty("alias")] 23 | public List Alias { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/ArcaeaUnlimited/UserBestContent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 6 | 7 | public class UserBestContent 8 | { 9 | [JsonProperty("account_info")] 10 | public AccountInfo AccountInfo { get; set; } 11 | 12 | [JsonProperty("record")] 13 | public ArcSongdata Record { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/ArcaeaUnlimited/UserBestsContent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 6 | 7 | public class UserBestsContent 8 | { 9 | [JsonProperty("best30_avg")] 10 | public double Best30Avg { get; set; } 11 | 12 | [JsonProperty("recent10_avg")] 13 | public double Recent10Avg { get; set; } 14 | 15 | [JsonProperty("account_info")] 16 | public AccountInfo AccountInfo { get; set; } 17 | 18 | [JsonProperty("best30_list")] 19 | public List Best30List { get; set; } 20 | 21 | [JsonProperty("best30_overflow")] 22 | public List? OverflowList { get; set; } 23 | } 24 | 25 | public class UserBestsSessionContent 26 | { 27 | [JsonProperty("session_info")] 28 | public string SessionInfo { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/ArcaeaUnlimited/UserInfoContent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 6 | 7 | public class UserInfoContent 8 | { 9 | [JsonProperty("account_info")] 10 | public AccountInfo AccountInfo { get; set; } 11 | 12 | [JsonProperty("recent_score")] 13 | public List RecentScore { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/PartnerPosInfoBase/PartnerPosInfoBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Andreal.Core.Common; 3 | using Andreal.Core.Data.Sqlite; 4 | 5 | namespace Andreal.Core.Data.Json.Arcaea.PartnerPosInfoBase; 6 | 7 | internal static class PartnerPosInfoBase 8 | { 9 | private static readonly Lazy>> Dict = new(() => new(Init())); 10 | 11 | private static readonly PosInfoItem ImgV1 = new() { PositionX = 770, PositionY = 58, Size = 950 }; 12 | private static readonly PosInfoItem ImgV2 = new() { PositionX = 850, PositionY = 0, Size = 1400 }; 13 | private static readonly PosInfoItem ImgV4 = new() { PositionX = 550, PositionY = 50, Size = 1500 }; 14 | 15 | private static Dictionary> Init() 16 | { 17 | var ls = new Dictionary>(); 18 | foreach (var (key, value) in GlobalConfig.Locations) ls.Add(key, value.ToDictionary(i => i.Partner)); 19 | return ls; 20 | } 21 | 22 | internal static PosInfoItem? Get(string partner, ImgVersion imgVersion) 23 | => imgVersion switch 24 | { 25 | ImgVersion.ImgV1 => Dict.Value["1"].TryGetValue(partner, out var result) ? result : ImgV1, 26 | ImgVersion.ImgV2 => Dict.Value["2"].TryGetValue(partner, out var result) ? result : ImgV2, 27 | ImgVersion.ImgV4 => Dict.Value["4"].TryGetValue(partner, out var result) ? result : ImgV4, 28 | _ => null 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Arcaea/PartnerPosInfoBase/PosInfoItem.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Data.Json.Arcaea.PartnerPosInfoBase; 2 | 3 | #pragma warning disable CS8618 4 | // ReSharper disable MemberCanBeInternal 5 | public class PosInfoItem 6 | { 7 | public string Partner { get; set; } 8 | public int PositionX { get; set; } 9 | public int PositionY { get; set; } 10 | public int Size { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Bandori/BandoriRoomInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Bandori; 6 | 7 | [Serializable] 8 | public class BandoriRoomInfo 9 | { 10 | [JsonProperty("response")] 11 | public List Response { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Bandori/ResponseItem.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Bandori; 6 | 7 | [Serializable] 8 | public class ResponseItem 9 | { 10 | [JsonProperty("raw_message")] 11 | public string RawMessage { get; set; } 12 | 13 | [JsonProperty("source_info")] 14 | public SourceInfo SourceInfo { get; set; } 15 | 16 | [JsonProperty("time")] 17 | public long Time { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Bandori/SourceInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Bandori; 6 | 7 | [Serializable] 8 | public class SourceInfo 9 | { 10 | [JsonProperty("name")] 11 | public string Name { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Hitokoto/Hitokoto.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Hitokoto; 6 | 7 | [Serializable] 8 | public class Hitokoto 9 | { 10 | [JsonProperty("hitokoto")] 11 | public string Content { get; set; } 12 | 13 | [JsonProperty("from")] 14 | public string From { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Ycm/CarsItem.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Ycm; 6 | 7 | [Serializable] 8 | public class CarsItem 9 | { 10 | [JsonProperty("add_time")] 11 | public long AddTime { get; set; } 12 | 13 | [JsonProperty("creator_id")] 14 | public string CreatorID { get; set; } 15 | 16 | [JsonProperty("data_from")] 17 | public string DataFrom { get; set; } 18 | 19 | [JsonProperty("description")] 20 | public string Description { get; set; } 21 | 22 | [JsonProperty("id")] 23 | public long ID { get; set; } 24 | 25 | [JsonProperty("more_info")] 26 | public string MoreInfo { get; set; } 27 | 28 | [JsonProperty("room_id")] 29 | public string RoomID { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Json/Ycm/YcmResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Andreal.Core.Data.Json.Ycm; 6 | 7 | [Serializable] 8 | public class YcmResponse 9 | { 10 | [JsonProperty("car_type")] 11 | public string CarType { get; set; } 12 | 13 | [JsonProperty("code")] 14 | public int Code { get; set; } 15 | 16 | [JsonProperty("message")] 17 | public string Message { get; set; } 18 | 19 | [JsonProperty("cars")] 20 | public List Cars { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Sqlite/BotUserInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Andreal.Core.Utils; 3 | using SQLite; 4 | 5 | namespace Andreal.Core.Data.Sqlite; 6 | 7 | [Serializable] 8 | [Table("BotUserInfo")] 9 | internal class BotUserInfo 10 | { 11 | private static Lazy> _list 12 | = new(() => new(SqliteHelper.SelectAll().ToDictionary(i => i.Uin))); 13 | 14 | [PrimaryKey] [Column("QQId")] 15 | public long Uin { get; set; } 16 | 17 | [Column("ArcId")] 18 | public int ArcCode { get; set; } 19 | 20 | [Column("IsHide")] 21 | public int IsHide { get; set; } 22 | 23 | [Column("IsText")] 24 | public int IsText { get; set; } 25 | 26 | [Column("ImgVer")] 27 | public ImgVersion UiVersion { get; set; } 28 | 29 | internal static void Set(BotUserInfo user) 30 | { 31 | if (_list.Value.ContainsKey(user.Uin)) 32 | { 33 | _list.Value[user.Uin] = user; 34 | SqliteHelper.Update(user); 35 | } 36 | else 37 | { 38 | _list.Value.TryAdd(user.Uin, user); 39 | SqliteHelper.Insert(user); 40 | } 41 | } 42 | 43 | internal static BotUserInfo? Get(long uin) => _list.Value.TryGetValue(uin, out var user) ? user : null; 44 | } 45 | -------------------------------------------------------------------------------- /Andreal.Core/Data/Sqlite/ImgVersion.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Data.Sqlite; 2 | 3 | internal enum ImgVersion 4 | { 5 | ImgV1 = 0, 6 | ImgV2, 7 | ImgV3, 8 | ImgV4 9 | } 10 | -------------------------------------------------------------------------------- /Andreal.Core/Executor/ExecutorBase.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Common; 2 | using Andreal.Core.Data.Sqlite; 3 | using Konata.Core; 4 | 5 | namespace Andreal.Core.Executor; 6 | 7 | [Serializable] 8 | internal abstract class ExecutorBase 9 | { 10 | protected readonly MessageInfo Info; 11 | 12 | protected ExecutorBase(MessageInfo info) 13 | { 14 | Info = info; 15 | } 16 | 17 | protected Bot Bot => Info.Bot; 18 | protected bool IsGroup => Info.MessageType == MessageInfoType.Group; 19 | protected string[] Command => Info.CommandWithoutPrefix; 20 | protected int CommandLength => Info.CommandWithoutPrefix.Length; 21 | protected BotUserInfo? User => Info.UserInfo.Value; 22 | protected static RobotReply RobotReply => GlobalConfig.RobotReply; 23 | } 24 | -------------------------------------------------------------------------------- /Andreal.Core/Executor/OtherExecutor.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Common; 2 | using Andreal.Core.Data.Api; 3 | using Andreal.Core.Data.Sqlite; 4 | using Andreal.Core.Message; 5 | using Andreal.Core.Utils; 6 | using Konata.Core.Interfaces.Api; 7 | using Konata.Core.Message.Model; 8 | 9 | namespace Andreal.Core.Executor; 10 | 11 | #pragma warning disable CS8600 12 | #pragma warning disable CS8602 13 | #pragma warning disable CS8604 14 | 15 | [Serializable] 16 | internal class OtherExecutor : ExecutorBase 17 | { 18 | public OtherExecutor(MessageInfo info) : base(info) { } 19 | 20 | [CommandPrefix("/help", "/arc help")] 21 | private MessageChain HelpMessage() => CommandLength == 0 ? RobotReply.HelpMessage : null; 22 | 23 | [CommandPrefix("/yiyan")] 24 | private static async Task Hitokoto() => await OtherApi.HitokotoApi(); 25 | 26 | [CommandPrefix("/jrrp")] 27 | private async Task Jrrp() => RobotReply.OnJrrpResult(await OtherApi.JrrpApi(Info.FromQQ)); 28 | 29 | [CommandPrefix("/dismiss")] 30 | private async Task Dismiss() 31 | { 32 | if (Info.Message.Chain.FindChain()?.Any(i => i.AtUin == Info.Bot.Uin) != true) return null; 33 | if (!IsGroup) return null; 34 | if (!Info.MasterCheck() && !await Info.PermissionCheck()) return null; 35 | 36 | await Info.SendMessage(RobotReply.GroupLeave); 37 | await Task.Delay(5000); 38 | await Bot.GroupLeave(Info.FromGroup); 39 | 40 | return null; 41 | } 42 | 43 | [CommandPrefix("/geterr")] 44 | private static MessageChain ExceptionReport() => LastExceptionHelper.GetDetails(); 45 | 46 | [CommandPrefix("/config")] 47 | private MessageChain Config() 48 | { 49 | if (CommandLength != 1) return RobotReply.ParameterLengthError; 50 | if (User == null) return RobotReply.NotBind; 51 | 52 | switch (Command[0]) 53 | { 54 | case "hide": 55 | User.IsHide ^= 1; 56 | BotUserInfo.Set(User); 57 | return $"私密模式已{(User.IsHide == 1 ? "开启" : "关闭")}。"; 58 | 59 | case "text": 60 | case "txt": 61 | User.IsText = 1; 62 | BotUserInfo.Set(User); 63 | return "默认显示方式已更改为文字。"; 64 | 65 | case "pic": 66 | case "img": 67 | User.IsText = 0; 68 | BotUserInfo.Set(User); 69 | return "默认显示方式已更改为图片。"; 70 | 71 | default: 72 | return RobotReply.ConfigNotFound; 73 | } 74 | } 75 | 76 | [CommandPrefix("/ycm")] 77 | private static async Task GetBandoriRoom() => await OtherApi.BandoriYcmApi(); 78 | 79 | [CommandPrefix("/state")] 80 | private static MessageChain Statement() => BotStatementHelper.Statement; 81 | } 82 | -------------------------------------------------------------------------------- /Andreal.Core/Executor/PermissionExecutor.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Common; 2 | using Andreal.Core.Message; 3 | using Andreal.Core.Model.Arcaea; 4 | using Konata.Core.Interfaces.Api; 5 | 6 | namespace Andreal.Core.Executor; 7 | 8 | [Serializable] 9 | internal class PermissionExecutor : ExecutorBase 10 | { 11 | public PermissionExecutor(MessageInfo info) : base(info) { } 12 | 13 | [CommandPrefix("/echo")] 14 | private async Task Echo() 15 | { 16 | if (Info.MasterCheck()) await Info.SendMessageOnly(string.Join(" ", Command)); 17 | return null; 18 | } 19 | 20 | [CommandPrefix("/remove")] 21 | private async Task RemoveGroup() 22 | { 23 | if (!Info.MasterCheck()) return null; 24 | if (CommandLength != 1) return RobotReply.ParameterLengthError; 25 | await Info.Bot.GroupLeave(uint.Parse(Command[0])); 26 | return "Removed."; 27 | } 28 | 29 | [CommandPrefix("/update")] 30 | private MessageChain? Update() 31 | { 32 | if (!Info.MasterCheck()) return null; 33 | ArcaeaCharts.Init(); 34 | return "Arcaea songlist updated"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Andreal.Core/Message/IMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Message; 2 | 3 | internal interface IMessage { } 4 | -------------------------------------------------------------------------------- /Andreal.Core/Message/ImageMessage.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.UI; 2 | using Path = Andreal.Core.Common.Path; 3 | 4 | namespace Andreal.Core.Message; 5 | 6 | [Serializable] 7 | public class ImageMessage : IMessage 8 | { 9 | private readonly string _path; 10 | 11 | private ImageMessage(string path) 12 | { 13 | _path = path; 14 | } 15 | 16 | public override string ToString() => _path; 17 | 18 | internal static ImageMessage FromPath(Path path) => new(path); 19 | 20 | public static implicit operator ImageMessage(Image value) 21 | { 22 | var pth = Path.RandImageFileName(); 23 | value.SaveAsJpgWithQuality(pth); 24 | value.Dispose(); 25 | return FromPath(pth); 26 | } 27 | 28 | public static implicit operator ImageMessage(BackGround value) 29 | { 30 | var pth = Path.RandImageFileName(); 31 | value.SaveAsJpgWithQuality(pth); 32 | value.Dispose(); 33 | return FromPath(pth); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Andreal.Core/Message/MessageChain.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Message; 2 | 3 | public class MessageChain 4 | { 5 | private IEnumerable _messages; 6 | 7 | internal MessageChain() 8 | { 9 | _messages = Array.Empty(); 10 | } 11 | 12 | internal MessageChain(params IMessage[] messages) 13 | { 14 | _messages = messages.ToList(); 15 | } 16 | 17 | internal void Append(IMessage? message) 18 | { 19 | if (message is not null) _messages = _messages.Append(message); 20 | } 21 | 22 | internal void Append(string? message) 23 | { 24 | if (message is not null) _messages = _messages.Append((TextMessage)message); 25 | } 26 | 27 | internal MessageChain Prepend(IMessage message) 28 | { 29 | _messages = _messages.Prepend(message); 30 | return this; 31 | } 32 | 33 | internal IEnumerable ToArray() => _messages; 34 | 35 | public static implicit operator MessageChain(string value) => new((TextMessage)value); 36 | 37 | public static implicit operator MessageChain(TextMessage value) => new(value); 38 | 39 | public static implicit operator MessageChain(ImageMessage value) => new(value); 40 | } 41 | -------------------------------------------------------------------------------- /Andreal.Core/Message/ReplyMessage.cs: -------------------------------------------------------------------------------- 1 | using Konata.Core.Message; 2 | 3 | namespace Andreal.Core.Message; 4 | 5 | [Serializable] 6 | public class ReplyMessage : IMessage 7 | { 8 | public readonly MessageStruct Message; 9 | 10 | internal ReplyMessage(MessageStruct message) 11 | { 12 | Message = message; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Andreal.Core/Message/TextMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Message; 2 | 3 | [Serializable] 4 | public class TextMessage : IMessage 5 | { 6 | private readonly string _message; 7 | 8 | private TextMessage(string message) 9 | { 10 | _message = message; 11 | } 12 | 13 | public override string ToString() => _message; 14 | 15 | public static implicit operator string(TextMessage value) => value._message; 16 | 17 | public static implicit operator TextMessage(string value) => new(value); 18 | } 19 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/ArcaeaChart.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Andreal.Core.UI; 3 | using Newtonsoft.Json; 4 | using Path = Andreal.Core.Common.Path; 5 | 6 | #pragma warning disable CS8618 7 | 8 | namespace Andreal.Core.Model.Arcaea; 9 | 10 | public class ArcaeaChart 11 | { 12 | private static readonly ConcurrentDictionary SongImage = new(); 13 | 14 | [JsonProperty("name_en")] 15 | public string NameEn { get; set; } 16 | 17 | [JsonProperty("name_jp")] 18 | public string NameJp { get; set; } 19 | 20 | [JsonProperty("artist")] 21 | public string Artist { get; set; } 22 | 23 | [JsonProperty("bpm")] 24 | public string Bpm { get; set; } 25 | 26 | [JsonProperty("bpm_base")] 27 | public double BpmBase { get; set; } 28 | 29 | [JsonProperty("set")] 30 | public string Set { get; set; } 31 | 32 | [JsonProperty("set_friendly")] 33 | public string SetFriendly { get; set; } 34 | 35 | [JsonProperty("time")] 36 | public int Time { get; set; } 37 | 38 | [JsonProperty("side")] 39 | public int Side { get; set; } 40 | 41 | [JsonProperty("world_unlock")] 42 | public bool WorldUnlock { get; set; } 43 | 44 | [JsonProperty("remote_download")] 45 | public bool RemoteDownload { get; set; } 46 | 47 | [JsonProperty("bg")] 48 | public string Bg { get; set; } 49 | 50 | [JsonProperty("date")] 51 | public int Date { get; set; } 52 | 53 | [JsonProperty("version")] 54 | public string Version { get; set; } 55 | 56 | [JsonProperty("difficulty")] 57 | public int Difficulty { get; set; } 58 | 59 | [JsonProperty("rating")] 60 | public int Rating { get; set; } 61 | 62 | [JsonProperty("note")] 63 | public int Note { get; set; } 64 | 65 | [JsonProperty("chart_designer")] 66 | public string ChartDesigner { get; set; } 67 | 68 | [JsonProperty("jacket_designer")] 69 | public string JacketDesigner { get; set; } 70 | 71 | [JsonProperty("jacket_override")] 72 | public bool JacketOverride { get; set; } 73 | 74 | [JsonProperty("audio_override")] 75 | public bool AudioOverride { get; set; } 76 | 77 | internal string SongID { get; set; } 78 | 79 | internal double Const => (double)Rating / 10; 80 | 81 | internal int RatingClass { get; set; } 82 | 83 | internal DifficultyInfo DifficultyInfo => DifficultyInfo.GetByIndex(RatingClass); 84 | 85 | internal string ConstString => $"[{DifficultyInfo.ShortStr} {Const:0.0}]"; 86 | 87 | internal string NameWithPackageAndConst => $"{NameEn} (Package: {SetFriendly}) {ConstString}"; 88 | 89 | internal string GetSongName(byte length) => NameEn.Length < length + 3 ? NameEn : $"{NameEn[..length]}..."; 90 | 91 | internal async Task GetSongImage() 92 | { 93 | var path = await Path.ArcaeaSong(this); 94 | 95 | try 96 | { 97 | if (!SongImage.TryGetValue(path, out var stream)) 98 | { 99 | await using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 100 | 101 | using var reader = new BinaryReader(fileStream); 102 | var bytes = reader.ReadBytes((int)fileStream.Length); 103 | stream = new MemoryStream(bytes); 104 | reader.Close(); 105 | fileStream.Close(); 106 | 107 | SongImage.TryAdd(path, stream); 108 | } 109 | 110 | var img = new Image(stream); 111 | if (img.Width == 512) return img; 112 | var newimg = new Image(img, 512, 512); 113 | newimg.SaveAsPng(path); 114 | img.Dispose(); 115 | return newimg; 116 | } 117 | catch (Exception e) 118 | { 119 | SongImage.TryRemove(path, out var s); 120 | s?.DisposeAsync(); 121 | throw new ArgumentException("GetSongImage Failed.", NameEn, e); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/ArcaeaCharts.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Andreal.Core.Data.Api; 3 | using Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 4 | using Andreal.Core.Utils; 5 | using Newtonsoft.Json; 6 | using Path = Andreal.Core.Common.Path; 7 | 8 | namespace Andreal.Core.Model.Arcaea; 9 | 10 | public static partial class ArcaeaCharts 11 | { 12 | private static readonly ConcurrentDictionary Songs = new(); 13 | private static readonly ConcurrentDictionary> Aliases = new(); 14 | 15 | internal static ArcaeaSong? QueryByID(string? songid) => GetByID(songid); 16 | 17 | internal static List? Query(string alias) 18 | { 19 | if (string.IsNullOrWhiteSpace(alias)) return default; 20 | 21 | if (AliasCache.ContainsKey(alias)) return AliasCache[alias]; 22 | 23 | var data = GetByID(alias) ?? GetByName(Songs, alias) ?? GetByAlias(alias); 24 | 25 | if (data != null) return new() { data }; 26 | 27 | var abbrdata = new List(); 28 | 29 | Abbreviations.ForAllItems>((song, value) => 30 | { 31 | if (StringHelper.Equals(value, alias) && !abbrdata.Contains(song)) abbrdata.Add(song); 32 | }); 33 | 34 | if (abbrdata.Count > 0) return abbrdata; 35 | 36 | return GetByPriorityQueue(alias); 37 | } 38 | 39 | internal static List GetSongAlias(string songid) => Aliases[songid]; 40 | 41 | internal static ArcaeaSong RandomSong() => Songs.Values.GetRandomItem(); 42 | 43 | internal static ArcaeaChart? RandomSong(double start, double end) 44 | { 45 | ArcaeaChart[] ls = GetByConstRange(start, end).ToArray(); 46 | if (ls.Length == 0) return null; 47 | return ls.GetRandomItem(); 48 | } 49 | } 50 | 51 | public static partial class ArcaeaCharts 52 | { 53 | [NonSerialized] 54 | private static readonly ConcurrentDictionary> Abbreviations = new(); 55 | 56 | [NonSerialized] 57 | private static readonly ConcurrentDictionary> Names = new(); 58 | 59 | [NonSerialized] 60 | private static readonly ConcurrentDictionary> AliasCache = new(); 61 | 62 | static ArcaeaCharts() 63 | { 64 | Init(); 65 | } 66 | 67 | public static void Init() 68 | { 69 | Songs.Clear(); 70 | Aliases.Clear(); 71 | Abbreviations.Clear(); 72 | Names.Clear(); 73 | 74 | List? slst; 75 | 76 | try 77 | { 78 | slst = UnofficialArcaeaAPI.SongList().Result?.DeserializeContent().Songs; 79 | if (slst != null) File.WriteAllText(Path.TmpSongList, JsonConvert.SerializeObject(slst)); 80 | } 81 | catch 82 | { 83 | if (!File.Exists(Path.TmpSongList)) throw; 84 | slst = JsonConvert.DeserializeObject>(File.ReadAllText(Path.TmpSongList)); 85 | } 86 | 87 | if (slst == null) return; 88 | 89 | foreach (var songitem in slst) 90 | { 91 | songitem.Difficulties.SongID = songitem.SongID; 92 | 93 | for (var i = 0; i < songitem.Difficulties.Count; ++i) 94 | { 95 | songitem.Difficulties[i].RatingClass = i; 96 | songitem.Difficulties[i].SongID = songitem.SongID; 97 | } 98 | 99 | Aliases.TryAdd(songitem.SongID, songitem.Alias); 100 | Songs.TryAdd(songitem.SongID, songitem.Difficulties); 101 | } 102 | 103 | foreach (var (_, value) in Songs) 104 | { 105 | var abbrs = new List(); 106 | var names = new List(); 107 | 108 | for (var index = 0; index < value.Count; index++) 109 | { 110 | if (index == 0 || value[index].AudioOverride) 111 | { 112 | abbrs.Add(value[index].NameEn.GetAbbreviation()); 113 | names.Add(value[index].NameEn); 114 | 115 | if (string.IsNullOrWhiteSpace(value[index].NameJp)) continue; 116 | 117 | abbrs.Add(value[index].NameJp.GetAbbreviation()); 118 | names.Add(value[index].NameJp); 119 | } 120 | } 121 | 122 | Abbreviations.TryAdd(value, abbrs); 123 | Names.TryAdd(value, names); 124 | } 125 | } 126 | } 127 | 128 | public static partial class ArcaeaCharts 129 | { 130 | private static ArcaeaSong? GetByID(string? songid) => songid is not null && Songs.TryGetValue(songid, out var value) ? value : null; 131 | 132 | private static ArcaeaSong? GetByName(ConcurrentDictionary values, string alias) 133 | { 134 | values.TryTakeValues((ArcaeaChart item) => StringHelper.Equals(item.NameEn, alias) || StringHelper.Equals(item.NameJp, alias), 135 | out var result); 136 | 137 | return result; 138 | } 139 | 140 | private static ArcaeaSong? GetByAlias(string alias) 141 | { 142 | Aliases.TryTakeKey>(value => StringHelper.Equals(value, alias), out var result); 143 | return GetByID(result); 144 | } 145 | 146 | private static List? GetByPriorityQueue(string alias) 147 | { 148 | var dic = new PriorityQueue(); 149 | 150 | Aliases.ForAllItems>((song, sid) => Enqueue(dic, alias, sid, GetByID(song)!, 1, 4)); 151 | 152 | dic.TryPeek(out _, out var firstpriority); 153 | 154 | if (firstpriority != 1) 155 | foreach (var (sid, song) in Songs) 156 | Enqueue(dic, alias, sid, song, 2, 5); 157 | 158 | dic.TryPeek(out _, out firstpriority); 159 | 160 | if (firstpriority != 2) Names.ForAllItems>((song, name) => Enqueue(dic, alias, name, song, 3, 6)); 161 | 162 | if (dic.Count == 0) return default; 163 | 164 | dic.TryDequeue(out var firstobj, out var lowestpriority); 165 | 166 | var ls = new List { firstobj! }; 167 | 168 | while (dic.TryDequeue(out var obj, out var priority) && priority == lowestpriority) 169 | if (!ls.Contains(obj)) 170 | ls.Add(obj); 171 | 172 | AliasCache.TryAdd(alias, ls); 173 | return ls; 174 | } 175 | 176 | private static void Enqueue( 177 | PriorityQueue dic, 178 | string alias, 179 | string key, 180 | ArcaeaSong song, 181 | byte upperpriority, 182 | byte lowerpriority) 183 | { 184 | if (StringHelper.Contains(key, alias)) dic.Enqueue(song, upperpriority); 185 | if (StringHelper.Contains(alias, key)) dic.Enqueue(song, lowerpriority); 186 | } 187 | 188 | internal static IEnumerable GetByConst(double @const) 189 | { 190 | const double lerance = 0.001; 191 | 192 | // ReSharper disable once LoopCanBePartlyConvertedToQuery 193 | foreach (var song in Songs.Values) 194 | // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator 195 | { 196 | foreach (var chart in song) 197 | if (Math.Abs(chart.Const - @const) < lerance) 198 | yield return chart; 199 | } 200 | } 201 | 202 | private static IEnumerable GetByConstRange(double lowerlimit, double upperlimit) 203 | { 204 | // ReSharper disable once LoopCanBeConvertedToQuery 205 | foreach (var song in Songs.Values) 206 | // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator 207 | { 208 | foreach (var chart in song) 209 | if (chart.Const >= lowerlimit && chart.Const <= upperlimit) 210 | yield return chart; 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/ArcaeaSong.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Message; 2 | using Path = Andreal.Core.Common.Path; 3 | 4 | namespace Andreal.Core.Model.Arcaea; 5 | 6 | public class ArcaeaSong : List, IEquatable 7 | { 8 | internal string SongID { get; set; } = null!; 9 | 10 | internal string NameWithPackage => $"{this[0].NameEn}\n(Package: {this[0].SetFriendly})"; 11 | 12 | public bool Equals(ArcaeaSong? other) 13 | { 14 | if (ReferenceEquals(null, other)) return false; 15 | if (ReferenceEquals(this, other)) return true; 16 | return SongID.Equals(other.SongID); 17 | } 18 | 19 | internal async Task FullConstString() 20 | { 21 | var msg = new MessageChain(); 22 | 23 | for (var i = 0; i < Count; i++) 24 | if (i == 2 || this[i].JacketOverride) 25 | msg.Append(ImageMessage.FromPath(await Path.ArcaeaSong(this[i]))); 26 | 27 | msg.Append(NameWithPackage); 28 | 29 | for (var i = 0; i < Count; i++) 30 | { 31 | msg.Append("\n" + this[i].ConstString); 32 | if (this[i].AudioOverride) msg.Append($" ({this[i].NameEn})"); 33 | } 34 | 35 | return msg; 36 | } 37 | 38 | public override bool Equals(object? obj) 39 | { 40 | if (ReferenceEquals(null, obj)) return false; 41 | if (ReferenceEquals(this, obj)) return true; 42 | if (obj.GetType() != GetType()) return false; 43 | return Equals((ArcaeaSong)obj); 44 | } 45 | 46 | // ReSharper disable once NonReadonlyMemberInGetHashCode 47 | public override int GetHashCode() => SongID.GetHashCode(); 48 | } 49 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/Best30Data.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 2 | 3 | namespace Andreal.Core.Model.Arcaea; 4 | 5 | internal class Best30Data : IBest30Data 6 | { 7 | internal Best30Data(UserBestsContent b30data) 8 | { 9 | B30data = b30data; 10 | } 11 | 12 | private UserBestsContent B30data { get; } 13 | 14 | private string Best30Avg => B30data.Best30Avg.ToString("0.0000"); 15 | 16 | private string Recent10Avg => B30data.Recent10Avg > 0 ? B30data.Recent10Avg.ToString("0.0000") : "--"; 17 | 18 | private List Best30List => B30data.Best30List.Select(i => new RecordInfo(i)).ToList(); 19 | 20 | string IBest30Data.Best30Avg => Best30Avg; 21 | 22 | string IBest30Data.Recent10Avg => Recent10Avg; 23 | 24 | List IBest30Data.Best30List => Best30List; 25 | } 26 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/Best40Data.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 2 | 3 | namespace Andreal.Core.Model.Arcaea; 4 | 5 | internal class Best40Data : IBest40Data 6 | { 7 | internal Best40Data(UserBestsContent b40data) 8 | { 9 | B40data = b40data; 10 | } 11 | 12 | private UserBestsContent B40data { get; } 13 | 14 | private string Best30Avg => B40data.Best30Avg.ToString("0.0000"); 15 | 16 | private string Recent10Avg => B40data.Recent10Avg > 0 ? B40data.Recent10Avg.ToString("0.0000") : "--"; 17 | 18 | private List Best30List => B40data.Best30List.Select(i => new RecordInfo(i)).ToList(); 19 | 20 | private List? OverflowList => B40data.OverflowList?.Select(i => new RecordInfo(i)).ToList(); 21 | 22 | string IBest40Data.Best30Avg => Best30Avg; 23 | 24 | string IBest40Data.Recent10Avg => Recent10Avg; 25 | 26 | List IBest40Data.Best30List => Best30List; 27 | 28 | List? IBest40Data.OverflowList => OverflowList; 29 | } 30 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/DifficultyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Drawing; 3 | 4 | #pragma warning disable CS8618 5 | 6 | namespace Andreal.Core.Model.Arcaea; 7 | 8 | [Serializable] 9 | internal class DifficultyInfo 10 | { 11 | private static readonly ConcurrentDictionary List = new(); 12 | 13 | static DifficultyInfo() 14 | { 15 | List.TryAdd(3, new() { LongStr = "Beyond", ShortStr = "BYD", Alias = new[] { "byn", "byd", "beyond" }, Color = Color.FromArgb(165, 20, 49) }); 16 | 17 | List.TryAdd(2, new() { LongStr = "Future", ShortStr = "FTR", Alias = new[] { "ftr", "future" }, Color = Color.FromArgb(115, 35, 100) }); 18 | 19 | List.TryAdd(1, new() { LongStr = "Present", ShortStr = "PRS", Alias = new[] { "prs", "present" }, Color = Color.FromArgb(120, 155, 80) }); 20 | 21 | List.TryAdd(0, new() { LongStr = "Past", ShortStr = "PST", Alias = new[] { "pst", "past" }, Color = Color.FromArgb(20, 165, 215) }); 22 | } 23 | 24 | private string[] Alias { get; set; } 25 | internal string LongStr { get; private set; } 26 | internal string ShortStr { get; private set; } 27 | internal Color Color { get; private set; } 28 | 29 | internal static DifficultyInfo GetByIndex(int index) => List[index]; 30 | 31 | internal static (string, int) DifficultyConverter(string dif) 32 | { 33 | foreach (var (key, value) in List) 34 | { 35 | foreach (var alias in value.Alias.Where(dif.EndsWith)) return (dif[..^alias.Length], key); 36 | } 37 | 38 | return (dif, -1); 39 | } 40 | 41 | public static implicit operator string(DifficultyInfo info) => info.ShortStr; 42 | } 43 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/IBest30Data.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Model.Arcaea; 2 | 3 | internal interface IBest30Data 4 | { 5 | internal string Best30Avg { get; } 6 | internal string Recent10Avg { get; } 7 | internal List Best30List { get; } 8 | } 9 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/IBest40Data.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Model.Arcaea; 2 | 3 | internal interface IBest40Data 4 | { 5 | internal string Best30Avg { get; } 6 | internal string Recent10Avg { get; } 7 | internal List Best30List { get; } 8 | internal List? OverflowList { get; } 9 | } 10 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/PlayerInfo.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 2 | using Andreal.Core.Data.Sqlite; 3 | 4 | namespace Andreal.Core.Model.Arcaea; 5 | 6 | [Serializable] 7 | internal class PlayerInfo 8 | { 9 | private readonly bool _isHide; 10 | 11 | private readonly string _playerCode, _playerName; 12 | 13 | public PlayerInfo(AccountInfo accountInfo, BotUserInfo user) 14 | { 15 | _playerCode = accountInfo.Code.ToString("D9"); 16 | _playerName = accountInfo.Name; 17 | _isHide = user.IsHide == 0; 18 | ImgVersion = user.UiVersion; 19 | Partner = accountInfo.Character; 20 | IsAwakened = accountInfo.IsCharUncapped && !accountInfo.IsCharUncappedOverride; 21 | Potential = accountInfo.Rating; 22 | } 23 | 24 | internal string PlayerName => _isHide ? _playerName : $"{_playerName[0]}......{_playerName[^1]}"; 25 | 26 | internal string PlayerCode => _isHide ? _playerCode : "xxxxxxxxx"; 27 | 28 | internal ImgVersion ImgVersion { get; } 29 | 30 | internal int Partner { get; } 31 | 32 | internal bool IsAwakened { get; } 33 | 34 | internal short Potential { get; } 35 | } 36 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/RecordData.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Data.Sqlite; 2 | using Andreal.Core.Message; 3 | using Andreal.Core.UI.ImageGenerator; 4 | 5 | namespace Andreal.Core.Model.Arcaea; 6 | 7 | [Serializable] 8 | internal class RecordData 9 | { 10 | internal RecordData(PlayerInfo playerInfo, RecordInfo recordInfo, BotUserInfo userInfo) 11 | { 12 | PlayerInfo = playerInfo; 13 | RecordInfo = recordInfo; 14 | UserInfo = userInfo; 15 | } 16 | 17 | private PlayerInfo PlayerInfo { get; } 18 | private RecordInfo RecordInfo { get; } 19 | private BotUserInfo UserInfo { get; } 20 | 21 | private TextMessage RecordTextResult 22 | => $"{PlayerInfo.PlayerName}({PlayerInfo.PlayerCode}) 的最近记录\n曲名:{RecordInfo.SongName(50)} {RecordInfo.SongInfo.ConstString}\n分数:{RecordInfo.Score}\nPure:{RecordInfo.Pure} (+{RecordInfo.MaxPure}) Far:{RecordInfo.Far} Lost:{RecordInfo.Lost}\n单曲PTT:{RecordInfo.Rating}\n时间:{RecordInfo.TimeStr}"; 23 | 24 | internal async Task GetResult() 25 | { 26 | if (UserInfo.IsText == 1) return RecordTextResult; 27 | var imageGenerator = new ArcRecordImageGenerator(PlayerInfo, RecordInfo); 28 | 29 | switch (UserInfo.UiVersion) 30 | { 31 | case ImgVersion.ImgV1: 32 | return await imageGenerator.Version1(); 33 | 34 | case ImgVersion.ImgV3: 35 | return await imageGenerator.Version3(); 36 | 37 | case ImgVersion.ImgV2: 38 | default: 39 | return await imageGenerator.Version2(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/RecordInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Andreal.Core.Common; 3 | using Andreal.Core.Data.Json.Arcaea.ArcaeaUnlimited; 4 | using Andreal.Core.UI; 5 | 6 | namespace Andreal.Core.Model.Arcaea; 7 | 8 | [Serializable] 9 | internal class RecordInfo 10 | { 11 | private readonly int _score; 12 | 13 | public RecordInfo(ArcSongdata recentdata) 14 | { 15 | Difficulty = recentdata.Difficulty; 16 | SongInfo = ArcaeaCharts.QueryByID(recentdata.SongID)?[Difficulty] ?? throw new ArgumentException(MessageInfo.RobotReply.NoSongFound); 17 | Cleartype = recentdata.ClearType; 18 | SongID = recentdata.SongID; 19 | Rating = recentdata.Rating.ToString("0.0000"); 20 | Pure = recentdata.Pure; 21 | MaxPure = recentdata.MaxPure; 22 | Far = recentdata.Far; 23 | Lost = recentdata.Lost; 24 | Time = DateTime.UnixEpoch.AddMilliseconds(recentdata.TimePlayed).ToLocalTime(); 25 | _score = recentdata.Score; 26 | } 27 | 28 | internal ArcaeaChart SongInfo { get; } 29 | 30 | internal sbyte Cleartype { get; } 31 | 32 | internal int Difficulty { get; } 33 | 34 | internal bool IsRecent => Difficulty == -128; 35 | 36 | internal string SongID { get; } 37 | 38 | internal string Rating { get; } 39 | 40 | internal string Pure { get; } 41 | 42 | internal string MaxPure { get; } 43 | 44 | internal string Far { get; } 45 | 46 | internal string Lost { get; } 47 | 48 | internal DateTime Time { get; } 49 | 50 | internal string TimeStr => Time.ToString("yyyy/MM/dd HH:mm:ss"); 51 | 52 | internal DifficultyInfo DifficultyInfo => SongInfo.DifficultyInfo; 53 | 54 | internal double Const => SongInfo.Const; 55 | 56 | internal string Score 57 | { 58 | get 59 | { 60 | var scorestr = _score.ToString(); 61 | var result = new StringBuilder(); 62 | var len = scorestr.Length; 63 | for (var i = 0; i < 8; i++) 64 | { 65 | var j = len - 8 + i; 66 | result.Append(j < 0 ? '0' : scorestr[j]); 67 | if (i is 1 or 4) result.Append('\''); 68 | } 69 | 70 | return result.ToString(); 71 | } 72 | } 73 | 74 | internal string Rate 75 | => _score switch 76 | { 77 | >= 9900000 => "[EX+]", 78 | >= 9800000 => "[EX]", 79 | >= 9500000 => "[AA]", 80 | >= 9200000 => "[A]", 81 | >= 8900000 => "[B]", 82 | >= 8600000 => "[C]", 83 | _ => "[D]" 84 | }; 85 | 86 | internal Task GetSongImage() => SongInfo.GetSongImage(); 87 | 88 | internal string SongName(byte length) => SongInfo.GetSongName(length); 89 | 90 | internal bool Compare(RecordInfo? info) 91 | { 92 | if (info == null) return true; 93 | return _score > info._score; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Andreal.Core/Model/Arcaea/Side.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Model.Arcaea; 2 | 3 | internal enum Side 4 | { 5 | Hikari, 6 | Tairitsu, 7 | Achromic 8 | } 9 | -------------------------------------------------------------------------------- /Andreal.Core/UI/BackGround.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Drawing.Drawing2D; 3 | using System.Drawing.Imaging; 4 | using System.Drawing.Text; 5 | using Andreal.Core.UI.Model; 6 | using Path = Andreal.Core.Common.Path; 7 | 8 | namespace Andreal.Core.UI; 9 | 10 | #pragma warning disable CA1416 11 | 12 | public class BackGround : Image, IDisposable 13 | { 14 | private Graphics? _g; 15 | 16 | internal BackGround(Path path) : base(path) { } 17 | 18 | private BackGround(Bitmap bitmap) : base(bitmap) { } 19 | 20 | internal BackGround(int width, int height) : base(width, height) { } 21 | 22 | internal BackGround(Image origin, int width, int height) : base(origin, width, height) { } 23 | 24 | public new void Dispose() 25 | { 26 | if (Alreadydisposed) return; 27 | _g?.Dispose(); 28 | base.Dispose(); 29 | } 30 | 31 | ~BackGround() 32 | { 33 | _g?.Dispose(); 34 | base.Dispose(); 35 | } 36 | 37 | internal Graphics GraphicsFromBackGround() 38 | { 39 | if (_g is not null) return _g; 40 | _g = Graphics.FromImage(Bitmap); 41 | _g.InterpolationMode = InterpolationMode.HighQualityBicubic; 42 | _g.PixelOffsetMode = PixelOffsetMode.HighQuality; 43 | _g.CompositingQuality = CompositingQuality.HighQuality; 44 | _g.SmoothingMode = SmoothingMode.HighQuality; 45 | _g.TextRenderingHint = TextRenderingHint.AntiAlias; 46 | return _g; 47 | } 48 | 49 | internal new BackGround Cut(Rectangle rectangle) => new(Bitmap.Clone(rectangle, PixelFormat.Format32bppArgb)); 50 | 51 | internal void FillColor(System.Drawing.Color color, int alpha = 120) 52 | => GraphicsFromBackGround().FillRectangle(new SolidBrush(System.Drawing.Color.FromArgb(alpha, color)), 0, 0, Width, Height); 53 | 54 | internal void Draw(params IGraphicsModel[] graphicsModelCollection) 55 | { 56 | foreach (var i in graphicsModelCollection) i.Draw(GraphicsFromBackGround()); 57 | } 58 | 59 | internal BackGround Blur(byte round) 60 | { 61 | StackBlur.StackBlurRGBA32(Bitmap, round); 62 | return this; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Color.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.UI; 2 | 3 | [Serializable] 4 | internal static class Color 5 | { 6 | internal static readonly System.Drawing.Color White = System.Drawing.Color.White, 7 | Black = System.Drawing.Color.Black, 8 | Hikari = FromArgb(150, 100, 200, 225), 9 | Tairitsu = FromArgb(150, 50, 20, 75), 10 | Achromic = FromArgb(150, 180, 180, 180), 11 | PmColor = FromArgb(150, 180, 200), 12 | ArcGray = FromArgb(60, 60, 60), 13 | ArcPurple = FromArgb(31, 30, 51), 14 | ArcRed = FromArgb(104, 9, 52), 15 | GnaqGray = FromArgb(110, 110, 110), 16 | AzusaGray = FromArgb(90, 90, 90); 17 | 18 | internal static System.Drawing.Color FromArgb(int alpha, System.Drawing.Color baseColor) => System.Drawing.Color.FromArgb(alpha, baseColor); 19 | 20 | internal static System.Drawing.Color FromArgb(int r, int g, int b) => System.Drawing.Color.FromArgb(r, g, b); 21 | 22 | internal static System.Drawing.Color FromArgb( 23 | int a, 24 | int r, 25 | int g, 26 | int b) 27 | => System.Drawing.Color.FromArgb(a, r, g, b); 28 | 29 | internal static System.Drawing.Color GetBySide(int side) 30 | => side switch 31 | { 32 | 0 => Hikari, 33 | 1 => Tairitsu, 34 | 2 => Achromic, 35 | _ => White 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Font.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Drawing.Text; 3 | using Path = Andreal.Core.Common.Path; 4 | 5 | namespace Andreal.Core.UI; 6 | 7 | #pragma warning disable CA1416 8 | 9 | [Serializable] 10 | internal static class Font 11 | { 12 | private static PrivateFontCollection _pfc; 13 | private static Dictionary _fontFamily; 14 | 15 | static Font() 16 | { 17 | _pfc = new(); 18 | foreach (var s in new DirectoryInfo(Path.ArcaeaFontRoot).GetFiles().Select(s => s.FullName)) _pfc.AddFontFile(s); 19 | _fontFamily = _pfc.Families.ToDictionary(i => i.Name, i => i); 20 | Exo64 = GetFont("Exo", 64); 21 | Exo60 = GetFont("Exo", 60); 22 | Exo44 = GetFont("Exo", 44); 23 | Exo40 = GetFont("Exo", 40); 24 | Exo36 = GetFont("Exo", 36); 25 | Exo32 = GetFont("Exo", 32); 26 | Exo26 = GetFont("Exo", 26); 27 | Exo24 = GetFont("Exo", 24); 28 | Exo20 = GetFont("Exo", 20); 29 | Andrea108 = GetFont("Andrea", 108); 30 | Andrea90 = GetFont("Andrea", 90); 31 | Andrea72 = GetFont("Andrea", 72); 32 | Andrea60 = GetFont("Andrea", 60); 33 | Andrea56 = GetFont("Andrea", 56); 34 | Andrea36 = GetFont("Andrea", 36); 35 | Andrea28 = GetFont("Andrea", 28); 36 | Andrea20 = GetFont("Andrea", 20); 37 | Beatrice36 = GetFont("Beatrice", 36); 38 | Beatrice26 = GetFont("Beatrice", 26); 39 | Beatrice24 = GetFont("Beatrice", 24); 40 | Beatrice20 = GetFont("Beatrice", 20); 41 | ExoLight42 = GetFont("Exo Andrea", 42); 42 | ExoLight36 = GetFont("Exo Andrea", 36); 43 | ExoLight28 = GetFont("Exo Andrea", 28); 44 | ExoLight24 = GetFont("Exo Andrea", 24); 45 | ExoLight20 = GetFont("Exo Andrea", 20); 46 | KazesawaLight72 = GetFont("Kazesawa Light", 72); 47 | KazesawaLight56 = GetFont("Kazesawa Light", 56); 48 | KazesawaLight48 = GetFont("Kazesawa Light", 48); 49 | KazesawaLight40 = GetFont("Kazesawa Light", 40); 50 | KazesawaLight32 = GetFont("Kazesawa Light", 32); 51 | KazesawaLight24 = GetFont("Kazesawa Light", 24); 52 | KazesawaRegular56 = GetFont("Kazesawa Regular", 56); 53 | KazesawaRegular27 = GetFont("Kazesawa Regular", 27); 54 | } 55 | 56 | private static System.Drawing.Font GetFont(string name, float emSize) => new(_fontFamily[name], emSize); 57 | 58 | internal static readonly System.Drawing.Font Exo64, 59 | Exo60, 60 | Exo44, 61 | Exo40, 62 | Exo36, 63 | Exo32, 64 | Exo26, 65 | Exo24, 66 | Exo20, 67 | Andrea108, 68 | Andrea90, 69 | Andrea72, 70 | Andrea60, 71 | Andrea56, 72 | Andrea36, 73 | Andrea28, 74 | Andrea20, 75 | Beatrice36, 76 | Beatrice26, 77 | Beatrice24, 78 | Beatrice20, 79 | ExoLight42, 80 | ExoLight36, 81 | ExoLight28, 82 | ExoLight24, 83 | ExoLight20, 84 | KazesawaLight72, 85 | KazesawaLight56, 86 | KazesawaLight48, 87 | KazesawaLight40, 88 | KazesawaLight32, 89 | KazesawaLight24, 90 | KazesawaRegular56, 91 | KazesawaRegular27; 92 | } 93 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Image.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Drawing.Drawing2D; 3 | using System.Drawing.Imaging; 4 | using System.Runtime.InteropServices; 5 | using Path = Andreal.Core.Common.Path; 6 | 7 | namespace Andreal.Core.UI; 8 | 9 | #pragma warning disable CA1416 10 | 11 | public class Image : IDisposable 12 | { 13 | protected readonly Bitmap Bitmap; 14 | 15 | private protected bool Alreadydisposed; 16 | 17 | internal Image(Path path) 18 | { 19 | Bitmap = new(path); 20 | Bitmap.SetResolution(96, 96); 21 | } 22 | 23 | internal Image(int width, int height) 24 | { 25 | Bitmap = new(width, height); 26 | Bitmap.SetResolution(96, 96); 27 | } 28 | 29 | internal Image(Bitmap bitmap) 30 | { 31 | lock (bitmap) Bitmap = bitmap; 32 | Bitmap.SetResolution(96, 96); 33 | } 34 | 35 | internal Image(Image origin, int width, int height) 36 | { 37 | lock (origin) Bitmap = new(origin.Bitmap, width, height); 38 | Bitmap.SetResolution(96, 96); 39 | } 40 | 41 | internal Image(Stream stream) 42 | { 43 | lock (stream) Bitmap = (Bitmap)System.Drawing.Image.FromStream(stream); 44 | Bitmap.SetResolution(96, 96); 45 | } 46 | 47 | internal int Width => Bitmap.Width; 48 | 49 | internal int Height => Bitmap.Height; 50 | 51 | internal System.Drawing.Color MainColor => DeserializeColor(); 52 | 53 | public void Dispose() 54 | { 55 | if (Alreadydisposed) return; 56 | Bitmap.Dispose(); 57 | GC.SuppressFinalize(this); 58 | Alreadydisposed = true; 59 | } 60 | 61 | ~Image() 62 | { 63 | Bitmap.Dispose(); 64 | } 65 | 66 | private System.Drawing.Color DeserializeColor() 67 | { 68 | var bm = Bitmap.LockBits(new(0, 0, Bitmap.Width, Bitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); 69 | var ptr = bm.Scan0; 70 | var bytes = Math.Abs(bm.Stride) * Bitmap.Height; 71 | var rgbValues = new byte[bytes]; 72 | Marshal.Copy(ptr, rgbValues, 0, bytes); 73 | Bitmap.UnlockBits(bm); 74 | long red = 0, green = 0, blue = 0, count = 10; 75 | for (var counter = 0; counter < rgbValues.Length; counter += 12) 76 | { 77 | blue += rgbValues[counter]; 78 | green += rgbValues[counter + 1]; 79 | red += rgbValues[counter + 2]; 80 | ++count; 81 | } 82 | 83 | count = count / 2 * 3; 84 | var col = System.Drawing.Color.FromArgb((byte)(red / count), (byte)(green / count), (byte)(blue / count)); 85 | 86 | return col; 87 | } 88 | 89 | internal Image Cut(Rectangle rectangle) => new(Bitmap.Clone(rectangle, PixelFormat.Format32bppArgb)); 90 | 91 | internal void SaveAsPng(Path path) => Bitmap.Save(path, ImageFormat.Png); 92 | 93 | internal void SaveAsJpgWithQuality(Path path, int quality = 50) => ImageExtend.SaveAsJpeg(Bitmap, path, quality); 94 | 95 | internal static class ImageExtend 96 | { 97 | internal static void PngWithWhiteBg(Path originPath, Path newPath) 98 | { 99 | using Image img = new(originPath); 100 | using var bmpTemp = new Bitmap(img.Width, img.Height); 101 | using var g = Graphics.FromImage(bmpTemp); 102 | g.SmoothingMode = SmoothingMode.AntiAlias; 103 | g.Clear(System.Drawing.Color.White); 104 | DrawImage(g, img, 0, 0, img.Width, img.Height); 105 | bmpTemp.Save(newPath, ImageFormat.Png); 106 | } 107 | 108 | internal static void DrawImage( 109 | Graphics g, 110 | Image image, 111 | int posX, 112 | int posY) 113 | => g.DrawImage(image.Bitmap, posX, posY); 114 | 115 | internal static void DrawImage( 116 | Graphics g, 117 | Image image, 118 | int posX, 119 | int posY, 120 | int newWidth, 121 | int newHeight) 122 | => g.DrawImage(image.Bitmap, posX, posY, newWidth, newHeight); 123 | 124 | private static ImageCodecInfo? _imageCodecInfo; 125 | private static ImageCodecInfo ImageCodecInfo => _imageCodecInfo ??= GetCodecInfo("image/jpeg")!; 126 | 127 | private static ImageCodecInfo? GetCodecInfo(string mimeType) 128 | => ImageCodecInfo.GetImageEncoders().FirstOrDefault(ici => ici.MimeType == mimeType); 129 | 130 | public static void SaveAsJpeg(Bitmap bmp, Path path, int quality) 131 | { 132 | var ps = new EncoderParameters() { Param = { [0] = new(Encoder.Quality, quality) } }; 133 | bmp.Save(path, ImageCodecInfo, ps); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Andreal.Core/UI/ImageGenerator/ArcBackgroundGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using Andreal.Core.Model.Arcaea; 3 | using Andreal.Core.UI.Model; 4 | using Path = Andreal.Core.Common.Path; 5 | 6 | namespace Andreal.Core.UI.ImageGenerator; 7 | 8 | #pragma warning disable CA1416 9 | 10 | internal class ArcBackgroundGenerator 11 | { 12 | private readonly ArcaeaChart _info; 13 | 14 | public ArcBackgroundGenerator(RecordInfo recordInfo) 15 | { 16 | _info = recordInfo.SongInfo; 17 | } 18 | 19 | internal async Task ArcV1() 20 | { 21 | var path = Path.ArcaeaBackground(1, _info); 22 | return path.FileInfo.Exists ? new(path) : await GenerateArcV1(path); 23 | } 24 | 25 | private async Task GenerateArcV1(Path path) 26 | { 27 | using var song = await _info.GetSongImage(); 28 | using var temp = song.Cut(new(0, 87, 512, 341)); 29 | var background = new BackGround(temp, 1440, 960); 30 | using var masktmp = background.Blur(20).Cut(new(50, 50, 1340, 860)).Blur(80); 31 | background.FillColor(song.MainColor); 32 | background.Draw(new ImageModel(Path.ArcaeaBg1Mask, 0, 0, 1440, 960), new ImageModel(masktmp, 50, 50, 1340, 860), 33 | new ImageModel(Path.ArcaeaDivider, 100, 840, 1240), new TextWithStrokeModel("Pure", Font.Exo40, Color.White, 518, 488), 34 | new TextWithStrokeModel("Far", Font.Exo40, Color.White, 518, 553), 35 | new TextWithStrokeModel("Lost", Font.Exo40, Color.White, 518, 618), 36 | new TextWithStrokeModel("PTT", Font.Exo40, Color.White, 518, 683), 37 | new TextWithStrokeModel("Generated by Project Andreal", Font.KazesawaLight24, Color.White, 80, 865)); 38 | background.SaveAsPng(path); 39 | return background; 40 | } 41 | 42 | internal async Task ArcV2() 43 | { 44 | var path = Path.ArcaeaBackground(2, _info); 45 | return path.FileInfo.Exists ? new(path) : await GenerateArcV2(path); 46 | } 47 | 48 | private async Task GenerateArcV2(Path path) 49 | { 50 | using var song = await _info.GetSongImage(); 51 | using var temp = song.Cut(new(0, 112, 512, 288)); 52 | var background = new BackGround(temp, 1920, 1080).Blur(60); 53 | background.FillColor(song.MainColor); 54 | background.Draw(new TextWithShadowModel("Play PTT", Font.Exo36, 123, 355), new TextWithShadowModel("Pure", Font.Exo32, 127, 455), 55 | new TextWithShadowModel("Far", Font.Exo32, 127, 525), new TextWithShadowModel("Lost", Font.Exo32, 410, 525), 56 | new TextWithShadowModel("Played at", Font.Exo32, 127, 595), 57 | new ImageModel(new BackGround(song.Cut(new(0, 19, 512, 48)), 1920, 180).Blur(20), 0, 740), 58 | new LineModel(Color.White, 3, new(0, 740), new(1920, 740)), new LineModel(Color.White, 3, new(0, 920), new(1920, 920)), 59 | new LineModel(Color.White, 1, new(0, 705), new(1920, 705)), new LineModel(Color.White, 1, new(0, 955), new(1920, 955)), 60 | new RectangleModel(Color.GetBySide(_info.Side), new(145, 685, 320, 320)), new ImageModel(song, 130, 670, 320, 320), 61 | new TextWithShadowModel(_info.GetSongName(50), Font.Andrea56, 510, 750)); 62 | background.SaveAsPng(path); 63 | return background; 64 | } 65 | 66 | internal async Task ArcV3() 67 | { 68 | var path = Path.ArcaeaBackground(3, _info); 69 | return path.FileInfo.Exists ? new(path) : await GenerateArcV3(path); 70 | } 71 | 72 | private async Task GenerateArcV3(Path path) 73 | { 74 | using var song = await _info.GetSongImage(); 75 | using var temp = song.Cut(new(78, 0, 354, 512)); 76 | var background = new BackGround(temp, 1000, 1444).Blur(10); 77 | background.FillColor(Color.White, 100); 78 | background.Draw(new ImageModel(Path.ArcaeaBg3Mask(_info.Side), 0, 0, 1000), 79 | new TextOnlyModel(_info.GetSongName(30), Font.Beatrice36, Color.Black, 500, 860, StringAlignment.Center), 80 | new ImageModel(song, 286, 408, 428), new TextOnlyModel("PlayPtt:", Font.Exo24, Color.GnaqGray, 110, 1275), 81 | new TextOnlyModel("PlayTime:", Font.Exo24, Color.GnaqGray, 110, 1355), 82 | new TextOnlyModel("Pure", Font.Exo24, Color.Black, 635, 1260), new TextOnlyModel("Far", Font.Exo24, Color.Black, 635, 1315), 83 | new TextOnlyModel("Lost", Font.Exo24, Color.Black, 635, 1370)); 84 | background.SaveAsPng(path); 85 | return background; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Andreal.Core/UI/ImageGenerator/ArcBest30ImageGenerator.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Message; 2 | using Andreal.Core.Model.Arcaea; 3 | using Andreal.Core.UI.Model; 4 | using Path = Andreal.Core.Common.Path; 5 | 6 | namespace Andreal.Core.UI.ImageGenerator; 7 | 8 | internal class ArcBest30ImageGenerator 9 | { 10 | internal ArcBest30ImageGenerator(IBest30Data b30data, PlayerInfo info) 11 | { 12 | B30data = b30data; 13 | Info = info; 14 | } 15 | 16 | private IBest30Data B30data { get; } 17 | private PlayerInfo Info { get; } 18 | 19 | internal async Task Generate() 20 | { 21 | var bg = new BackGround(Path.ArcaeaBest30Bg); 22 | bg.Draw(new TextOnlyModel(Info.PlayerName, Font.Andrea72, Color.White, 345, 175), 23 | new TextOnlyModel($"ArcCode: {Info.PlayerCode}", Font.ExoLight28, Color.White, 370, 320), 24 | new TextOnlyModel($"Total Best 30: {B30data.Best30Avg}", Font.Andrea60, Color.White, 145, 480), 25 | new TextOnlyModel($"Recent Best 10: {B30data.Recent10Avg}", Font.Andrea60, Color.White, 1095, 480), 26 | new ImageModel(await Path.ArcaeaPartnerIcon(Info.Partner, Info.IsAwakened), 75, 130, 255), 27 | new PotentitalModel(Info.Potential, 175, 235)); 28 | 29 | var len = Math.Min(B30data.Best30List.Count, 30); 30 | for (var i = 0; i < len; ++i) 31 | { 32 | var record = B30data.Best30List[i]; 33 | int x = 75 + i % 2 * 950, y = 660 + i / 2 * 350; 34 | using var song = await record.GetSongImage(); 35 | 36 | bg.Draw( 37 | new PolygonModel(Color.White, new(x + 9, y), new(x + 891, y), new(x + 900, y + 9), new(x + 900, y + 291), new(x + 891, y + 300), 38 | new(x + 9, y + 300), new(x, y + 291), new(x, y + 9)), 39 | new PolygonModel(record.DifficultyInfo.Color, new(x + 278, y + 22), new(x + 278, y + 70), new(x + 503, y + 70), 40 | new(x + 458, y + 22)), new ImageModel(song, x + 22, y + 22, 256, 256), 41 | new TextOnlyModel($"[{record.Const:0.0}] {record.SongName(11)}", Font.Beatrice36, song.MainColor, x + 295, y + 80), 42 | new TextOnlyModel(record.Score, Font.Exo44, song.MainColor, x + 290, y + 145), 43 | new TextOnlyModel(record.Rating, Font.Exo26, System.Drawing.Color.White, x + 297, y + 24), 44 | new TextOnlyModel($"#{i + 1}", Font.Beatrice26, System.Drawing.Color.Black, x + 800, y + 24), 45 | new TextOnlyModel($"Pure: {record.Pure} (+{record.MaxPure})", Font.Beatrice20, System.Drawing.Color.FromArgb(105, 68, 100), 46 | x + 300, y + 235), 47 | new TextOnlyModel($"Far: {record.Far}", Font.Beatrice20, System.Drawing.Color.FromArgb(216, 157, 49), x + 570, y + 235), 48 | new TextOnlyModel($"Lost: {record.Lost}", Font.Beatrice20, System.Drawing.Color.FromArgb(159, 83, 109), x + 730, y + 235)); 49 | } 50 | 51 | return bg; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Andreal.Core/UI/ImageGenerator/ArcBest40ImageGenerator.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Message; 2 | using Andreal.Core.Model.Arcaea; 3 | using Andreal.Core.UI.Model; 4 | using Path = Andreal.Core.Common.Path; 5 | 6 | namespace Andreal.Core.UI.ImageGenerator; 7 | 8 | internal class ArcBest40ImageGenerator 9 | { 10 | internal ArcBest40ImageGenerator(IBest40Data b40data, PlayerInfo info) 11 | { 12 | B40data = b40data; 13 | Info = info; 14 | } 15 | 16 | private IBest40Data B40data { get; } 17 | private PlayerInfo Info { get; } 18 | 19 | internal async Task Generate() 20 | { 21 | var bg = new BackGround(Path.ArcaeaBest40Bg); 22 | bg.Draw(new TextOnlyModel(Info.PlayerName, Font.Andrea108, Color.White, 560, 190), 23 | new TextOnlyModel($"ArcCode: {Info.PlayerCode}", Font.ExoLight42, Color.White, 590, 405), 24 | new TextOnlyModel("Total Best 30:", Font.Andrea90, Color.White, 1593, 150), 25 | new TextOnlyModel("Recent Best 10:", Font.Andrea90, Color.White, 1595, 350), 26 | new TextOnlyModel(B40data.Best30Avg, Font.Andrea90, Color.White, 2373, 150), 27 | new TextOnlyModel(B40data.Recent10Avg, Font.Andrea90, Color.White, 2375, 350), 28 | new ImageModel(await Path.ArcaeaPartnerIcon(Info.Partner, Info.IsAwakened), 140, 120, 383), 29 | new PotentitalModel(Info.Potential, 305, 270, 300)); 30 | 31 | var len = Math.Min(B40data.Best30List.Count, 30); 32 | 33 | for (var i = 0; i < len; ++i) 34 | { 35 | var record = B40data.Best30List[i]; 36 | int x = 93 + i % 3 * 950, y = 590 + i / 3 * 350; 37 | 38 | using var song = await record.GetSongImage(); 39 | 40 | bg.Draw( 41 | new PolygonModel(Color.White, new(x + 9, y), new(x + 891, y), new(x + 900, y + 9), new(x + 900, y + 291), new(x + 891, y + 300), 42 | new(x + 9, y + 300), new(x, y + 291), new(x, y + 9)), 43 | new PolygonModel(record.DifficultyInfo.Color, new(x + 278, y + 22), new(x + 278, y + 70), new(x + 503, y + 70), 44 | new(x + 458, y + 22)), new ImageModel(song, x + 22, y + 22, 256, 256), 45 | new TextOnlyModel($"[{record.Const:0.0}] {record.SongName(11)}", Font.Beatrice36, song.MainColor, x + 295, y + 80), 46 | new TextOnlyModel(record.Score, Font.Exo44, song.MainColor, x + 290, y + 145), 47 | new TextOnlyModel(record.Rating, Font.Exo26, System.Drawing.Color.White, x + 297, y + 24), 48 | new TextOnlyModel($"#{i + 1}", Font.Beatrice26, System.Drawing.Color.Black, x + 800, y + 24), 49 | new TextOnlyModel($"Pure: {record.Pure} (+{record.MaxPure})", Font.Beatrice20, System.Drawing.Color.FromArgb(105, 68, 100), 50 | x + 300, y + 235), 51 | new TextOnlyModel($"Far: {record.Far}", Font.Beatrice20, System.Drawing.Color.FromArgb(216, 157, 49), x + 570, y + 235), 52 | new TextOnlyModel($"Lost: {record.Lost}", Font.Beatrice20, System.Drawing.Color.FromArgb(159, 83, 109), x + 730, y + 235)); 53 | } 54 | 55 | if (!(B40data.OverflowList?.Count > 0)) return bg; 56 | 57 | bg.Draw(new ImageModel(Path.ArcaeaDivider, 0, 4042, 2980)); 58 | 59 | var overLen = Math.Min(B40data.OverflowList.Count, 9) + 30; 60 | 61 | for (var i = 30; i < overLen; ++i) 62 | { 63 | var record = B40data.OverflowList[i - 30]; 64 | int x = 93 + i % 3 * 950, y = 625 + i / 3 * 350; 65 | 66 | using var song = await record.GetSongImage(); 67 | 68 | bg.Draw( 69 | new PolygonModel(Color.White, new(x + 9, y), new(x + 891, y), new(x + 900, y + 9), new(x + 900, y + 291), new(x + 891, y + 300), 70 | new(x + 9, y + 300), new(x, y + 291), new(x, y + 9)), 71 | new PolygonModel(record.DifficultyInfo.Color, new(x + 278, y + 22), new(x + 278, y + 70), new(x + 503, y + 70), 72 | new(x + 458, y + 22)), new ImageModel(song, x + 22, y + 22, 256, 256), 73 | new TextOnlyModel($"[{record.Const:0.0}] {record.SongName(11)}", Font.Beatrice36, song.MainColor, x + 295, y + 80), 74 | new TextOnlyModel(record.Score, Font.Exo44, song.MainColor, x + 290, y + 145), 75 | new TextOnlyModel(record.Rating, Font.Exo26, System.Drawing.Color.White, x + 297, y + 24), 76 | new TextOnlyModel($"#{i + 1}", Font.Beatrice26, System.Drawing.Color.Black, x + 800, y + 24), 77 | new TextOnlyModel($"Pure: {record.Pure} (+{record.MaxPure})", Font.Beatrice20, System.Drawing.Color.FromArgb(105, 68, 100), 78 | x + 300, y + 235), 79 | new TextOnlyModel($"Far: {record.Far}", Font.Beatrice20, System.Drawing.Color.FromArgb(216, 157, 49), x + 570, y + 235), 80 | new TextOnlyModel($"Lost: {record.Lost}", Font.Beatrice20, System.Drawing.Color.FromArgb(159, 83, 109), x + 730, y + 235)); 81 | } 82 | 83 | return bg; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Andreal.Core/UI/ImageGenerator/ArcRecord5ImageGenerator.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Message; 2 | using Andreal.Core.Model.Arcaea; 3 | using Andreal.Core.UI.Model; 4 | using Path = Andreal.Core.Common.Path; 5 | 6 | namespace Andreal.Core.UI.ImageGenerator; 7 | 8 | internal class ArcRecord5ImageGenerator 9 | { 10 | internal ArcRecord5ImageGenerator(IBest30Data b30data, bool isFloor) 11 | { 12 | B30data = b30data; 13 | IsFloor = isFloor; 14 | } 15 | 16 | private IBest30Data B30data { get; } 17 | private bool IsFloor { get; } 18 | 19 | internal async Task Generate() 20 | { 21 | var bg = new BackGround(Path.ArcaeaBest5Bg); 22 | 23 | bg.Draw(new TextWithStrokeModel(B30data.Best30Avg, Font.KazesawaLight48, Color.Black, 740, 90, Color.Black, 1), 24 | new TextWithStrokeModel(B30data.Recent10Avg, Font.KazesawaLight48, Color.Black, 740, 180, Color.Black, 1)); 25 | 26 | for (var i = 0; i < Math.Min(5, B30data.Best30List.Count); ++i) 27 | { 28 | var record = B30data.Best30List[i + (IsFloor ? Math.Max(0, B30data.Best30List.Count - 5) : 0)]; 29 | 30 | using var song = await record.GetSongImage(); 31 | 32 | bg.Draw(new ImageModel(song, 94, 304 + i * 340, 280, 280), 33 | new TextWithStrokeModel(record.SongName(18), Font.KazesawaLight72, song.MainColor, 420, 295 + i * 340, Color.Black, 1), 34 | new TextWithStrokeModel(record.Score, Font.KazesawaLight56, Color.ArcGray, 426, 403 + i * 340, Color.Black, 1), 35 | new TextWithStrokeModel($"{record.DifficultyInfo.LongStr} {record.Const:0.0}", Font.KazesawaLight56, record.DifficultyInfo.Color, 36 | 804, 403 + i * 340, Color.Black, 1), 37 | new TextWithStrokeModel("F", Font.KazesawaLight56, Color.ArcGray, 420, 493 + i * 340, Color.Black, 1), 38 | new TextWithStrokeModel("L", Font.KazesawaLight56, Color.ArcGray, 633, 493 + i * 340, Color.Black, 1), 39 | new TextWithStrokeModel("PTT", Font.KazesawaLight56, Color.ArcGray, 814, 493 + i * 340, Color.Black, 1), 40 | new TextWithStrokeModel(record.Far, Font.KazesawaLight56, Color.ArcGray, 470, 493 + i * 340, Color.Black, 1), 41 | new TextWithStrokeModel(record.Lost, Font.KazesawaLight56, Color.ArcGray, 683, 493 + i * 340, Color.Black, 1), 42 | new TextWithStrokeModel(record.Rating, Font.KazesawaLight56, Color.ArcGray, 964, 493 + i * 340, Color.Black, 1)); 43 | } 44 | 45 | return bg; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Andreal.Core/UI/ImageGenerator/ArcRecordImageGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using Andreal.Core.Message; 3 | using Andreal.Core.Model.Arcaea; 4 | using Andreal.Core.UI.Model; 5 | using Path = Andreal.Core.Common.Path; 6 | 7 | namespace Andreal.Core.UI.ImageGenerator; 8 | 9 | #pragma warning disable CA1416 10 | 11 | internal class ArcRecordImageGenerator 12 | { 13 | private static readonly string[] CleartypeString = { "[TL]", "[NC]", "[FR]", "[PM]", "[EC]", "[HC]" }; 14 | 15 | internal ArcRecordImageGenerator(PlayerInfo playerInfo, RecordInfo recordInfo) 16 | { 17 | PlayerInfo = playerInfo; 18 | RecordInfo = recordInfo; 19 | } 20 | 21 | private PlayerInfo PlayerInfo { get; } 22 | private RecordInfo RecordInfo { get; } 23 | 24 | internal async Task Version1() 25 | { 26 | var bg = await new ArcBackgroundGenerator(RecordInfo).ArcV1(); 27 | 28 | using var song = await RecordInfo.GetSongImage(); 29 | 30 | bg.Draw(new PartnerModel(PlayerInfo.Partner, PlayerInfo.IsAwakened, PlayerInfo.ImgVersion), new PotentitalModel(PlayerInfo.Potential, 87, 60), 31 | new ImageModel(Path.ArcaeaGlass, 810, 240, 630), 32 | new TextWithStrokeModel(PlayerInfo.PlayerName, Font.KazesawaLight40, Color.White, 275, 100), 33 | new TextWithStrokeModel("Code " + PlayerInfo.PlayerCode, Font.KazesawaLight32, Color.White, 275, 160), 34 | new ImageModel(Path.ArcaeaCleartypeV1(RecordInfo.Cleartype), -56, 224, 700), new ImageModel(song, 150, 430, 290), 35 | new ImageModel(Path.ArcaeaDifficultyForV1(RecordInfo.Difficulty), 333, 680, 150), 36 | new TextWithStrokeModel(RecordInfo.Const.ToString("0.0"), Font.Exo36, Color.White, 350, 677, Color.Black, 2, StringAlignment.Center), 37 | new TextWithStrokeModel(RecordInfo.SongName(23), Font.KazesawaRegular56, Color.White, 105, 253), 38 | new TextWithStrokeModel(RecordInfo.Score, Font.Exo64, RecordInfo.Cleartype == 3 ? Color.PmColor : Color.White, 515, 370), 39 | new TextWithStrokeModel(RecordInfo.Pure + $" (+{RecordInfo.MaxPure})", Font.Exo40, Color.White, 638, 488), 40 | new TextWithStrokeModel(RecordInfo.Far, Font.Exo40, Color.White, 638, 553), 41 | new TextWithStrokeModel(RecordInfo.Lost, Font.Exo40, Color.White, 638, 618), 42 | new TextWithStrokeModel(RecordInfo.Rating, Font.Exo40, Color.White, 638, 683), 43 | new TextWithStrokeModel("Played at " + RecordInfo.TimeStr, Font.Exo40, Color.White, 368, 758)); 44 | return bg; 45 | } 46 | 47 | internal async Task Version2() 48 | { 49 | var bg = await new ArcBackgroundGenerator(RecordInfo).ArcV2(); 50 | bg.Draw(new TextWithShadowModel($"{RecordInfo.DifficultyInfo.LongStr} {RecordInfo.Const:0.0}", Font.Andrea36, 514, 850), 51 | new PartnerModel(PlayerInfo.Partner, PlayerInfo.IsAwakened, PlayerInfo.ImgVersion), new PotentitalModel(PlayerInfo.Potential, 79, 38), 52 | new TextWithShadowModel(RecordInfo.IsRecent ? "Recent" : "Best", Font.Exo44, 120, 260), 53 | new TextWithShadowModel(RecordInfo.Score, Font.Exo36, 398, 270), 54 | new TextWithShadowModel($"{RecordInfo.Rate}{CleartypeString[RecordInfo.Cleartype]}", Font.Exo36, 730, 270), 55 | new TextWithShadowModel(RecordInfo.Rating, Font.Exo36, 398, 354), new TextWithShadowModel(RecordInfo.Pure, Font.Exo32, 240, 455), 56 | new TextWithShadowModel($"(+{RecordInfo.MaxPure})", Font.Exo32, 415, 455), 57 | new TextWithShadowModel(RecordInfo.Far, Font.Exo32, 240, 525), new TextWithShadowModel(RecordInfo.Lost, Font.Exo32, 560, 525), 58 | new TextWithShadowModel(RecordInfo.TimeStr, Font.Exo32, 350, 595), 59 | new TextWithShadowModel(PlayerInfo.PlayerName, Font.Andrea56, 290, 60), 60 | new TextWithShadowModel($"ArcCode: {PlayerInfo.PlayerCode}", Font.Andrea28, 297, 150)); 61 | return bg; 62 | } 63 | 64 | internal async Task Version3() 65 | { 66 | var bg = await new ArcBackgroundGenerator(RecordInfo).ArcV3(); 67 | bg.Draw(new ImageModel(Path.ArcaeaCleartypeV3(RecordInfo.Cleartype), 185, 1035, 630), 68 | new ImageModel(await Path.ArcaeaPartnerIcon(PlayerInfo.Partner, PlayerInfo.IsAwakened), 150, 160, 160), 69 | new PotentitalModel(PlayerInfo.Potential, 215, 215, 140), 70 | new TextOnlyModel(PlayerInfo.PlayerName, Font.Andrea36, Color.Black, 340, 200), 71 | new TextOnlyModel($"ArcCode: {PlayerInfo.PlayerCode}", Font.Andrea20, Color.GnaqGray, 340, 270), 72 | new TextOnlyModel($"{RecordInfo.DifficultyInfo.LongStr} | {RecordInfo.Const:0.0}", Font.Beatrice24, RecordInfo.DifficultyInfo.Color, 73 | 500, 925, StringAlignment.Center), 74 | new TextOnlyModel($"{RecordInfo.Score} {RecordInfo.Rate}", Font.Exo44, Color.Black, 500, 1130, StringAlignment.Center), 75 | new TextOnlyModel(RecordInfo.Rating, Font.Exo20, Color.GnaqGray, 260, 1280), 76 | new TextOnlyModel(RecordInfo.TimeStr, Font.Exo20, Color.GnaqGray, 260, 1360), 77 | new TextOnlyModel(RecordInfo.Pure + $" (+{RecordInfo.MaxPure})", Font.Exo20, Color.Black, 730, 1265), 78 | new TextOnlyModel(RecordInfo.Far, Font.Exo20, Color.Black, 730, 1320), 79 | new TextOnlyModel(RecordInfo.Lost, Font.Exo20, Color.Black, 730, 1375)); 80 | return bg; 81 | } 82 | 83 | internal async Task Version4(IBest30Data b30data) 84 | { 85 | var bg = new BackGround(Path.ArcaeaBg4(RecordInfo.Difficulty, RecordInfo.SongInfo.Set)); 86 | bg.Draw(new PotentitalModel(PlayerInfo.Potential, 80, 691, 100), 87 | new PartnerModel(PlayerInfo.Partner, PlayerInfo.IsAwakened, PlayerInfo.ImgVersion), 88 | new ImageModel(Path.ArcaeaCleartypeV4(RecordInfo.Cleartype), 116, 834, newHeight: 35), 89 | new ImageModel(await Path.ArcaeaPartnerIcon(15, false), 89, 384, 83), 90 | new ImageModel(await Path.ArcaeaPartnerIcon(7, false), 189, 384, 83), 91 | new TextOnlyModel($"Total Best 30 Avg: {b30data.Best30Avg}", Font.ExoLight20, Color.AzusaGray, 195, 792), 92 | new TextOnlyModel($"Recent Top 10 Avg: {b30data.Recent10Avg}", Font.ExoLight20, Color.AzusaGray, 195, 832), 93 | new TextOnlyModel($"Time: {RecordInfo.TimeStr}", Font.ExoLight20, Color.AzusaGray, 195, 872), 94 | new TextOnlyModel($"ArcCode: {PlayerInfo.PlayerCode}", Font.ExoLight20, Color.Black, 195, 748), 95 | new TextOnlyModel(PlayerInfo.PlayerName, Font.Andrea36, Color.Black, 195, 685), 96 | new TextOnlyModel($"{RecordInfo.SongName(30)} {RecordInfo.SongInfo.ConstString}", Font.Andrea36, Color.Black, 100, 470), 97 | new TextOnlyModel(RecordInfo.Score, Font.ExoLight36, Color.Black, 100, 535), 98 | new TextOnlyModel($"Pure: {RecordInfo.Pure + $"(+{RecordInfo.MaxPure})"}", Font.ExoLight24, Color.Black, 100, 600), 99 | new TextOnlyModel($"Far: {RecordInfo.Far} Lost: {RecordInfo.Lost} [{RecordInfo.Rating}]", Font.ExoLight24, Color.Black, 100, 645)); 100 | return bg; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Andreal.Core/UI/ImageGenerator/ArcSongLevelListImageGenerator.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Message; 2 | using Andreal.Core.Model.Arcaea; 3 | using Andreal.Core.UI.Model; 4 | using Path = Andreal.Core.Common.Path; 5 | 6 | namespace Andreal.Core.UI.ImageGenerator; 7 | 8 | #pragma warning disable CA1416 9 | 10 | internal class ArcSongLevelListImageGenerator 11 | { 12 | private static readonly System.Drawing.Color WhiteBackground = Color.FromArgb(180, 245, 245, 245); 13 | 14 | internal ArcSongLevelListImageGenerator(ArcaeaChart[] list) 15 | { 16 | List = list; 17 | } 18 | 19 | private ArcaeaChart[] List { get; } 20 | 21 | internal async Task Generate() 22 | { 23 | var height = 160 + 200 * List.Length; 24 | 25 | var bg = new BackGround(1080, height); 26 | bg.Draw(new ImageModel(Path.ArcaeaConstListBg, 0, 0, 1080, height), new RectangleModel(WhiteBackground, new(40, 40, 1000, height - 80))); 27 | 28 | for (var i = 0; i < List.Length; ++i) 29 | { 30 | var y = 110 + i * 200; 31 | 32 | var info = List[i]; 33 | 34 | using var song = await info.GetSongImage(); 35 | 36 | var color = song.MainColor; 37 | 38 | bg.Draw(new RectangleModel(Color.GetBySide(info.Side), new(212, y + 6, 140, 140)), 39 | new ImageModel(Path.ArcaeaDivider, 40, 64 + i * 200, 1000, 32), new ImageModel(song, 206, y, 140, 140), 40 | new TextOnlyModel(info.GetSongName(22), Font.KazesawaRegular27, color, 380, y - 5), 41 | new TextOnlyModel($"{info.DifficultyInfo.LongStr} {info.Const:0.0} (+{info.Note})", Font.Exo20, info.DifficultyInfo.Color, 390, 42 | y + 60), new TextOnlyModel(info.SetFriendly, Font.Beatrice20, System.Drawing.Color.Black, 390, y + 100)); 43 | } 44 | 45 | bg.Draw(new ImageModel(Path.ArcaeaDivider, 40, 64 + List.Length * 200, 1000, 32)); 46 | 47 | return bg; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/IGraphicsModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace Andreal.Core.UI.Model; 4 | 5 | internal interface IGraphicsModel 6 | { 7 | internal void Draw(Graphics g); 8 | } 9 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/ImageModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using Path = Andreal.Core.Common.Path; 3 | 4 | namespace Andreal.Core.UI.Model; 5 | 6 | #pragma warning disable CA1416 7 | 8 | internal class ImageModel : IGraphicsModel 9 | { 10 | private readonly Image? _image; 11 | private readonly int? _newWidth, _newHeight; 12 | private readonly Path? _path; 13 | private readonly int _posX, _posY; 14 | 15 | internal ImageModel( 16 | Image image, 17 | int posX, 18 | int posY, 19 | int? newWidth = null, 20 | int? newHeight = null) 21 | { 22 | _posX = posX; 23 | _posY = posY; 24 | _path = null; 25 | _image = image; 26 | _newWidth = newWidth; 27 | _newHeight = newHeight; 28 | } 29 | 30 | internal ImageModel( 31 | Path path, 32 | int posX, 33 | int posY, 34 | int? newWidth = null, 35 | int? newHeight = null) 36 | { 37 | _posX = posX; 38 | _posY = posY; 39 | _path = path; 40 | _image = null; 41 | _newWidth = newWidth; 42 | _newHeight = newHeight; 43 | } 44 | 45 | void IGraphicsModel.Draw(Graphics g) 46 | { 47 | var image = _image ?? new Image(_path!); 48 | 49 | if (_newWidth == null && _newHeight == null) 50 | { 51 | Image.ImageExtend.DrawImage(g, image, _posX, _posY); 52 | } 53 | else 54 | { 55 | var newWidth = _newWidth ?? _newHeight * image.Width / image.Height ?? image.Width; 56 | var newHeight = _newHeight ?? _newWidth * image.Height / image.Width ?? image.Height; 57 | Image.ImageExtend.DrawImage(g, image, _posX, _posY, newWidth, newHeight); 58 | } 59 | 60 | image.Dispose(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/LineModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace Andreal.Core.UI.Model; 4 | 5 | #pragma warning disable CA1416 6 | 7 | internal class LineModel : IGraphicsModel 8 | { 9 | private readonly System.Drawing.Color _color; 10 | private readonly Point _end; 11 | private readonly int _penwidth; 12 | private readonly Point _start; 13 | 14 | internal LineModel( 15 | System.Drawing.Color color, 16 | int penwidth, 17 | Point start, 18 | Point end) 19 | { 20 | _color = color; 21 | _penwidth = penwidth; 22 | _start = start; 23 | _end = end; 24 | } 25 | 26 | void IGraphicsModel.Draw(Graphics g) => g.DrawLine(new(_color, _penwidth), _start, _end); 27 | } 28 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/PartnerModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using Andreal.Core.Data.Json.Arcaea.PartnerPosInfoBase; 3 | using Andreal.Core.Data.Sqlite; 4 | using Path = Andreal.Core.Common.Path; 5 | 6 | namespace Andreal.Core.UI.Model; 7 | 8 | #pragma warning disable CA1416 9 | 10 | internal class PartnerModel : IGraphicsModel 11 | { 12 | private readonly ImageModel _imageModel; 13 | 14 | internal PartnerModel(int partner, bool awakened, ImgVersion imgVersion) 15 | { 16 | var location = PartnerPosInfoBase.Get($"{partner}{(awakened ? "u" : "")}", imgVersion)!; 17 | _imageModel = new(Path.ArcaeaPartner(partner, awakened).Result, location.PositionX, location.PositionY, location.Size, location.Size); 18 | } 19 | 20 | void IGraphicsModel.Draw(Graphics g) => (_imageModel as IGraphicsModel).Draw(g); 21 | } 22 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/PolygonModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace Andreal.Core.UI.Model; 4 | 5 | #pragma warning disable CA1416 6 | 7 | internal class PolygonModel : IGraphicsModel 8 | { 9 | private readonly System.Drawing.Color _color; 10 | private readonly Point[] _region; 11 | 12 | internal PolygonModel(System.Drawing.Color color, params Point[] region) 13 | { 14 | _color = color; 15 | _region = region; 16 | } 17 | 18 | void IGraphicsModel.Draw(Graphics g) 19 | { 20 | using var brash = new SolidBrush(_color); 21 | g.FillPolygon(brash, _region); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/PotentitalModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Drawing.Drawing2D; 3 | using Path = Andreal.Core.Common.Path; 4 | 5 | namespace Andreal.Core.UI.Model; 6 | 7 | #pragma warning disable CA1416 8 | 9 | internal class PotentitalModel : IGraphicsModel 10 | { 11 | private readonly int _posX, _posY, _size; 12 | private readonly short _potential; 13 | 14 | internal PotentitalModel( 15 | short potential, 16 | int positionX, 17 | int positionY, 18 | int size = 200) 19 | { 20 | _potential = potential; 21 | _posX = positionX; 22 | _posY = positionY; 23 | _size = size; 24 | } 25 | 26 | void IGraphicsModel.Draw(Graphics g) 27 | { 28 | using var temp = new BackGround(200, 200); 29 | using var gr = temp.GraphicsFromBackGround(); 30 | temp.Draw(new ImageModel(Path.ArcaeaRating(_potential), 21, 21, 158)); 31 | if (_potential < 0) 32 | { 33 | temp.Draw(new TextWithStrokeModel("--", Font.Exo64, Color.White, 65, 52, Color.ArcPurple, 8)); 34 | } 35 | else 36 | { 37 | var ptt = Convert.ToString(_potential + 10000, 10).ToCharArray(); 38 | var lx = 5 + 39 | (ptt[1] == '0' ? 0 : 40 | ptt[1] == '1' ? 23 : 37) + 41 | (ptt[2] == '1' ? 23 : 42 | ptt[2] == '7' ? 25 : 37); 43 | var rx = 20 + (ptt[3] == '1' ? 17 : 30) + (ptt[4] == '1' ? 17 : 30); 44 | var posx = 98 - (lx + rx) / 2; 45 | string l = ptt[1] == '0' ? $"{ptt[2]}" : $"{ptt[1]}{ptt[2]}", r = $".{ptt[3]}{ptt[4]}"; 46 | using var s = new StringFormat { Alignment = StringAlignment.Near }; 47 | using var path = new GraphicsPath(); 48 | using var pen = new Pen(_potential < 1300 ? Color.ArcPurple : Color.ArcRed, 8); 49 | path.AddString(l, Font.Exo60.FontFamily, 0, 60, new Rectangle(posx, 56, 150, 70), s); 50 | path.AddString(r, Font.Exo44.FontFamily, 0, 44, new Rectangle(posx + lx, 73, 150, 53), s); 51 | gr.DrawPath(pen, path); 52 | gr.FillPath(Brushes.White, path); 53 | } 54 | 55 | Image.ImageExtend.DrawImage(g, temp, _posX, _posY, _size, _size); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/RectangleModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace Andreal.Core.UI.Model; 4 | 5 | #pragma warning disable CA1416 6 | 7 | internal class RectangleModel : IGraphicsModel 8 | { 9 | private readonly System.Drawing.Color _color; 10 | private readonly Rectangle _rect; 11 | 12 | internal RectangleModel(System.Drawing.Color color, Rectangle rect) 13 | { 14 | _color = color; 15 | _rect = rect; 16 | } 17 | 18 | void IGraphicsModel.Draw(Graphics g) 19 | { 20 | using var brash = new SolidBrush(_color); 21 | g.FillRectangle(brash, _rect); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/TextOnlyModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace Andreal.Core.UI.Model; 4 | 5 | #pragma warning disable CA1416 6 | 7 | internal class TextOnlyModel : IGraphicsModel 8 | { 9 | private readonly System.Drawing.Color _color; 10 | private readonly System.Drawing.Font _font; 11 | private readonly int _posX, _posY; 12 | private readonly StringAlignment _stringAlignment; 13 | private readonly string _text; 14 | 15 | internal TextOnlyModel( 16 | string text, 17 | System.Drawing.Font font, 18 | System.Drawing.Color color, 19 | int posX, 20 | int posY, 21 | StringAlignment stringAlignment = StringAlignment.Near) 22 | { 23 | _text = text; 24 | _color = color; 25 | _font = font; 26 | _posX = posX; 27 | _posY = posY; 28 | _stringAlignment = stringAlignment; 29 | } 30 | 31 | void IGraphicsModel.Draw(Graphics g) 32 | { 33 | using var sf = new StringFormat { Alignment = _stringAlignment }; 34 | using var brush = new SolidBrush(_color); 35 | g.DrawString(_text, _font, brush, _posX, _posY, sf); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/TextWithShadowModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace Andreal.Core.UI.Model; 4 | 5 | #pragma warning disable CA1416 6 | 7 | internal class TextWithShadowModel : IGraphicsModel 8 | { 9 | private static readonly int[] Alpha = { 85, 42, 28, 21, 17 }; 10 | private readonly System.Drawing.Font _font; 11 | private readonly int _posX, _posY; 12 | private readonly string _text; 13 | 14 | internal TextWithShadowModel( 15 | string text, 16 | System.Drawing.Font font, 17 | int posX, 18 | int posY) 19 | { 20 | _text = text; 21 | _font = font; 22 | _posX = posX; 23 | _posY = posY; 24 | } 25 | 26 | void IGraphicsModel.Draw(Graphics g) 27 | { 28 | for (var i = 0; i < 5; ++i) 29 | { 30 | using Brush brush = new SolidBrush(Color.FromArgb(Alpha[i], Color.ArcPurple)); 31 | for (var j = 0; j < 5; ++j) 32 | { 33 | g.DrawString(_text, _font, brush, _posX + i, _posY + j); 34 | g.DrawString(_text, _font, brush, _posX - i, _posY - j); 35 | } 36 | 37 | g.DrawString(_text, _font, brush, _posX - i, _posY + i); 38 | g.DrawString(_text, _font, brush, _posX + i, _posY - i); 39 | } 40 | 41 | g.DrawString(_text, _font, Brushes.White, _posX, _posY); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Andreal.Core/UI/Model/TextWithStrokeModel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Drawing.Drawing2D; 3 | 4 | namespace Andreal.Core.UI.Model; 5 | 6 | #pragma warning disable CA1416 7 | 8 | internal class TextWithStrokeModel : IGraphicsModel 9 | { 10 | private static readonly System.Drawing.Color Defcolor = System.Drawing.Color.FromArgb(82, 82, 82); 11 | private readonly System.Drawing.Color _color; 12 | private readonly System.Drawing.Font _font; 13 | private readonly System.Drawing.Color? _penColor; 14 | private readonly int _posX, _posY, _penWidth; 15 | private readonly StringAlignment _stringAlignment; 16 | private readonly string _text; 17 | private readonly int? _width, _height; 18 | 19 | internal TextWithStrokeModel( 20 | string text, 21 | System.Drawing.Font font, 22 | System.Drawing.Color color, 23 | int posX, 24 | int posY, 25 | System.Drawing.Color? penColor = null, 26 | int penWidth = 3, 27 | StringAlignment stringAlignment = StringAlignment.Near, 28 | int? width = null, 29 | int? height = null) 30 | { 31 | _text = text; 32 | _color = color; 33 | _font = font; 34 | _posX = posX; 35 | _posY = posY; 36 | _penColor = penColor; 37 | _penWidth = penWidth; 38 | _stringAlignment = stringAlignment; 39 | _width = width; 40 | _height = height; 41 | } 42 | 43 | void IGraphicsModel.Draw(Graphics g) 44 | { 45 | var origin = new Point(_posX, _posY); 46 | using var s = new StringFormat { Alignment = _stringAlignment }; 47 | using var path = new GraphicsPath(); 48 | using var brush = new SolidBrush(_color); 49 | using var pen = new Pen(_penColor ?? Defcolor, _penWidth); 50 | path.AddString(_text, _font.FontFamily, (int)_font.Style, _font.SizeInPoints, 51 | _width == null || _height == null 52 | ? new(origin, g.MeasureString(_text + "?", _font, origin, s)) 53 | : new RectangleF(_posX, _posY, (float)_width, (float)_width), s); 54 | g.DrawPath(pen, path); 55 | g.FillPath(brush, path); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/AbbreviationHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Andreal.Core.Utils; 4 | 5 | internal static class AbbreviationHelper 6 | { 7 | internal static string GetAbbreviation(this string str) 8 | { 9 | var sb = new StringBuilder(); 10 | sb.Append(str[0]); 11 | 12 | for (var index = 0; index < str.Length - 1; ++index) 13 | if (str[index] == ' ') 14 | sb.Append(str[index + 1]); 15 | 16 | return sb.ToString(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/ArcaeaHelper.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Common; 2 | using Andreal.Core.Model.Arcaea; 3 | 4 | namespace Andreal.Core.Utils; 5 | 6 | internal static class ArcaeaHelper 7 | { 8 | internal static (double, double) ConvertToArcaeaRange(this string rawdata) 9 | => rawdata switch 10 | { 11 | "11" => (11.0, 11.6), 12 | "10+" => (10.7, 10.9), 13 | "10" => (10.0, 10.6), 14 | "9+" => (9.7, 9.9), 15 | "9" => (9.0, 9.6), 16 | "8" => (8.0, 8.9), 17 | "7" => (7.0, 7.9), 18 | "6" => (6.0, 6.9), 19 | "5" => (5.0, 5.9), 20 | "4" => (4.0, 4.9), 21 | "3" => (3.0, 3.9), 22 | "2" => (2.0, 2.9), 23 | "1" => (1.0, 1.9), 24 | _ => double.TryParse(rawdata, out var value) ? (value, value) : (-1, -1) 25 | }; 26 | 27 | internal static bool SongInfoParser( 28 | IEnumerable command, 29 | out ArcaeaSong song, 30 | out int dif, 31 | out string errMessage) 32 | { 33 | song = null!; 34 | dif = -1; 35 | errMessage = ""; 36 | 37 | var enumerable = command.ToArray(); 38 | 39 | if (enumerable.Length == 0) return false; 40 | 41 | (var songstr, dif) = DifficultyInfo.DifficultyConverter(enumerable[^1]); 42 | 43 | songstr = string.Join("", enumerable, 0, enumerable.Length - 1) + songstr; 44 | 45 | List? result = ArcaeaCharts.Query(songstr); 46 | 47 | if (result == null || result.Count == 0) 48 | { 49 | errMessage = MessageInfo.RobotReply.NoSongFound; 50 | return false; 51 | } 52 | 53 | if (result.Count > 1) 54 | { 55 | errMessage = result.Aggregate(MessageInfo.RobotReply.TooManySongFound, (cur, i) => cur + "\n" + i[0].NameEn); 56 | return false; 57 | } 58 | 59 | song = result[0]; 60 | 61 | if (song.SongID == "lasteternity") dif = 3; 62 | 63 | return true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/BotStatementHelper.cs: -------------------------------------------------------------------------------- 1 | using Andreal.Core.Message; 2 | 3 | namespace Andreal.Core.Utils; 4 | 5 | public static class BotStatementHelper 6 | { 7 | private static readonly DateTime Time = DateTime.Now; 8 | 9 | internal static ulong ProcessCount = 0, ExceptionCount = 0, WebExceptionCount = 0; 10 | 11 | public static ulong GroupMessageCount = 0, PrivateMessageCount = 0; 12 | 13 | internal static TextMessage Statement 14 | => $"状态简要:\n已运行: {DateTime.Now - Time:d\\.hh\\:mm\\:ss} day(s)\n群聊消息: {GroupMessageCount}\n私聊消息: {PrivateMessageCount}\n发送消息: {ProcessCount}\n记录异常: {ExceptionCount} + [Web] {WebExceptionCount}\n最近异常: \n{LastExceptionHelper.Get()}"; 15 | 16 | public static string Status 17 | => $"运行时间: {DateTime.Now - Time:d\\.hh\\:mm\\:ss} day(s) 群聊消息: {GroupMessageCount} 私聊消息: {PrivateMessageCount} 发送消息: {ProcessCount} 记录异常: {ExceptionCount} + [Web] {WebExceptionCount}"; 18 | } 19 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/ConcurrentDictionaryExtend.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Andreal.Core.Utils; 5 | 6 | internal static class ConcurrentDictionaryExtend 7 | { 8 | public static bool TryAddOrInsert(this ConcurrentDictionary dict, TKey key, TValue value) 9 | where TList : List, new() where TKey : notnull 10 | { 11 | if (!dict.ContainsKey(key)) return dict.TryAdd(key, new() { value }); 12 | 13 | dict[key].Add(value); 14 | return true; 15 | } 16 | 17 | public static void ForAllItems(this ConcurrentDictionary dict, Action action) 18 | where TList : List, new() where TKey : notnull 19 | { 20 | foreach (var (key, values) in dict) 21 | { 22 | foreach (var value in values) action(key, value); 23 | } 24 | } 25 | 26 | public static void ForAllItems( 27 | this ConcurrentDictionary dict, 28 | Action keyaction, 29 | Action kvaction) where TList : List, new() where TKey : notnull 30 | { 31 | foreach (var (key, values) in dict) 32 | { 33 | keyaction(key); 34 | foreach (var value in values) kvaction(key, value); 35 | } 36 | } 37 | 38 | public static bool TryTakeKey( 39 | this ConcurrentDictionary dict, 40 | Func action, 41 | [MaybeNullWhen(false)] out TKey result) where TList : List, new() where TKey : notnull 42 | { 43 | foreach (var (key, values) in dict) 44 | { 45 | // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator 46 | foreach (var value in values) 47 | { 48 | if (action(value)) 49 | { 50 | result = key; 51 | return true; 52 | } 53 | } 54 | } 55 | 56 | result = default; 57 | return false; 58 | } 59 | 60 | public static bool TryTakeValues( 61 | this ConcurrentDictionary dict, 62 | Func action, 63 | [MaybeNullWhen(false)] out TList result) where TList : List, new() where TKey : notnull 64 | { 65 | // ReSharper disable once LoopCanBePartlyConvertedToQuery 66 | foreach (var (_, values) in dict) 67 | { 68 | // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator 69 | foreach (var value in values) 70 | { 71 | if (action(value)) 72 | { 73 | result = values; 74 | return true; 75 | } 76 | } 77 | } 78 | 79 | result = default; 80 | return false; 81 | } 82 | 83 | public static bool TryTakeValue( 84 | this ConcurrentDictionary dict, 85 | Func action, 86 | [MaybeNullWhen(false)] out TValue result) where TList : List, new() where TKey : notnull 87 | { 88 | // ReSharper disable once LoopCanBePartlyConvertedToQuery 89 | foreach (var (_, values) in dict) 90 | { 91 | // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator 92 | foreach (var value in values) 93 | { 94 | if (action(value)) 95 | { 96 | result = value; 97 | return true; 98 | } 99 | } 100 | } 101 | 102 | result = default; 103 | return false; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/DateTimeHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Utils; 2 | 3 | internal static class DateTimeHelper 4 | { 5 | internal static string DateStringFromNow(this long unixTime) 6 | { 7 | var span = DateTime.UtcNow - DateTime.UnixEpoch.AddSeconds(unixTime); 8 | 9 | return span.TotalMinutes switch 10 | { 11 | > 1 => $"{(int)Math.Floor(span.TotalMinutes)}分钟前", 12 | _ => $"{(int)Math.Ceiling(span.TotalSeconds)}秒前" 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/LastExceptionHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Utils; 2 | 3 | internal static class LastExceptionHelper 4 | { 5 | private static (DateTime Time, Exception Exception) _lastException = (DateTime.Now, new ArgumentNullException()); 6 | 7 | internal static string GetDetails() => _lastException.Exception.ToString(); 8 | 9 | internal static string Get() => $"{_lastException.Item1}\n {_lastException.Exception.GetType().FullName}"; 10 | 11 | internal static void Set(Exception ex) => _lastException = (DateTime.Now, ex); 12 | } 13 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/RandStringHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Andreal.Core.Utils; 4 | 5 | internal static class RandStringHelper 6 | { 7 | private const string Chars = "0123456789abcdefghijklmnopqrstuvwxyz"; 8 | private static readonly Random Random = new(); 9 | 10 | public static string GetRandString(int length = 10) 11 | { 12 | var res = new StringBuilder(); 13 | for (var i = 0; i < length; i++) res.Append(Chars[Random.Next(36)]); 14 | return res.ToString(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/RandomHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Core.Utils; 2 | 3 | internal static class RandomHelper 4 | { 5 | private static readonly Random Random = new(); 6 | 7 | internal static T GetRandomItem(this T[] ls) => ls[Random.Next(ls.Length)]; 8 | 9 | internal static T GetRandomItem(this IEnumerable ls) 10 | { 11 | T[] enumerable = ls as T[] ?? ls.ToArray(); 12 | return GetRandomItem(enumerable); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/SqliteHelper.cs: -------------------------------------------------------------------------------- 1 | using SQLite; 2 | using Path = Andreal.Core.Common.Path; 3 | 4 | namespace Andreal.Core.Utils; 5 | 6 | internal static class SqliteHelper 7 | { 8 | private static readonly SQLiteConnection DbConnection; 9 | 10 | static SqliteHelper() 11 | { 12 | DbConnection = new(Path.Database, 13 | SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache | SQLiteOpenFlags.FullMutex); 14 | 15 | DbConnection.Execute("CREATE TABLE IF NOT EXISTS [BotUserInfo]([QQId] INTEGER PRIMARY KEY NOT NULL, [ArcId] INTEGER DEFAULT(-1), [IsHide] INTEGER DEFAULT(0), [IsText] INTEGER DEFAULT(0), [ImgVer] INTEGER DEFAULT(0));"); 16 | } 17 | 18 | internal static IEnumerable SelectAll() where T : new() => DbConnection.Table(); 19 | 20 | internal static void Insert(T obj) where T : new() => DbConnection.Insert(obj); 21 | 22 | internal static void Update(T obj) where T : new() => DbConnection.Update(obj); 23 | } 24 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/StringHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Andreal.Core.Utils; 4 | 5 | internal static class StringHelper 6 | { 7 | private static readonly Regex Reg = new(@"\s|\(|\)|(|)", RegexOptions.Compiled | RegexOptions.CultureInvariant); 8 | 9 | internal static bool Contains(string? raw, string? seed) 10 | => seed != null && raw != null && Reg.Replace(raw, "").Contains(Reg.Replace(seed, ""), StringComparison.OrdinalIgnoreCase); 11 | 12 | internal static bool Equals(string? raw, string? seed) 13 | => seed != null && raw != null && string.Equals(Reg.Replace(raw, ""), Reg.Replace(seed, ""), StringComparison.OrdinalIgnoreCase); 14 | } 15 | -------------------------------------------------------------------------------- /Andreal.Core/Utils/SystemHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Runtime.CompilerServices; 3 | using Andreal.Core.Common; 4 | using Andreal.Core.Data.Api; 5 | 6 | [assembly: InternalsVisibleTo("Andreal.Test")] 7 | 8 | namespace Andreal.Core.Utils; 9 | 10 | internal static class SystemHelper 11 | { 12 | public static void Init(AndrealConfig andrealConfig) 13 | { 14 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; 15 | ServicePointManager.ServerCertificateValidationCallback = ( 16 | _, 17 | _, 18 | _, 19 | _) => true; 20 | ServicePointManager.DefaultConnectionLimit = 512; 21 | ServicePointManager.Expect100Continue = false; 22 | ServicePointManager.UseNagleAlgorithm = false; 23 | ServicePointManager.ReusePort = true; 24 | ServicePointManager.CheckCertificateRevocationList = true; 25 | WebRequest.DefaultWebProxy = null; 26 | 27 | UnofficialArcaeaAPI.Init(andrealConfig); 28 | MessageInfo.Init(andrealConfig.Master); 29 | BackgroundTask.Init(); 30 | ConfigWatcher.Init(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Andreal.Test/AIRandomSongTest.cs: -------------------------------------------------------------------------------- 1 | namespace Andreal.Test; 2 | 3 | public class Tests 4 | { 5 | [SetUp] 6 | public void Setup() 7 | { 8 | var config = JsonConvert.DeserializeObject(File.ReadAllText(Core.Common.Path.Config))!; 9 | External.Initialize(config); 10 | } 11 | 12 | [Test] 13 | public void AIRandomSongTest() 14 | { 15 | var robotReply = GlobalConfig.RobotReply; 16 | var info = ArcaeaCharts.RandomSong(0, 15)!; 17 | var format = $"Ai酱:{robotReply.GetRandomAIReply(info.NameWithPackageAndConst, info.Artist)}"; 18 | Console.WriteLine(format); 19 | Assert.Pass(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Andreal.Test/Andreal.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Andreal.Test/Usings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; 2 | global using Andreal.Core.Common; 3 | global using Andreal.Core.Model.Arcaea; 4 | global using Newtonsoft.Json; 5 | -------------------------------------------------------------------------------- /Andreal.Window/Andreal.Window.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0-windows 6 | enable 7 | true 8 | true 9 | true 10 | true 11 | embedded 12 | true 13 | None 14 | win-x64 15 | true 16 | Assets\favicon.ico 17 | Andreal 18 | zh-Hans 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | MSBuild:Compile 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Andreal.Window/Assets/Fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/Fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /Andreal.Window/Assets/chevron-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/chevron-down.png -------------------------------------------------------------------------------- /Andreal.Window/Assets/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/close.png -------------------------------------------------------------------------------- /Andreal.Window/Assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/favicon.ico -------------------------------------------------------------------------------- /Andreal.Window/Assets/img_contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/img_contact.png -------------------------------------------------------------------------------- /Andreal.Window/Assets/img_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/img_map.png -------------------------------------------------------------------------------- /Andreal.Window/Assets/img_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/img_message.png -------------------------------------------------------------------------------- /Andreal.Window/Assets/img_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/img_setting.png -------------------------------------------------------------------------------- /Andreal.Window/Assets/min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Awbugl/Andreal/b053a189a8f9960c34faf8ef0375078e9580af19/Andreal.Window/Assets/min.png -------------------------------------------------------------------------------- /Andreal.Window/Common/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] 4 | -------------------------------------------------------------------------------- /Andreal.Window/Common/DelegateCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Input; 3 | 4 | namespace Andreal.Window.Common; 5 | 6 | public class DelegateCommand : ICommand 7 | { 8 | public Action? CommandAction { set; get; } 9 | public Func? CanExecuteFunc { get; set; } 10 | 11 | public void Execute(object? parameter) => CommandAction?.Invoke(parameter!); 12 | 13 | public bool CanExecute(object? parameter) => CanExecuteFunc?.Invoke(parameter) ?? false; 14 | 15 | public event EventHandler? CanExecuteChanged 16 | { 17 | add => CommandManager.RequerySuggested += value; 18 | remove => CommandManager.RequerySuggested -= value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Andreal.Window/Common/EnumDescriptionConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Windows.Data; 6 | 7 | namespace Andreal.Window.Common; 8 | 9 | internal class EnumDescriptionConverter : IValueConverter 10 | { 11 | private string GetEnumDescription(Enum enumObj) 12 | { 13 | var fieldInfo = enumObj.GetType().GetField(enumObj.ToString()); 14 | var attribArray = fieldInfo?.GetCustomAttributes(false); 15 | if (attribArray?.Any() != true) return enumObj.ToString(); 16 | 17 | DescriptionAttribute? attrib = null; 18 | 19 | foreach (var att in attribArray) 20 | { 21 | if (att is DescriptionAttribute attribute) 22 | attrib = attribute; 23 | } 24 | 25 | if (attrib != null) return attrib.Description; 26 | 27 | return enumObj.ToString(); 28 | } 29 | 30 | object IValueConverter.Convert( 31 | object value, 32 | Type targetType, 33 | object parameter, 34 | CultureInfo culture) 35 | { 36 | var myEnum = (Enum)value; 37 | var description = GetEnumDescription(myEnum); 38 | return description; 39 | } 40 | 41 | object IValueConverter.ConvertBack( 42 | object value, 43 | Type targetType, 44 | object parameter, 45 | CultureInfo culture) 46 | => string.Empty; 47 | } 48 | -------------------------------------------------------------------------------- /Andreal.Window/Common/ExceptionLog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | using Konata.Core; 6 | using Konata.Core.Common; 7 | 8 | namespace Andreal.Window.Common; 9 | 10 | internal class ExceptionLog : IComparable, IComparable 11 | { 12 | public DateTime Time { get; set; } 13 | public Exception? Exception { get; set; } 14 | 15 | public int CompareTo(object? obj) 16 | { 17 | if (ReferenceEquals(null, obj)) return 1; 18 | if (ReferenceEquals(this, obj)) return 0; 19 | return obj is ExceptionLog other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(ExceptionLog)}"); 20 | } 21 | 22 | public int CompareTo(ExceptionLog? other) 23 | { 24 | if (ReferenceEquals(this, other)) return 0; 25 | if (ReferenceEquals(null, other)) return 1; 26 | return Time.CompareTo(other.Time); 27 | } 28 | 29 | public static bool operator <(ExceptionLog? left, ExceptionLog? right) => Comparer.Default.Compare(left, right) < 0; 30 | 31 | public static bool operator >(ExceptionLog? left, ExceptionLog? right) => Comparer.Default.Compare(left, right) > 0; 32 | 33 | public static bool operator <=(ExceptionLog? left, ExceptionLog? right) => Comparer.Default.Compare(left, right) <= 0; 34 | 35 | public static bool operator >=(ExceptionLog? left, ExceptionLog? right) => Comparer.Default.Compare(left, right) >= 0; 36 | } 37 | 38 | internal class MessageLog : IComparable, IComparable 39 | { 40 | public DateTime Time { get; set; } 41 | public string Robot { get; set; } = ""; 42 | public string FromQQ { get; set; } = ""; 43 | public string FromGroup { get; set; } = ""; 44 | public string Message { get; set; } = ""; 45 | 46 | public int CompareTo(object? obj) 47 | { 48 | if (ReferenceEquals(null, obj)) return 1; 49 | if (ReferenceEquals(this, obj)) return 0; 50 | return obj is MessageLog other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(MessageLog)}"); 51 | } 52 | 53 | public int CompareTo(MessageLog? other) 54 | { 55 | if (ReferenceEquals(this, other)) return 0; 56 | if (ReferenceEquals(null, other)) return 1; 57 | return Time.CompareTo(other.Time); 58 | } 59 | 60 | public static bool operator <(MessageLog? left, MessageLog? right) => Comparer.Default.Compare(left, right) < 0; 61 | 62 | public static bool operator >(MessageLog? left, MessageLog? right) => Comparer.Default.Compare(left, right) > 0; 63 | 64 | public static bool operator <=(MessageLog? left, MessageLog? right) => Comparer.Default.Compare(left, right) <= 0; 65 | 66 | public static bool operator >=(MessageLog? left, MessageLog? right) => Comparer.Default.Compare(left, right) >= 0; 67 | } 68 | 69 | internal sealed class AccountLog : INotifyPropertyChanged, IComparable, IComparable 70 | { 71 | internal readonly Bot? Bot; 72 | private string _message = ""; 73 | private string _nick = ""; 74 | private string _state = ""; 75 | private OicqProtocol _protocol = OicqProtocol.AndroidPhone; 76 | 77 | public AccountLog( 78 | Bot bot, 79 | uint uin, 80 | string state, 81 | string message, 82 | OicqProtocol protocol) 83 | { 84 | Bot = bot; 85 | Robot = uin; 86 | State = state; 87 | Message = message; 88 | Protocol = protocol; 89 | } 90 | 91 | public uint Robot { get; } 92 | 93 | public string Nick 94 | { 95 | get => _nick; 96 | set 97 | { 98 | _nick = value; 99 | OnPropertyChanged(); 100 | } 101 | } 102 | 103 | public string State 104 | { 105 | get => _state; 106 | set 107 | { 108 | _state = value; 109 | OnPropertyChanged(); 110 | } 111 | } 112 | 113 | public string Message 114 | { 115 | get => _message; 116 | set 117 | { 118 | _message = value; 119 | OnPropertyChanged(); 120 | } 121 | } 122 | 123 | public OicqProtocol Protocol 124 | { 125 | get => _protocol; 126 | set 127 | { 128 | _protocol = value; 129 | OnPropertyChanged(); 130 | } 131 | } 132 | 133 | public int CompareTo(object? obj) 134 | { 135 | if (ReferenceEquals(null, obj)) return 1; 136 | if (ReferenceEquals(this, obj)) return 0; 137 | return obj is AccountLog other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(AccountLog)}"); 138 | } 139 | 140 | public int CompareTo(AccountLog? other) 141 | { 142 | if (ReferenceEquals(this, other)) return 0; 143 | if (ReferenceEquals(null, other)) return 1; 144 | return Robot.CompareTo(other.Robot); 145 | } 146 | 147 | public event PropertyChangedEventHandler? PropertyChanged; 148 | 149 | private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new(propertyName)); 150 | 151 | public static bool operator <(AccountLog? left, AccountLog? right) => Comparer.Default.Compare(left, right) < 0; 152 | 153 | public static bool operator >(AccountLog? left, AccountLog? right) => Comparer.Default.Compare(left, right) > 0; 154 | 155 | public static bool operator <=(AccountLog? left, AccountLog? right) => Comparer.Default.Compare(left, right) <= 0; 156 | 157 | public static bool operator >=(AccountLog? left, AccountLog? right) => Comparer.Default.Compare(left, right) >= 0; 158 | } 159 | -------------------------------------------------------------------------------- /Andreal.Window/Common/NotifyIconViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | 5 | namespace Andreal.Window.Common; 6 | 7 | internal class NotifyIconViewModel 8 | { 9 | public ICommand ShowWindowCommand 10 | => new DelegateCommand 11 | { 12 | CanExecuteFunc = _ => Application.Current.MainWindow?.IsVisible != true, CommandAction = _ => Application.Current.MainWindow?.Show() 13 | }; 14 | 15 | public ICommand HideWindowCommand 16 | => new DelegateCommand 17 | { 18 | CanExecuteFunc = _ => Application.Current.MainWindow?.IsVisible == true, CommandAction = _ => Application.Current.MainWindow?.Hide() 19 | }; 20 | 21 | public ICommand ExitApplicationCommand => new DelegateCommand { CanExecuteFunc = _ => true, CommandAction = _ => Environment.Exit(0) }; 22 | } 23 | -------------------------------------------------------------------------------- /Andreal.Window/Common/TimeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | 5 | namespace Andreal.Window.Common; 6 | 7 | internal class TimeConverter : IValueConverter 8 | { 9 | public object Convert( 10 | object value, 11 | Type targetType, 12 | object parameter, 13 | CultureInfo culture) 14 | => ((DateTime)value).ToString("yyyy/MM/dd HH:mm:ss"); 15 | 16 | public object ConvertBack( 17 | object value, 18 | Type targetType, 19 | object parameter, 20 | CultureInfo culture) 21 | => string.Empty; 22 | } 23 | -------------------------------------------------------------------------------- /Andreal.Window/UI/App.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Andreal.Window/UI/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using Andreal.Core.Common; 6 | using Andreal.Window.Common; 7 | using MessageBox = System.Windows.MessageBox; 8 | 9 | #pragma warning disable CS4014 10 | 11 | namespace Andreal.Window.UI; 12 | 13 | internal partial class App 14 | { 15 | private static Mutex? _mutex; 16 | 17 | protected override void OnStartup(StartupEventArgs e) 18 | { 19 | _mutex = new(true, "AndrealOnlyRunMutex"); 20 | if (!_mutex.WaitOne(0, false) && 21 | MessageBox.Show("已有在运行的Andreal,是否要打开新的实例?", "Andreal提示", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) 22 | Environment.Exit(0); 23 | 24 | Current.DispatcherUnhandledException += (_, args) => 25 | { 26 | ExceptionLogger.Log(args.Exception); 27 | args.Handled = true; 28 | }; 29 | 30 | TaskScheduler.UnobservedTaskException += (_, args) => 31 | { 32 | ExceptionLogger.Log(args.Exception.InnerException); 33 | args.SetObserved(); 34 | }; 35 | 36 | AppDomain.CurrentDomain.UnhandledException += (_, args) => ExceptionLogger.Log(args.ExceptionObject as Exception); 37 | 38 | ExceptionLogger.OnExceptionRecorded += exception => 39 | { 40 | Program.Add(Program.Exceptions, new() { Time = DateTime.Now, Exception = exception }); 41 | if (Program.Exceptions.Count > 100) Program.RemoveFirst(Program.Exceptions); 42 | }; 43 | 44 | FindResource("Taskbar"); 45 | base.OnStartup(e); 46 | 47 | var window = new SourceDownloader(); 48 | window.Show(); 49 | 50 | MainWindow = new MainWindow(); 51 | MainWindow.Hide(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Andreal.Window/UI/Login.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 24 | 25 | 56 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 95 | 96 | 97 | 101 | 105 | 106 | 107 | 108 | 112 | 116 | 117 | 118 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /Andreal.Window/UI/Login.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text.RegularExpressions; 3 | using System.Windows; 4 | using System.Windows.Controls.Primitives; 5 | using System.Windows.Input; 6 | using Andreal.Window.Common; 7 | 8 | namespace Andreal.Window.UI; 9 | 10 | internal partial class Login 11 | { 12 | private static readonly Regex Regex = new("[^0-9]+", RegexOptions.Compiled); 13 | 14 | internal Login() 15 | { 16 | InitializeComponent(); 17 | } 18 | 19 | private async void OnLoginBtnClick(object sender, RoutedEventArgs e) 20 | { 21 | if (string.IsNullOrWhiteSpace(UserTextBox.Text) || string.IsNullOrWhiteSpace(PwdBox.Password)) 22 | { 23 | MessageBox.Show("请输入QQ和密码!"); 24 | return; 25 | } 26 | 27 | if (!uint.TryParse(UserTextBox.Text, out var uin) || uin <= 10000) 28 | { 29 | MessageBox.Show("请输入正确的QQ号!"); 30 | return; 31 | } 32 | 33 | if (Program.Accounts.Any(i => i.Robot == uin)) 34 | { 35 | MessageBox.Show("此QQ号已登录或正在登录中!"); 36 | Close(); 37 | return; 38 | } 39 | 40 | await Program.OnPreLogin(uin, PwdBox.Password); 41 | 42 | Close(); 43 | } 44 | 45 | private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 46 | { 47 | try 48 | { 49 | DragMove(); 50 | } 51 | catch 52 | { 53 | //ignored 54 | } 55 | } 56 | 57 | private void OnCloseBtnClick(object sender, RoutedEventArgs e) => Close(); 58 | 59 | private void OnPreviewTextInput(object sender, TextCompositionEventArgs e) => e.Handled = Regex.IsMatch(e.Text); 60 | 61 | private void OnKeyDown(object sender, KeyEventArgs e) 62 | { 63 | if (e.Key == Key.Enter) LoginButton.RaiseEvent(new(ButtonBase.ClickEvent)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Andreal.Window/UI/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Threading; 6 | using Andreal.Core.Utils; 7 | using Andreal.Window.Common; 8 | using Andreal.Window.UI.UserControl; 9 | using ExceptionLog = Andreal.Window.UI.UserControl.ExceptionLog; 10 | using MessageLog = Andreal.Window.UI.UserControl.MessageLog; 11 | 12 | namespace Andreal.Window.UI; 13 | 14 | internal partial class MainWindow 15 | { 16 | private readonly ConcurrentDictionary _controlers; 17 | 18 | private WindowStatus _status = WindowStatus.None; 19 | 20 | public MainWindow() 21 | { 22 | InitializeComponent(); 23 | var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; 24 | timer.Tick += (_, _) => Status.Text = BotStatementHelper.Status; 25 | timer.Start(); 26 | _controlers = new(); 27 | _controlers.TryAdd(WindowStatus.None, new()); 28 | 29 | Version.Text = Program.Version; 30 | } 31 | 32 | private System.Windows.Controls.UserControl GetNewUserControl(WindowStatus status) 33 | => status switch 34 | { 35 | WindowStatus.AccountManage => new Accounts(), 36 | WindowStatus.Setting => new Setting(), 37 | WindowStatus.ReplySetting => new ReplySetting(), 38 | WindowStatus.MessageLog => new MessageLog(), 39 | WindowStatus.ExceptionLog => new ExceptionLog(), 40 | _ => new() 41 | }; 42 | 43 | private void ChangeUserControl(WindowStatus status) 44 | { 45 | if (_status == status) return; 46 | _controlers[_status].Visibility = Visibility.Collapsed; 47 | _status = status; 48 | _controlers.GetOrAdd(_status, GetNewUserControl); 49 | _controlers[_status].Visibility = Visibility.Visible; 50 | Label.Content = _controlers[_status]; 51 | } 52 | 53 | private void OnMinBtnClick(object sender, RoutedEventArgs e) => Hide(); 54 | 55 | private void OnAccountManageClick(object sender, RoutedEventArgs e) => ChangeUserControl(WindowStatus.AccountManage); 56 | 57 | private void OnSettingClick(object sender, RoutedEventArgs e) => ChangeUserControl(WindowStatus.Setting); 58 | 59 | private void OnMessagePushClick(object sender, MouseButtonEventArgs e) => ChangeUserControl(WindowStatus.MessageLog); 60 | 61 | private void OnExceptionLogClick(object sender, MouseButtonEventArgs e) => ChangeUserControl(WindowStatus.ExceptionLog); 62 | 63 | private void OnReplySettingClick(object sender, MouseButtonEventArgs e) => ChangeUserControl(WindowStatus.ReplySetting); 64 | 65 | private void OnMainWindowClosed(object? sender, EventArgs e) => Environment.Exit(0); 66 | 67 | private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 68 | { 69 | try 70 | { 71 | Focus(); 72 | DragMove(); 73 | } 74 | catch 75 | { 76 | //ignored 77 | } 78 | } 79 | 80 | internal enum WindowStatus 81 | { 82 | None, 83 | AccountManage, 84 | Setting, 85 | ReplySetting, 86 | MessageLog, 87 | ExceptionLog 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Andreal.Window/UI/SliderSubmit.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 43 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | 80 | 81 | 83 | 87 | 88 | 89 | 90 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Andreal.Window/UI/SliderSubmit.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using Konata.Core; 6 | using Konata.Core.Interfaces.Api; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace Andreal.Window.UI; 10 | 11 | internal partial class SliderSubmit : IDisposable 12 | { 13 | private readonly Bot _bot; 14 | private readonly string _sliderUrl; 15 | private HttpClient? _httpClient; 16 | 17 | internal SliderSubmit(Bot bot, string sliderUrl) 18 | { 19 | _bot = bot; 20 | _sliderUrl = sliderUrl.Replace("ssl.captcha.qq.com", "txhelper.glitch.me"); 21 | _httpClient = new(); 22 | _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); 23 | _httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); 24 | InitializeComponent(); 25 | Dispatcher.Invoke(GetCode); 26 | } 27 | 28 | private void GetCode() 29 | { 30 | var str = _httpClient!.GetStringAsync(_sliderUrl).Result; 31 | CodeBlock.Text = JObject.Parse(str)["code"]!.ToString(); 32 | } 33 | 34 | private void OnSubmit(object sender, RoutedEventArgs e) 35 | { 36 | var str = _httpClient!.GetStringAsync(_sliderUrl).Result; 37 | var ticket = JObject.Parse(str)["ticket"]!.ToString(); 38 | _bot.SubmitSliderTicket(ticket); 39 | Close(); 40 | } 41 | 42 | private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 43 | { 44 | try 45 | { 46 | Focus(); 47 | DragMove(); 48 | } 49 | catch 50 | { 51 | //ignored 52 | } 53 | } 54 | 55 | public void Dispose() 56 | { 57 | _httpClient?.Dispose(); 58 | _httpClient = null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Andreal.Window/UI/SliderVerify.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Andreal.Window/UI/SliderVerify.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using System.Windows.Input; 4 | using Konata.Core; 5 | using Konata.Core.Interfaces.Api; 6 | using Microsoft.Web.WebView2.Core.DevToolsProtocolExtension; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace Andreal.Window.UI; 10 | 11 | internal partial class SliderVerify : IDisposable 12 | { 13 | private readonly Bot _bot; 14 | private readonly string _sliderUrl; 15 | private DevToolsProtocolHelper? _cdpHelper; 16 | 17 | private string _ticket = ""; 18 | 19 | private string _ticketId = ""; 20 | 21 | internal SliderVerify(Bot bot, string sliderUrl) 22 | { 23 | _sliderUrl = sliderUrl; 24 | _bot = bot; 25 | InitializeComponent(); 26 | InitializeAsync(); 27 | } 28 | 29 | private DevToolsProtocolHelper CdpHelper => _cdpHelper ??= WebBrowser.CoreWebView2.GetDevToolsProtocolHelper(); 30 | 31 | private async void InitializeAsync() 32 | { 33 | try 34 | { 35 | await WebBrowser.EnsureCoreWebView2Async(); 36 | await CdpHelper.Network.EnableAsync(); 37 | 38 | await 39 | CdpHelper.Network 40 | .SetUserAgentOverrideAsync("Mozilla/5.0 (Linux; Android 10; PCT-AL10 Build/HUAWEIPCT-AL10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.72 MQQBrowser/6.2 TBS/046011 Mobile Safari/537.36 V1_AND_SQ_8.8.88_2770_YYB_D A_8088800 QQ/8.8.88.7830 NetType/WIFI WebP/0.3.0 Pixel/1080", 41 | platform: "Android"); 42 | 43 | WebBrowser.CoreWebView2.Navigate(_sliderUrl); 44 | } 45 | catch 46 | { 47 | MessageBox.Show("未找到Webview运行时!\n将使用滑块验证助手进行扫码验证", "提示"); 48 | var window = new SliderSubmit(_bot, _sliderUrl); 49 | window.ShowDialog(); 50 | Close(); 51 | } 52 | 53 | CdpHelper.Network.ResponseReceived += (_, args) => 54 | { 55 | if (args.Response.Url == "https://t.captcha.qq.com/cap_union_new_verify") _ticketId = args.RequestId; 56 | }; 57 | 58 | CdpHelper.Network.LoadingFinished += async (_, args) => 59 | { 60 | if (args.RequestId != _ticketId) return; 61 | 62 | _ticketId = args.RequestId; 63 | _ticket = JObject.Parse((await CdpHelper.Network.GetResponseBodyAsync(_ticketId)).Body)["ticket"]!.ToString(); 64 | 65 | _bot.SubmitSliderTicket(_ticket); 66 | 67 | Close(); 68 | }; 69 | } 70 | 71 | private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 72 | { 73 | try 74 | { 75 | Focus(); 76 | DragMove(); 77 | } 78 | catch 79 | { 80 | //ignored 81 | } 82 | } 83 | 84 | public void Dispose() 85 | { 86 | WebBrowser?.Dispose(); 87 | WebBrowser = null; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Andreal.Window/UI/SmsCodeVerify.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 43 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | 80 | 81 | 82 | 84 | 88 | 89 | 96 | 97 | 98 | 99 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /Andreal.Window/UI/SmsCodeVerify.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | using Konata.Core; 5 | using Konata.Core.Interfaces.Api; 6 | 7 | namespace Andreal.Window.UI; 8 | 9 | internal partial class SmsCodeVerify 10 | { 11 | private static readonly Regex Regex = new("[^0-9]+", RegexOptions.Compiled); 12 | 13 | private readonly Bot _bot; 14 | 15 | internal SmsCodeVerify(Bot bot, string phone) 16 | { 17 | InitializeComponent(); 18 | _bot = bot; 19 | PhoneBlock.Text = phone; 20 | } 21 | 22 | private void OnSubmit(object sender, RoutedEventArgs e) 23 | { 24 | _bot.SubmitSmsCode(CodeBox.Text); 25 | Close(); 26 | } 27 | 28 | private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 29 | { 30 | try 31 | { 32 | Focus(); 33 | DragMove(); 34 | } 35 | catch 36 | { 37 | //ignored 38 | } 39 | } 40 | 41 | private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) => e.Handled = Regex.IsMatch(e.Text); 42 | } 43 | -------------------------------------------------------------------------------- /Andreal.Window/UI/SourceDownloader.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Andreal.Window/UI/SourceDownloader.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.IO; 5 | using System.Net.Http; 6 | using System.Net.Http.Handlers; 7 | using System.Windows; 8 | using System.Windows.Forms; 9 | using System.Windows.Input; 10 | using Andreal.Core.Common; 11 | using Andreal.Window.Common; 12 | using Newtonsoft.Json; 13 | using static Andreal.Core.Common.Path; 14 | using Application = System.Windows.Application; 15 | using MessageBox = System.Windows.Forms.MessageBox; 16 | 17 | namespace Andreal.Window.UI; 18 | 19 | internal partial class SourceDownloader 20 | { 21 | private HttpClient? _client; 22 | private readonly BackgroundWorker _worker = new(); 23 | 24 | internal SourceDownloader() 25 | { 26 | InitializeComponent(); 27 | ProgressBar.Maximum = 100; 28 | var processMessageHander = new ProgressMessageHandler(new HttpClientHandler()); 29 | processMessageHander.HttpReceiveProgress += (_, e) => { _worker.ReportProgress(e.ProgressPercentage); }; 30 | _client = new(processMessageHander); 31 | _client.BaseAddress = new("https://assets.awbugl.top/andreal/"); 32 | _client.Timeout = TimeSpan.FromMinutes(5); 33 | _worker.WorkerReportsProgress = true; 34 | _worker.DoWork += Download; 35 | _worker.ProgressChanged += ProgressChanged; 36 | _worker.RunWorkerAsync(); 37 | _worker.RunWorkerCompleted += WorkerCompleted; 38 | } 39 | 40 | private void WorkerCompleted(object? sender, RunWorkerCompletedEventArgs e) 41 | { 42 | if ((int)ProgressBar.Value != 100) 43 | Dispatcher.Invoke(() => 44 | { 45 | MessageBox.Show("下载失败,请检查网络连接或重试。", "下载失败", MessageBoxButtons.OK, MessageBoxIcon.Error); 46 | Close(); 47 | Environment.Exit(0); 48 | return; 49 | }); 50 | 51 | var mainWindow = Application.Current.MainWindow!; 52 | mainWindow.Dispatcher.InvokeAsync(Program.ProgramInit); 53 | mainWindow.Show(); 54 | _client?.Dispose(); 55 | _client = null!; 56 | Close(); 57 | } 58 | 59 | private void ProgressChanged(object? sender, ProgressChangedEventArgs e) 60 | { 61 | var value = e.ProgressPercentage; 62 | Dispatcher.Invoke(() => 63 | { 64 | ProgressBar.Value = value; 65 | Block.Text = value + "%"; 66 | }); 67 | } 68 | 69 | private static string GetPath(string id) 70 | => id switch 71 | { 72 | "config" => AndreaConfigRoot, 73 | "source" => ArcaeaSourceRoot, 74 | "fonts" => ArcaeaFontRoot, 75 | _ => throw new ArgumentOutOfRangeException(nameof(id), id, null) 76 | }; 77 | 78 | private void Download(object? sender, DoWorkEventArgs e) 79 | { 80 | try 81 | { 82 | Directory.CreateDirectory(AndreaConfigRoot + "BotInfo/"); 83 | Directory.CreateDirectory(ArcaeaSourceRoot); 84 | Directory.CreateDirectory(ArcaeaFontRoot); 85 | 86 | var list = JsonConvert.DeserializeObject>(_client!.GetStringAsync("list.json").Result)!; 87 | 88 | Dispatcher.Invoke(() => 89 | { 90 | Notice.Text = "正在下载:"; 91 | ProgressBar.Visibility = Visibility.Visible; 92 | }); 93 | 94 | foreach (var (key, ls) in list) 95 | { 96 | foreach (var i in ls) 97 | { 98 | var path = GetPath(key) + i; 99 | var requestUri = $"{key}/{i}"; 100 | Dispatcher.Invoke(() => 101 | { 102 | TextBlock.Text = requestUri; 103 | ProgressBar.Value = 0; 104 | }); 105 | if (File.Exists(path)) continue; 106 | var config = _client.GetByteArrayAsync(requestUri).Result; 107 | File.WriteAllBytes(path, config); 108 | } 109 | } 110 | 111 | Dispatcher.Invoke(() => { ProgressBar.Value = 100; }); 112 | } 113 | catch (Exception ex) 114 | { 115 | ExceptionLogger.Log(ex); 116 | Dispatcher.Invoke(() => { ProgressBar.Value = 0; }); 117 | } 118 | } 119 | 120 | private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 121 | { 122 | try 123 | { 124 | Focus(); 125 | DragMove(); 126 | } 127 | catch 128 | { 129 | //ignored 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/Accounts.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/Accounts.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Specialized; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using Andreal.Window.Common; 6 | using Konata.Core.Interfaces.Api; 7 | 8 | namespace Andreal.Window.UI.UserControl; 9 | 10 | internal partial class Accounts 11 | { 12 | public Accounts() 13 | { 14 | InitializeComponent(); 15 | 16 | List.ItemsSource = Program.Accounts; 17 | Program.Accounts.CollectionChanged += OnAccountsChanged; 18 | } 19 | 20 | public ICommand LoginAccountCommand 21 | => new DelegateCommand 22 | { 23 | CanExecuteFunc = obj => 24 | { 25 | try 26 | { 27 | return (obj as AccountLog)?.Bot?.IsOnline() == false; 28 | } 29 | catch 30 | { 31 | return false; 32 | } 33 | }, 34 | CommandAction = OnLoginAccountCommandExecute 35 | }; 36 | 37 | public ICommand LogoutAccountCommand 38 | => new DelegateCommand 39 | { 40 | CanExecuteFunc = obj => 41 | { 42 | try 43 | { 44 | return (obj as AccountLog)?.Bot?.IsOnline() == true; 45 | } 46 | catch 47 | { 48 | return false; 49 | } 50 | }, 51 | CommandAction = OnLogoutAccountCommandExecute 52 | }; 53 | 54 | public ICommand DeleteAccountCommand 55 | => new DelegateCommand 56 | { 57 | CanExecuteFunc = obj => 58 | { 59 | try 60 | { 61 | return obj != null; 62 | } 63 | catch 64 | { 65 | return false; 66 | } 67 | }, 68 | CommandAction = OnDeleteAccountCommandExecute 69 | }; 70 | 71 | private void OnAccountsChanged(object? sender, NotifyCollectionChangedEventArgs e) 72 | => List.GetBindingExpression(ItemsControl.ItemsSourceProperty)?.UpdateTarget(); 73 | 74 | private void OnMouseRightDown(object? sender, MouseButtonEventArgs e) 75 | { 76 | if (sender is not DataGridRow row) return; 77 | 78 | if (row.Item is AccountLog item) 79 | foreach (var items in row.ContextMenu!.Items) 80 | { 81 | var i = (MenuItem)items; 82 | i.CommandParameter = item; 83 | i.Command = i.Header switch 84 | { 85 | "上线" => LoginAccountCommand, 86 | "离线" => LogoutAccountCommand, 87 | "删除" => DeleteAccountCommand, 88 | _ => i.Command 89 | }; 90 | } 91 | } 92 | 93 | private void OnAddAccountCommandExecute(object parameter, RoutedEventArgs routedEventArgs) => new Login().ShowDialog(); 94 | 95 | private async void OnLoginAccountCommandExecute(object bot) 96 | { 97 | var log = bot as AccountLog; 98 | var loginresult = await log!.Bot.Login(); 99 | await Program.OnLogin(log.Bot!, loginresult); 100 | } 101 | 102 | private async void OnLogoutAccountCommandExecute(object bot) => await (bot as AccountLog)?.Bot?.Logout()!; 103 | 104 | private async void OnDeleteAccountCommandExecute(object bot) => await Program.OnRemove((bot as AccountLog)!); 105 | } 106 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/ExceptionLog.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/ExceptionLog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Specialized; 2 | using System.Windows.Controls; 3 | using Andreal.Window.Common; 4 | 5 | namespace Andreal.Window.UI.UserControl; 6 | 7 | internal partial class ExceptionLog 8 | { 9 | public ExceptionLog() 10 | { 11 | InitializeComponent(); 12 | 13 | List.ItemsSource = Program.Exceptions; 14 | Program.Exceptions.CollectionChanged += OnExceptionsChanged; 15 | } 16 | 17 | private void OnExceptionsChanged(object? sender, NotifyCollectionChangedEventArgs e) 18 | { 19 | var b = List.GetBindingExpression(ItemsControl.ItemsSourceProperty); 20 | b?.UpdateTarget(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/MessageLog.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/MessageLog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Specialized; 2 | using System.Windows.Controls; 3 | using Andreal.Window.Common; 4 | 5 | namespace Andreal.Window.UI.UserControl; 6 | 7 | internal partial class MessageLog 8 | { 9 | public MessageLog() 10 | { 11 | InitializeComponent(); 12 | 13 | List.ItemsSource = Program.Messages; 14 | Program.Messages.CollectionChanged += OnMessagesChanged; 15 | } 16 | 17 | private void OnMessagesChanged(object? sender, NotifyCollectionChangedEventArgs e) 18 | { 19 | var b = List.GetBindingExpression(ItemsControl.ItemsSourceProperty); 20 | b?.UpdateTarget(); 21 | 22 | if (List.Items.MoveCurrentToLast()) 23 | { 24 | List.ScrollIntoView(List.Items.CurrentItem); 25 | List.UpdateLayout(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/ReplySetting.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 26 | 27 | 28 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 57 | 61 | 62 | 63 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/ReplySetting.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using Andreal.Core.Common; 7 | using Andreal.Core.Model.Arcaea; 8 | using Newtonsoft.Json; 9 | using MessageBox = System.Windows.Forms.MessageBox; 10 | using Path = Andreal.Core.Common.Path; 11 | 12 | namespace Andreal.Window.UI.UserControl; 13 | 14 | internal partial class ReplySetting 15 | { 16 | internal ReplySetting() 17 | { 18 | InitializeComponent(); 19 | ComboBox.ItemsSource = typeof(RobotReply).GetProperties(); 20 | } 21 | 22 | private PropertyInfo? CurrentProperty { get; set; } 23 | 24 | private void OnSaveButtonClick(object sender, RoutedEventArgs e) 25 | { 26 | if (CurrentProperty is null) return; 27 | 28 | CurrentProperty.SetValue(GlobalConfig.RobotReply, TextBox.Text); 29 | File.WriteAllText(Path.RobotReply, JsonConvert.SerializeObject(GlobalConfig.RobotReply, Formatting.Indented)); 30 | } 31 | 32 | private void ComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) 33 | { 34 | if (((ComboBox)sender).SelectedItem is PropertyInfo info) 35 | { 36 | CurrentProperty = info; 37 | TextBox.Text = CurrentProperty.GetValue(GlobalConfig.RobotReply) as string; 38 | } 39 | } 40 | 41 | private void OnClearTempImageButtonClick(object sender, RoutedEventArgs e) 42 | { 43 | foreach (var j in new DirectoryInfo(Path.TempImageRoot).GetFiles()) j.Delete(); 44 | MessageBox.Show("发送图片缓存清除完成。"); 45 | } 46 | 47 | private void OnClearBackgroundImageButtonClick(object sender, RoutedEventArgs e) 48 | { 49 | foreach (var j in new DirectoryInfo(Path.ArcaeaBackgroundRoot).GetFiles()) j.Delete(); 50 | MessageBox.Show("查分背景缓存清除完成。"); 51 | } 52 | 53 | private async void OnUpdateButtonClick(object sender, RoutedEventArgs e) 54 | { 55 | await Task.Run(ArcaeaCharts.Init); 56 | MessageBox.Show("Arc曲目列表已更新。"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Andreal.Window/UI/UserControl/Setting.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Input; 9 | using Andreal.Core.Common; 10 | using Andreal.Core.Data.Api; 11 | using Andreal.Window.Common; 12 | using Konata.Core.Common; 13 | using Newtonsoft.Json; 14 | using Path = Andreal.Core.Common.Path; 15 | 16 | namespace Andreal.Window.UI.UserControl; 17 | 18 | internal partial class Setting 19 | { 20 | private static readonly Regex Regex = new("[^0-9]+", RegexOptions.Compiled); 21 | 22 | internal Setting() 23 | { 24 | InitializeComponent(); 25 | 26 | MasterQQ.Text = Program.Config.Master.ToString(); 27 | 28 | if (Program.Config.Api.TryGetValue("unlimited", out var auavalue)) 29 | { 30 | AUAUrl.Text = auavalue.Url; 31 | AUAToken.Text = auavalue.Token; 32 | } 33 | 34 | ProtocolComboBox.SelectedItem = Program.Config.Protocol; 35 | SliderComboBox.SelectedItem = Program.Config.SliderType; 36 | EnableProcess.IsChecked = Program.Config.EnableHandleMessage; 37 | AutoFriendRequest.IsChecked = Program.Config.Settings.FriendAdd; 38 | AutoGroupRequest.IsChecked = Program.Config.Settings.GroupAdd; 39 | WhiteList.Text = string.Join('\n', Program.Config.Settings.GroupInviterWhitelist); 40 | } 41 | 42 | private OicqProtocol CurrentProtocol { get; set; } = OicqProtocol.AndroidPhone; 43 | 44 | private void OnPreviewTextInput(object sender, TextCompositionEventArgs e) => e.Handled = Regex.IsMatch(e.Text); 45 | 46 | private void OnSaveButtonClick(object sender, RoutedEventArgs e) 47 | { 48 | if (!uint.TryParse(MasterQQ.Text, out var master)) 49 | { 50 | MessageBox.Show("Bot管理员QQ号解析失败!"); 51 | return; 52 | } 53 | 54 | List whitelist; 55 | 56 | try 57 | { 58 | whitelist = WhiteList.Text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(uint.Parse) 59 | .ToList(); 60 | } 61 | catch 62 | { 63 | MessageBox.Show("群白名单的读取失败!请按格式填写。(分行、QQ号)", "提示"); 64 | return; 65 | } 66 | 67 | var config = new AndrealConfig 68 | { 69 | Master = master, 70 | Protocol = CurrentProtocol, 71 | Accounts = Program.Config.Accounts, 72 | EnableHandleMessage = EnableProcess.IsChecked ?? true, 73 | Settings = new() 74 | { 75 | FriendAdd = AutoFriendRequest.IsChecked ?? false, 76 | GroupAdd = AutoGroupRequest.IsChecked ?? false, 77 | GroupInviterWhitelist = whitelist 78 | } 79 | }; 80 | 81 | var auauri = AUAUrl.Text; 82 | var auatoken = AUAToken.Text; 83 | 84 | if (!string.IsNullOrWhiteSpace(auauri) && !string.IsNullOrWhiteSpace(auatoken)) 85 | { 86 | config.Api["unlimited"] = new() { Url = auauri, Token = auatoken }; 87 | UnofficialArcaeaAPI.Init(config); 88 | } 89 | 90 | Program.Config = config; 91 | File.WriteAllText(Path.Config, JsonConvert.SerializeObject(config, Formatting.Indented)); 92 | } 93 | 94 | private void ComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) 95 | { 96 | if (((ComboBox)sender).SelectedItem is OicqProtocol protocol) CurrentProtocol = protocol; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Andreal.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Andreal.Core", "Andreal.Core\Andreal.Core.csproj", "{7BB7AA90-ADFF-407B-8FED-02EE49A23279}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Andreal.Window", "Andreal.Window\Andreal.Window.csproj", "{4D2908B1-D724-43E3-986A-3B1EA6325317}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Konata.Core", "..\Konata.Core\Konata.Core\Konata.Core.csproj", "{3FAE53BD-304E-4A73-96A2-99B6D6635445}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Andreal.Test", "Andreal.Test\Andreal.Test.csproj", "{2E6E3EE4-1DFB-40CC-9006-B329CAB96D8C}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Release|Any CPU = Release|Any CPU 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {7BB7AA90-ADFF-407B-8FED-02EE49A23279}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {7BB7AA90-ADFF-407B-8FED-02EE49A23279}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {7BB7AA90-ADFF-407B-8FED-02EE49A23279}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {7BB7AA90-ADFF-407B-8FED-02EE49A23279}.Release|Any CPU.Build.0 = Release|Any CPU 21 | {4D2908B1-D724-43E3-986A-3B1EA6325317}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {4D2908B1-D724-43E3-986A-3B1EA6325317}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {4D2908B1-D724-43E3-986A-3B1EA6325317}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {4D2908B1-D724-43E3-986A-3B1EA6325317}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {3FAE53BD-304E-4A73-96A2-99B6D6635445}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {3FAE53BD-304E-4A73-96A2-99B6D6635445}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {3FAE53BD-304E-4A73-96A2-99B6D6635445}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {3FAE53BD-304E-4A73-96A2-99B6D6635445}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {2E6E3EE4-1DFB-40CC-9006-B329CAB96D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {2E6E3EE4-1DFB-40CC-9006-B329CAB96D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {2E6E3EE4-1DFB-40CC-9006-B329CAB96D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {2E6E3EE4-1DFB-40CC-9006-B329CAB96D8C}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /Andreal.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 |  2 | <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 3 | <TestAncestor> 4 | <TestId>NUnit3x::2E6E3EE4-1DFB-40CC-9006-B329CAB96D8C::net6.0::Andreal.Test.Tests.AIRandomSongTest</TestId> 5 | </TestAncestor> 6 | </SessionState> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Andreal 2 | 3 | 基于 Konata.Core 和 UnofficialArcaeaAPI 的开源 ArcaeaBot。 4 | 5 | ---- 6 | 7 | #### 用户须知 8 | 9 | > 您应知悉,使用本项目将违反 [Arcaea 使用条款](https://arcaea.lowiro.com/zh/terms_of_service) 中的 3.2-4 和 10 | > 3.2-6,以及 [Arcaea 二次创作管理条例](https://arcaea.lowiro.com/zh/derivative_policy) 。 11 | > 12 | > 因使用本项目而造成的损失,Andreal 开发组不承担任何责任。 13 | 14 | ---- 15 | 16 | #### ~~部署步骤~~ 已失效 17 | 18 | 1. ~~申请 `UnofficialArcaeaAPI Token`(详见常见问题)~~ 19 | 20 | 2. ~~[下载最新版本 Andreal](https://github.com/Awbugl/Andreal/releases/)~~ 21 | 22 | 3. ~~运行 Andreal~~ 23 | 24 | 4. ~~填写基础配置页面的配置项并保存~~ 25 | 26 | 5. ~~账号管理界面右键添加新帐号~~ 27 | 28 | * ~~若一切顺利,此时您的 Bot 就已可用。~~ 29 | 30 | ---- 31 | 32 | #### 配置文件夹 `.\Andreal\Config\` 33 | 34 | > 请勿自行操作、删除、泄露给第三方。 35 | > 36 | > Bot迁移时,只迁移配置文件夹即可。 37 | 38 | | 文件(夹) | 说明 | 39 | |:-------------------|:------------------| 40 | | config.json | Andreal 主要配置文件 | 41 | | positioninfo.json | Arcaea 图查立绘位置配置文件 | 42 | | replytemplate.json | Bot 回复模板 | 43 | | Andreal.db | 用户绑定信息数据库 | 44 | | BotInfo/ | Bot 登录信息文件夹 | 45 | 46 | ---- 47 | 48 | #### 数据共享协议 49 | 50 | > 使用 UnofficialArcaeaAPI 服务将默认您允许 UnofficialArcaeaAPI 收集/记录/脱敏使用 您的使用记录,包括且不限于 Arcaea 用户名、游玩记录等; 51 | > 52 | > **我们将与其他 ArcaeaBot 共享我们从您那里收集的 Arcaea 用户名**。 53 | 54 | ---- 55 | 56 | #### 常见问题 57 | 58 | **1. 为什么需要申请 UnofficialArcaeaAPI Token?如何申请?** 59 | 60 | > **Token 作用** 61 | > 62 | > UnofficialArcaeaAPI 是由 TheSnowfield 与 Awbugl 基于 BotArcAPI 开发的第三方查分API。 63 | > 64 | > 为了保证服务的质量与稳定性,API维护组需要对使用量进行统计,并且对恶意请求进行拦截。 65 | > 66 | > 您申请获得的 Token 需要将其填入 Andreal 的配置中,以便 API 维护组对您的使用情况进行统计。 67 | > 68 | > UnofficialArcaeaAPI 的部分接口不会响应无 Token 请求。为保证您的正常使用,我们建议您申请 Token。 69 | 70 | > **申请流程** 71 | > 72 | > 请在 QQ / Discord 私聊联系 Awbugl (或加入 IO鸽子窝 `191234485`),并提供 用途、预计每日峰值调用次数 (如果是 Bot,下同)、 73 | > Bot 代称及依赖的框架; 74 | > 75 | > Token 申请一般会在5个工作日内回复。友善交流(可能)会提升处理效率;Token 申请成功前请不要退群,以免无法联系您。 76 | > 77 | > 请在 Token 申请成功后加入 IOLab QA Center `574250621`,AUA/Andreal 技术问题请在 QA 群提问。 78 | 79 | ---- 80 | **2. Bot 都有哪些指令可用?** 81 | 82 | > 请查看 [Andreal Wiki](https://www.showdoc.com.cn/andrea)。 83 | 84 | ---- 85 | **3. 为什么部署之后 Bot 无法查分?** 86 | 87 | > 请检查配置项是否正确填写;Andreal 是否为最新版本。 88 | 89 | ---- 90 | **4. 无法访问 Github 下载文件?** 91 | 92 | > IO鸽子窝 `191234485` 提供文件副本下载。 93 | 94 | ---- 95 | **5. 用户信息在哪里存储?** 96 | 97 | > Andreal 使用本地 sqlite 存储用户数据。由于**数据共享协议**,我们将共享用户的 Arcaea 用户名、游玩记录等,请知悉。 98 | 99 | ---- 100 | **6. Bot 是怎么实现 Arcaea 查分的?** 101 | 102 | > 请查看 [相关专栏](https://www.bilibili.com/read/cv15871643)。 103 | 104 | ---- 105 | **7. 使用 Andreal 有何限制?** 106 | 107 | > 除了违法行为与商业盈利行为以外,您可以任意应用这份开源项目。 108 | 109 | ---- 110 | 111 | #### 赞助 UnofficialArcaeaAPI 112 | 113 | ##### 赞助所得将全部用于API的服务器维护。 114 | 115 | > [爱发电](https://afdian.net/a/Awbugl)。 116 | 117 | ---- 118 | 119 | #### 感谢 120 | 121 | > 本项目的 Arcaea 数据来源于 UnofficialArcaeaAPI。 122 | > 123 | > 本项目的图片UI设计来源于 GNAQ、linwenxuan04、雨笙Fracture (按首字母排序)。 124 | > 125 | > 感谢所有赞助者的支持。 126 | 127 | 128 | #### 特别致谢 129 | 130 | 特别致谢 JetBrains 为开发人员提供了免费的开源许可证。 131 | 132 | [](https://jb.gg/OpenSourceSupport) 133 | -------------------------------------------------------------------------------- /UpdateLog.md: -------------------------------------------------------------------------------- 1 | ### 2023.5.8 v0.5.6 2 | 3 | - Konata更新,尝试修复T544、T545导致的登录失败问题。 4 | - 修复B30命中缓存时回复不正确的问题。 5 | 6 | ### 2023.5.6 v0.5.5 7 | 8 | - 接入UAA。弃用ALA相关配置项。 9 | 10 | ### 2023.4.1 v0.5.3 11 | 12 | ``` 13 | 人工智能真是个好东西。 14 | 15 | AI将重新定义Arcaea的未来——无论是谱面、故事,还是音乐本身,都将改变。 16 | 我们早就将这项技术运用自如,过去的几年间,AI广泛运用于玩家个人潜力值的计算过程。我们相信在不久的将来,人工智能将为Arcaea带来一场彻底的变革! 17 | 18 | 拭目以待吧! 19 | ``` 20 | 21 | - 接入LowiroGPT(Ai酱)。 22 | 23 | ### 2023.3.18 v0.5.2 24 | 25 | - 修改了默认的设备信息和版本。 26 | 27 | ### 2022.12.22 v0.5.1 28 | 29 | - 优化指令匹配处理逻辑,提高响应速度; 30 | - 提前适配AUA新的状态码处理方案。 31 | 32 | ### 2022.11.18 v0.5.0 33 | 34 | - 重构图片素材资源管理方案,在启动前会检查文件缺失等相关问题; 35 | - 在登录失败后添加删除BotInfo文件的相关提示; 36 | - 使用滑块验证助手的 '验证码-服务器' 逻辑代替原 '扫码-复制ticket' 逻辑; 37 | - 修改了默认的设备信息和版本; 38 | - 新增AndroidPad、iPad协议; 39 | - 不同的账号现在可使用不同的协议登录,之前的账号需要手动修改BotInfo切换协议(默认为Android协议); 40 | - 增加进程检查,防止意外多开; 41 | - 微调了部分UI界面。 42 | 43 | ### 2022.10.7 v0.4.8 44 | 45 | - 添加Songlist本地缓存:在AUA不可用时会转用缓存。 46 | - 屏蔽了部分来自Konata的异常信息。 47 | 48 | ### 2022.9.3 v0.4.7 49 | 50 | - 修复了掉线后重新登录可能导致的UI停止响应问题; 51 | - 修复了Webview文件未正确安装时导致的意外报错; 52 | - 添加新Bot状态:在线(群消息受限)、新事件:Bot风控事件。 53 | - 添加特性:在登录环境异常时进行自动重试。 54 | 55 | ### 2022.8.20 v0.4.6 56 | 57 | - 提前适配AUA新版token验证方案。 58 | 59 | ### 2022.8.11 v0.4.5 60 | 61 | - 由于上游数据来源之一(sekai.best)可用性较低,移除了Pjsk模块。 62 | 63 | ### 2022.8.11 v0.4.4 64 | 65 | - 修复了Botinfo文件不会覆盖缓存文件的Bug; 66 | - 优化了Bot登录时的逻辑; 67 | - 修改了使用滑块助手时的UI; 68 | 69 | ### 2022.7.25 v0.4.3 70 | 71 | - 修复了部分曲目难度解析出错的问题; 72 | - 修改了滑块验证相关逻辑:现在可选择webview验证/滑块助手验证。 73 | 74 | ### 2022.7.18 v0.4.2 75 | 76 | - 修复了ptt>=13.00时的新ptt背景图白底的问题; 77 | - 修复了AUA远程文件不存在时导致的崩溃问题; 78 | - 修复了lasteternity的相关问题。 79 | 80 | ### 2022.7.7 v0.4.1 81 | 82 | - 添加Achromic Side的对应背景; 83 | - 添加ptt>=13.00时的新ptt背景图; 84 | - 修复了与Arcaea4.0相关的部分崩溃问题。 85 | 86 | ### 2022.6.13 v0.4.0-fixpatch-2 87 | 88 | - 修复Bot被禁言时发送消息可能的崩溃问题。 89 | 90 | ### 2022.6.4 v0.4.0-fixpatch 91 | 92 | - 修复图片上传时可能的问题; 93 | 注:若服务器无edge浏览器,可下载安装群文件的webview2installer 94 | 95 | ### 2022.6.3 v0.4.0 96 | 97 | **与0.3.0版本的自定义回复文件不兼容!** 98 | 99 | ###### 从旧版本迁移方案:复制./Config/BotInfo文件夹&手动修改config.json填写账号 100 | 101 | - 使用wpf构建全新的操作界面; 102 | - 新增支持手表协议; 103 | - 添加/a chart指令(2D谱面预览); 104 | - 添加/remove指令(Master指令,强制退出某群); 105 | - 当Bot被禁言时自动退群; 106 | - 修复了pjsk模块的别名相关问题。 107 | 108 | ### 2022.5.7 v0.3.0-fixpatch-1 109 | 110 | - 修复别名匹配的问题; 111 | - 修复查询最高分数时的难度解析问题; 112 | 113 | ### 2022.4.28 v0.3.0 114 | 115 | - 适配新版AUA。 116 | 117 | ### 2022.4.16 v0.2.9 118 | 119 | - 修复BotMaster不能触发/dismiss的问题; 120 | - 修复pjsk模块初始化时可能的问题。 121 | 122 | ### 2022.4.15 v0.2.8 123 | 124 | - 修复指令长度解析有误的问题; 125 | - 修复/a b40初始化时的问题。 126 | 127 | ### 2022.4.15 v0.2.7 128 | 129 | - 加入/a b40及相应背景图。 感谢linwenxuan04的UI设计; 130 | - 修复pjsk数据初始化时可能的问题。 131 | 132 | ### 2022.4.10 v0.2.6 133 | 134 | - 增加Bot离线/冻结提示; 135 | - config.json 添加配置项 group_inviter_whitelist;(Bot会自动同意来自白名单(以及Master)的群邀请) 136 | 137 | ### 2022.4.9 v0.2.5 138 | 139 | - 在config.json中自行配置中加入enable_handle_message,在风控时可暂时关闭消息处理; 140 | - 修复解析私聊消息时偶尔触发的Bug (Konata 0.3.1); 141 | 142 | ### 2022.4.6 v0.2.4 143 | 144 | - 在config.json中加入approve_settings:配置bot是否自动同意好友、群邀请。 145 | 146 | ### 2022.3.31 v0.2.3 147 | 148 | - 在config.json中自行配置中加入master;master可调用 /echo 和 /update 两条指令。 149 | - 修复读取arcaea曲绘时可能的访问冲突问题; 150 | - 修复别名查询的显示问题; 151 | - 自定义回复模板增加退群回复; 152 | - /dismiss指令现在只会在bot被管理员、群主@的情况下才生效。 153 | --------------------------------------------------------------------------------