Compare commits
11 Commits
Canary-1.2
...
20bae91376
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20bae91376 | ||
|
|
c279c7d5de | ||
|
|
753ca01c0d | ||
|
|
5aedeebfe9 | ||
|
|
5c67efd291 | ||
|
|
b2354768c4 | ||
|
|
745bd91250 | ||
|
|
07ab817557 | ||
|
|
0e8a41b198 | ||
|
|
969e94f913 | ||
|
|
2e2d26d49d |
@@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
|
||||
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
|
||||
|
||||
public IJitMemoryBlock Block { get; }
|
||||
public IJitMemoryAllocator Allocator { get; }
|
||||
|
||||
public nint Pointer => Block.Pointer;
|
||||
|
||||
@@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
|
||||
granularity = DefaultGranularity;
|
||||
}
|
||||
|
||||
Allocator = allocator;
|
||||
Block = allocator.Reserve(maxSize);
|
||||
_maxSize = maxSize;
|
||||
_sizeGranularity = granularity;
|
||||
|
||||
@@ -2,6 +2,8 @@ using ARMeilleure.CodeGen;
|
||||
using ARMeilleure.CodeGen.Unwinding;
|
||||
using ARMeilleure.Memory;
|
||||
using ARMeilleure.Native;
|
||||
using Humanizer;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -18,9 +20,8 @@ namespace ARMeilleure.Translation.Cache
|
||||
private static readonly int _pageMask = _pageSize - 1;
|
||||
|
||||
private const int CodeAlignment = 4; // Bytes.
|
||||
private const int CacheSize = 2047 * 1024 * 1024;
|
||||
private const int CacheSize = 128 * 1024 * 1024;
|
||||
|
||||
private static ReservedRegion _jitRegion;
|
||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||
|
||||
private static CacheMemoryAllocator _cacheAllocator;
|
||||
@@ -30,6 +31,9 @@ namespace ARMeilleure.Translation.Cache
|
||||
private static readonly Lock _lock = new();
|
||||
private static bool _initialized;
|
||||
|
||||
private static readonly List<ReservedRegion> _jitRegions = new();
|
||||
private static int _activeRegionIndex = 0;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
|
||||
@@ -48,7 +52,9 @@ namespace ARMeilleure.Translation.Cache
|
||||
return;
|
||||
}
|
||||
|
||||
_jitRegion = new ReservedRegion(allocator, CacheSize);
|
||||
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
||||
_jitRegions.Add(firstRegion);
|
||||
_activeRegionIndex = 0;
|
||||
|
||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
||||
{
|
||||
@@ -59,7 +65,9 @@ namespace ARMeilleure.Translation.Cache
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
|
||||
JitUnwindWindows.InstallFunctionTableHandler(
|
||||
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
|
||||
);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
@@ -75,8 +83,8 @@ namespace ARMeilleure.Translation.Cache
|
||||
Debug.Assert(_initialized);
|
||||
|
||||
int funcOffset = Allocate(code.Length);
|
||||
|
||||
nint funcPtr = _jitRegion.Pointer + funcOffset;
|
||||
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
||||
nint funcPtr = targetRegion.Pointer + funcOffset;
|
||||
|
||||
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
@@ -90,9 +98,9 @@ namespace ARMeilleure.Translation.Cache
|
||||
}
|
||||
else
|
||||
{
|
||||
ReprotectAsWritable(funcOffset, code.Length);
|
||||
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
|
||||
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||
ReprotectAsExecutable(funcOffset, code.Length);
|
||||
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
||||
|
||||
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
@@ -116,52 +124,83 @@ namespace ARMeilleure.Translation.Cache
|
||||
{
|
||||
Debug.Assert(_initialized);
|
||||
|
||||
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
||||
|
||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||
foreach (var region in _jitRegions)
|
||||
{
|
||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||
_cacheEntries.RemoveAt(entryIndex);
|
||||
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
||||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
|
||||
|
||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||
{
|
||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||
_cacheEntries.RemoveAt(entryIndex);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReprotectAsWritable(int offset, int size)
|
||||
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
|
||||
{
|
||||
int endOffs = offset + size;
|
||||
|
||||
int regionStart = offset & ~_pageMask;
|
||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||
|
||||
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
}
|
||||
|
||||
private static void ReprotectAsExecutable(int offset, int size)
|
||||
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
|
||||
{
|
||||
int endOffs = offset + size;
|
||||
|
||||
int regionStart = offset & ~_pageMask;
|
||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||
|
||||
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
}
|
||||
|
||||
private static int Allocate(int codeSize)
|
||||
{
|
||||
codeSize = AlignCodeSize(codeSize);
|
||||
|
||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
||||
|
||||
if (allocOffset < 0)
|
||||
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
||||
{
|
||||
throw new OutOfMemoryException("JIT Cache exhausted.");
|
||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
||||
|
||||
if (allocOffset >= 0)
|
||||
{
|
||||
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||
_activeRegionIndex = i;
|
||||
return allocOffset;
|
||||
}
|
||||
}
|
||||
|
||||
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||
int exhaustedRegion = _activeRegionIndex;
|
||||
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
|
||||
_jitRegions.Add(newRegion);
|
||||
_activeRegionIndex = _jitRegions.Count - 1;
|
||||
|
||||
int newRegionNumber = _activeRegionIndex;
|
||||
|
||||
return allocOffset;
|
||||
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
|
||||
|
||||
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||
|
||||
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
|
||||
if (allocOffsetNew < 0)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
|
||||
}
|
||||
|
||||
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
|
||||
return allocOffsetNew;
|
||||
}
|
||||
|
||||
|
||||
private static int AlignCodeSize(int codeSize)
|
||||
{
|
||||
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
|
||||
@@ -185,18 +224,21 @@ namespace ARMeilleure.Translation.Cache
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
|
||||
|
||||
if (index < 0)
|
||||
foreach (var region in _jitRegions)
|
||||
{
|
||||
index = ~index - 1;
|
||||
}
|
||||
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
entry = _cacheEntries[index];
|
||||
entryIndex = index;
|
||||
return true;
|
||||
if (index < 0)
|
||||
{
|
||||
index = ~index - 1;
|
||||
}
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
entry = _cacheEntries[index];
|
||||
entryIndex = index;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using ARMeilleure.Memory;
|
||||
using Humanizer;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -15,9 +17,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
||||
private static readonly int _pageMask = _pageSize - 1;
|
||||
|
||||
private const int CodeAlignment = 4; // Bytes.
|
||||
private const int CacheSize = 2047 * 1024 * 1024;
|
||||
private const int CacheSize = 128 * 1024 * 1024;
|
||||
|
||||
private static ReservedRegion _jitRegion;
|
||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||
|
||||
private static CacheMemoryAllocator _cacheAllocator;
|
||||
@@ -26,6 +27,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
||||
|
||||
private static readonly Lock _lock = new();
|
||||
private static bool _initialized;
|
||||
private static readonly List<ReservedRegion> _jitRegions = new();
|
||||
private static int _activeRegionIndex = 0;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
@@ -45,7 +48,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
||||
return;
|
||||
}
|
||||
|
||||
_jitRegion = new ReservedRegion(allocator, CacheSize);
|
||||
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
||||
_jitRegions.Add(firstRegion);
|
||||
_activeRegionIndex = 0;
|
||||
|
||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
||||
{
|
||||
@@ -65,8 +70,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
||||
Debug.Assert(_initialized);
|
||||
|
||||
int funcOffset = Allocate(code.Length);
|
||||
|
||||
nint funcPtr = _jitRegion.Pointer + funcOffset;
|
||||
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
||||
nint funcPtr = targetRegion.Pointer + funcOffset;
|
||||
|
||||
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
@@ -80,18 +85,11 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
||||
}
|
||||
else
|
||||
{
|
||||
ReprotectAsWritable(funcOffset, code.Length);
|
||||
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
||||
ReprotectAsExecutable(funcOffset, code.Length);
|
||||
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
|
||||
Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
|
||||
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
||||
|
||||
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
|
||||
}
|
||||
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
|
||||
}
|
||||
|
||||
Add(funcOffset, code.Length);
|
||||
@@ -106,50 +104,80 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
||||
{
|
||||
Debug.Assert(_initialized);
|
||||
|
||||
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
||||
|
||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||
foreach (var region in _jitRegions)
|
||||
{
|
||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||
_cacheEntries.RemoveAt(entryIndex);
|
||||
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
||||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
|
||||
|
||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||
{
|
||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||
_cacheEntries.RemoveAt(entryIndex);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReprotectAsWritable(int offset, int size)
|
||||
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
|
||||
{
|
||||
int endOffs = offset + size;
|
||||
|
||||
int regionStart = offset & ~_pageMask;
|
||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||
|
||||
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
}
|
||||
|
||||
private static void ReprotectAsExecutable(int offset, int size)
|
||||
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
|
||||
{
|
||||
int endOffs = offset + size;
|
||||
|
||||
int regionStart = offset & ~_pageMask;
|
||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||
|
||||
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
}
|
||||
|
||||
private static int Allocate(int codeSize)
|
||||
{
|
||||
codeSize = AlignCodeSize(codeSize);
|
||||
|
||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
||||
|
||||
if (allocOffset < 0)
|
||||
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
||||
{
|
||||
throw new OutOfMemoryException("JIT Cache exhausted.");
|
||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
||||
|
||||
if (allocOffset >= 0)
|
||||
{
|
||||
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||
_activeRegionIndex = i;
|
||||
return allocOffset;
|
||||
}
|
||||
}
|
||||
|
||||
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||
int exhaustedRegion = _activeRegionIndex;
|
||||
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
|
||||
_jitRegions.Add(newRegion);
|
||||
_activeRegionIndex = _jitRegions.Count - 1;
|
||||
|
||||
int newRegionNumber = _activeRegionIndex;
|
||||
|
||||
return allocOffset;
|
||||
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
|
||||
|
||||
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||
|
||||
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
|
||||
if (allocOffsetNew < 0)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
|
||||
}
|
||||
|
||||
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
|
||||
return allocOffsetNew;
|
||||
}
|
||||
|
||||
private static int AlignCodeSize(int codeSize)
|
||||
|
||||
@@ -150,6 +150,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
{ BsdSocketOption.SoLinger, SocketOptionName.Linger },
|
||||
{ BsdSocketOption.SoOobInline, SocketOptionName.OutOfBandInline },
|
||||
{ BsdSocketOption.SoReusePort, SocketOptionName.ReuseAddress },
|
||||
{ BsdSocketOption.SoNoSigpipe, SocketOptionName.DontLinger },
|
||||
{ BsdSocketOption.SoSndBuf, SocketOptionName.SendBuffer },
|
||||
{ BsdSocketOption.SoRcvBuf, SocketOptionName.ReceiveBuffer },
|
||||
{ BsdSocketOption.SoSndLoWat, SocketOptionName.SendLowWater },
|
||||
|
||||
@@ -1525,151 +1525,151 @@
|
||||
{
|
||||
"ID": "GameListHeaderDeveloper",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Developed by {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
"ar_SA": "المطور",
|
||||
"de_DE": "Entwickler",
|
||||
"el_GR": "Προγραμματιστής",
|
||||
"en_US": "Developer",
|
||||
"es_ES": "Desarrollador",
|
||||
"fr_FR": "Développeur",
|
||||
"he_IL": "מפתח",
|
||||
"it_IT": "Sviluppatore",
|
||||
"ja_JP": "開発元",
|
||||
"ko_KR": "개발자",
|
||||
"no_NO": "Utvikler",
|
||||
"pl_PL": "Twórca",
|
||||
"pt_BR": "Desenvolvedor",
|
||||
"ru_RU": "Разработчик",
|
||||
"sv_SE": "Utvecklare",
|
||||
"th_TH": "ผู้พัฒนา",
|
||||
"tr_TR": "Geliştirici",
|
||||
"uk_UA": "Розробник",
|
||||
"zh_CN": "制作商",
|
||||
"zh_TW": "開發者"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderVersion",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"ar_SA": "الإصدار",
|
||||
"de_DE": "",
|
||||
"el_GR": "Έκδοση: {0}",
|
||||
"en_US": "Version: {0}",
|
||||
"es_ES": "Versión: {0}",
|
||||
"el_GR": "Έκδοση",
|
||||
"en_US": "Version",
|
||||
"es_ES": "Versión",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "Versione: {0}",
|
||||
"ja_JP": "バージョン: {0}",
|
||||
"ko_KR": "버전: {0}",
|
||||
"no_NO": "Versjon: {0}",
|
||||
"pl_PL": "Wersja: {0}",
|
||||
"pt_BR": "Versão: {0}",
|
||||
"ru_RU": "Версия: {0}",
|
||||
"he_IL": "גרסה",
|
||||
"it_IT": "Versione",
|
||||
"ja_JP": "バージョン",
|
||||
"ko_KR": "버전",
|
||||
"no_NO": "Versjon",
|
||||
"pl_PL": "Wersja",
|
||||
"pt_BR": "Versão",
|
||||
"ru_RU": "Версия",
|
||||
"sv_SE": "",
|
||||
"th_TH": "เวอร์ชั่น: {0}",
|
||||
"tr_TR": "Sürüm: {0}",
|
||||
"uk_UA": "Версія: {0}",
|
||||
"zh_CN": "版本: {0}",
|
||||
"zh_TW": "版本: {0}"
|
||||
"th_TH": "เวอร์ชั่น",
|
||||
"tr_TR": "Sürüm",
|
||||
"uk_UA": "Версія",
|
||||
"zh_CN": "版本",
|
||||
"zh_TW": "版本"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderTimePlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Spielzeit: {0}",
|
||||
"el_GR": "Χρόνος: {0}",
|
||||
"en_US": "Play Time: {0}",
|
||||
"es_ES": "Tiempo jugado: {0}",
|
||||
"fr_FR": "Temps de jeu: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Tempo di gioco: {0}",
|
||||
"ja_JP": "プレイ時間: {0}",
|
||||
"ko_KR": "플레이 타임: {0}",
|
||||
"no_NO": "Spilletid: {0}",
|
||||
"pl_PL": "Czas w grze: {0}",
|
||||
"pt_BR": "Tempo de jogo: {0}",
|
||||
"ru_RU": "Время в игре: {0}",
|
||||
"sv_SE": "Speltid: {0}",
|
||||
"th_TH": "เล่นไปแล้ว: {0}",
|
||||
"tr_TR": "Oynama Süresi: {0}",
|
||||
"uk_UA": "Зіграно часу: {0}",
|
||||
"zh_CN": "游玩时长: {0}",
|
||||
"zh_TW": "遊玩時數: {0}"
|
||||
"ar_SA": "وقت اللعب",
|
||||
"de_DE": "Spielzeit",
|
||||
"el_GR": "Χρόνος",
|
||||
"en_US": "Play Time",
|
||||
"es_ES": "Tiempo jugado",
|
||||
"fr_FR": "Temps de jeu",
|
||||
"he_IL": "זמן משחק",
|
||||
"it_IT": "Tempo di gioco",
|
||||
"ja_JP": "プレイ時間",
|
||||
"ko_KR": "플레이 타임",
|
||||
"no_NO": "Spilletid",
|
||||
"pl_PL": "Czas w grze:",
|
||||
"pt_BR": "Tempo de jogo",
|
||||
"ru_RU": "Время в игре",
|
||||
"sv_SE": "Speltid",
|
||||
"th_TH": "เล่นไปแล้ว",
|
||||
"tr_TR": "Oynama Süresi",
|
||||
"uk_UA": "Зіграно часу",
|
||||
"zh_CN": "游玩时长",
|
||||
"zh_TW": "遊玩時數"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderLastPlayed",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Zuletzt gespielt: {0}",
|
||||
"el_GR": "Παίχτηκε: {0}",
|
||||
"en_US": "Last Played: {0}",
|
||||
"es_ES": "Jugado por última vez: {0}",
|
||||
"fr_FR": "Dernière partie jouée: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Ultima partita: {0}",
|
||||
"ja_JP": "最終プレイ日時: {0}",
|
||||
"ko_KR": "마지막 플레이: {0}",
|
||||
"no_NO": "Sist Spilt: {0}",
|
||||
"pl_PL": "Ostatnio grane: {0}",
|
||||
"pt_BR": "Último jogo: {0}",
|
||||
"ru_RU": "Последний запуск: {0}",
|
||||
"sv_SE": "Senast spelad: {0}",
|
||||
"th_TH": "เล่นล่าสุด: {0}",
|
||||
"tr_TR": "Son Oynama Tarihi: {0}",
|
||||
"uk_UA": "Востаннє зіграно: {0}",
|
||||
"zh_CN": "最近游玩: {0}",
|
||||
"zh_TW": "最近遊玩: {0}"
|
||||
"ar_SA": "آخر مرة لُعبت",
|
||||
"de_DE": "Zuletzt gespielt",
|
||||
"el_GR": "Παίχτηκε",
|
||||
"en_US": "Last Played",
|
||||
"es_ES": "Jugado por última vez",
|
||||
"fr_FR": "Dernière partie jouée",
|
||||
"he_IL": "שוחק לאחרונה",
|
||||
"it_IT": "Ultima partita",
|
||||
"ja_JP": "最終プレイ日時",
|
||||
"ko_KR": "마지막 플레이",
|
||||
"no_NO": "Sist Spilt",
|
||||
"pl_PL": "Ostatnio grane",
|
||||
"pt_BR": "Último jogo",
|
||||
"ru_RU": "Последний запуск",
|
||||
"sv_SE": "Senast spelad",
|
||||
"th_TH": "เล่นล่าสุด",
|
||||
"tr_TR": "Son Oynama Tarihi",
|
||||
"uk_UA": "Востаннє зіграно",
|
||||
"zh_CN": "最近游玩",
|
||||
"zh_TW": "最近遊玩"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileExtension",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Dateiformat: {0}",
|
||||
"el_GR": "Κατάληξη: {0}",
|
||||
"en_US": "Extension: {0}",
|
||||
"es_ES": "Extensión: {0}",
|
||||
"fr_FR": "Extension du Fichier: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Estensione: {0}",
|
||||
"ja_JP": "ファイル拡張子: {0}",
|
||||
"ko_KR": "파일 확장자: {0}",
|
||||
"no_NO": "Fil Eks.: {0}",
|
||||
"pl_PL": "Rozszerzenie pliku: {0}",
|
||||
"pt_BR": "Extensão: {0}",
|
||||
"ru_RU": "Расширение файла: {0}",
|
||||
"sv_SE": "Filänd: {0}",
|
||||
"th_TH": "นามสกุลไฟล์: {0}",
|
||||
"tr_TR": "Dosya Uzantısı: {0}",
|
||||
"uk_UA": "Розширення файлу: {0}",
|
||||
"zh_CN": "扩展名: {0}",
|
||||
"zh_TW": "副檔名: {0}"
|
||||
"ar_SA": "صيغة الملف",
|
||||
"de_DE": "Dateiformat",
|
||||
"el_GR": "Κατάληξη",
|
||||
"en_US": "File Ext",
|
||||
"es_ES": "Extensión",
|
||||
"fr_FR": "Extension du Fichier",
|
||||
"he_IL": "סיומת קובץ",
|
||||
"it_IT": "Estensione",
|
||||
"ja_JP": "ファイル拡張子",
|
||||
"ko_KR": "파일 확장자",
|
||||
"no_NO": "Fil Eks.",
|
||||
"pl_PL": "Rozszerzenie pliku",
|
||||
"pt_BR": "Extensão",
|
||||
"ru_RU": "Расширение файла",
|
||||
"sv_SE": "Filänd",
|
||||
"th_TH": "นามสกุลไฟล์",
|
||||
"tr_TR": "Dosya Uzantısı",
|
||||
"uk_UA": "Розширення файлу",
|
||||
"zh_CN": "扩展名",
|
||||
"zh_TW": "副檔名"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderFileSize",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Dateigröße: {0}",
|
||||
"el_GR": "Μέγεθος Αρχείου: {0}",
|
||||
"en_US": "File Size: {0}",
|
||||
"es_ES": "Tamaño del archivo: {0}",
|
||||
"fr_FR": "Taille du Fichier: {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Dimensione file: {0}",
|
||||
"ja_JP": "ファイルサイズ: {0}",
|
||||
"ko_KR": "파일 크기: {0}",
|
||||
"no_NO": "Fil Størrelse: {0}",
|
||||
"pl_PL": "Rozmiar pliku: {0}",
|
||||
"pt_BR": "Tamanho: {0}",
|
||||
"ru_RU": "Размер файла: {0}",
|
||||
"sv_SE": "Filstorlek: {0}",
|
||||
"th_TH": "ขนาดไฟล์: {0}",
|
||||
"tr_TR": "Dosya Boyutu: {0}",
|
||||
"uk_UA": "Розмір файлу: {0}",
|
||||
"zh_CN": "大小: {0}",
|
||||
"zh_TW": "檔案大小: {0}"
|
||||
"ar_SA": "حجم الملف",
|
||||
"de_DE": "Dateigröße",
|
||||
"el_GR": "Μέγεθος Αρχείου",
|
||||
"en_US": "File Size",
|
||||
"es_ES": "Tamaño del archivo",
|
||||
"fr_FR": "Taille du Fichier",
|
||||
"he_IL": "גודל הקובץ",
|
||||
"it_IT": "Dimensione file",
|
||||
"ja_JP": "ファイルサイズ",
|
||||
"ko_KR": "파일 크기",
|
||||
"no_NO": "Fil Størrelse",
|
||||
"pl_PL": "Rozmiar pliku",
|
||||
"pt_BR": "Tamanho",
|
||||
"ru_RU": "Размер файла",
|
||||
"sv_SE": "Filstorlek",
|
||||
"th_TH": "ขนาดไฟล์",
|
||||
"tr_TR": "Dosya Boyutu",
|
||||
"uk_UA": "Розмір файлу",
|
||||
"zh_CN": "大小",
|
||||
"zh_TW": "檔案大小"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1697,106 +1697,6 @@
|
||||
"zh_TW": "路徑"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderCompatibilityStatus",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Compatibility:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderTitleId",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Title ID:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderHostedGames",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Hosted Games: {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListHeaderPlayerCount",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Online Players: {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuOpenUserSaveDirectory",
|
||||
"Translations": {
|
||||
@@ -2672,56 +2572,6 @@
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuShowGameData",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show Game Info",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuShowGameDataToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show stats & details about the currently selected game.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuOpenModsDirectory",
|
||||
"Translations": {
|
||||
|
||||
@@ -32,9 +32,6 @@ namespace Ryujinx.Ava
|
||||
public static MainWindow MainWindow => Current!
|
||||
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
|
||||
.MainWindow.Cast<MainWindow>();
|
||||
|
||||
public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current!
|
||||
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>();
|
||||
|
||||
public static bool IsClipboardAvailable(out IClipboard clipboard)
|
||||
{
|
||||
|
||||
@@ -25,11 +25,6 @@
|
||||
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
|
||||
Icon="{ext:Icon mdi-gamepad}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
|
||||
<MenuItem
|
||||
Click="OpenApplicationData_Click"
|
||||
Header="{ext:Locale GameListContextMenuShowGameData}"
|
||||
Icon="{ext:Icon mdi-chart-line}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuShowGameDataToolTip}"/>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="OpenUserSaveDirectory_Click"
|
||||
@@ -122,7 +117,6 @@
|
||||
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasDlc}"
|
||||
Click="ExtractAocRomFs_Click"
|
||||
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
|
||||
|
||||
@@ -334,7 +334,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
return;
|
||||
|
||||
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary);
|
||||
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
|
||||
|
||||
if (selectedDlc is not null)
|
||||
{
|
||||
@@ -392,12 +392,6 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
|
||||
}
|
||||
|
||||
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
await ApplicationDataView.Show(viewModel.SelectedApplication);
|
||||
}
|
||||
|
||||
public async void RunApplication_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
@@ -407,8 +401,12 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
|
||||
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
|
||||
x:DataType="viewModels:ApplicationDataViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Margin="0"
|
||||
MaxWidth="256"
|
||||
MinWidth="256"
|
||||
Source="{Binding AppData.Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
<Border Margin="5, 0" Width="1" Height="256" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
ColumnDefinitions="*">
|
||||
<StackPanel Grid.Row="0">
|
||||
<TextBlock HorizontalAlignment="Left"
|
||||
Text="{Binding FormattedVersion}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Left"
|
||||
Text="{Binding FormattedDeveloper}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedFileExtension}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedFileSize}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel Grid.Row="2"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Vertical"
|
||||
Spacing="5">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderCompatibilityStatus}" />
|
||||
<Button
|
||||
Click="PlayabilityStatus_OnClick"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding AppData.HasPlayabilityInfo}"
|
||||
Background="{DynamicResource AppListBackgroundColor}"
|
||||
Padding="0">
|
||||
<TextBlock
|
||||
Margin="1.5"
|
||||
Tag="{Binding AppData.IdString}"
|
||||
Text="{Binding AppData.LocalizedStatus}"
|
||||
Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Button.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="MinWidth"
|
||||
Value="0" />
|
||||
<!-- avoids very wide buttons from the overall project avalonia style -->
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderTitleId}" />
|
||||
<Button
|
||||
Click="IdString_OnClick"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource AppListBackgroundColor}"
|
||||
Padding="0">
|
||||
<TextBlock
|
||||
Margin="1.5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding AppData.IdString}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding AppData.HasLdnGames}"
|
||||
Text="{Binding FormattedLdnInfo}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Separator IsVisible="{Binding AppData.HasLdnGames}" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||
<StackPanel
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Vertical"
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedLastPlayed}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FormattedPlayTime}"
|
||||
IsVisible="{Binding AppData.HasPlayedPreviously}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -1,86 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using Ryujinx.Ava.Utilities.Compat;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class ApplicationDataView : UserControl
|
||||
{
|
||||
public static async Task Show(ApplicationData appData)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = appData.Name,
|
||||
PrimaryButtonText = string.Empty,
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
MinWidth = 256,
|
||||
Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) }
|
||||
};
|
||||
|
||||
Style closeButton = new(x => x.Name("CloseButton"));
|
||||
closeButton.Setters.Add(new Setter(WidthProperty, 160d));
|
||||
|
||||
Style closeButtonParent = new(x => x.Name("CommandSpace"));
|
||||
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
|
||||
Avalonia.Layout.HorizontalAlignment.Center));
|
||||
|
||||
contentDialog.Styles.Add(closeButton);
|
||||
contentDialog.Styles.Add(closeButtonParent);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
public ApplicationDataView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button { Content: TextBlock playabilityLabel })
|
||||
return;
|
||||
|
||||
if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window))
|
||||
window.Close(ContentDialogResult.None);
|
||||
|
||||
await CompatibilityList.Show((string)playabilityLabel.Tag);
|
||||
}
|
||||
|
||||
private async void IdString_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel mwvm)
|
||||
return;
|
||||
|
||||
if (sender is not Button { Content: TextBlock idText })
|
||||
return;
|
||||
|
||||
if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard))
|
||||
return;
|
||||
|
||||
ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text);
|
||||
if (appData is null)
|
||||
return;
|
||||
|
||||
await clipboard.SetTextAsync(appData.IdString);
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
"Copied Title ID",
|
||||
$"{appData.Name} ({appData.IdString})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,6 @@
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding HasLdnGames}"
|
||||
Text="{Binding Converter={helpers:MultiplayerInfoConverter}}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
@@ -15,17 +12,16 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not ApplicationData { HasLdnGames: true } applicationData)
|
||||
return "";
|
||||
if (value is ApplicationData applicationData)
|
||||
{
|
||||
if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0)
|
||||
{
|
||||
return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
|
||||
return new StringBuilder()
|
||||
.AppendLine(
|
||||
LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames]
|
||||
.Format(applicationData.GameCount))
|
||||
.Append(
|
||||
LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount]
|
||||
.Format(applicationData.PlayerCount))
|
||||
.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class ApplicationDataViewModel : BaseModel
|
||||
{
|
||||
public ApplicationData AppData { get; }
|
||||
|
||||
public ApplicationDataViewModel(ApplicationData appData) => AppData = appData;
|
||||
|
||||
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
|
||||
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
|
||||
|
||||
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
|
||||
public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString);
|
||||
public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString);
|
||||
public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
|
||||
|
||||
public string FormattedLdnInfo =>
|
||||
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" +
|
||||
$"\n" +
|
||||
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}";
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
|
||||
{
|
||||
_dlcs = appLibrary.FindDlcsFor(titleId)
|
||||
_dlcs = appLibrary.DownloadableContents.Items
|
||||
.Where(x => x.Dlc.TitleIdBase == titleId)
|
||||
.Select(x => x.Dlc)
|
||||
.OrderBy(it => it.IsBundled ? 0 : 1)
|
||||
.ThenBy(it => it.TitleId)
|
||||
.ToArray();
|
||||
|
||||
@@ -69,7 +69,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
(DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id);
|
||||
IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items
|
||||
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
|
||||
|
||||
bool hasBundledContent = false;
|
||||
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
|
||||
|
||||
@@ -349,9 +349,16 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
|
||||
public bool HasCompatibilityEntry
|
||||
{
|
||||
get
|
||||
{
|
||||
DynamicData.Kernel.Optional<ApplicationData> appData =
|
||||
ApplicationLibrary.Applications.Lookup(SelectedApplication.Id);
|
||||
|
||||
public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
|
||||
return appData.HasValue && appData.Value.HasPlayabilityInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
(TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id);
|
||||
IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items
|
||||
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
|
||||
|
||||
bool hasBundledContent = false;
|
||||
SelectedUpdate = new TitleUpdateViewModelNoUpdate();
|
||||
|
||||
@@ -49,9 +49,6 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
|
||||
public int PlayerCount { get; set; }
|
||||
public int GameCount { get; set; }
|
||||
|
||||
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
|
||||
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
|
||||
@@ -128,16 +128,11 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
|
||||
if (appData.HasValue)
|
||||
return appData.Value.Name;
|
||||
|
||||
if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
|
||||
return id.ToString("X16");
|
||||
|
||||
string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!;
|
||||
int idx = name.IndexOf('[');
|
||||
if (idx != -1)
|
||||
name = name[..idx];
|
||||
if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
|
||||
return Path.GetFileNameWithoutExtension(dlcData.FileName);
|
||||
|
||||
return name;
|
||||
return id.ToString("X16");
|
||||
}
|
||||
|
||||
public bool FindApplication(ulong id, out ApplicationData foundData)
|
||||
@@ -147,30 +142,6 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
|
||||
return appData.HasValue;
|
||||
}
|
||||
|
||||
public bool FindUpdate(ulong id, out TitleUpdateModel foundData)
|
||||
{
|
||||
Gommon.Optional<TitleUpdateModel> appData =
|
||||
TitleUpdates.Keys.FindFirst(x => x.TitleId == id);
|
||||
foundData = appData.HasValue ? appData.Value : null;
|
||||
|
||||
return appData.HasValue;
|
||||
}
|
||||
|
||||
public TitleUpdateModel[] FindUpdatesFor(ulong id)
|
||||
=> TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id)
|
||||
=> TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public DownloadableContentModel[] FindDlcsFor(ulong id)
|
||||
=> DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id)
|
||||
=> DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
|
||||
|
||||
public bool HasDlcs(ulong id)
|
||||
=> DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL));
|
||||
|
||||
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
|
||||
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>
|
||||
|
||||
Reference in New Issue
Block a user