using UnityEngine; using UnityEngine.UI; using System.IO; using System.Text.RegularExpressions; using System; using System.Collections.Generic; using System.Text; using System.Runtime.InteropServices; public class readDeviceInfo : MonoBehaviour { private const string LabelColorHex = "#2457c6"; private const string ValueColorHex = "#6b8acd"; private const float ColumnPaddingPixels = 32f; private const int MinimumGapSpaces = 2; public Button reread_button; public Button export_button; public Text deviceInfo_text; void Start() { if (reread_button != null) { reread_button.onClick.AddListener(read_deviceInfo); } if (export_button != null) { export_button.onClick.AddListener(ExportToFile); } if (deviceInfo_text != null) { ConfigureDeviceInfoText(); } read_deviceInfo(); } void OnDestroy() { if (reread_button != null) reread_button.onClick.RemoveListener(read_deviceInfo); if (export_button != null) export_button.onClick.RemoveListener(ExportToFile); } void Update() { } private void read_deviceInfo() { ConfigureDeviceInfoText(); var entries = new System.Collections.Generic.List>(); void AddEntry(string title, object value) { string str; if (value == null) str = ""; else if (value is bool b) str = b ? "是" : "否"; else str = value.ToString(); entries.Add(new KeyValuePair(title ?? string.Empty, str ?? string.Empty)); } AddEntry("平台", Application.platform.ToString()); AddEntry("数据目录", Application.dataPath); AddEntry("persistent 数据路径", Application.persistentDataPath); AddEntry("操作系统", SystemInfo.operatingSystem); AddEntry("设备名称", SystemInfo.deviceName); AddEntry("设备型号", SystemInfo.deviceModel); AddEntry("设备唯一ID", SystemInfo.deviceUniqueIdentifier); AddEntry("CPU", SystemInfo.processorType); AddEntry("CPU核心数", SystemInfo.processorCount); AddEntry("系统内存 (MB)", SystemInfo.systemMemorySize); AddEntry("图形设备名称", SystemInfo.graphicsDeviceName); AddEntry("图形设备供应商", SystemInfo.graphicsDeviceVendor); AddEntry("DirectX", SystemInfo.graphicsDeviceType.ToString()); AddEntry("显存 (MB)", SystemInfo.graphicsMemorySize); AddEntry("屏幕分辨率", $"{Screen.currentResolution.width} x {Screen.currentResolution.height} @ {Screen.currentResolution.refreshRateRatio.value:0.##}Hz"); AddEntry("屏幕DPI", Screen.dpi); AddEntry("显示模式", Screen.fullScreenMode.ToString()); try { AddEntry("Battery Level", $"{SystemInfo.batteryLevel * 100:F0}%"); } catch { AddEntry("Battery Status", "Not Supported"); } AddEntry("主程序版本", "v1.1h02e"); AddEntry("内置小游戏版本", "v1.301"); AddEntry("游戏代码版本", "code::testingVersion1062"); AddEntry("登录渠道", "Steam Client"); AddEntry(".NET Framework", "8.0 +"); AddEntry("Max Texture Size", SystemInfo.maxTextureSize); AddEntry("启用后台运行", Application.runInBackground); AddEntry("启用多线程渲染", SystemInfo.graphicsMultiThreaded); string output = BuildTwoColumnOutput(entries); if (deviceInfo_text != null) { deviceInfo_text.text = output; } } void ExportToFile() { if (deviceInfo_text == null) { return; } string path = ShowSaveFileDialog(); if (string.IsNullOrEmpty(path)) { return; } try { string raw = deviceInfo_text.text; string cleaned = Regex.Replace(raw, "<.*?>", string.Empty); File.WriteAllText(path, cleaned); } catch (System.Exception) { } } private string BuildTwoColumnOutput(System.Collections.Generic.IReadOnlyList> entries) { if (entries == null || entries.Count == 0) { return string.Empty; } float maxLabelWidth = 0f; for (int i = 0; i < entries.Count; i++) { maxLabelWidth = Mathf.Max(maxLabelWidth, MeasurePlainTextWidth(entries[i].Key)); } float availableWidth = GetAvailableTextWidth(); float targetLabelWidth = maxLabelWidth + ColumnPaddingPixels; if (availableWidth > 0f) { targetLabelWidth = Mathf.Min(targetLabelWidth, availableWidth * 0.38f); } float spaceWidth = Mathf.Max(1f, MeasurePlainTextWidth(" ")); int continuationIndentSpaces = Mathf.Max(MinimumGapSpaces, Mathf.CeilToInt(targetLabelWidth / spaceWidth) + MinimumGapSpaces); string continuationIndent = new string(' ', continuationIndentSpaces); StringBuilder sb = new StringBuilder(); for (int i = 0; i < entries.Count; i++) { string label = entries[i].Key ?? string.Empty; string value = entries[i].Value ?? string.Empty; int gapSpaces = Mathf.Max( MinimumGapSpaces, Mathf.CeilToInt((targetLabelWidth - MeasurePlainTextWidth(label)) / spaceWidth) + MinimumGapSpaces); string padding = new string(' ', gapSpaces); float firstLineValueWidth = availableWidth > 0f ? Mathf.Max(32f, availableWidth - MeasurePlainTextWidth(label) - MeasurePlainTextWidth(padding)) : 0f; float continuationValueWidth = availableWidth > 0f ? Mathf.Max(32f, availableWidth - MeasurePlainTextWidth(continuationIndent)) : 0f; List wrappedLines = WrapValueLines(value, firstLineValueWidth, continuationValueWidth); sb.Append("") .Append(EscapeRichText(label)) .Append("") .Append(padding) .Append("") .Append(EscapeRichText(wrappedLines[0])) .Append(""); for (int lineIndex = 1; lineIndex < wrappedLines.Count; lineIndex++) { sb.AppendLine(); sb.Append(continuationIndent) .Append("") .Append(EscapeRichText(wrappedLines[lineIndex])) .Append(""); } if (i < entries.Count - 1) { sb.AppendLine(); } } return sb.ToString(); } private void ConfigureDeviceInfoText() { if (deviceInfo_text == null) { return; } deviceInfo_text.supportRichText = true; deviceInfo_text.alignment = TextAnchor.UpperLeft; deviceInfo_text.horizontalOverflow = HorizontalWrapMode.Wrap; deviceInfo_text.verticalOverflow = VerticalWrapMode.Overflow; deviceInfo_text.resizeTextForBestFit = false; } private float GetAvailableTextWidth() { if (deviceInfo_text == null) { return 0f; } RectTransform rectTransform = deviceInfo_text.rectTransform; if (rectTransform == null) { return 0f; } float width = rectTransform.rect.width; return width > 0f ? width : 0f; } private float MeasurePlainTextWidth(string text) { if (deviceInfo_text == null || string.IsNullOrEmpty(text)) { return 0f; } TextGenerationSettings settings = deviceInfo_text.GetGenerationSettings(Vector2.zero); settings.richText = false; settings.generateOutOfBounds = true; TextGenerator generator = new TextGenerator(); return generator.GetPreferredWidth(text, settings) / deviceInfo_text.pixelsPerUnit; } private List WrapValueLines(string value, float firstLineWidth, float continuationLineWidth) { string normalized = NormalizeLineBreaks(value); if (string.IsNullOrEmpty(normalized)) { return new List { string.Empty }; } string[] rawLines = normalized.Split('\n'); List wrapped = new List(); for (int i = 0; i < rawLines.Length; i++) { string rawLine = rawLines[i]; List lineParts = WrapSingleLine(rawLine, wrapped.Count == 0 ? firstLineWidth : continuationLineWidth); if (lineParts.Count == 0) { lineParts.Add(string.Empty); } wrapped.Add(lineParts[0]); for (int j = 1; j < lineParts.Count; j++) { wrapped.Add(lineParts[j]); } } return wrapped.Count > 0 ? wrapped : new List { string.Empty }; } private List WrapSingleLine(string text, float maxWidth) { List lines = new List(); if (string.IsNullOrEmpty(text) || maxWidth <= 0f) { lines.Add(text ?? string.Empty); return lines; } string remaining = text; while (!string.IsNullOrEmpty(remaining)) { if (MeasurePlainTextWidth(remaining) <= maxWidth) { lines.Add(remaining); break; } int splitIndex = FindSplitIndex(remaining, maxWidth); if (splitIndex <= 0 || splitIndex >= remaining.Length) { lines.Add(remaining); break; } string current = remaining.Substring(0, splitIndex).TrimEnd(); if (current.Length == 0) { current = remaining.Substring(0, Mathf.Min(1, remaining.Length)); splitIndex = current.Length; } lines.Add(current); remaining = remaining.Substring(splitIndex).TrimStart(); } return lines; } private int FindSplitIndex(string text, float maxWidth) { int lastBreakableIndex = -1; int fallbackIndex = -1; for (int i = 1; i <= text.Length; i++) { string candidate = text.Substring(0, i); if (MeasurePlainTextWidth(candidate) <= maxWidth) { fallbackIndex = i; if (IsBreakableCharacter(text[i - 1])) { lastBreakableIndex = i; } continue; } break; } if (lastBreakableIndex > 0) { return lastBreakableIndex; } return fallbackIndex; } private static bool IsBreakableCharacter(char c) { return char.IsWhiteSpace(c) || c == '/' || c == '\\' || c == '_' || c == '-' || c == '.' || c == ':' || c == ')' || c == ']'; } private static string NormalizeLineBreaks(string input) { if (string.IsNullOrEmpty(input)) { return string.Empty; } return input.Replace("\r\n", "\n").Replace('\r', '\n'); } private static string EscapeRichText(string input) { if (string.IsNullOrEmpty(input)) { return string.Empty; } return input.Replace("&", "&") .Replace("<", "<") .Replace(">", ">"); } string ShowSaveFileDialog() { #if UNITY_EDITOR string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); string path = UnityEditor.EditorUtility.SaveFilePanel("保存设备信息", desktop, "device_info", "txt"); return string.IsNullOrEmpty(path) ? null : path; #elif UNITY_STANDALONE_WIN try { // Use native Windows SaveFile dialog via comdlg32 GetSaveFileName OpenFileName ofn = new OpenFileName(); ofn.structSize = Marshal.SizeOf(ofn); ofn.filter = "Text files (*.txt)\0*.txt\0All files (*.*)\0*.*\0"; ofn.file = new string('\0', 260); ofn.maxFile = ofn.file.Length; ofn.fileTitle = new string('\0', 260); ofn.maxFileTitle = ofn.fileTitle.Length; ofn.initialDir = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); ofn.title = "保存设备信息"; ofn.defExt = "txt"; ofn.flags = OpenFileNameFlags.OFN_OVERWRITEPROMPT; if (GetSaveFileName(ofn)) { // Trim any trailing nulls return ofn.file.TrimEnd('\0'); } } catch (Exception) { } return null; #else string defaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "device_info.txt"); return defaultPath; #endif } #if UNITY_STANDALONE_WIN [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private class OpenFileName { public int structSize = 0; public IntPtr dlgOwner = IntPtr.Zero; public IntPtr instance = IntPtr.Zero; public string filter = null; public string customFilter = null; public int maxCustFilter = 0; public int filterIndex = 0; public string file = null; public int maxFile = 0; public string fileTitle = null; public int maxFileTitle = 0; public string initialDir = null; public string title = null; public int flags = 0; public short fileOffset = 0; public short fileExtension = 0; public string defExt = null; public IntPtr custData = IntPtr.Zero; public IntPtr hook = IntPtr.Zero; public string templateName = null; public IntPtr reservedPtr = IntPtr.Zero; public int reservedInt = 0; public int flagsEx = 0; } private static class OpenFileNameFlags { public const int OFN_OVERWRITEPROMPT = 0x00000002; } [DllImport("comdlg32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern bool GetSaveFileName([In, Out] OpenFileName ofn); #endif }