using System.Collections;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using System.Xaml;
using System.Xml.Linq;
using Microsoft.VisualBasic;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.IO;
using PCL.Core.Logging;
using PCL.Core.Utils;
using PCL.Core.Utils.Codecs;
using PCL.Core.Utils.Hash;
using PCL.Core.Utils.OS;
using Brush = System.Windows.Media.Brush;
using Color = System.Windows.Media.Color;
using ColorConverter = System.Windows.Media.ColorConverter;
using Size = System.Windows.Size;
namespace PCL;
public static class ModBase
{
#region 声明
// 下列版本信息由更新器自动修改
public static readonly string versionBaseName = Basics.VersionName;
public static readonly string versionStandardCode = Basics.Metadata.Version.StandardVersion;
public static readonly string upstreamVersion = Basics.Metadata.Version.UpstreamVersion;
public static readonly string commitHash = Basics.Metadata.Version.Commit;
public static readonly string commitHashShort = Basics.Metadata.Version.CommitDigest;
public static readonly int versionCode = Basics.VersionCode;
#if DEBUG
public const string versionBranchName = "Debug";
public const string versionBranchCode = "100";
#elif DEBUGCI
public const string versionBranchName = "CI";
public const string versionBranchCode = "50";
#else
public const string versionBranchName = "Publish";
public const string versionBranchCode = "0";
#endif
///
/// 主窗口句柄。
///
public static nint frmHandle;
// 龙猫味石山小记: 用最不靠谱的实现写出能跑的代码 (AppDomain.CurrentDomain.SetupInformation.ApplicationBase 获取到的是当前工作目录而不是可执行文件所在目录)
///
/// 程序可执行文件所在目录,以“\”结尾。
///
public static readonly string exePath = (Basics.ExecutableDirectory.EndsWith(@"\")
? Basics.ExecutableDirectory
: Basics.ExecutableDirectory + @"\");
///
/// 程序内嵌图片文件夹路径,以“/”结尾。
///
public static readonly string pathImage = "pack://application:,,,/Plain Craft Launcher 2;component/Images/";
///
/// 当前程序的语言。
///
public static string currentLang = "zh_CN";
///
/// 设置对象。
///
public static ModSetup setup = new();
///
/// 程序的打开计时。
///
public static long applicationStartTick = TimeUtils.GetTimeTick();
///
/// 程序打开时的时间。
///
public static DateTime applicationOpenTime = DateTime.Now;
///
/// 程序是否已结束。
///
public static bool isProgramEnded = false;
///
/// 程序的缓存文件夹路径,以 \ 结尾。
///
public static string pathTemp = Paths.Temp + @"\";
///
/// AppData 中的 PCL 文件夹路径,以 \ 结尾。
///
public static string pathAppdata = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PCL") + @"\";
///
/// AppData 中的 PCLCE 配置文件夹路径,以 \ 结尾。
///
public static string pathAppdataConfig = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) +
(versionBranchName == "Debug" ? @"\.pclcedebug\" : @"\.pclce\");
#endregion
#region 自定义类
///
/// 支持小数与常见类型隐式转换的颜色。
///
public class MyColor
{
public double a = 255d;
public double b;
public double g;
public double r;
// 构造函数
public MyColor()
{
}
public MyColor(Color col)
{
a = col.A;
r = col.R;
g = col.G;
b = col.B;
}
public MyColor(string hexString)
{
var stringColor = (Color)ColorConverter.ConvertFromString(hexString);
a = stringColor.A;
r = stringColor.R;
g = stringColor.G;
b = stringColor.B;
}
public MyColor(double newA, MyColor col)
{
a = newA;
r = col.r;
g = col.g;
b = col.b;
}
public MyColor(double newR, double newG, double newB)
{
a = 255d;
r = newR;
g = newG;
b = newB;
}
public MyColor(double newA, double newR, double newG, double newB)
{
a = newA;
r = newR;
g = newG;
b = newB;
}
public MyColor(Brush brush)
{
var color = ((SolidColorBrush)brush).Color;
a = color.A;
r = color.R;
g = color.G;
b = color.B;
}
public MyColor(SolidColorBrush brush)
{
var color = brush.Color;
a = color.A;
r = color.R;
g = color.G;
b = color.B;
}
public MyColor(object obj)
{
if (obj is null)
{
a = 255d;
r = 255d;
g = 255d;
b = 255d;
}
else if (obj is SolidColorBrush)
{
// 避免反复获取 Color 对象造成性能下降
var color = ((SolidColorBrush)obj).Color;
a = color.A;
r = color.R;
g = color.G;
b = color.B;
}
else
{
a = Convert.ToDouble(((dynamic)obj).A);
r = Convert.ToDouble(((dynamic)obj).R);
g = Convert.ToDouble(((dynamic)obj).G);
b = Convert.ToDouble(((dynamic)obj).B);
}
}
// 类型转换
public static implicit operator MyColor(string str)
{
return new MyColor(str);
}
public static implicit operator MyColor(Color col)
{
return new MyColor(col);
}
public static implicit operator Color(MyColor conv)
{
return Color.FromArgb(MathByte(conv.a), MathByte(conv.r), MathByte(conv.g), MathByte(conv.b));
}
public static implicit operator System.Drawing.Color(MyColor conv)
{
return System.Drawing.Color.FromArgb(MathByte(conv.a), MathByte(conv.r), MathByte(conv.g),
MathByte(conv.b));
}
public static implicit operator MyColor(SolidColorBrush bru)
{
return new MyColor(bru.Color);
}
public static implicit operator SolidColorBrush(MyColor conv)
{
return new SolidColorBrush(Color.FromArgb(MathByte(conv.a), MathByte(conv.r), MathByte(conv.g),
MathByte(conv.b)));
}
public static implicit operator MyColor(Brush bru)
{
return new MyColor(bru);
}
public static implicit operator Brush(MyColor conv)
{
return new SolidColorBrush(Color.FromArgb(MathByte(conv.a), MathByte(conv.r), MathByte(conv.g),
MathByte(conv.b)));
}
// 颜色运算
public static MyColor operator +(MyColor a, MyColor b)
{
return new MyColor { a = a.a + b.a, b = a.b + b.b, g = a.g + b.g, r = a.r + b.r };
}
public static MyColor operator -(MyColor a, MyColor b)
{
return new MyColor { a = a.a - b.a, b = a.b - b.b, g = a.g - b.g, r = a.r - b.r };
}
public static MyColor operator *(MyColor a, double b)
{
return new MyColor { a = a.a * b, b = a.b * b, g = a.g * b, r = a.r * b };
}
public static MyColor operator /(MyColor a, double b)
{
return new MyColor { a = a.a / b, b = a.b / b, g = a.g / b, r = a.r / b };
}
public static bool operator ==(MyColor a, MyColor b)
{
if (a is null && b is null)
return true;
if (a is null || b is null)
return false;
return a.a == b.a && a.r == b.r && a.g == b.g && a.b == b.b;
}
public static bool operator !=(MyColor a, MyColor b)
{
if (a is null && b is null)
return false;
if (a is null || b is null)
return true;
return !(a.a == b.a && a.r == b.r && a.g == b.g && a.b == b.b);
}
// HSL
public double Hue(double v1, double v2, double vH)
{
if (vH < 0d)
vH += 1d;
if (vH > 1d)
vH -= 1d;
if (vH < 0.16667d)
return v1 + (v2 - v1) * 6d * vH;
if (vH < 0.5d)
return v2;
if (vH < 0.66667d)
return v1 + (v2 - v1) * (4d - vH * 6d);
return v1;
}
public MyColor FromHSL(double sH, double sS, double sL)
{
if (sS == 0d)
{
r = sL * 2.55d;
g = r;
b = r;
}
else
{
var h = sH / 360d;
var s = sS / 100d;
var l = sL / 100d;
s = l < 0.5d ? s * l + l : s * (1.0d - l) + l;
l = 2d * l - s;
r = 255d * Hue(l, s, h + 1d / 3d);
g = 255d * Hue(l, s, h);
b = 255d * Hue(l, s, h - 1d / 3d);
}
a = 255d;
return this;
}
public MyColor FromHSL2(double sH, double sS, double sL)
{
if (sS == 0d)
{
r = sL * 2.55d;
g = r;
b = r;
}
else
{
// 初始化
sH = (sH + 3600000d) % 360d;
var cent = new[]
{
+0.1d, -0.06d, -0.3d, -0.19d, -0.15d, -0.24d, -0.32d, -0.09d, +0.18d, +0.05d, -0.12d, -0.02d, +0.1d,
-0.06d
}; // 0, 30, 60
// 90, 120, 150
// 180, 210, 240
// 270, 300, 330
// 最后两位与前两位一致,加是变亮,减是变暗
// 计算色调对应的亮度片区
var center = sH / 30.0d;
var intCenter = (int)Math.Round(Math.Floor(center)); // 亮度片区编号
center = 50d -
((1d - center + intCenter) * cent[intCenter] + (center - intCenter) * cent[intCenter + 1]) *
sS;
// center = 50 + (cent(intCenter) + (center - intCenter) * (cent(intCenter + 1) - cent(intCenter))) * sS
sL = (sL < center ? sL / center : 1d + (sL - center) / (100d - center)) * 50d;
FromHSL(sH, sS, sL);
}
a = 255d;
return this;
}
public MyColor Alpha(double sA)
{
a = sA;
return this;
}
public override string ToString()
{
return "(" + a + "," + r + "," + g + "," + b + ")";
}
public override bool Equals(object obj)
{
return obj is MyColor other && a == other.a && r == other.r && g == other.g && b == other.b;
}
}
///
/// 支持负数与浮点数的矩形。
///
public class MyRect
{
// 构造函数
public MyRect()
{
}
public MyRect(double left, double top, double width, double height)
{
Left = left;
Top = top;
Width = width;
Height = height;
}
// 属性
public double Width { get; set; }
public double Height { get; set; }
public double Left { get; set; }
public double Top { get; set; }
}
///
/// 模块加载状态枚举。
///
public enum LoadState
{
Waiting,
Loading,
Finished,
Failed,
Aborted
}
///
/// 执行返回值。
///
public enum ProcessReturnValues
{
///
/// 执行成功,或进程被中断。
///
Aborted = -1,
///
/// 执行成功。
///
Success = 0,
///
/// 执行失败。
///
Fail = 1,
///
/// 执行时出现未经处理的异常。
///
Exception = 2,
///
/// 执行超时。
///
Timeout = 3,
///
/// 取消执行。可能是由于不满足执行的前置条件。
///
Cancel = 4,
///
/// 任务成功完成。
///
TaskDone = 5
}
///
/// 可以使用 Equals 和等号的 List。
///
public class EqualableList : List
{
public override bool Equals(object obj)
{
if (obj as List is null)
// 类型不同
return false;
// 类型相同
var objList = (List)obj;
if (objList.Count != Count)
return false;
for (int i = 0, loopTo = objList.Count - 1; i <= loopTo; i++)
if (!objList[i].Equals(this[i]))
return false;
return true;
}
public static bool operator ==(EqualableList left, EqualableList right)
{
return EqualityComparer>.Default.Equals(left, right);
}
public static bool operator !=(EqualableList left, EqualableList right)
{
return !(left == right);
}
}
#endregion
#region 数学
///
/// 2~65 进制的转换。
///
public static string RadixConvert(string input, int fromRadix, int toRadix)
{
const string digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/+=";
// 零与负数的处理
if (string.IsNullOrEmpty(input))
return "0";
var isNegative = input.StartsWithF("-");
if (isNegative)
input = input.TrimStart('-');
// 转换为十进制
var realNum = 0L;
var scale = 1L;
foreach (var digit in input.Reverse().Select(l => digits.IndexOfF(l.ToString())))
{
realNum += digit * scale;
scale *= fromRadix;
}
// 转换为指定进制
var result = "";
while (realNum > 0L)
{
var newNum = (int)(realNum % toRadix);
realNum = (long)Math.Round((realNum - newNum) / (double)toRadix);
result = digits[newNum] + result;
}
// 负数的结束处理与返回
return (isNegative ? "-" : "") + result;
}
///
/// 计算二阶贝塞尔曲线。
///
public static double MathBezier(double x, double x1, double y1, double x2, double y2, double acc = 0.01d)
{
if (x <= 0d || double.IsNaN(x)) return 0d;
if (x >= 1d) return 1d;
double a, b;
a = x;
do
{
b = 3 * a * ((0.33333333 + x1 - x2) * a * a + (x2 - 2 * x1) * a + x1);
a += (x - b) * 0.5;
} while (!(Math.Abs(b - x) < acc)); // 精度
return 3 * a * ((0.33333333 + y1 - y2) * a * a + (y2 - 2 * y1) * a + y1);
}
///
/// 将一个数字限制为 0~255 的 Byte 值。
///
public static byte MathByte(double d)
{
if (d < 0d)
d = 0d;
if (d > 255d)
d = 255d;
return (byte)Math.Round(Math.Round(d));
}
///
/// 提供 MyColor 类型支持的 Math.Round。
///
public static MyColor MathRound(MyColor col, int w = 0)
{
return new MyColor
{ a = Math.Round(col.a, w), r = Math.Round(col.r, w), g = Math.Round(col.g, w), b = Math.Round(col.b, w) };
}
///
/// 获取两数间的百分比。小数点精确到 6 位。
///
///
public static double MathPercent(double valueA, double valueB, double percent)
{
return Math.Round(valueA * (1d - percent) + valueB * percent, 6); // 解决 Double 计算错误
}
///
/// 获取两颜色间的百分比,根据 RGB 计算。小数点精确到 6 位。
///
public static MyColor MathPercent(MyColor valueA, MyColor valueB, double percent)
{
return MathRound(valueA * (1d - percent) + valueB * percent, 6); // 解决Double计算错误
}
///
/// 将数值限定在某个范围内。
///
public static double MathClamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
///
/// 符号函数。
///
public static int MathSgn(double value)
{
if (value == 0d) return 0;
if (value > 0d) return 1;
return -1;
}
#endregion
#region 文件
// =============================
// ini
// =============================
private static readonly ConcurrentDictionary> iniCache = new();
///
/// 清除某 ini 文件的运行时缓存。
///
/// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。
public static void IniClearCache(string fileName)
{
if (!fileName.Contains(@":\"))
fileName = $@"{exePath}PCL\{fileName}.ini";
if (iniCache.ContainsKey(fileName))
iniCache.Remove(fileName, out _);
}
///
/// 获取 ini 文件缓存。如果没有,则新读取 ini 文件内容。
/// 在文件不存在或读取失败时返回 Nothing。
///
/// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。
private static ConcurrentDictionary IniGetContent(string fileName)
{
try
{
// 还原文件路径
if (!fileName.Contains(@":\"))
fileName = $@"{exePath}PCL\{fileName}.ini";
// 检索缓存
if (iniCache.ContainsKey(fileName))
return iniCache[fileName];
// 读取文件
if (!File.Exists(fileName))
return null;
var ini = new ConcurrentDictionary();
foreach (var line in ReadFile(fileName)
.Split("\r\n".ToArray(), StringSplitOptions.RemoveEmptyEntries))
{
var index = line.IndexOfF(":");
if (index > 0)
ini[line.Substring(0, index)] = line.Substring(index + 1); // 可能会有重复键,见 #3616
}
iniCache[fileName] = ini;
return ini;
}
catch (Exception ex)
{
Log(ex, $"生成 ini 文件缓存失败({fileName})", LogLevel.Hint);
return null;
}
}
///
/// 读取 ini 文件。这可能会使用到缓存。
///
/// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。
/// 键。
/// 没有找到键时返回的默认值。
public static string ReadIni(string fileName, string key, string defaultValue = "")
{
var content = IniGetContent(fileName);
if (content is null || !content.ContainsKey(key))
return defaultValue;
return content[key];
}
///
/// 判断 ini 文件中是否包含某个键。这可能会使用到缓存。
///
public static bool HasIniKey(string fileName, string key)
{
var content = IniGetContent(fileName);
return content is not null && content.ContainsKey(key);
}
///
/// 从 ini 文件中移除某个键。这会更新缓存。
///
public static void DeleteIniKey(string fileName, string key)
{
WriteIni(fileName, key, null);
}
///
/// 写入 ini 文件,这会更新缓存。
/// 若 Value 为 Nothing,则删除该键。
///
/// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。
/// 键。
/// 值。
///
public static void WriteIni(string fileName, string key, string value)
{
try
{
// 预处理
if (key.Contains(":"))
throw new Exception($"尝试写入 ini 文件 {fileName} 的键名中包含了冒号:{key}");
key = key.Replace("\r", "").Replace("\n", "");
value = value?.Replace("\r", "").Replace("\n", "");
// 防止争用
lock (writeIniLock)
{
// 获取目前文件
var content = IniGetContent(fileName);
if (content is null)
content = new ConcurrentDictionary();
// 更新值
if (value is null)
{
if (!content.ContainsKey(key))
return; // 无需处理
content.Remove(key, out _);
}
else
{
if (content.ContainsKey(key) && (content[key] ?? "") == (value ?? ""))
return; // 无需处理
content[key] = value;
}
// 写入文件
var fileContent = new StringBuilder();
foreach (var pair in content)
{
fileContent.Append(pair.Key);
fileContent.Append(":");
fileContent.Append(pair.Value);
fileContent.Append("\r\n");
}
if (!fileName.Contains(@":\"))
fileName = $@"{exePath}PCL\{fileName}.ini";
WriteFile(fileName, fileContent.ToString());
}
}
catch (Exception ex)
{
Log(ex, $"写入文件失败({fileName} → {key}:{value})", LogLevel.Hint);
}
}
private static readonly object writeIniLock = new();
// 路径处理
///
/// 从文件路径或者 Url 获取不包含文件名的路径,或获取文件夹的父文件夹路径。
/// 取决于原路径格式,路径以 / 或 \ 结尾。
/// 不包含路径将会抛出异常。
///
public static string GetPathFromFullPath(string filePath)
{
string getPathFromFullPathRet = default;
if (!(filePath.Contains(@"\") || filePath.Contains("/")))
throw new Exception("不包含路径:" + filePath);
if (filePath.EndsWithF(@"\") || filePath.EndsWithF("/"))
{
// 是文件夹路径
var isRight = filePath.EndsWithF(@"\");
filePath = filePath.Substring(0, filePath.Length - 1);
getPathFromFullPathRet = filePath.Substring(0, filePath.LastIndexOfAny(new[] { '\\', '/' })) +
(isRight ? @"\" : "/");
}
else
{
// 是文件路径
getPathFromFullPathRet = filePath.Substring(0, filePath.LastIndexOfAny(new[] { '\\', '/' }) + 1);
if (string.IsNullOrEmpty(getPathFromFullPathRet))
throw new Exception("不包含路径:" + filePath);
}
return getPathFromFullPathRet;
}
///
/// 从文件路径或者 Url 获取不包含路径的文件名。不包含文件名将会抛出异常。
///
public static string GetFileNameFromPath(string filePath)
{
filePath = filePath.Replace("/", @"\");
if (filePath.EndsWithF(@"\"))
throw new Exception("不包含文件名:" + filePath);
if (filePath.Contains("?"))
filePath = filePath.Substring(0, filePath.IndexOfF("?")); // 去掉网络参数后的 ?
if (filePath.Contains(@"\"))
filePath = filePath.Substring(filePath.LastIndexOfF(@"\") + 1);
var length = filePath.Length;
if (length == 0)
throw new Exception("不包含文件名:" + filePath);
if (length > 250)
throw new PathTooLongException("文件名过长:" + filePath);
return filePath;
}
///
/// 从文件路径或者 Url 获取不包含路径与扩展名的文件名。不包含文件名将会抛出异常。
///
public static string GetFileNameWithoutExtentionFromPath(string filePath)
{
return Path.GetFileNameWithoutExtension(filePath);
}
///
/// 从文件夹路径获取文件夹名。
///
public static string GetFolderNameFromPath(string folderPath)
{
if (folderPath.EndsWithF(@":\") || folderPath.EndsWithF(@":\\"))
return folderPath.Substring(0, 1);
if (folderPath.EndsWithF(@"\") || folderPath.EndsWithF("/"))
folderPath = folderPath.Substring(0, folderPath.Length - 1);
return GetFileNameFromPath(folderPath);
}
// 读取、写入、复制文件
///
/// 复制文件。会自动创建文件夹、会覆盖已有的文件。
///
public static void CopyFile(string fromPath, string toPath)
{
try
{
// 还原文件路径
if (!fromPath.Contains(@":\"))
fromPath = exePath + fromPath;
if (!toPath.Contains(@":\"))
toPath = exePath + toPath;
// 如果复制同一个文件则跳过
if ((fromPath ?? "") == (toPath ?? ""))
return;
// 确保目录存在
Directory.CreateDirectory(GetPathFromFullPath(toPath));
// 复制文件
File.Copy(fromPath, toPath, true);
}
catch (Exception ex)
{
throw new Exception("复制文件出错:" + fromPath + " → " + toPath, ex);
}
}
///
/// 读取文件,如果失败则返回空数组。
///
public static byte[] ReadFileBytes(string filePath, Encoding encoding = null)
{
try
{
// 还原文件路径
if (!filePath.Contains(@":\"))
filePath = exePath + filePath;
if (File.Exists(filePath))
using (var readStream =
new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var ms = new MemoryStream())
{
readStream.CopyTo(ms);
return ms.ToArray();
}
}
Log("[System] 欲读取的文件不存在,已返回空内容:" + filePath);
return Array.Empty();
}
catch (Exception ex)
{
Log(ex, "读取文件出错:" + filePath);
return Array.Empty();
}
}
///
/// 读取文件,如果失败则返回空字符串。
///
/// 文件完整或相对路径。
public static string ReadFile(string filePath, Encoding encoding = null)
{
string readFileRet = default;
var fileBytes = ReadFileBytes(filePath);
readFileRet = encoding is null ? DecodeBytes(fileBytes) : encoding.GetString(fileBytes);
return readFileRet;
}
///
/// 读取流中的所有文本。
///
public static string ReadFile(Stream stream, Encoding encoding = null)
{
try
{
var readedContent = new MemoryStream();
stream.CopyTo(readedContent);
var bts = readedContent.ToArray();
return (encoding ?? EncodingDetector.DetectEncoding(bts)).GetString(bts);
}
catch (Exception ex)
{
Log(ex, "读取流出错");
return "";
}
}
///
/// 写入文件。
///
/// 文件完整或相对路径。
/// 文件内容。
/// 是否将文件内容追加到当前文件,而不是覆盖它。
public static void WriteFile(string filePath, string text, bool append = false, Encoding? encoding = null)
{
// 处理相对路径
if (!filePath.Contains(@":\"))
filePath = exePath + filePath;
// 确保目录存在
Directory.CreateDirectory(GetPathFromFullPath(filePath));
// 写入文件
if (append)
// 追加目前文件
using (var writer = new StreamWriter(filePath, true,
encoding ?? EncodingDetector.DetectEncoding(ReadFileBytes(filePath))))
{
writer.Write(text);
}
else
{
// 直接写入字节
var bytes = encoding is null ? new UTF8Encoding(false).GetBytes(text) : encoding.GetBytes(text);
var tempPath = filePath + ".pcltmp." + Guid.NewGuid().ToString("N");
File.WriteAllBytes(tempPath, bytes);
File.Move(tempPath, filePath, true);
}
}
///
/// 写入文件。
/// 如果 CanThrow 设置为 False,返回是否写入成功。
///
/// 文件完整或相对路径。
/// 文件内容。
/// 是否将文件内容追加到当前文件,而不是覆盖它。
public static void WriteFile(string filePath, byte[] content, bool append = false)
{
// 处理相对路径
if (!filePath.Contains(@":\"))
filePath = exePath + filePath;
// 确保目录存在
Directory.CreateDirectory(GetPathFromFullPath(filePath));
// 写入文件
File.WriteAllBytes(filePath, content);
}
///
/// 将流写入文件。
///
/// 文件完整或相对路径。
public static bool WriteFile(string filePath, Stream stream)
{
try
{
// 还原文件路径
if (!filePath.Contains(@":\"))
filePath = exePath + filePath;
// 确保目录存在
Directory.CreateDirectory(GetPathFromFullPath(filePath));
// 读取流
using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
fs.SetLength(0L);
stream.CopyTo(fs);
}
return true;
}
catch (Exception ex)
{
Log(ex, "保存流出错");
return false;
}
}
///
/// 解码 Bytes。
///
public static string DecodeBytes(byte[] bytes)
{
var length = bytes.Length;
if (length < 3)
return Encoding.UTF8.GetString(bytes);
// 根据 BOM 判断编码
if (bytes[0] >= 0xEF)
{
// 有 BOM 类型
if (bytes[0] == 0xEF && bytes[1] == 0xBB) return Encoding.UTF8.GetString(bytes, 3, length - 3);
if (bytes[0] == 0xFE && bytes[1] == 0xFF) return Encoding.BigEndianUnicode.GetString(bytes, 3, length - 3);
if (bytes[0] == 0xFF && bytes[1] == 0xFE) return Encoding.Unicode.GetString(bytes, 3, length - 3);
return Encoding.GetEncoding("GB18030").GetString(bytes, 3, length - 3);
}
// 无 BOM 文件:GB18030(ANSI)或 UTF8
var uTF8 = Encoding.UTF8.GetString(bytes);
var errorChar = Encoding.UTF8.GetString(new[] { (byte)239, (byte)191, (byte)189 }).ToCharArray()[0];
if (uTF8.Contains(errorChar)) return Encoding.GetEncoding("GB18030").GetString(bytes);
return uTF8;
}
public static object GetHexString(Memory bytes)
{
var sb = new StringBuilder(bytes.Length * 2);
foreach (var c in bytes.Span)
sb.Append(c.ToString("x2"));
return sb.ToString();
}
// 文件校验
///
/// 获取文件 MD5,若失败则返回空字符串。
///
public static string GetFileMD5(string filePath)
{
var retry = false;
Re: ;
try
{
// 获取 MD5
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
return (string)GetHexString(MD5Provider.Instance.ComputeHash(fs));
}
}
catch (Exception ex)
{
if (retry || ex is FileNotFoundException)
{
Log(ex, "获取文件 MD5 失败:" + filePath);
return "";
}
retry = true;
Log(ex, "获取文件 MD5 可重试失败:" + filePath, LogLevel.Normal);
Thread.Sleep(RandomUtils.NextInt(200, 500));
goto Re;
}
}
///
/// 获取文件 SHA512,若失败则返回空字符串。
///
public static string GetFileSHA512(string filePath)
{
var retry = false;
Re: ;
try
{
// '检测该文件是否在下载中,若在下载则放弃检测
// If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return ""
// 获取 SHA512
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
return (string)GetHexString(SHA512Provider.Instance.ComputeHash(fs));
}
}
catch (Exception ex)
{
if (retry || ex is FileNotFoundException)
{
Log(ex, "获取文件 SHA512 失败:" + filePath);
return "";
}
retry = true;
Log(ex, "获取文件 SHA512 可重试失败:" + filePath, LogLevel.Normal);
Thread.Sleep(RandomUtils.NextInt(200, 500));
goto Re;
}
}
///
/// 获取文件 SHA256,若失败则返回空字符串。
///
public static string GetFileSHA256(string filePath)
{
var retry = false;
Re: ;
try
{
// '检测该文件是否在下载中,若在下载则放弃检测
// If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return ""
// 获取 SHA256
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
return (string)GetHexString(SHA256Provider.Instance.ComputeHash(fs));
}
}
catch (Exception ex)
{
if (retry || ex is FileNotFoundException)
{
Log(ex, "获取文件 SHA256 失败:" + filePath);
return "";
}
retry = true;
Log(ex, "获取文件 SHA256 可重试失败:" + filePath, LogLevel.Normal);
Thread.Sleep(RandomUtils.NextInt(200, 500));
goto Re;
}
}
///
/// 获取文件 SHA1,若失败则返回空字符串。
///
public static string GetFileSHA1(string filePath)
{
var retry = false;
Re: ;
try
{
// 获取 SHA1
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
return (string)GetHexString(SHA1Provider.Instance.ComputeHash(fs));
}
}
catch (Exception ex)
{
if (retry || ex is FileNotFoundException)
{
Log(ex, "获取文件 SHA1 失败:" + filePath);
return "";
}
retry = true;
Log(ex, "获取文件 SHA1 可重试失败:" + filePath, LogLevel.Normal);
Thread.Sleep(RandomUtils.NextInt(200, 500));
goto Re;
}
}
///
/// 获取流的 SHA1,若失败则返回空字符串。
///
public static string GetAuthSHA1(Stream inputStream)
{
try
{
return (string)GetHexString(SHA1Provider.Instance.ComputeHash(inputStream));
}
catch (Exception ex)
{
Log(ex, "获取流 SHA1 失败");
return "";
}
}
///
/// 文件的校验规则。
///
public class FileChecker
{
///
/// 文件的准确大小。
/// 不检查则为 -1。
///
public long actualSize = -1;
///
/// 是否可以使用已经存在的文件。
///
public bool canUseExistsFile = true;
///
/// 文件的 MD5、SHA1 或 SHA256。会根据输入字符串的长度自动判断种类。
/// 不检查则为 Nothing。
///
public string hash;
///
/// 是否要求为 JSON 文件。
/// 即,开头结尾必须为 {} 或 []。
///
public bool isJson;
///
/// 文件的最小大小。
/// 不检查则为 -1。
///
public long minSize = -1;
public FileChecker(long minSize = -1, long actualSize = -1, string hash = null, bool canUseExistsFile = true,
bool isJson = false)
{
this.actualSize = actualSize;
this.minSize = minSize;
this.hash = hash;
this.canUseExistsFile = canUseExistsFile;
this.isJson = isJson;
}
///
/// 检查文件。若成功则返回 Nothing,失败则返回错误的描述文本,描述文本不以句号结尾。不会抛出错误。
///
public string Check(string localPath)
{
try
{
Log($"[Checker] 开始校验文件 {localPath}", LogLevel.Developer);
var info = new FileInfo(localPath);
if (!info.Exists)
return "文件不存在:" + localPath;
var fileSize = info.Length;
var errorMessage = new List();
var allowIgnore = false; // 允许相信哈希正确但是大小不正确
if (!string.IsNullOrEmpty(hash))
{
if (hash.Length < 35) // MD5
{
var computedHash = GetFileMD5(localPath);
if ((hash.ToLowerInvariant() ?? "") != (computedHash ?? ""))
errorMessage.Add("文件 MD5 应为 " + hash + ",实际为 " + computedHash);
}
else if (hash.Length == 64) // SHA256
{
var computedHash = GetFileSHA256(localPath);
if ((hash.ToLowerInvariant() ?? "") != (computedHash ?? ""))
errorMessage.Add("文件 SHA256 应为 " + hash + ",实际为 " + computedHash);
}
else // SHA1 (40)
{
var computedHash = GetFileSHA1(localPath);
if ((hash.ToLowerInvariant() ?? "") != (computedHash ?? ""))
errorMessage.Add("文件 SHA1 应为 " + hash + ",实际为 " + computedHash);
}
allowIgnore = errorMessage.Count == 0;
}
if (actualSize >= 0L && actualSize != fileSize && !allowIgnore) // 不允许忽略大小不正确的情况
errorMessage.Add($"文件大小应为 {actualSize} B,实际为 {fileSize} B" +
(fileSize < 2000L ? ",内容为" + ReadFile(localPath) : ""));
if (minSize >= 0L && minSize > fileSize)
errorMessage.Add($"文件大小应大于 {minSize} B,实际为 {fileSize} B" +
(fileSize < 2000L ? ",内容为:" + ReadFile(localPath) : ""));
if (isJson)
{
var content = ReadFile(localPath);
if (string.IsNullOrEmpty(content))
throw new Exception("读取到的文件为空");
try
{
GetJson(content);
}
catch (Exception ex)
{
throw new Exception(Lang.Text("Common.Error.InvalidJson"), ex);
}
}
if (errorMessage.Count != 0)
{
errorMessage.Insert(0, $"实际校验地址:{localPath}");
return errorMessage.Join(";");
}
return null;
}
catch (Exception ex)
{
Log(ex, "检查文件出错");
return ex.ToString();
}
}
}
///
/// 等待文件就绪可读,在指定超时时间内轮询检查文件是否存在且内容非空。
///
/// 文件路径。
/// 超时时间(毫秒)。
public static void WaitForFileReady(string filePath, int timeoutMs = 2000)
{
WaitForFileReady(filePath, timeoutMs, false);
}
///
/// 等待文件就绪可读,在指定超时时间内轮询检查文件是否存在且内容非空。
///
/// 文件路径。
/// 超时时间(毫秒)。
/// 是否要求文件为合法 JSON。
public static void WaitForFileReady(string filePath, int timeoutMs, bool requireJson)
{
filePath = filePath.Contains(@":\") ? filePath : exePath + filePath;
var start = Environment.TickCount;
long lastSize = -1;
while (Environment.TickCount - start < timeoutMs)
{
if (File.Exists(filePath))
{
try
{
var info = new FileInfo(filePath);
var size = info.Length;
if (size <= 0)
continue;
if (!requireJson)
{
if (size == lastSize)
return;
lastSize = size;
}
else
{
var content = ReadFile(filePath);
if (!string.IsNullOrEmpty(content) && content.Trim().StartsWith("{"))
return;
}
}
catch (IOException)
{
}
}
Thread.Sleep(50);
}
}
///
/// 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 Jar 以 zip 方式解压。
/// 会尝试创建,但不会清空目标文件夹。
///
public static void ExtractFile(string compressFilePath, string destDirectory, Encoding encode = null,
Action progressIncrementHandler = null)
{
Directory.CreateDirectory(destDirectory);
destDirectory = Path.GetFullPath(destDirectory);
if (!destDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
destDirectory += Path.DirectorySeparatorChar.ToString();
if (compressFilePath.EndsWithF(".gz", true))
// 以 gz 方式解压
using (var compressedFile = new FileStream(compressFilePath, FileMode.Open, FileAccess.Read))
{
using (var decompressStream = new GZipStream(compressedFile, CompressionMode.Decompress))
{
using (var extractFileStream =
new FileStream(
Path.Combine(destDirectory,
GetFileNameFromPath(compressFilePath).ToLower().Replace(".tar", "")
.Replace(".gz", "")), FileMode.OpenOrCreate, FileAccess.Write))
{
decompressStream.CopyTo(extractFileStream);
}
}
}
else
// 以 zip 方式解压
using (var archive = ZipFile.Open(compressFilePath, ZipArchiveMode.Read,
encode ?? Encoding.GetEncoding("GB18030")))
{
var totalCount = archive.Entries.Count;
foreach (var entry in archive.Entries)
{
if (progressIncrementHandler is not null)
progressIncrementHandler(1d / totalCount);
var destinationPath = Path.GetFullPath(Path.Combine(destDirectory, entry.FullName));
if (!destinationPath.StartsWithF(destDirectory))
throw new Exception(
$"解压文件 {entry.FullName} 错误:解压文件路径 {destinationPath} 不在目标目录 {destDirectory} 内");
if (destinationPath.EndsWithF(@"\") || destinationPath.EndsWithF("/"))
{
}
else
{
Directory.CreateDirectory(GetPathFromFullPath(destinationPath));
entry.ExtractToFile(destinationPath, true);
}
}
}
}
///
/// 删除文件夹,返回删除的文件个数。通过参数选择是否抛出异常。
///
public static int DeleteDirectory(string path, bool ignoreIssue = false)
{
if (!Directory.Exists(path))
return 0;
var deletedCount = 0;
string[] files;
try
{
files = Directory.GetFiles(path);
}
catch (DirectoryNotFoundException ex) // #4549
{
Log(ex, $"疑似为孤立符号链接,尝试直接删除({path})", LogLevel.Developer);
Directory.Delete(path);
return 0;
}
foreach (var filePath in files)
{
var retriedFile = false;
RetryFile: ;
try
{
File.Delete(filePath);
deletedCount += 1;
}
catch (Exception ex)
{
if (!retriedFile)
{
retriedFile = true;
Log(ex, $"删除文件失败,将在 0.3s 后重试({filePath})");
Thread.Sleep(300);
goto RetryFile;
}
if (ignoreIssue)
Log(ex, "删除单个文件可忽略地失败");
else
throw;
}
}
foreach (var str in Directory.GetDirectories(path))
DeleteDirectory(str, ignoreIssue);
var retriedDir = false;
RetryDir: ;
try
{
Directory.Delete(path, true);
}
catch (Exception ex)
{
if (!retriedDir && !RunInUi())
{
retriedDir = true;
Log(ex, $"删除文件夹失败,将在 0.3s 后重试({path})");
Thread.Sleep(300);
goto RetryDir;
}
if (ignoreIssue)
Log(ex, "删除单个文件夹可忽略地失败");
else
throw;
}
return deletedCount;
}
///
/// 复制文件夹,失败会抛出异常。
///
public static void CopyDirectory(string fromPath, string toPath, Action progressIncrementHandler = null)
{
fromPath = fromPath.Replace("/", @"\");
if (!fromPath.EndsWithF(@"\"))
fromPath += @"\";
toPath = toPath.Replace("/", @"\");
if (!toPath.EndsWithF(@"\"))
toPath += @"\";
var allFiles = EnumerateFiles(fromPath).ToList();
var fileCount = allFiles.Count;
foreach (var file in allFiles)
{
CopyFile(file.FullName, file.FullName.Replace(fromPath, toPath));
if (progressIncrementHandler is not null)
progressIncrementHandler(1d / fileCount);
}
}
///
/// 遍历文件夹中的所有文件。
///
public static IEnumerable EnumerateFiles(string directory)
{
var info = new DirectoryInfo(ShortenPath(directory));
if (!info.Exists)
return new List();
return info.EnumerateFiles("*", SearchOption.AllDirectories);
}
///
/// 若路径长度大于指定值,则将长路径转换为短路径。
///
public static string ShortenPath(string longPath, int shortenThreshold = 247)
{
if (longPath.Length <= shortenThreshold)
return longPath;
var shortPath = new StringBuilder(260);
GetShortPathName(longPath, shortPath, 260);
return shortPath.ToString();
}
public static void MoveDirectory(string sourceDir, string targetDir)
{
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
foreach (var filePath in Directory.GetFiles(sourceDir))
{
var fileName = GetFileNameFromPath(filePath);
File.Move(filePath, Path.Combine(targetDir, fileName));
}
foreach (var dirPath in Directory.GetDirectories(sourceDir))
{
var dirName = GetFolderNameFromPath(dirPath);
MoveDirectory(dirPath, Path.Combine(targetDir, dirName));
}
}
[DllImport("kernel32", EntryPoint = "GetShortPathNameA")]
private static extern int GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, int cchBuffer);
public static void CreateSymbolicLink(string linkPath, string targetPath, int flags)
{
var cMDProcess = new Process();
var linkDPath = ModLaunch.ExtractLinkD();
{
var withBlock = cMDProcess.StartInfo;
withBlock.FileName = linkDPath;
withBlock.Arguments = $"\"{linkPath}\" \"{targetPath}\"";
withBlock.CreateNoWindow = true;
withBlock.UseShellExecute = false;
}
cMDProcess.Start();
while (!cMDProcess.HasExited)
{
}
}
#endregion
#region 文本
public static char vbLQ = Convert.ToChar(8220);
public static char vbRQ = Convert.ToChar(8221);
///
/// 返回一个枚举对应的字符串。
///
/// 一个已经实例化的枚举类型。
public static string GetStringFromEnum(Enum enumData)
{
return Enum.GetName(enumData.GetType(), enumData);
}
///
/// 将文件大小转化为适合的文本形式,如“1.28 M”。
///
/// 以字节为单位的大小表示。
public static string GetString(long fileSize)
{
return ByteStream.GetReadableLength(fileSize, provider: Lang.Culture);
}
///
/// 获取 JSON 对象。
///
public static JsonNode GetJson(string data)
{
try
{
return JsonCompat.ParseNode(data);
}
catch (Exception ex)
{
var dataText = data ?? "";
var length = dataText.Length;
throw new Exception("格式化 JSON 失败:" + (length > 2000
? dataText.Substring(0, 500) + $"...(全长 {length} 个字符)..." + dataText.Substring(length - 500)
: dataText), ex);
}
}
///
/// 将第一个字符转换为大写,其余字符转换为小写。
///
public static string Capitalize(this string word)
{
if (string.IsNullOrEmpty(word))
return word;
return word.Substring(0, 1).ToUpperInvariant() + word.Substring(1).ToLowerInvariant();
}
///
/// 将字符串统一至某个长度,过短则以 Code 将其右侧填充,过长则截取靠左的指定长度。
///
public static string StrFill(string str, string code, byte length)
{
if (str.Length > length)
return str.Substring(0, length);
return str.PadRight(length, code[0]).Substring(str.Length) + str;
}
///
/// 将一个小数显示为固定的小数点后位数形式,将向零取整。
/// 如 12 保留 2 位则输出 12.00,而 95.678 保留 2 位则输出 95.67。
///
public static string StrFillNum(double num, int length)
{
return Lang.Number(num, $"F{length}");
}
///
/// 移除字符串首尾的标点符号、回车,以及括号中、冒号后的补充说明内容。
///
public static object StrTrim(string str, bool removeQuote = true)
{
if (removeQuote)
str = str.Split("(")[0].Split(":")[0].Split("(")[0].Split(":")[0];
return str.Trim('.', '。', '!', ' ', '!', '?', '?', '\r',
'\n');
}
///
/// 连接字符串。
///
public static string Join(this IEnumerable list, string split)
{
var builder = new StringBuilder();
var isFirst = true;
foreach (var element in list)
{
if (isFirst)
isFirst = false;
else
builder.Append(split);
if (element is not null)
builder.Append(element);
}
return builder.ToString();
}
///
/// 分割字符串。
///
public static string[] Split(this string fullStr, string splitStr)
{
if (splitStr.Length == 1) return fullStr.Split(splitStr[0]);
return fullStr.Split(new[] { splitStr }, StringSplitOptions.None);
}
///
/// 获取字符串哈希值。
///
public static ulong GetHash(string str)
{
ulong getHashRet = default;
getHashRet = 5381UL;
for (int i = 0, loopTo = str.Length - 1; i <= loopTo; i++)
getHashRet = (getHashRet << 5) ^ getHashRet ^ str[i];
return getHashRet ^ 0xA98F501BC684032FUL;
}
///
/// 获取字符串 MD5。
///
public static string GetStringMD5(string str)
{
return (string)GetHexString(MD5Provider.Instance.ComputeHash(str));
}
///
/// 检查字符串中的字符是否均为 ASCII 字符。
///
public static bool IsASCII(this string input)
{
return input.All(c => c < 128);
}
///
/// 获取在子字符串第一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024。
/// 如果未找到子字符串则不裁切。
///
public static string BeforeFirst(this string str, string text, bool ignoreCase = false)
{
var pos = string.IsNullOrEmpty(text) ? -1 : str.IndexOfF(text, ignoreCase);
if (pos >= 0) return str.Substring(0, pos);
return str;
}
///
/// 获取在子字符串最后一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024/11。
/// 如果未找到子字符串则不裁切。
///
public static string BeforeLast(this string str, string text, bool ignoreCase = false)
{
var pos = string.IsNullOrEmpty(text) ? -1 : str.LastIndexOfF(text, ignoreCase);
if (pos >= 0) return str.Substring(0, pos);
return str;
}
///
/// 获取在子字符串第一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 11/08。
/// 如果未找到子字符串则不裁切。
///
public static string AfterFirst(this string str, string text, bool ignoreCase = false)
{
var pos = string.IsNullOrEmpty(text) ? -1 : str.IndexOfF(text, ignoreCase);
if (pos >= 0) return str.Substring(pos + text.Length);
return str;
}
///
/// 获取在子字符串最后一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 08。
/// 如果未找到子字符串则不裁切。
///
public static string AfterLast(this string str, string text, bool ignoreCase = false)
{
var pos = string.IsNullOrEmpty(text) ? -1 : str.LastIndexOfF(text, ignoreCase);
if (pos >= 0) return str.Substring(pos + text.Length);
return str;
}
///
/// 获取处于两个子字符串之间的部分,裁切尽可能多的内容。
/// 等效于 AfterLast 后接 BeforeFirst。
/// 如果未找到子字符串则不裁切。
///
public static string Between(this string str, string after, string before, bool ignoreCase = false)
{
var startPos = string.IsNullOrEmpty(after) ? -1 : str.LastIndexOfF(after, ignoreCase);
if (startPos >= 0)
startPos += after.Length;
else
startPos = 0;
var endPos = string.IsNullOrEmpty(before) ? -1 : str.IndexOfF(before, startPos, ignoreCase);
if (endPos >= 0) return str.Substring(startPos, endPos - startPos);
if (startPos > 0) return str.Substring(startPos);
return str;
}
///
/// 高速的 StartsWith。
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool StartsWithF(this string str, string prefix, bool ignoreCase = false)
{
return str.StartsWith(prefix, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
///
/// 高速的 EndsWith。
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EndsWithF(this string str, string suffix, bool ignoreCase = false)
{
return str.EndsWith(suffix, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
///
/// 支持可变大小写判断的 Contains。
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ContainsF(this string str, string subStr, bool ignoreCase = false)
{
return str.IndexOf(subStr, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) >= 0;
}
///
/// 高速的 IndexOf。
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int IndexOfF(this string str, string subStr, bool ignoreCase = false)
{
return str.IndexOf(subStr, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
///
/// 高速的 IndexOf。
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int IndexOfF(this string str, string subStr, int startIndex, bool ignoreCase = false)
{
return str.IndexOf(subStr, startIndex,
ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
///
/// 高速的 LastIndexOf。
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LastIndexOfF(this string str, string subStr, bool ignoreCase = false)
{
return str.LastIndexOf(subStr, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
///
/// 高速的 LastIndexOf。
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LastIndexOfF(this string str, string subStr, int startIndex, bool ignoreCase = false)
{
return str.LastIndexOf(subStr, startIndex,
ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
///
/// 不会报错的 Val。
/// 如果输入有误,返回 0。
///
public static double Val(object str)
{
try
{
return str is "&" ? 0d : Conversion.Val(str);
}
catch
{
return 0d;
}
}
// 转义
///
/// 为字符串进行 XML 转义。
///
public static string EscapeXML(string str)
{
if (str.StartsWithF("{"))
str = "{}" + str; // #4187
return str.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("'", "'")
.Replace("\"", """).Replace("\r\n", "
");
}
///
/// 为字符串进行 Like 关键字转义。
///
public static string EscapeLikePattern(string input)
{
var sb = new StringBuilder();
foreach (var c in input)
switch (c)
{
case '[':
case ']':
case '*':
case '?':
case '#':
{
sb.Append('[').Append(c).Append(']');
break;
}
default:
{
sb.Append(c);
break;
}
}
return sb.ToString();
}
// 正则
///
/// 搜索字符串中的所有正则匹配项。
///
public static List RegexSearch(this string str, string regex, RegexOptions options = RegexOptions.None)
{
List regexSearchRet = default;
try
{
regexSearchRet = new List();
var regexSearchRes = new Regex(regex, options).Matches(str);
if (regexSearchRes is null)
return regexSearchRet;
foreach (Match item in regexSearchRes)
regexSearchRet.Add(item.Value);
}
catch (Exception ex)
{
Log(ex, "正则匹配全部项出错");
return new List();
}
return regexSearchRet;
}
///
/// 搜索字符串中的所有正则匹配项。
///
/// 要搜索的字符串
/// 正则表达式对象
/// 所有匹配项的列表
public static List RegexSearch(this string str, Regex regex)
{
try
{
var result = new List();
foreach (Match item in regex.Matches(str))
{
result.Add(item.Value);
}
return result;
}
catch (Exception ex)
{
Log(ex, "正则匹配全部项出错");
return new List();
}
}
///
/// 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。
///
public static string RegexSeek(this string str, string regex, RegexOptions options = RegexOptions.None)
{
try
{
var result = Regex.Match(str, regex, options).Value;
return string.IsNullOrEmpty(result) ? null : result;
}
catch (Exception ex)
{
Log(ex, "正则匹配第一项出错");
return null;
}
}
///
/// 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。
///
public static string RegexSeek(this string str, Regex regex, RegexOptions options = RegexOptions.None)
{
try
{
var result = regex.Match(str, (int)options).Value;
return string.IsNullOrEmpty(result) ? null : result;
}
catch (Exception ex)
{
Log(ex, "正则匹配第一项出错");
return null;
}
}
///
/// 检查字符串是否匹配某正则模式。
///
public static bool RegexCheck(this string str, string regex, RegexOptions options = RegexOptions.None)
{
try
{
return Regex.IsMatch(str, regex, options);
}
catch (Exception ex)
{
Log(ex, "正则检查出错");
return false;
}
}
///
/// 进行正则替换,会抛出错误。
///
public static string RegexReplace(this string allContents, string searchRegex, string replaceTo,
RegexOptions options = RegexOptions.None)
{
return Regex.Replace(allContents, searchRegex, replaceTo, options);
}
///
/// 对每个正则匹配分别进行替换,会抛出错误。
///
public static string RegexReplaceEach(this string allContents, string searchRegex, MatchEvaluator replaceTo,
RegexOptions options = RegexOptions.None)
{
return Regex.Replace(allContents, searchRegex, replaceTo, options);
}
#endregion
#region 搜索
///
/// 获取搜索文本的相似度。
///
/// 被搜索的长内容。
/// 用户输入的搜索文本。
private static double SearchSimilarity(string source, string query)
{
var qp = 0;
var lenSum = 0d;
source = source.ToLower().Replace(" ", "");
query = query.ToLower().Replace(" ", "");
var sourceLength = source.Length;
var queryLength = query.Length; // 用于计算最后因数的长度缓存
while (qp < queryLength)
{
// 对 qp 作为开始位置计算
var sp = 0;
var lenMax = 0;
var spMax = 0;
// 查找以 qp 为头的最大子串
while (sp < source.Length)
{
// 对每个 sp 作为开始位置计算最大子串
var len = 0;
while (qp + len < queryLength && sp + len < source.Length && source[sp + len] == query[qp + len])
len += 1;
// 存储 len
if (len > lenMax)
{
lenMax = len;
spMax = sp;
}
// 根据结果增加 sp
sp += Math.Max(1, len);
}
if (lenMax > 0)
{
source = source.Substring(0, spMax) +
(source.Count() > spMax + lenMax
? source.Substring(spMax + lenMax)
: string.Empty); // 将源中的对应字段替换空
// 存储 lenSum
var incWeight = Math.Pow(1.4d, 3 + lenMax) - 3.6d; // 根据长度加成
incWeight *= 1d + 0.3d * Math.Max(0, 3 - Math.Abs(qp - spMax)); // 根据位置加成
lenSum += incWeight;
}
// 根据结果增加 qp
qp += Math.Max(1, lenMax);
}
// 计算结果:重复字段量 × 源长度影响比例
return lenSum / queryLength * (3d / Math.Pow(sourceLength + 15, 0.5d)) *
(queryLength <= 2 ? 3 - queryLength : 1);
}
///
/// 获取多段文本加权后的相似度。
///
private static double SearchSimilarityWeighted(List source, string query)
{
var totalWeight = 0d;
var sum = 0d;
foreach (var pair in source)
{
if (pair.aliases.Any())
sum += pair.aliases.Max(a => SearchSimilarity(a, query)) * pair.weight;
totalWeight += pair.weight;
}
return sum / totalWeight;
}
///
/// 用于搜索的项目。
///
public class SearchEntry
{
///
/// 是否完全匹配。
///
public bool absoluteRight;
///
/// 该项目对应的源数据。
///
public T item;
///
/// 该项目用于搜索的文本源。
/// 在搜索时,会对每个文本源单独加权,但单个文本源内的多个别名只取最高的一个的相似度。
///
public List searchSource;
///
/// 相似度。
///
public double similarity;
}
///
/// 单个用于搜索的文本源。
///
public class SearchSource
{
public string[] aliases;
public double weight;
public SearchSource(string[] aliases, double weight = 1)
{
this.aliases = aliases;
this.weight = weight;
}
public SearchSource(string text, double weight = 1)
{
aliases = new[] { text };
this.weight = weight;
}
}
///
/// 本地搜索返回的最大模糊结果数。
///
public const int MaxLocalSearchDepth = 25;
///
/// 进行多段文本加权搜索,获取相似度较高的数项结果。
///
/// 返回的最大模糊结果数。
/// 返回结果要求的最低相似度。
public static List> Search(List> entries, string query, int maxBlurCount = 5,
double minBlurSimilarity = 0.1d)
{
var resultList = new List>();
if (entries is null || !entries.Any()) return resultList;
// Preprocess query into parts
var queryParts = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (queryParts.Length == 0)
{
resultList.AddRange(entries);
return resultList;
}
// Precompute query parts in lowercase for case-insensitive comparison
var queryPartsLower = queryParts.Select(q => q.ToLower()).ToArray();
// Process each entry to compute similarity and absolute match status
foreach (var entry in entries)
{
entry.similarity = SearchSimilarityWeighted(entry.searchSource, query);
// Preprocess search source keys: remove spaces and convert to lowercase
var processedSources = entry.searchSource.Select(s =>
{
for (var i = 0; i < s.aliases.Length; i++)
s.aliases[i] = s.aliases[i].Replace(" ", "").ToLower();
return s.aliases;
}).ToList();
// Check if all query parts are matched exactly by at least one source
var isAbsoluteRight = true;
foreach (var qp in queryPartsLower)
{
var found = false;
foreach (var ps in processedSources)
if (ps.Any(p => p.Contains(qp)))
{
found = true;
break;
}
if (!found)
{
isAbsoluteRight = false;
break;
}
}
entry.absoluteRight = isAbsoluteRight;
}
// Sort by absolute match (descending), then by similarity (descending)
var sortedEntries = entries.OrderByDescending(e => e.absoluteRight).ThenByDescending(e => e.similarity)
.ToList();
// Build the final result list
var blurCount = 0;
foreach (var entry in sortedEntries)
if (entry.absoluteRight)
{
resultList.Add(entry);
}
else
{
if (entry.similarity < minBlurSimilarity || blurCount >= maxBlurCount) break;
resultList.Add(entry);
blurCount += 1;
}
return resultList;
}
#endregion
#region 系统
public static bool IsUtf8CodePage()
{
return Encoding.Default.CodePage == 65001;
}
///
/// 线程安全的 List。
/// 通过在 For Each 循环中使用一个浅表副本规避多线程操作或移除自身导致的异常。
///
public class SafeList : IEnumerable, IDisposable, ICollection
{
private readonly List _internalList;
private readonly ReaderWriterLockSlim _lock = new();
public SafeList()
{
_internalList = new List();
}
public SafeList(IEnumerable data)
{
_internalList = new List(data);
}
public T this[int index]
{
get => _internalList[index];
set => _internalList[index] = value;
}
public void Add(T item)
{
_lock.EnterWriteLock();
try
{
_internalList.Add(item);
}
finally
{
_lock.ExitWriteLock();
}
}
public bool Remove(T item)
{
_lock.EnterWriteLock();
try
{
return _internalList.Remove(item);
}
finally
{
_lock.ExitWriteLock();
}
}
public void Clear()
{
_lock.EnterWriteLock();
try
{
_internalList.Clear();
}
finally
{
_lock.ExitWriteLock();
}
}
public int Count
{
get
{
_lock.EnterReadLock();
try
{
return _internalList.Count;
}
finally
{
_lock.ExitReadLock();
}
}
}
public bool IsReadOnly => ((ICollection)_internalList).IsReadOnly;
public bool Contains(T item)
{
return ((ICollection)_internalList).Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
((ICollection)_internalList).CopyTo(array, arrayIndex);
}
public void Dispose()
{
_lock.Dispose();
}
public IEnumerator GetEnumerator()
{
return ToList().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public List ToList()
{
_lock.EnterReadLock();
try
{
return _internalList.ToList();
}
finally
{
_lock.ExitReadLock();
}
}
public void RemoveAt(int index)
{
_lock.EnterWriteLock();
try
{
_internalList.RemoveAt(index);
}
finally
{
_lock.ExitWriteLock();
}
}
}
///
/// 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。
///
public static string pathPure = GetPureASCIIDir();
private static string GetPureASCIIDir()
{
if (exePath.IsASCII()) return exePath + @"PCL\";
if (pathAppdata.IsASCII()) return pathAppdata;
if (pathTemp.IsASCII()) return pathTemp;
return Path.Combine(SystemPaths.DriveLetter, "ProgramData", "PCL");
}
///
/// 指示接取到这个异常的函数进行重试。
///
public class RestartException : Exception
{
}
///
/// 指示用户手动取消了操作,或用户已知晓操作被取消的原因。
///
public class CancelledException : Exception
{
}
///
/// 判断对象是否为某个泛型类型的实例。
///
public static bool IsInstanceOfGenericType(this Type genericType, object obj)
{
if (obj is null)
return false;
var t = obj.GetType();
while (t is not null)
{
if (t.IsGenericType && ReferenceEquals(t.GetGenericTypeDefinition(), genericType))
return true;
t = t.BaseType;
}
return false;
}
private static int uuid = 1;
private static object uuidLock;
///
/// 获取一个全程序内不会重复的数字(伪 Uuid)。
///
public static int GetUuid()
{
if (uuidLock is null)
uuidLock = new object();
lock (uuidLock)
{
uuid += 1;
return uuid;
}
}
///
/// 将元素与 List 的混合体拆分为元素组。
///
public static List GetFullList(IList data)
{
List getFullListRet = default;
getFullListRet = new List();
for (int i = 0, loopTo = data.Count - 1; i <= loopTo; i++)
if (data[i] is ICollection)
getFullListRet.AddRange((IEnumerable)data[i]);
else
getFullListRet.Add((T)data[i]);
return getFullListRet;
}
///
/// 数组去重。
///
public static List Distinct(this ICollection arr, ComparisonBoolean isEqual)
{
var resultArray = new List();
for (int i = 0, loopTo = arr.Count - 1; i <= loopTo; i++)
{
for (int ii = i + 1, loopTo1 = arr.Count - 1; ii <= loopTo1; ii++)
if (isEqual(arr.ElementAtOrDefault(i), arr.ElementAtOrDefault(ii)))
goto NextElement;
resultArray.Add(arr.ElementAtOrDefault(i));
NextElement: ;
}
return resultArray;
}
///
/// 对集合的每个元素执行指定操作。
///
public static IEnumerable ForEach(this IEnumerable collection, Action action)
{
foreach (var item in collection)
action(item);
return collection;
}
///
/// 用于储存 RaiseByMouse 的 EventArgs。
///
public sealed class RouteEventArgs : EventArgs
{
public bool handled = false;
public bool raiseByMouse;
public RouteEventArgs(bool raiseByMouse = false)
{
this.raiseByMouse = raiseByMouse;
}
}
///
/// 前台运行文件。
///
/// 文件名。可以为“notepad”等缩写。
/// 运行参数。
public static void ShellOnly(string fileName, string arguments = "")
{
try
{
fileName = ShortenPath(fileName);
using (var program = new Process())
{
program.StartInfo.Arguments = arguments;
program.StartInfo.FileName = fileName;
program.StartInfo.UseShellExecute = true;
Log("[System] 执行外部命令:" + fileName + " " + arguments);
program.Start();
}
}
catch (Exception ex)
{
Log(
ex,
"打开文件或程序失败:" + fileName,
LogLevel.Msgbox,
userSummary: Lang.Text("SystemDialog.File.OpenFailed.Message", fileName));
}
}
///
/// 前台运行文件并返回返回值。
///
/// 文件名。可以为“notepad”等缩写。
/// 运行参数。
/// 等待该程序结束的最长时间(毫秒)。超时会返回 Result.Timeout。
public static ProcessReturnValues ShellAndGetExitCode(string fileName, string arguments = "", int timeout = 1000000)
{
try
{
using (var program = new Process())
{
program.StartInfo.Arguments = arguments;
program.StartInfo.FileName = fileName;
Log("[System] 执行外部命令并等待返回码:" + fileName + " " + arguments);
program.Start();
if (program.WaitForExit(timeout)) return (ProcessReturnValues)program.ExitCode;
return ProcessReturnValues.Timeout;
}
}
catch (Exception ex)
{
Log(ex, "执行命令失败:" + fileName, LogLevel.Msgbox);
return ProcessReturnValues.Fail;
}
}
///
/// 静默运行文件并返回输出流字符串。执行失败会抛出异常。
///
/// 文件名。可以为“notepad”等缩写。
/// 运行参数。
/// 等待该程序结束的最长时间(毫秒)。超时会抛出错误。
public static string ShellAndGetOutput(string fileName, string arguments = "", int timeout = 1000000,
string workingDirectory = null)
{
var info = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
// 设置工作目录(如果提供)
if (!string.IsNullOrEmpty(workingDirectory)) info.WorkingDirectory = workingDirectory.TrimEnd('\\');
Log("[System] 执行外部命令并等待返回结果:" + fileName + " " + arguments);
using (var program = new Process { StartInfo = info })
{
program.Start();
// 异步读取输出和错误流
var outputTask = program.StandardOutput.ReadToEndAsync();
var errorTask = program.StandardError.ReadToEndAsync();
// 等待进程退出或超时
if (program.WaitForExit(timeout))
{
// 确保异步读取完成
Task.WaitAll(outputTask, errorTask);
}
else
{
// 超时后终止进程
program.Kill();
// 仍然尝试获取已输出的内容
Task.WaitAll(outputTask, errorTask);
}
// 合并结果并返回
return outputTask.Result + errorTask.Result;
}
}
///
/// 在新的工作线程中执行代码。
///
public static Thread RunInNewThread(Action action, string name = null,
ThreadPriority priority = ThreadPriority.Normal)
{
var th = new Thread(() =>
{
try
{
action();
}
catch (ThreadInterruptedException ex)
{
Log(name + ":线程已中止");
}
catch (Exception ex)
{
Log(ex, name + ":线程执行失败", LogLevel.Feedback);
}
}) { Name = name ?? "Runtime New Invoke " + GetUuid() + "#", Priority = priority };
th.Start();
return th;
}
///
/// 确保在 UI 线程中执行代码。
/// 如果当前并非 UI 线程,则会阻断当前线程,直至 UI 线程执行完毕。
/// 为防止线程互锁,请仅在开始加载动画、从 UI 获取输入时使用!
///
public static Output RunInUiWait