mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a643c3bb1 | |||
| 0d446f4e89 |
@@ -95,7 +95,6 @@ internal static partial class Program
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
|
||||
Log.Info(BuildInfo.Banner);
|
||||
Log.Info(HostSystemInfo.Summary);
|
||||
|
||||
ebootPath = Path.GetFullPath(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||
|
||||
@@ -48,7 +48,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TargetPath>LICENSE.txt</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
|
||||
<Content Include="..\SharpEmu.GUI\Languages\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
{
|
||||
"_languageName": "Русский",
|
||||
|
||||
"Page.Library": "Библиотека",
|
||||
"Page.Options": "Настройки",
|
||||
"Page.GameCount.One": "1 игра",
|
||||
"Page.GameCount.Other": "Игр: {0}",
|
||||
|
||||
"Library.SearchWatermark": "Поиск…",
|
||||
"Library.AddFolder": "+ Добавить папку",
|
||||
"Library.Rescan": "⟳ Сканировать",
|
||||
"Library.OpenFile": "Открыть файл…",
|
||||
|
||||
"Library.Context.Launch": "Запустить",
|
||||
"Library.Context.OpenFolder": "Открыть папку с игрой",
|
||||
"Library.Context.CopyPath": "Скопировать путь",
|
||||
"Library.Context.CopyTitleId": "Скопировать ID игры",
|
||||
"Library.Context.Remove": "Удалить из библиотеки",
|
||||
|
||||
"Library.Empty.Title": "Ваша библиотека пуста",
|
||||
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
|
||||
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
|
||||
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
|
||||
"Library.Empty.AddFolder": "+ Добавить папку с играми",
|
||||
|
||||
"Library.Loading": "Загрузка библиотеки…",
|
||||
|
||||
"Options.General": "Основные",
|
||||
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
||||
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
||||
"Options.Section.Launcher": "ЛАУНЧЕР",
|
||||
|
||||
"Options.CpuEngine.Label": "Движок ЦП",
|
||||
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
|
||||
"Options.CpuEngine.Native": "Нативный",
|
||||
|
||||
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
|
||||
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
|
||||
|
||||
"Options.LogLevel.Label": "Уровень логгирования",
|
||||
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Лимит трассировки импортов",
|
||||
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
|
||||
|
||||
"Options.LogToFile.Label": "Запись лога в файл",
|
||||
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
|
||||
|
||||
"Options.LogFilePath.Label": "Путь к файлу лога",
|
||||
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
|
||||
"Options.LogFilePath.Select": "Выбрать…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Переопределить файл лога",
|
||||
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
|
||||
|
||||
"Options.TitleMusic.Label": "Музыка игры",
|
||||
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
|
||||
|
||||
"Options.Discord.Label": "Статус Discord",
|
||||
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
|
||||
|
||||
"Options.Language.Label": "Язык эмулятора",
|
||||
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
|
||||
|
||||
"Common.On": "Включено",
|
||||
"Common.Off": "Выключено",
|
||||
|
||||
"Console.Title": "КОНСОЛЬ",
|
||||
"Console.SearchWatermark": "Поиск...",
|
||||
"Console.AutoScroll": "Авто-прокрутка",
|
||||
"Console.Split": "Разделить",
|
||||
"Console.Copy": "Скопировать",
|
||||
"Console.Clear": "Очистить",
|
||||
"Console.WindowTitle": "Консоль SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Ничего не выбрано",
|
||||
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
|
||||
"Launch.Idle": "Ожидание",
|
||||
"Launch.Console": "≡ Консоль",
|
||||
"Launch.Launch": "▶ Запустить",
|
||||
"Launch.Stop": "■ Остановить",
|
||||
"Launch.Running": "Запущено - {0}",
|
||||
"Launch.Stopping": "Остановка…",
|
||||
"Launch.Exited": "Завершено с кодом {0} ({1})",
|
||||
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Лог: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
|
||||
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "некорректные аргументы",
|
||||
"Exit.EbootNotFound": "eboot не найден",
|
||||
"Exit.RuntimeException": "ошибка выполнения",
|
||||
"Exit.EmulationError": "ошибка эмуляции",
|
||||
"Exit.Unknown": "неизвестная ошибка",
|
||||
|
||||
"Status.EmulatorLocating": "Эмулятор: поиск…",
|
||||
"Status.EmulatorPath": "Эмулятор: {0}",
|
||||
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Сканирование библиотеки…",
|
||||
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
|
||||
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
|
||||
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
|
||||
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
|
||||
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
|
||||
"Status.Running": "Запущено: {0}",
|
||||
"Status.Stopping": "Остановка…",
|
||||
"Status.Idle": "Ожидание",
|
||||
|
||||
"Clipboard.Path": "Путь",
|
||||
"Clipboard.TitleId": "ID игры",
|
||||
|
||||
"Discord.Playing": "Играет в {0}",
|
||||
"Discord.Browsing": "Просматривает библиотеку",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
|
||||
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
|
||||
"Dialog.PsExecutables": "Исполняемые файлы PS",
|
||||
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
|
||||
"Dialog.PlainTextFiles": "Текстовые файлы",
|
||||
"Dialog.LogFiles": "Логи"
|
||||
}
|
||||
@@ -6,11 +6,10 @@ using System.Text.Json;
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Loads UI strings for the launcher. Every language ships embedded in the
|
||||
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
|
||||
/// self-contained; an optional Languages/<code>.json file next to the
|
||||
/// executable overrides the embedded copy for that code, so a translation
|
||||
/// fix or a brand-new language never needs a rebuild.
|
||||
/// Loads UI strings for the launcher from Languages/<code>.json next to the
|
||||
/// executable. The files are plain, pretty-printed JSON (a flat key/value
|
||||
/// map) so they stay easy to open and translate by hand; a missing or
|
||||
/// unreadable key falls back to the key name itself.
|
||||
/// </summary>
|
||||
public sealed class Localization
|
||||
{
|
||||
@@ -18,16 +17,13 @@ public sealed class Localization
|
||||
|
||||
public sealed record LanguageInfo(string Code, string NativeName);
|
||||
|
||||
private const string EmbeddedResourcePrefix = "Languages.";
|
||||
private const string EmbeddedResourceSuffix = ".json";
|
||||
|
||||
private Dictionary<string, string> _strings = new();
|
||||
|
||||
private Localization()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
|
||||
/// <summary>Directory holding the *.json language files; user-editable.</summary>
|
||||
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
||||
|
||||
public string CurrentCode { get; private set; } = "en";
|
||||
@@ -36,73 +32,32 @@ public sealed class Localization
|
||||
|
||||
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||
|
||||
/// <summary>
|
||||
/// Languages available either embedded in the binary or as a loose
|
||||
/// override file, sorted by code. A loose file's declared name wins when
|
||||
/// the same code exists in both places.
|
||||
/// </summary>
|
||||
/// <summary>Languages discovered under <see cref="LanguagesDirectory"/>, sorted by code.</summary>
|
||||
public List<LanguageInfo> DiscoverLanguages()
|
||||
{
|
||||
var languages = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var code in EmbeddedLanguageCodes())
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is not null)
|
||||
{
|
||||
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||
}
|
||||
}
|
||||
|
||||
var languages = new List<LanguageInfo>();
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
|
||||
{
|
||||
var code = Path.GetFileNameWithoutExtension(file);
|
||||
using var stream = File.OpenRead(file);
|
||||
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||
languages.Add(new LanguageInfo(code, ReadLanguageName(file) ?? code));
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// No loose Languages directory: the embedded languages still stand.
|
||||
// Missing Languages directory: no languages to offer.
|
||||
}
|
||||
|
||||
var result = languages.Values.ToList();
|
||||
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
|
||||
return result;
|
||||
languages.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
|
||||
return languages;
|
||||
}
|
||||
|
||||
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
||||
public void Load(string code)
|
||||
{
|
||||
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
|
||||
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||
{
|
||||
foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) &&
|
||||
name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal))
|
||||
{
|
||||
yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream? OpenEmbeddedLanguageStream(string code) =>
|
||||
typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}");
|
||||
|
||||
private static string? ReadLanguageName(Stream stream)
|
||||
private static string? ReadLanguageName(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
|
||||
name.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
@@ -111,54 +66,44 @@ public sealed class Localization
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Malformed file: fall back to the code as its own display name.
|
||||
// Malformed file: fall back to the file's code as its display name.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool TryLoadLooseFile(string code)
|
||||
/// <summary>Loads a language by file code (e.g. "en"), falling back to English.</summary>
|
||||
public void Load(string code)
|
||||
{
|
||||
if (!TryLoadFile(code) && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TryLoadFile("en");
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadFile(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadEmbedded(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is null)
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
return TryLoad(code, reader.ReadToEnd());
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(path));
|
||||
if (loaded is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_strings = loaded;
|
||||
CurrentCode = code;
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoad(string code, string json)
|
||||
{
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (loaded is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_strings = loaded;
|
||||
CurrentCode = code;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,14 +30,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Languages\*.json">
|
||||
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||
with the emulator's pad HLE. They are dependency-free, so they are
|
||||
compiled in directly rather than pulling a reference to all of
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -134,10 +133,9 @@ public static class VideoOutExports
|
||||
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
||||
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
||||
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
|
||||
var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName} \u00b7 {HostSystemInfo.GpuName}";
|
||||
lock (_stateGate)
|
||||
{
|
||||
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}";
|
||||
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Microsoft.Win32;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
/// <summary>Provides best-effort information about the host system.</summary>
|
||||
public static class HostSystemInfo
|
||||
{
|
||||
private static readonly Lazy<string> CpuNameValue = new(GetCpuName);
|
||||
private static readonly Lazy<string> GpuNameValue = new(GetPreferredGpuName);
|
||||
private static readonly Lazy<string> MemoryDescriptionValue = new(GetMemoryDescription);
|
||||
|
||||
/// <summary>Host CPU name, or a safe fallback when it cannot be determined.</summary>
|
||||
public static string CpuName => CpuNameValue.Value;
|
||||
|
||||
/// <summary>Preferred physical GPU name, or a safe fallback when it cannot be determined.</summary>
|
||||
public static string GpuName => GpuNameValue.Value;
|
||||
|
||||
/// <summary>Returns a concise description of the host hardware for diagnostic logs.</summary>
|
||||
public static string Summary =>
|
||||
$"Host hardware: CPU: {CpuName}; GPU: {GpuName}; RAM: {MemoryDescriptionValue.Value}.";
|
||||
|
||||
private static string GetCpuName()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return $"{Environment.ProcessorCount} logical processors";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
|
||||
if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return Regex.Replace(
|
||||
name.Trim(),
|
||||
@"\s+\d+-Core Processor$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Hardware information is diagnostic only.
|
||||
}
|
||||
|
||||
return $"{Environment.ProcessorCount} logical processors";
|
||||
}
|
||||
|
||||
private static string GetPreferredGpuName()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var preferredName = "unknown";
|
||||
var preferredScore = int.MinValue;
|
||||
for (uint index = 0; ; index++)
|
||||
{
|
||||
var device = new DisplayDevice
|
||||
{
|
||||
cb = Marshal.SizeOf<DisplayDevice>(),
|
||||
};
|
||||
|
||||
if (!EnumDisplayDevices(null, index, ref device, 0))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var name = device.DeviceString?.Trim();
|
||||
var score = ScoreGpu(name);
|
||||
if (score > preferredScore)
|
||||
{
|
||||
preferredName = name!;
|
||||
preferredScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
return preferredScore > 0 ? preferredName : "unknown";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Hardware information is diagnostic only.
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private static int ScoreGpu(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) ||
|
||||
name.Contains("virtual", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("remote", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("basic display", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("geforce", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("radeon", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static string GetMemoryDescription()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = new MemoryStatusEx
|
||||
{
|
||||
dwLength = (uint)Marshal.SizeOf<MemoryStatusEx>(),
|
||||
};
|
||||
if (GlobalMemoryStatusEx(ref status))
|
||||
{
|
||||
var megabytes = status.ullTotalPhys / (1024 * 1024);
|
||||
var gigabytes = status.ullTotalPhys / (1024d * 1024 * 1024);
|
||||
return $"{megabytes:N0} MB ({gigabytes:N1} GB)";
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Hardware information is diagnostic only.
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct DisplayDevice
|
||||
{
|
||||
public int cb;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string? DeviceName;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string? DeviceString;
|
||||
public int StateFlags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string? DeviceId;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string? DeviceKey;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||
private struct MemoryStatusEx
|
||||
{
|
||||
public uint dwLength;
|
||||
public uint dwMemoryLoad;
|
||||
public ulong ullTotalPhys;
|
||||
public ulong ullAvailPhys;
|
||||
public ulong ullTotalPageFile;
|
||||
public ulong ullAvailPageFile;
|
||||
public ulong ullTotalVirtual;
|
||||
public ulong ullAvailVirtual;
|
||||
public ulong ullAvailExtendedVirtual;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumDisplayDevices(string? deviceName, uint deviceNum, ref DisplayDevice displayDevice, uint flags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);
|
||||
}
|
||||
Reference in New Issue
Block a user