Compare commits

...

17 Commits

Author SHA1 Message Date
Neo
3969191605 UI: Menubar & Game Context Menu Updates (ryubing/ryujinx!134)
See merge request ryubing/ryujinx!134
2025-09-02 03:32:06 -05:00
Hack茶ん
07c7b39053 Update Korean translation (ryubing/ryujinx!136)
See merge request ryubing/ryujinx!136
2025-09-01 17:57:44 -05:00
Hack茶ん
053efaa414 Update Korean translation (ryubing/ryujinx!135)
See merge request ryubing/ryujinx!135
2025-09-01 01:36:27 -05:00
GreemDev
56e6339553 hle: cheats: Prevent NullRef and throw a TamperCompilationException instead
for null base instruction byte arrays on the current block in EndConditionalBlock
2025-08-31 23:06:42 -05:00
shinyoyo
042362ee2b Update Simplified Chinese translation. (ryubing/ryujinx!133)
See merge request ryubing/ryujinx!133
2025-08-30 22:40:05 -05:00
GreemDev
7347ee2212 [ci skip] chore: UI: Add localization key for LDN Game Viewer filters dropdown button heading 2025-08-30 22:13:38 -05:00
LotP
01a9b636af Memory changes 2.1 (ryubing/ryujinx!132)
See merge request ryubing/ryujinx!132
2025-08-30 20:30:17 -05:00
GreemDev
6e47d8548c feature: UI: LDN Games Viewer
This window can be accessed via "Help" menu in the title bar.
This menu's data is synced with the in-app-list LDN game data, and that has been modified to hide unjoinable games (in-progress and/or private (needing a passphrase)). You can still see these games in the list.
2025-08-30 19:54:00 -05:00
GreemDev
da340f5615 chore: remove redundant CloseWindow helper 2025-08-30 00:40:39 -05:00
GreemDev
be249f7bdc chore: move NFC tags URL to SharedConstants.cs 2025-08-30 00:35:16 -05:00
GreemDev
462c93e1ff fix key number 5 locale 2025-08-28 20:32:48 -05:00
Babib3l
573a6f32fe nullify + update spanish and french translations (ryubing/ryujinx!125)
See merge request ryubing/ryujinx!125
2025-08-28 13:28:24 -05:00
GreemDev
7846f58cad [ci skip] chore: Change LDN server URL (it's the same server, just a more official URL) 2025-08-27 22:49:51 -05:00
GreemDev
0203065fed ui: fix: Missing "Loading" text when shader cache is disabled and PPTC doesn't trigger 2025-08-27 22:35:09 -05:00
shinyoyo
7319a2dafc Nullify & update Simplified Chinese translation. (ryubing/ryujinx!124)
See merge request ryubing/ryujinx!124
2025-08-27 00:41:47 -05:00
Hack茶ん
f992735656 Nullify & Update Korean translation (ryubing/ryujinx!122)
See merge request ryubing/ryujinx!122
2025-08-27 00:40:11 -05:00
GreemDev
48b9f2ab93 docs: compat: High on Life: Menus 2025-08-26 20:12:36 -05:00
41 changed files with 2565 additions and 947 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1440,6 +1440,7 @@
0100C2700E338000,"Heroland",,playable,2020-08-05 15:35:39
01007AC00E012000,"HexaGravity",,playable,2021-05-28 13:47:48
01004E800F03C000,"Hidden",slow,ingame,2022-10-05 10:56:53
0100C1101EE5A000,"High on Life",,menus,2025-08-26 19:11:00
0100F6A00A684000,"Higurashi no Naku Koro ni Hō",audio,ingame,2021-09-18 14:40:28
0100F8D0129F4000,"Himehibi 1 gakki - Princess Days",crash,nothing,2021-11-03 08:34:19
0100F3D008436000,"Hiragana Pixel Party",,playable,2021-01-14 08:36:50
1 title_id game_name labels status last_updated
1440 0100C2700E338000 Heroland playable 2020-08-05 15:35:39
1441 01007AC00E012000 HexaGravity playable 2021-05-28 13:47:48
1442 01004E800F03C000 Hidden slow ingame 2022-10-05 10:56:53
1443 0100C1101EE5A000 High on Life menus 2025-08-26 19:11:00
1444 0100F6A00A684000 Higurashi no Naku Koro ni Hō audio ingame 2021-09-18 14:40:28
1445 0100F8D0129F4000 Himehibi 1 gakki - Princess Days crash nothing 2021-11-03 08:34:19
1446 0100F3D008436000 Hiragana Pixel Party playable 2021-01-14 08:36:50

View File

@@ -2,8 +2,18 @@ namespace Ryujinx.Common
{
public static class SharedConstants
{
public const string DefaultLanPlayHost = "ryuldn.vudjun.com";
public const string DefaultLanPlayHost = "ldn.ryujinx.app";
public const short LanPlayPort = 30456;
public const string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
public const string DefaultLanPlayWebHost = DefaultLanPlayHost;
public const string AmiiboTagsUrl = "https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json";
public const string FaqWikiUrl = "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/FAQ-&-Troubleshooting";
public const string SetupGuideWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
public const string MultiplayerWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
}
}

View File

@@ -81,16 +81,8 @@ namespace Ryujinx.Graphics.Device
if (index < Size)
{
uint alignedOffset = index * RegisterSize;
Func<int> readCallback = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_readCallbacks), (nint)index);
if (readCallback != null)
{
return readCallback();
}
else
{
return GetRefUnchecked<int>(alignedOffset);
}
return _readCallbacks[index]?.Invoke() ?? GetRefUnchecked<int>(alignedOffset);
}
return 0;
@@ -107,7 +99,7 @@ namespace Ryujinx.Graphics.Device
GetRefIntAlignedUncheck(index) = data;
Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_writeCallbacks), (nint)index)?.Invoke(data);
_writeCallbacks[index]?.Invoke(data);
}
}
@@ -124,7 +116,7 @@ namespace Ryujinx.Graphics.Device
changed = storage != data;
storage = data;
Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_writeCallbacks), (nint)index)?.Invoke(data);
_writeCallbacks[index]?.Invoke(data);
}
else
{

View File

@@ -109,7 +109,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (index < BlockSize)
{
int groupIndex = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_registerToGroupMapping), (nint)index);
int groupIndex = _registerToGroupMapping[index];
if (groupIndex != 0)
{
groupIndex--;

View File

@@ -120,11 +120,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
public void ExcludeModifiedRegions(ulong address, ulong size, Action<ulong, ulong> action)
{
// Slices a given region using the modified regions in the list. Calls the action for the new slices.
bool lockOwner = Lock.IsReadLockHeld;
if (!lockOwner)
{
Lock.EnterReadLock();
}
Lock.EnterReadLock();
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlaps(address, size);
@@ -145,10 +141,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
current = current.Next;
}
if (!lockOwner)
{
Lock.ExitReadLock();
}
Lock.ExitReadLock();
if ((long)size > 0)
{
@@ -179,9 +172,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
return;
}
BufferModifiedRange buffPost = null;
bool extendsPost = false;
bool extendsPre = false;
if (first == last)
{
@@ -196,14 +187,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (first.Address < address)
{
first.Value.Size = address - first.Address;
extendsPre = true;
Update(first);
if (first.EndAddress > endAddress)
{
buffPost = new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
first.Value.SyncNumber, first.Value.Parent);
extendsPost = true;
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
first.Value.SyncNumber, first.Value.Parent));
}
}
else
@@ -212,6 +201,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
first.Value.Size = first.EndAddress - endAddress;
first.Value.Address = endAddress;
Update(first);
}
else
{
@@ -219,11 +209,6 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
if (extendsPre && extendsPost)
{
Add(buffPost);
}
Add(new BufferModifiedRange(address, size, syncNumber, this));
Lock.ExitWriteLock();
@@ -231,6 +216,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
BufferModifiedRange buffPre = null;
BufferModifiedRange buffPost = null;
bool extendsPost = false;
bool extendsPre = false;
if (first.Address < address)
{
@@ -329,7 +317,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
public bool HasRange(ulong address, ulong size)
{
Lock.EnterReadLock();
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> _) = FindOverlaps(address, size);
RangeItem<BufferModifiedRange> first = FindOverlapFast(address, size);
bool result = first is not null;
Lock.ExitReadLock();
return result;
@@ -386,9 +374,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong clampAddress = Math.Max(address, overlap.Address);
ulong clampEnd = Math.Min(endAddress, overlap.EndAddress);
Lock.EnterWriteLock();
ClearPart(overlap, clampAddress, clampEnd);
Lock.ExitWriteLock();
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction);
}
@@ -418,40 +404,33 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong endAddress = address + size;
ulong currentSync = _context.SyncNumber;
int rangeCount = 0;
List<RangeItem<BufferModifiedRange>> overlaps = [];
// Range list must be consistent for this operation
Lock.EnterReadLock();
if (_migrationTarget != null)
{
rangeCount = -1;
}
else
{
// We use the non-span method here because the array is partially modified by the code, which would invalidate a span.
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlaps(address, size);
RangeItem<BufferModifiedRange> current = first;
while (last != null && current != last.Next)
{
rangeCount++;
overlaps.Add(current);
current = current.Next;
}
}
Lock.ExitReadLock();
if (rangeCount == -1)
{
_migrationTarget!.WaitForAndFlushRanges(address, size);
return;
}
Lock.EnterWriteLock();
// We use the non-span method here because the array is partially modified by the code, which would invalidate a span.
(RangeItem<BufferModifiedRange> first, RangeItem<BufferModifiedRange> last) = FindOverlaps(address, size);
RangeItem<BufferModifiedRange> current = first;
while (last != null && current != last.Next)
{
overlaps.Add(current);
current = current.Next;
}
int rangeCount = overlaps.Count;
if (rangeCount == 0)
{
Lock.ExitWriteLock();
return;
}
@@ -474,6 +453,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (highestDiff == long.MinValue)
{
Lock.ExitWriteLock();
return;
}
@@ -481,6 +462,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
_context.Renderer.WaitSync(currentSync + (ulong)highestDiff);
RemoveRangesAndFlush(overlaps.ToArray(), rangeCount, highestDiff, currentSync, address, endAddress);
Lock.ExitWriteLock();
}
/// <summary>
@@ -607,22 +590,17 @@ namespace Ryujinx.Graphics.Gpu.Memory
return;
}
BufferModifiedRange buffPost = null;
bool extendsPost = false;
bool extendsPre = false;
if (first == last)
{
if (first.Address < address)
{
first.Value.Size = address - first.Address;
extendsPre = true;
Update(first);
if (first.EndAddress > endAddress)
{
buffPost = new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
first.Value.SyncNumber, first.Value.Parent);
extendsPost = true;
Add(new BufferModifiedRange(endAddress, first.EndAddress - endAddress,
first.Value.SyncNumber, first.Value.Parent));
}
}
else
@@ -631,6 +609,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
first.Value.Size = first.EndAddress - endAddress;
first.Value.Address = endAddress;
Update(first);
}
else
{
@@ -638,16 +617,14 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
if (extendsPre && extendsPost)
{
Add(buffPost);
}
Lock.ExitWriteLock();
return;
}
BufferModifiedRange buffPre = null;
BufferModifiedRange buffPost = null;
bool extendsPost = false;
bool extendsPre = false;
if (first.Address < address)
{

View File

@@ -50,7 +50,7 @@ namespace Ryujinx.HLE.HOS.Tamper
Logger.Error?.Print(LogClass.TamperMachine, ex.ToString());
}
Logger.Error?.Print(LogClass.TamperMachine, "There was a problem while compiling the Atmosphere cheat");
Logger.Error?.Print(LogClass.TamperMachine, $"There was a problem while compiling the Atmosphere cheat '{name}'");
return null;
}
@@ -126,7 +126,7 @@ namespace Ryujinx.HLE.HOS.Tamper
DebugLog.Emit(instruction, context);
break;
default:
throw new TamperCompilationException($"Code type {codeType} not implemented in Atmosphere cheat");
throw new TamperCompilationException($"Code type {codeType} not implemented in Atmosphere cheat compiler");
}
}

View File

@@ -40,7 +40,8 @@ namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
}
// Use the conditional begin instruction stored in the stack.
byte[] upperInstruction = context.CurrentBlock.BaseInstruction;
byte[] upperInstruction = context.CurrentBlock.BaseInstruction
?? throw new TamperCompilationException($"Base instruction in current block was null; termination type '{terminationType}'");
CodeType codeType = InstructionHelper.GetCodeType(upperInstruction);
// Pop the current block of operations from the stack so control instructions

View File

@@ -87,6 +87,43 @@ namespace Ryujinx.Memory.Range
return false;
}
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The RangeItem to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected override bool Update(RangeItem<T> item)
{
int index = BinarySearch(item.Address);
RangeItem<T> rangeItem = new(item.Value) { Previous = item.Previous, Next = item.Next };
if (index > 0)
{
Items[index - 1].Next = rangeItem;
}
if (index < Count - 1)
{
Items[index + 1].Previous = rangeItem;
}
foreach (ulong addr in item.QuickAccessAddresses)
{
_quickAccess.Remove(addr);
_fastQuickAccess.Remove(addr);
}
Items[index] = rangeItem;
if (item.Address != rangeItem.Address)
_quickAccess.Remove(item.Address);
_quickAccess[rangeItem.Address] = rangeItem;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Insert(int index, RangeItem<T> item)
{
@@ -193,10 +230,9 @@ namespace Ryujinx.Memory.Range
return;
}
int startIndex = BinarySearch(startItem.Address);
int endIndex = BinarySearch(endItem.Address);
(int startIndex, int endIndex) = BinarySearchEdges(startItem.Address, endItem.EndAddress);
for (int i = startIndex; i <= endIndex; i++)
for (int i = startIndex; i < endIndex; i++)
{
_quickAccess.Remove(Items[i].Address);
foreach (ulong addr in Items[i].QuickAccessAddresses)
@@ -206,23 +242,23 @@ namespace Ryujinx.Memory.Range
}
}
if (endIndex < Count - 1)
if (endIndex < Count)
{
Items[endIndex + 1].Previous = startIndex > 0 ? Items[startIndex - 1] : null;
Items[endIndex].Previous = startIndex > 0 ? Items[startIndex - 1] : null;
}
if (startIndex > 0)
{
Items[startIndex - 1].Next = endIndex < Count - 1 ? Items[endIndex + 1] : null;
Items[startIndex - 1].Next = endIndex < Count ? Items[endIndex] : null;
}
if (endIndex < Count - 1)
if (endIndex < Count)
{
Array.Copy(Items, endIndex + 1, Items, startIndex, Count - endIndex - 1);
Array.Copy(Items, endIndex, Items, startIndex, Count - endIndex);
}
Count -= endIndex - startIndex + 1;
Count -= endIndex - startIndex;
}
/// <summary>

View File

@@ -81,12 +81,73 @@ namespace Ryujinx.Memory.Range
{
if (Items[index].Value.Equals(item))
{
RangeItem<T> rangeItem = new(item) { Previous = Items[index].Previous, Next = Items[index].Next };
if (index > 0)
{
Items[index - 1].Next = rangeItem;
}
if (index < Count - 1)
{
Items[index + 1].Previous = rangeItem;
}
foreach (ulong address in Items[index].QuickAccessAddresses)
{
_quickAccess.Remove(address);
}
Items[index] = new RangeItem<T>(item);
Items[index] = rangeItem;
return true;
}
if (Items[index].Address > item.Address)
{
break;
}
index++;
}
}
return false;
}
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The RangeItem to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected override bool Update(RangeItem<T> item)
{
int index = BinarySearch(item.Address);
if (index >= 0)
{
while (index < Count)
{
if (Items[index].Equals(item))
{
RangeItem<T> rangeItem = new(item.Value) { Previous = item.Previous, Next = item.Next };
if (index > 0)
{
Items[index - 1].Next = rangeItem;
}
if (index < Count - 1)
{
Items[index + 1].Previous = rangeItem;
}
foreach (ulong address in item.QuickAccessAddresses)
{
_quickAccess.Remove(address);
}
Items[index] = rangeItem;
return true;
}

View File

@@ -30,7 +30,7 @@ namespace Ryujinx.Memory.Range
return u1 == u2;
}
public int GetHashCode(ulong value) => (int)(value >> 5);
public int GetHashCode(ulong value) => (int)(value << 5);
public static readonly AddressEqualityComparer Comparer = new();
}
@@ -63,6 +63,13 @@ namespace Ryujinx.Memory.Range
/// <returns>True if the item was located and updated, false otherwise</returns>
protected abstract bool Update(T item);
/// <summary>
/// Updates an item's end address on the list. Address must be the same.
/// </summary>
/// <param name="item">The RangeItem to be updated</param>
/// <returns>True if the item was located and updated, false otherwise</returns>
protected abstract bool Update(RangeItem<T> item);
public abstract bool Remove(T item);
public abstract void RemoveRange(RangeItem<T> startItem, RangeItem<T> endItem);

View File

@@ -182,11 +182,15 @@ namespace Ryujinx.Memory.Tracking
{
if (region.Guest)
{
_guestVirtualRegions.Lock.EnterWriteLock();
_guestVirtualRegions.Remove(region);
_guestVirtualRegions.Lock.ExitWriteLock();
}
else
{
_virtualRegions.Lock.EnterWriteLock();
_virtualRegions.Remove(region);
_virtualRegions.Lock.ExitWriteLock();
}
}

View File

@@ -19,7 +19,7 @@ namespace Ryujinx.Ava.Common.Locale
private readonly Dictionary<LocaleKeys, string> _localeStrings;
private readonly ConcurrentDictionary<LocaleKeys, object[]> _dynamicValues;
private string _localeLanguageCode;
public string CurrentLanguageCode => _localeLanguageCode;
public static LocaleManager Instance { get; } = new();
public event Action LocaleChanged;
@@ -47,8 +47,12 @@ namespace Ryujinx.Ava.Common.Locale
private void Load()
{
string localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) ?
ConfigurationState.Instance.UI.LanguageCode.Value : CultureInfo.CurrentCulture.Name.Replace('-', '_');
string localeLanguageCode = CultureInfo.CurrentCulture.Name.Replace('-', '_');
if (Program.PreviewerDetached && ConfigurationState.Instance.UI.LanguageCode.Value is { } lang)
{
if (!string.IsNullOrEmpty(lang))
localeLanguageCode = lang;
}
LoadLanguage(localeLanguageCode);
@@ -63,6 +67,15 @@ namespace Ryujinx.Ava.Common.Locale
public static string GetUnformatted(LocaleKeys key) => Instance.Get(key);
public static string GetFormatted(LocaleKeys key, params object[] values)
=> GetUnformatted(key).Format(values);
public static string FormatDynamicValue(LocaleKeys key, params object[] values)
=> Instance.UpdateAndGetDynamicValue(key, values);
public static void Associate(LocaleKeys key, params object[] values)
=> Instance.SetDynamicValues(key, values);
public string Get(LocaleKeys key) =>
_localeStrings.TryGetValue(key, out string value)
? value
@@ -107,9 +120,6 @@ namespace Ryujinx.Ava.Common.Locale
_ => false
};
public static string FormatDynamicValue(LocaleKeys key, params object[] values)
=> Instance.UpdateAndGetDynamicValue(key, values);
public void SetDynamicValues(LocaleKeys key, params object[] values)
{
_dynamicValues[key] = values;
@@ -161,12 +171,14 @@ namespace Ryujinx.Ava.Common.Locale
{
if (locale.Translations.Count < _localeData.Value.Languages.Count)
{
throw new Exception($"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
throw new Exception(
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
if (locale.Translations.Count > _localeData.Value.Languages.Count)
{
throw new Exception($"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
throw new Exception(
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
@@ -178,7 +190,8 @@ namespace Ryujinx.Ava.Common.Locale
if (string.IsNullOrEmpty(str))
{
throw new Exception($"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
throw new Exception(
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
}
localeStrings[localeKey] = str;

View File

@@ -1169,10 +1169,8 @@ namespace Ryujinx.Ava.Systems
string frameTime = Device.Statistics.GetGameFrameTime().ToString("00.00");
return Device.TurboMode
? LocaleManager.GetUnformatted(LocaleKeys.FpsTurboStatusBarText)
.Format(frameRate, frameTime, Device.TickScalar)
: LocaleManager.GetUnformatted(LocaleKeys.FpsStatusBarText)
.Format(frameRate, frameTime);
? LocaleManager.GetFormatted(LocaleKeys.FpsTurboStatusBarText, frameRate, frameTime, Device.TickScalar)
: LocaleManager.GetFormatted(LocaleKeys.FpsStatusBarText, frameRate, frameTime);
}
public async Task ShowExitPrompt()

View File

@@ -14,6 +14,7 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.Configuration.System;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
@@ -29,7 +30,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
@@ -85,7 +85,6 @@ namespace Ryujinx.Ava.Systems.AppLibrary
private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc);
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
{
@@ -865,16 +864,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
{
try
{
string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer;
if (string.IsNullOrEmpty(ldnWebHost))
{
ldnWebHost = SharedConstants.DefaultLanPlayWebHost;
}
using HttpClient httpClient = new();
string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
LdnGameData[] ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData).ToArray();
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs(ldnGameDataArray));
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs(await LdnGameModel.GetAllAsync()));
return;
}
catch (Exception ex)

View File

@@ -1,49 +0,0 @@
using Gommon;
using LibHac.Ns;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Systems.AppLibrary
{
public struct LdnGameData
{
public string Id { get; set; }
public int PlayerCount { get; set; }
public int MaxPlayerCount { get; set; }
public string GameName { get; set; }
public string TitleId { get; set; }
public string Mode { get; set; }
public string Status { get; set; }
public IEnumerable<string> Players { get; set; }
public static Array GetArrayForApp(
LdnGameData[] receivedData, ref ApplicationControlProperty acp)
{
LibHac.Common.FixedArrays.Array8<ulong> communicationId = acp.LocalCommunicationId;
return new Array(receivedData.Where(game =>
communicationId.AsReadOnlySpan().Contains(game.TitleId.ToULong())
));
}
public class Array
{
private readonly LdnGameData[] _ldnDatas;
internal Array(IEnumerable<LdnGameData> receivedData)
{
_ldnDatas = receivedData.ToArray();
}
public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount);
public int GameCount => _ldnDatas.Length;
}
}
public static class LdnGameDataHelper
{
public static LdnGameData.Array Where(this LdnGameData[] unfilteredDatas, ref ApplicationControlProperty acp)
=> LdnGameData.GetArrayForApp(unfilteredDatas, ref acp);
}
}

View File

@@ -1,4 +1,7 @@
using Ryujinx.Ava.UI.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Systems.AppLibrary
{
@@ -6,12 +9,17 @@ namespace Ryujinx.Ava.Systems.AppLibrary
{
public static new readonly LdnGameDataReceivedEventArgs Empty = new(null);
public LdnGameDataReceivedEventArgs(LdnGameData[] ldnData)
public LdnGameDataReceivedEventArgs(LdnGameModel[] ldnData)
{
LdnData = ldnData ?? [];
}
public LdnGameDataReceivedEventArgs(IEnumerable<LdnGameModel> ldnData)
{
LdnData = ldnData?.ToArray() ?? [];
}
public LdnGameData[] LdnData { get; set; }
public LdnGameModel[] LdnData { get; }
}
}

View File

@@ -1,8 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.AppLibrary
{
[JsonSerializable(typeof(IEnumerable<LdnGameData>))]
internal partial class LdnGameDataSerializerContext : JsonSerializerContext;
}

View File

@@ -11,6 +11,7 @@ using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE;
using System;
using System.Collections.Generic;
using System.Linq;
using RyuLogger = Ryujinx.Common.Logging.Logger;
@@ -689,6 +690,16 @@ namespace Ryujinx.Ava.Systems.Configuration
: ldnServer;
}
public string GetLdnWebServer()
{
if (Environment.GetEnvironmentVariable("RYULDN_WEB_HOST") is not { } ldnWebServer)
ldnWebServer = LdnServer;
return string.IsNullOrEmpty(ldnWebServer)
? SharedConstants.DefaultLanPlayWebHost
: ldnWebServer;
}
public MultiplayerSection()
{
LanInterfaceId = new ReactiveObject<string>();

View File

@@ -1,11 +1,9 @@
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Systems

View File

@@ -71,12 +71,12 @@
Command="{Binding OpenTitleUpdateManager}"
CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuManageTitleUpdates}"
Icon="{ext:Icon fa-solid fa-code-compare}" />
Icon="{ext:Icon fa-solid fa-diagram-predecessor}" />
<MenuItem
Command="{Binding OpenDownloadableContentManager}"
CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuManageDlc}"
Icon="{ext:Icon fa-solid fa-download}" />
Icon="{ext:Icon fa-solid fa-puzzle-piece}" />
<MenuItem
Command="{Binding OpenCheatManager}"
CommandParameter="{Binding}"
@@ -92,12 +92,12 @@
Command="{Binding OpenModsDirectory}"
CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuOpenModsDirectory}"
Icon="{ext:Icon fa-solid fa-folder}" />
Icon="{ext:Icon fa-solid fa-folder-closed}" />
<MenuItem
Command="{Binding OpenSdModsDirectory}"
CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuOpenSdModsDirectory}"
Icon="{ext:Icon fa-solid fa-folder}"
Icon="{ext:Icon fa-solid fa-folder-closed}"
ToolTip.Tip="{ext:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
<Separator />
<MenuItem
@@ -128,12 +128,12 @@
Command="{Binding OpenPtcDirectory}"
CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
Icon="{ext:Icon fa-solid fa-folder}" />
Icon="{ext:Icon fa-solid fa-folder-closed}" />
<MenuItem
Command="{Binding OpenShaderCacheDirectory}"
CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
Icon="{ext:Icon fa-solid fa-folder}" />
Icon="{ext:Icon fa-solid fa-folder-closed}" />
</MenuItem>
<MenuItem Header="{ext:Locale GameListContextMenuExtractData}" Icon="{ext:Icon fa-solid fa-file-export}">
<MenuItem

View File

@@ -1,26 +1,5 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.Input;
using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Systems.AppLibrary;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Views.Dialog;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.HLE.HOS;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.IO;
using Path = System.IO.Path;
namespace Ryujinx.Ava.UI.Controls
{

View File

@@ -0,0 +1,20 @@
using Avalonia.Data.Converters;
using CommandLine;
using Ryujinx.Ava.Common.Locale;
using System;
using System.Globalization;
namespace Ryujinx.Ava.UI.Helpers
{
public class LocaleKeyValueConverter : IValueConverter
{
private static readonly Lazy<LocaleKeyValueConverter> _shared = new(() => new());
public static LocaleKeyValueConverter Shared => _shared.Value;
public object Convert(object value, Type _, object __, CultureInfo ___)
=> LocaleManager.Instance[value.Cast<LocaleKeys>()];
public object ConvertBack(object value, Type _, object __, CultureInfo ___)
=> throw new NotSupportedException();
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

View File

@@ -2,7 +2,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System.Xml.Linq;
namespace Ryujinx.Ava.UI.Models.Input
{

View File

@@ -0,0 +1,180 @@
using Gommon;
using Humanizer;
using LibHac.Ns;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Common.Utilities;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Models
{
public record LdnGameModel
{
public string Id { get; private init; }
public bool IsPublic { get; private init; }
public short PlayerCount { get; private init; }
public short MaxPlayerCount { get; private init; }
public TitleTuple Title { get; private init; }
public ConnectionType ConnectionType { get; private init; }
public bool IsJoinable { get; private init; }
public ushort SceneId { get; private init; }
public string[] Players { get; private init; }
public string PlayersLabel =>
LocaleManager.GetFormatted(LocaleKeys.LdnGameListPlayersAndPlayerCount, PlayerCount, MaxPlayerCount);
public string FormattedPlayers =>
Players.Chunk(4)
.Select(x => x.FormatCollection(s => s, prefix: " ", separator: ", "))
.JoinToString("\n ");
public DateTimeOffset CreatedAt { get; init; }
public string FormattedCreatedAt
=> LocaleManager.GetFormatted(LocaleKeys.LdnGameListCreatedAt, CreatedAt.Humanize());
public string CreatedAtToolTip => CreatedAt.DateTime.ToString(CultureInfo.CurrentUICulture);
public LocaleKeys ConnectionTypeLocaleKey => ConnectionType switch
{
ConnectionType.MasterServerProxy => LocaleKeys.LdnGameListConnectionTypeMasterServerProxy,
ConnectionType.PeerToPeer => LocaleKeys.LdnGameListConnectionTypeP2P,
_ => throw new ArgumentOutOfRangeException(nameof(ConnectionType),
$"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{ConnectionType}'")
};
public LocaleKeys ConnectionTypeToolTipLocaleKey => ConnectionType switch
{
ConnectionType.MasterServerProxy => LocaleKeys.LdnGameListConnectionTypeMasterServerProxyToolTip,
ConnectionType.PeerToPeer => LocaleKeys.LdnGameListConnectionTypeP2PToolTip,
_ => throw new ArgumentOutOfRangeException(nameof(ConnectionType),
$"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{ConnectionType}'")
};
public record struct TitleTuple
{
public required string Name { get; init; }
public required string Id { get; init; }
public required string Version { get; init; }
}
public static Array GetArrayForApp(
LdnGameModel[] receivedData,
ref ApplicationControlProperty acp,
bool onlyJoinable = true,
bool onlyPublic = true)
{
LibHac.Common.FixedArrays.Array8<ulong> communicationId = acp.LocalCommunicationId;
return new Array(receivedData.Where(game =>
communicationId.AsReadOnlySpan().Contains(game.Title.Id.ToULong())
), onlyJoinable, onlyPublic);
}
public class Array : IEnumerable<LdnGameModel>
{
private readonly LdnGameModel[] _ldnDatas;
internal Array(IEnumerable<LdnGameModel> receivedData, bool onlyJoinable = false, bool onlyPublic = false)
{
if (onlyJoinable)
receivedData = receivedData.Where(x => x.IsJoinable);
if (onlyPublic)
receivedData = receivedData.Where(x => x.IsPublic);
_ldnDatas = receivedData.ToArray();
}
public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount);
public int GameCount => _ldnDatas.Length;
public IEnumerator<LdnGameModel> GetEnumerator() => (_ldnDatas as IEnumerable<LdnGameModel>).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _ldnDatas.GetEnumerator();
}
public static async Task<IEnumerable<LdnGameModel>> GetAllAsync(HttpClient client = null)
=> LdnGameJsonModel.ParseArray(await GetAllAsyncRequestImpl(client))
.Select(FromJson);
private static async Task<string> GetAllAsyncRequestImpl(HttpClient client = null)
{
var ldnWebHost = ConfigurationState.Instance.Multiplayer.GetLdnWebServer();
LocaleManager.Associate(LocaleKeys.LdnGameListRefreshToolTip, ldnWebHost);
try
{
if (client != null)
return await client.GetStringAsync($"https://{ldnWebHost}/api/public_games");
using HttpClient httpClient = new();
return await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
}
catch
{
return "[]";
}
}
private static LdnGameModel FromJson(LdnGameJsonModel json) =>
new()
{
Id = json.Id,
IsPublic = json.IsPublic,
PlayerCount = json.PlayerCount,
MaxPlayerCount = json.MaxPlayerCount,
Title = new TitleTuple { Name = json.TitleName, Id = json.TitleId, Version = json.TitleVersion },
ConnectionType = json.ConnectionType switch
{
"P2P" => ConnectionType.PeerToPeer,
"Master Server Proxy" => ConnectionType.MasterServerProxy,
_ => throw new ArgumentOutOfRangeException(nameof(json),
$"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{json.ConnectionType}'")
},
IsJoinable = json.Joinability is "Joinable",
SceneId = json.SceneId,
Players = json.Players,
CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(json.CreatedAtUnixTimestamp).ToLocalTime()
};
}
public class LdnGameJsonModel
{
[JsonPropertyName("id")] public string Id { get; set; }
[JsonPropertyName("is_public")] public bool IsPublic { get; set; }
[JsonPropertyName("player_count")] public short PlayerCount { get; set; }
[JsonPropertyName("max_player_count")] public short MaxPlayerCount { get; set; }
[JsonPropertyName("game_name")] public string TitleName { get; set; }
[JsonPropertyName("title_id")] public string TitleId { get; set; }
[JsonPropertyName("title_version")] public string TitleVersion { get; set; }
[JsonPropertyName("mode")] public string ConnectionType { get; set; }
[JsonPropertyName("status")] public string Joinability { get; set; }
[JsonPropertyName("scene_id")] public ushort SceneId { get; set; }
[JsonPropertyName("players")] public string[] Players { get; set; }
[JsonPropertyName("created_at")] public long CreatedAtUnixTimestamp { get; set; }
public static LdnGameJsonModel Parse(string value)
=> JsonHelper.Deserialize(value, LdnGameJsonModelSerializerContext.Default.LdnGameJsonModel);
public static LdnGameJsonModel[] ParseArray(string value)
=> JsonHelper.Deserialize(value, LdnGameJsonModelSerializerContext.Default.LdnGameJsonModelArray);
}
public enum ConnectionType
{
PeerToPeer,
MasterServerProxy
}
[JsonSerializable(typeof(LdnGameJsonModel[]))]
partial class LdnGameJsonModelSerializerContext : JsonSerializerContext;
}

View File

@@ -2,7 +2,8 @@
x:Class="Ryujinx.Ava.RyujinxApp"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sty="using:FluentAvalonia.Styling">
xmlns:sty="using:FluentAvalonia.Styling"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@@ -19,7 +20,7 @@
</Application.Styles>
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="About Ryujinx" Click="AboutRyujinx_OnClick" />
<NativeMenuItem Header="{ext:Locale MenuBarHelpAbout}" Click="AboutRyujinx_OnClick" />
</NativeMenu>
</NativeMenu.Menu>
</Application>

View File

@@ -442,7 +442,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
try
{
HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json"));
HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, SharedConstants.AmiiboTagsUrl));
if (response.IsSuccessStatusCode)
{
@@ -461,7 +461,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
try
{
HttpResponseMessage response = await _httpClient.GetAsync("https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json");
HttpResponseMessage response = await _httpClient.GetAsync(SharedConstants.AmiiboTagsUrl);
if (response.IsSuccessStatusCode)
{

View File

@@ -145,8 +145,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
}
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentEntries));
}

View File

@@ -1,7 +1,6 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Common.Locale;

View File

@@ -0,0 +1,207 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using System;
using System.Collections.Generic;
using System.Linq;
using Ryujinx.Ava.Systems.AppLibrary;
using Ryujinx.Ava.UI.Models;
using System.ComponentModel;
using System.Net.Http;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class LdnGamesListViewModel : BaseModel, IDisposable
{
public MainWindowViewModel Mwvm { get; }
private readonly HttpClient _refreshClient;
private (int PlayerCount, int Name) _sorting;
private IEnumerable<LdnGameModel> _visibleEntries;
private string[] _ownedGameTitleIds = [];
private Func<LdnGameModel, object> _sortKeySelector = x => x.Title.Name; // Default sort by Title name
public IEnumerable<LdnGameModel> VisibleEntries => ApplyFilters();
private IEnumerable<LdnGameModel> ApplyFilters()
{
if (_visibleEntries is null)
{
_visibleEntries = Mwvm.LdnModels;
SortApply();
}
var filtered = _visibleEntries;
if (OnlyShowForOwnedGames)
filtered = filtered.Where(x => _ownedGameTitleIds.ContainsIgnoreCase(x.Title.Id));
if (OnlyShowPublicGames)
filtered = filtered.Where(x => x.IsPublic);
if (OnlyShowJoinableGames)
filtered = filtered.Where(x => x.IsJoinable);
return filtered;
}
public LdnGamesListViewModel()
{
if (Program.PreviewerDetached)
{
Mwvm = RyujinxApp.MainWindow.ViewModel;
}
}
private void AppCountUpdated(object _, ApplicationCountUpdatedEventArgs __)
=> _ownedGameTitleIds = Mwvm.ApplicationLibrary.Applications.Keys.Select(x => x.ToString("X16")).ToArray();
public LdnGamesListViewModel(MainWindowViewModel mwvm)
{
if (Program.PreviewerDetached)
{
Mwvm = mwvm;
_visibleEntries = Mwvm.LdnModels;
_refreshClient = new HttpClient();
AppCountUpdated(null, null);
Mwvm.ApplicationLibrary.ApplicationCountUpdated += AppCountUpdated;
Mwvm.PropertyChanged += Mwvm_OnPropertyChanged;
}
}
void IDisposable.Dispose()
{
if (Program.PreviewerDetached)
{
_visibleEntries = null;
_refreshClient.Dispose();
Mwvm.ApplicationLibrary.ApplicationCountUpdated -= AppCountUpdated;
Mwvm.PropertyChanged -= Mwvm_OnPropertyChanged;
}
GC.SuppressFinalize(this);
}
private void Mwvm_OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(MainWindowViewModel.LdnModels))
OnPropertyChanged(nameof(VisibleEntries));
}
[ObservableProperty] private bool _isRefreshing;
private bool _onlyShowForOwnedGames;
private bool _onlyShowPublicGames = true;
private bool _onlyShowJoinableGames = true;
public async Task RefreshAsync()
{
IsRefreshing = true;
await Mwvm.ApplicationLibrary.RefreshLdn();
IsRefreshing = false;
OnPropertyChanged(nameof(VisibleEntries));
}
public bool OnlyShowForOwnedGames
{
get => _onlyShowForOwnedGames;
set
{
OnPropertyChanging();
OnPropertyChanging(nameof(VisibleEntries));
_onlyShowForOwnedGames = value;
OnPropertyChanged();
OnPropertyChanged(nameof(VisibleEntries));
}
}
public bool OnlyShowPublicGames
{
get => _onlyShowPublicGames;
set
{
OnPropertyChanging();
OnPropertyChanging(nameof(VisibleEntries));
_onlyShowPublicGames = value;
OnPropertyChanged();
OnPropertyChanged(nameof(VisibleEntries));
}
}
public bool OnlyShowJoinableGames
{
get => _onlyShowJoinableGames;
set
{
OnPropertyChanging();
OnPropertyChanging(nameof(VisibleEntries));
_onlyShowJoinableGames = value;
OnPropertyChanged();
OnPropertyChanged(nameof(VisibleEntries));
}
}
public void NameSorting(int nameSort = 0)
{
_sorting.Name = nameSort;
SortApply();
}
public void StatusSorting(int statusSort = 0)
{
_sorting.PlayerCount = statusSort;
SortApply();
}
public void Search(string searchTerm)
{
if (string.IsNullOrEmpty(searchTerm))
{
SetEntries(Mwvm.LdnModels);
SortApply();
return;
}
SetEntries(Mwvm.LdnModels.Where(x =>
x.Title.Name.ContainsIgnoreCase(searchTerm)
|| x.Title.Id.ContainsIgnoreCase(searchTerm)));
SortApply();
}
private void SetEntries(IEnumerable<LdnGameModel> entries)
{
_visibleEntries = entries.ToList();
OnPropertyChanged(nameof(VisibleEntries));
}
private void SortApply()
{
try
{
_visibleEntries = (_sorting switch
{
(0, 0) => _visibleEntries.OrderBy(x => _sortKeySelector(x) ?? string.Empty), // A - Z
(0, 1) => _visibleEntries.OrderByDescending(x => _sortKeySelector(x) ?? string.Empty), // Z - A
(1, 0) => _visibleEntries.OrderBy(x => x.PlayerCount).ThenBy(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count low - high, then A - Z
(1, 1) => _visibleEntries.OrderBy(x => x.PlayerCount).ThenByDescending(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count high - low, then A - Z
(2, 0) => _visibleEntries.OrderByDescending(x => x.PlayerCount).ThenBy(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count low - high, then Z - A
(2, 1) => _visibleEntries.OrderByDescending(x => x.PlayerCount).ThenByDescending(x => x.Title.Name, StringComparer.OrdinalIgnoreCase), // Player count high - low, then Z - A
_ => _visibleEntries.OrderBy(x => x.PlayerCount)
}).ToList();
}
catch
{
}
OnPropertyChanged(nameof(VisibleEntries));
}
}
}

View File

@@ -32,6 +32,7 @@ using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Common.UI;
@@ -57,7 +58,6 @@ using Key = Ryujinx.Input.Key;
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
using Path = System.IO.Path;
using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState;
using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
namespace Ryujinx.Ava.UI.ViewModels
{
@@ -111,6 +111,7 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] private bool _isSubMenuOpen;
[ObservableProperty] private ApplicationContextMenu _listAppContextMenu;
[ObservableProperty] private ApplicationContextMenu _gridAppContextMenu;
[ObservableProperty] private bool _isRyuLdnEnabled;
[ObservableProperty] private bool _updateAvailable;
public static AsyncRelayCommand UpdateCommand { get; } = Commands.Create(async () =>
@@ -142,7 +143,25 @@ namespace Ryujinx.Ava.UI.ViewModels
private ApplicationData _gridSelectedApplication;
// Key is Title ID
public SafeDictionary<string, LdnGameData.Array> LdnData = [];
/// <summary>
/// At any given time, this dictionary contains the filtered data from <see cref="_ldnModels"/>.
/// Filtered in this case meaning installed games only.
/// </summary>
public SafeDictionary<string, LdnGameModel.Array> UsableLdnData = [];
private LdnGameModel[] _ldnModels;
public LdnGameModel[] LdnModels
{
get => _ldnModels;
set
{
_ldnModels = value;
LocaleManager.Associate(LocaleKeys.LdnGameListTitle, value.Length);
LocaleManager.Associate(LocaleKeys.LdnGameListSearchBoxWatermark, value.Length);
OnPropertyChanged();
}
}
public MainWindow Window { get; init; }
@@ -165,11 +184,28 @@ namespace Ryujinx.Ava.UI.ViewModels
{
LoadConfigurableHotKeys();
IsRyuLdnEnabled = ConfigurationState.Instance.Multiplayer.Mode.Value is MultiplayerMode.LdnRyu;
ConfigurationState.Instance.Multiplayer.Mode.Event += OnLdnModeChanged;
Volume = ConfigurationState.Instance.System.AudioVolume;
CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value;
}
}
~MainWindowViewModel()
{
if (Program.PreviewerDetached)
{
ConfigurationState.Instance.Multiplayer.Mode.Event -= OnLdnModeChanged;
}
}
private void OnLdnModeChanged(object sender, ReactiveEventArgs<MultiplayerMode> e) =>
Dispatcher.UIThread.Post(() =>
{
IsRyuLdnEnabled = e.NewValue is MultiplayerMode.LdnRyu;
});
public void Initialize(
ContentManager contentManager,
IStorageProvider storageProvider,
@@ -313,11 +349,11 @@ namespace Ryujinx.Ava.UI.ViewModels
if (ts.HasValue)
{
var formattedPlayTime = ValueFormatUtils.FormatTimeSpan(ts.Value);
LocaleManager.Instance.SetDynamicValues(LocaleKeys.GameListLabelTotalTimePlayed, formattedPlayTime);
LocaleManager.Associate(LocaleKeys.GameListLabelTotalTimePlayed, formattedPlayTime);
ShowTotalTimePlayed = formattedPlayTime != string.Empty;
return;
}
ShowTotalTimePlayed = ts.HasValue;
}
@@ -1168,11 +1204,11 @@ namespace Ryujinx.Ava.UI.ViewModels
_rendererWaitEvent.Set();
}
private async Task LoadContentFromFolder(LocaleKeys localeMessageAddedKey, LocaleKeys localeMessageRemovedKey, LoadContentFromFolderDelegate onDirsSelected)
private async Task LoadContentFromFolder(LocaleKeys localeMessageAddedKey, LocaleKeys localeMessageRemovedKey, LoadContentFromFolderDelegate onDirsSelected, LocaleKeys dirSelectDialogTitle)
{
IReadOnlyList<IStorageFolder> result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle],
Title = LocaleManager.Instance[dirSelectDialogTitle],
AllowMultiple = true,
});
@@ -1469,7 +1505,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
IReadOnlyList<IStorageFile> result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
Title = LocaleManager.Instance[LocaleKeys.LoadApplicationFromFileDialogTitle],
AllowMultiple = false,
FileTypeFilter = new List<FilePickerFileType>
{
@@ -1545,7 +1581,8 @@ namespace Ryujinx.Ava.UI.ViewModels
await LoadContentFromFolder(
LocaleKeys.AutoloadDlcAddedMessage,
LocaleKeys.AutoloadDlcRemovedMessage,
ApplicationLibrary.AutoLoadDownloadableContents);
ApplicationLibrary.AutoLoadDownloadableContents,
LocaleKeys.LoadDLCFromFolderDialogTitle);
}
public async Task LoadTitleUpdatesFromFolder()
@@ -1553,14 +1590,15 @@ namespace Ryujinx.Ava.UI.ViewModels
await LoadContentFromFolder(
LocaleKeys.AutoloadUpdateAddedMessage,
LocaleKeys.AutoloadUpdateRemovedMessage,
ApplicationLibrary.AutoLoadTitleUpdates);
ApplicationLibrary.AutoLoadTitleUpdates,
LocaleKeys.LoadTitleUpdatesFromFolderDialogTitle);
}
public async Task OpenFolder()
{
IReadOnlyList<IStorageFolder> result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.OpenFolderDialogTitle],
Title = LocaleManager.Instance[LocaleKeys.LoadUnpackedGameFromFolderDialogTitle],
AllowMultiple = false,
});
@@ -1576,7 +1614,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool InitializeUserConfig(ApplicationData application)
public static bool InitializeUserConfig(ApplicationData application)
{
// Code where conditions will be met before loading the user configuration (Global Config)
string backendThreadingInit = Program.BackendThreadingArg ?? ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString();
@@ -1613,11 +1651,8 @@ namespace Ryujinx.Ava.UI.ViewModels
public async Task LoadApplication(ApplicationData application, bool startFullscreen = false, BlitStruct<ApplicationControlProperty>? customNacpData = null)
{
if (InitializeUserConfig(application))
{
return;
}
if (AppHost != null)
{
@@ -1665,13 +1700,9 @@ namespace Ryujinx.Ava.UI.ViewModels
CanUpdate = false;
LoadHeading = application.Name;
application.Name ??= AppHost.Device.Processes.ActiveApplication.Name;
if (string.IsNullOrWhiteSpace(application.Name))
{
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
application.Name = AppHost.Device.Processes.ActiveApplication.Name;
}
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, application.Name);
SwitchToRenderer(startFullscreen);

View File

@@ -4,7 +4,6 @@ using DynamicData.Binding;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Ryujinx.Ava.UI.ViewModels

View File

@@ -7,6 +7,7 @@
mc:Ignorable="d"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common"
x:DataType="viewModels:MainWindowViewModel"
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView">
<Design.DataContext>
@@ -38,36 +39,29 @@
Header="{ext:Locale MenuBarFileOpenUnpacked}"
Icon="{ext:Icon fa-solid fa-folder-open}"
IsEnabled="{Binding EnableNonGameRunningControls}" />
<MenuItem
Command="{Binding LoadDlcFromFolder}"
Header="{ext:Locale MenuBarFileLoadDlcFromFolder}"
Icon="{ext:Icon fa-solid fa-download}"
IsEnabled="{Binding EnableNonGameRunningControls}" />
<MenuItem
Command="{Binding LoadTitleUpdatesFromFolder}"
Header="{ext:Locale MenuBarFileLoadTitleUpdatesFromFolder}"
Icon="{ext:Icon fa-solid fa-code-compare}"
Icon="{ext:Icon fa-solid fa-diagram-predecessor}"
IsEnabled="{Binding EnableNonGameRunningControls}" />
<MenuItem
Command="{Binding LoadDlcFromFolder}"
Header="{ext:Locale MenuBarFileLoadDlcFromFolder}"
Icon="{ext:Icon fa-solid fa-puzzle-piece}"
IsEnabled="{Binding EnableNonGameRunningControls}" />
<MenuItem Header="{ext:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}" Icon="{ext:Icon fa-solid fa-microchip}">
<MenuItem
Name="MiiAppletMenuItem"
Header="{ext:Locale MenuBarFileOpenAppletOpenMiiApplet}"
Icon="{ext:Icon fa-solid fa-person}"
ToolTip.Tip="{ext:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
</MenuItem>
<Separator />
<MenuItem
Command="{Binding OpenRyujinxFolder}"
Header="{ext:Locale MenuBarFileOpenEmuFolder}"
Icon="{ext:Icon fa-solid fa-folder-closed}" />
<MenuItem
Command="{Binding OpenScreenshotsFolder}"
Header="{ext:Locale MenuBarFileOpenScreenshotsFolder}"
Icon="{ext:Icon fa-solid fa-desktop}" />
<MenuItem
Command="{Binding OpenLogsFolder}"
Header="{ext:Locale MenuBarFileOpenLogsFolder}"
Icon="{ext:Icon fa-solid fa-file-lines}" />
Icon="{ext:Icon fa-solid fa-terminal}" />
<MenuItem
Command="{Binding OpenScreenshotsFolder}"
Header="{ext:Locale MenuBarFileOpenScreenshotsFolder}"
Icon="{ext:Icon fa-solid fa-image}" />
<Separator />
<MenuItem
Name="CloseRyujinxMenuItem"
@@ -131,11 +125,6 @@
Icon="{ext:Icon fa-solid fa-globe}"
Classes="withCheckbox">
</MenuItem>
<MenuItem
Name="ToggleFileTypesMenuItem"
Padding="-10,0,0,0"
Header="{ext:Locale MenuBarShowFileTypes}" />
<Separator />
<MenuItem
Name="OpenSettingsMenuItem"
Padding="0"
@@ -221,20 +210,25 @@
<MenuItem Command="{Binding InstallFirmwareFromFile}" Header="{ext:Locale MenuBarActionsInstallFirmwareFromFile}" Icon="{ext:Icon fa-solid fa-file-code}" />
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarActionsInstallFirmwareFromDirectory}" Icon="{ext:Icon fa-solid fa-folder-closed}" />
</MenuItem>
<MenuItem Header="{ext:Locale MenuBarActionsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}">
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" />
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" />
<MenuItem Header="{ext:Locale MenuBarActionsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}" Icon="{ext:Icon fa-solid fa-clipboard}">
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" Icon="{ext:Icon fa-solid fa-square-plus}" />
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" Icon="{ext:Icon fa-solid fa-square-minus}" />
</MenuItem>
<Separator />
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBarActionsXCITrimmer}" Icon="{ext:Icon fa-solid fa-scissors}" />
<MenuItem Header="{ext:Locale MenuBarActionsTools}" Icon="{ext:Icon fa-solid fa-toolbox}">
<MenuItem
Name="MiiAppletMenuItem" Header="{ext:Locale MenuBarActionsOpenMiiEditor}" Icon="{ext:Icon fa-solid fa-face-grin-wide}" ToolTip.Tip="{ext:Locale MenuBarActionsOpenMiiEditorToolTip}" />
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBarActionsXCITrimmer}" Icon="{ext:Icon fa-solid fa-scissors}" />
</MenuItem>
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}">
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}">
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}" Icon="{ext:Icon fa-solid fa-window-restore}">
<MenuItem Name="WindowSize720PMenuItem" Header="{ext:Locale MenuBarViewWindow720}" CommandParameter="1280 720" />
<MenuItem Name="WindowSize1080PMenuItem" Header="{ext:Locale MenuBarViewWindow1080}" CommandParameter="1920 1080" />
<MenuItem Name="WindowSize1440PMenuItem" Header="{ext:Locale MenuBarViewWindow1440}" CommandParameter="2560 1440" />
<MenuItem Name="WindowSize2160PMenuItem" Header="{ext:Locale MenuBarViewWindow2160}" CommandParameter="3840 2160" />
</MenuItem>
<MenuItem Name="ToggleFileTypesMenuItem" Header="{ext:Locale MenuBarShowFileTypes}" Icon="{ext:Icon fa-solid fa-tags}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelp}">
<MenuItem
@@ -250,26 +244,31 @@
Name="CompatibilityListMenuItem"
Header="{ext:Locale CompatibilityListOpen}"
Icon="{ext:Icon fa-solid fa-database}"/>
<MenuItem
Name="LdnGameListMenuItem"
Header="{ext:Locale LdnGameListOpen}"
Icon="{ext:Icon fa-solid fa-people-group}"
IsEnabled="{Binding IsRyuLdnEnabled}"/>
<Separator />
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" >
<MenuItem
Name="FaqMenuItem"
Header="{ext:Locale MenuBarHelpFaq}"
Icon="{ext:Icon fa-brands fa-gitlab}"
CommandParameter="https://git.ryujinx.app/ryubing/ryujinx/-/wikis/FAQ-&amp;-Troubleshooting"
ToolTip.Tip="{ext:Locale MenuBarHelpFaqTooltip}" />
<MenuItem
Name="SetupGuideMenuItem"
Header="{ext:Locale MenuBarHelpSetup}"
Icon="{ext:Icon fa-brands fa-gitlab}"
CommandParameter="https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&amp;-Configuration-Guide"
CommandParameter="{x:Static common:SharedConstants.SetupGuideWikiUrl}"
ToolTip.Tip="{ext:Locale MenuBarHelpSetupTooltip}" />
<MenuItem
Name="LdnGuideMenuItem"
Header="{ext:Locale MenuBarHelpMultiplayer}"
Icon="{ext:Icon fa-brands fa-gitlab}"
CommandParameter="https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide"
CommandParameter="{x:Static common:SharedConstants.MultiplayerWikiUrl}"
ToolTip.Tip="{ext:Locale MenuBarHelpMultiplayerTooltip}" />
<MenuItem
Name="FaqMenuItem"
Header="{ext:Locale MenuBarHelpFaq}"
Icon="{ext:Icon fa-brands fa-gitlab}"
CommandParameter="{x:Static common:SharedConstants.FaqWikiUrl}"
ToolTip.Tip="{ext:Locale MenuBarHelpFaqTooltip}" />
</MenuItem>
</MenuItem>
</Menu>

View File

@@ -38,7 +38,7 @@ namespace Ryujinx.Ava.UI.Views.Main
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
MiiAppletMenuItem.Command = Commands.Create(OpenMiiApplet);
CloseRyujinxMenuItem.Command = Commands.Create(CloseWindow);
CloseRyujinxMenuItem.Command = Commands.Create(() => Window?.Close());
OpenSettingsMenuItem.Command = Commands.Create(OpenSettings);
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
@@ -49,6 +49,7 @@ namespace Ryujinx.Ava.UI.Views.Main
XciTrimmerMenuItem.Command = Commands.Create(XciTrimmerView.Show);
AboutWindowMenuItem.Command = Commands.Create(AboutView.Show);
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityListWindow.Show());
LdnGameListMenuItem.Command = Commands.Create(() => LdnGamesListWindow.Show());
UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand;
@@ -60,6 +61,14 @@ namespace Ryujinx.Ava.UI.Views.Main
WindowSize1080PMenuItem.Command =
WindowSize1440PMenuItem.Command =
WindowSize2160PMenuItem.Command = Commands.Create<string>(ChangeWindowSize);
LocaleManager.Instance.LocaleChanged += OnLocaleChanged;
}
private void OnLocaleChanged()
{
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
Menu.Close();
}
private IEnumerable<CheckBox> GenerateToggleFileTypeItems() =>
@@ -79,6 +88,7 @@ namespace Ryujinx.Ava.UI.Views.Main
const string LocalePath = "Ryujinx/Assets/Locale.json";
string languageJson = EmbeddedResources.ReadAllText(LocalePath);
string currentLanguageCode = LocaleManager.Instance.CurrentLanguageCode;
LocalesJson locales = JsonHelper.Deserialize(languageJson, LocalesJsonContext.Default.LocalesJson);
@@ -104,7 +114,7 @@ namespace Ryujinx.Ava.UI.Views.Main
Padding = new Thickness(15, 0, 0, 0),
Margin = new Thickness(3, 0, 3, 0),
HorizontalAlignment = HorizontalAlignment.Stretch,
Header = languageName,
Header = language == currentLanguageCode ? $"{languageName} ✔" : languageName,
Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(language))
};
@@ -235,8 +245,5 @@ namespace Ryujinx.Ava.UI.Views.Main
Window.Arrange(new Rect(Window.Position.X, Window.Position.Y, windowWidthScaled, windowHeightScaled));
});
}
public void CloseWindow() => Window.Close();
}
}

View File

@@ -148,12 +148,27 @@
Text="{Binding FileExtension}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
<Button
HorizontalContentAlignment="Left"
Click="LdnGames_OnClick"
VerticalAlignment="Center"
IsVisible="{Binding HasLdnGames}"
Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}"
TextAlignment="Start"
TextWrapping="Wrap"/>
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
HorizontalAlignment="Stretch"
Tag="{Binding IdString}"
Text="{Binding Converter={x:Static helpers:MultiplayerInfoConverter.Instance}}"
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>
<TextBlock
HorizontalAlignment="Stretch"
IsVisible="{Binding HasIndependentConfiguration}"

View File

@@ -38,6 +38,14 @@ namespace Ryujinx.Ava.UI.Views.Misc
await CompatibilityListWindow.Show((string)playabilityLabel.Tag);
}
private async void LdnGames_OnClick(object sender, RoutedEventArgs e)
{
if (sender is not Button { Content: TextBlock ldnGamesLabel })
return;
await LdnGamesListWindow.Show((string)ldnGamesLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{

View File

@@ -51,7 +51,7 @@ namespace Ryujinx.Ava.UI.Windows
if (DataContext is not CompatibilityViewModel cvm)
return;
cvm.NameSorting(int.Parse(sortStrategy));
cvm.NameSorting(int.Parse(sortStrategy));
}
}
@@ -65,6 +65,5 @@ namespace Ryujinx.Ava.UI.Windows
cvm.StatusSorting(int.Parse(sortStrategy));
}
}
}
}

View File

@@ -0,0 +1,349 @@
<window:StyleableAppWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:facontrols="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
CanResize="False"
mc:Ignorable="d"
MinWidth="800"
MinHeight="745"
x:Class="Ryujinx.Ava.UI.Windows.LdnGamesListWindow"
x:DataType="viewModels:LdnGamesListViewModel">
<window:StyleableAppWindow.DataContext>
<viewModels:LdnGamesListViewModel />
</window:StyleableAppWindow.DataContext>
<Grid RowDefinitions="Auto,*">
<!-- UI FlushControls -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto" Name="FlushControls">
<controls:RyujinxLogo
Grid.Column="0"
Margin="15, 0, 7, 0"
ToolTip.Tip="{ext:WindowTitle LdnGameListTitle, False}"/>
<TextBox Grid.Column="1"
Name="SearchBoxFlush"
Margin="0, 5, 0, 5"
HorizontalAlignment="Stretch"
Watermark="{ext:Locale LdnGameListSearchBoxWatermark}"
TextChanged="TextBox_OnTextChanged"/>
<Button
Grid.Column="2"
Name="InfoFlush"
Margin="10, 5, 0, 5"
MinWidth="32"
MinHeight="32"
ClipToBounds="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{ext:Icon fa-solid fa-info}"
ToolTip.Tip="{ext:Locale LdnGameListInfoButtonToolTip}"/>
<Button
Grid.Column="3"
Name="RefreshFlush"
Margin="10, 5, 0, 5"
MinWidth="32"
MinHeight="32"
ClipToBounds="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsEnabled="{Binding !IsRefreshing}"
ToolTip.Tip="{ext:Locale LdnGameListRefreshToolTip}">
<facontrols:SymbolIcon Symbol="Refresh" />
</Button>
<StackPanel Grid.Column="4" Orientation="Horizontal" Margin="10, 5, 0, 5">
<DropDownButton
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{ext:Locale CommonSort}"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel
Margin="0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel>
<RadioButton
IsCheckedChanged="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameAscending}"
GroupName="Sort"
IsChecked="True"
Tag="0" />
<RadioButton
IsCheckedChanged="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameDescending}"
GroupName="Sort"
Tag="1" />
</StackPanel>
<Border
Width="60"
Height="2"
Margin="5"
HorizontalAlignment="Stretch"
BorderBrush="White"
BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortDisable}"
GroupName="Order"
IsChecked="true"
Tag="0" />
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortAscending}"
GroupName="Order"
Tag="1" />
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortDescending}"
GroupName="Order"
Tag="2" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
<DropDownButton
Grid.Column="5"
Margin="10, 0, 148, 0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{ext:Locale LdnGameListFiltersHeading}"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel>
<CheckBox IsChecked="{Binding OnlyShowForOwnedGames}">
<TextBlock Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</CheckBox>
<CheckBox IsChecked="{Binding OnlyShowPublicGames}">
<TextBlock Text="{ext:Locale LdnGameListFiltersOnlyShowPublicGames}" />
</CheckBox>
<CheckBox IsChecked="{Binding OnlyShowJoinableGames}">
<TextBlock Text="{ext:Locale LdnGameListFiltersOnlyShowJoinableGames}" />
</CheckBox>
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</Grid>
<!-- UI NormalControls -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Name="NormalControls">
<TextBox Name="SearchBoxNormal" Grid.Column="0" Margin="20, 5, 0, 5" HorizontalAlignment="Stretch"
Watermark="{ext:Locale LdnGameListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
<Button
Grid.Column="1"
Name="InfoNormal"
Margin="10, 5, 0, 5"
MinWidth="32"
MinHeight="32"
ClipToBounds="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{ext:Icon fa-solid fa-info}"
ToolTip.Tip="{ext:Locale LdnGameListInfoButtonToolTip}"/>
<Button Grid.Column="2" Name="RefreshNormal" Margin="10, 5, 0, 5" MinWidth="32" MinHeight="32" ClipToBounds="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" IsEnabled="{Binding !IsRefreshing}">
<facontrols:SymbolIcon Symbol="Refresh" />
</Button>
<StackPanel Grid.Column="3" Orientation="Horizontal" Margin="10, 5, 0, 5">
<DropDownButton
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{ext:Locale CommonSort}"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel
Margin="0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel>
<RadioButton
IsCheckedChanged="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameAscending}"
GroupName="Sort"
IsChecked="True"
Tag="0" />
<RadioButton
IsCheckedChanged="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameDescending}"
GroupName="Sort"
Tag="1" />
</StackPanel>
<Border
Width="60"
Height="2"
Margin="5"
HorizontalAlignment="Stretch"
BorderBrush="White"
BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortDisable}"
GroupName="Order"
IsChecked="true"
Tag="0" />
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortAscending}"
GroupName="Order"
Tag="1" />
<RadioButton
IsCheckedChanged="Sort_PlayerCount_Checked"
Content="{ext:Locale LdnGameListPlayerSortDescending}"
GroupName="Order"
Tag="2" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
<DropDownButton
Grid.Column="4"
Margin="10, 5, 20, 5"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{ext:Locale LdnGameListFiltersHeading}"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel>
<CheckBox IsChecked="{Binding OnlyShowForOwnedGames}">
<TextBlock Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</CheckBox>
<CheckBox IsChecked="{Binding OnlyShowPublicGames}">
<TextBlock Text="{ext:Locale LdnGameListFiltersOnlyShowPublicGames}" />
</CheckBox>
<CheckBox IsChecked="{Binding OnlyShowJoinableGames}">
<TextBlock Text="{ext:Locale LdnGameListFiltersOnlyShowJoinableGames}" />
</CheckBox>
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</Grid>
<!-- List of open LDN games -->
<ScrollViewer Grid.Row="1">
<ListBox Margin="12, 0, 13, 0"
Background="Transparent"
ItemsSource="{Binding VisibleEntries}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel
ItemHeight="125"
ItemWidth="450"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:LdnGameModel}">
<Border
Margin="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="4"
Background="Transparent">
<Grid ColumnDefinitions="Auto,Auto,*" RowDefinitions="Auto,Auto,Auto,*" Width="420" Height="110" HorizontalAlignment="Center">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{Binding Title.Name}"
Margin="7, 0,0, 0"
Width="250"
ToolTip.Tip="{Binding Title.Id}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
ClipToBounds="False"
TextAlignment="Left"
TextWrapping="Wrap" />
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{Binding Title.Version}"
Margin="7, 0,0, 0"
Width="250"
ToolTip.Tip="{Binding Title.Id}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
ClipToBounds="False"
TextAlignment="Left"/>
<TextBlock Grid.Row="2" Grid.Column="0"
Text="{Binding FormattedCreatedAt}"
ToolTip.Tip="{Binding CreatedAtToolTip}"
Margin="7, 0,0, 0"
Width="250"
VerticalAlignment="Center"
HorizontalAlignment="Left"
ClipToBounds="False"
TextAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="2"
Margin="0, 0,7, 0"
IsVisible="{Binding IsJoinable}"
Text="{ext:Locale LdnGameListJoinable}"
ToolTip.Tip="{ext:Locale LdnGameListJoinableToolTip}"
Foreground="MediumSeaGreen"
VerticalAlignment="Center"
HorizontalAlignment="Right"
ClipToBounds="False"
TextAlignment="Right" />
<TextBlock Grid.Row="0" Grid.Column="2"
Margin="0, 0,7, 0"
IsVisible="{Binding !IsJoinable}"
Text="{ext:Locale LdnGameListNotJoinable}"
ToolTip.Tip="{ext:Locale LdnGameListNotJoinableToolTip}"
Foreground="IndianRed"
TextDecorations="Underline"
VerticalAlignment="Center"
HorizontalAlignment="Right"
ClipToBounds="False"
TextAlignment="Right" />
<TextBlock Grid.Row="1" Grid.Column="2"
Margin="0, 0,7, 0"
IsVisible="{Binding IsPublic}"
Text="{ext:Locale LdnGameListPublic}"
ToolTip.Tip="{ext:Locale LdnGameListPublicToolTip}"
Foreground="LawnGreen"
VerticalAlignment="Center"
HorizontalAlignment="Right"
ClipToBounds="False"
TextAlignment="Right" />
<TextBlock Grid.Row="1" Grid.Column="2"
Margin="0, 0,7, 0"
IsVisible="{Binding !IsPublic}"
Text="{ext:Locale LdnGameListPrivate}"
ToolTip.Tip="{ext:Locale LdnGameListPrivateToolTip}"
Foreground="DarkRed"
VerticalAlignment="Center"
HorizontalAlignment="Right"
ClipToBounds="False"
TextAlignment="Right" />
<TextBlock Grid.Row="2" Grid.Column="2"
Margin="0, 0,7, 0"
Text="{Binding ConnectionTypeLocaleKey, Converter={x:Static helpers:LocaleKeyValueConverter.Shared}}"
ToolTip.Tip="{Binding ConnectionTypeToolTipLocaleKey, Converter={x:Static helpers:LocaleKeyValueConverter.Shared}}"
VerticalAlignment="Center"
HorizontalAlignment="Right"
TextWrapping="NoWrap"
ClipToBounds="False"
TextAlignment="Right" />
<StackPanel Margin="7, 0,0, 0" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="{Binding PlayersLabel}" TextAlignment="Left" />
<TextBlock Text="{Binding FormattedPlayers}" TextAlignment="Center" TextWrapping="Wrap"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Grid>
</window:StyleableAppWindow>

View File

@@ -0,0 +1,79 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Windows
{
public partial class LdnGamesListWindow : StyleableAppWindow
{
public static async Task Show(string searchTerm = null)
{
using LdnGamesListViewModel ldnGamesListVm = new(RyujinxApp.MainWindow.ViewModel);
await ShowAsync(new LdnGamesListWindow
{
DataContext = ldnGamesListVm,
SearchBoxFlush = { Text = searchTerm ?? string.Empty },
SearchBoxNormal = { Text = searchTerm ?? string.Empty }
});
}
public LdnGamesListWindow() : base(useCustomTitleBar: true, 37)
{
Title = RyujinxApp.FormatTitle(LocaleKeys.LdnGameListTitle);
InitializeComponent();
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
NormalControls.IsVisible = ConfigurationState.Instance.ShowOldUI;
RefreshFlush.Command = RefreshNormal.Command =
Commands.Create(() => (DataContext as LdnGamesListViewModel)?.RefreshAsync().OrCompleted());
InfoFlush.Command = InfoNormal.Command =
Commands.Create(() => OpenHelper.OpenUrl(SharedConstants.MultiplayerWikiUrl));
}
// ReSharper disable once UnusedMember.Local
// its referenced in the axaml but rider keeps yelling at me that its unused so
private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e)
{
if (DataContext is not LdnGamesListViewModel cvm)
return;
if (sender is not TextBox searchBox)
return;
cvm.Search(searchBox.Text);
}
public void Sort_Name_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton { Tag: string sortStrategy })
{
if (DataContext is not LdnGamesListViewModel cvm)
return;
cvm.NameSorting(int.Parse(sortStrategy));
}
}
public void Sort_PlayerCount_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton { Tag: string sortStrategy })
{
if (DataContext is not LdnGamesListViewModel cvm)
return;
cvm.StatusSorting(int.Parse(sortStrategy));
}
}
}
}

View File

@@ -8,7 +8,6 @@ using Avalonia.Threading;
using DynamicData;
using FluentAvalonia.UI.Controls;
using Gommon;
using LibHac.Ns;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
@@ -18,6 +17,7 @@ using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.Configuration.UI;
using Ryujinx.Ava.UI.Applet;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
@@ -185,12 +185,11 @@ namespace Ryujinx.Ava.UI.Windows
{
Dispatcher.UIThread.Post(() =>
{
ViewModel.LdnData.Clear();
ViewModel.LdnModels = e.LdnData;
ViewModel.UsableLdnData.Clear();
foreach (ApplicationData application in ViewModel.Applications.Where(it => it.HasControlHolder))
{
ref ApplicationControlProperty controlHolder = ref application.ControlHolder.Value;
ViewModel.LdnData[application.IdString] = e.LdnData.Where(ref controlHolder);
ViewModel.UsableLdnData[application.IdString] = LdnGameModel.GetArrayForApp(e.LdnData, ref application.ControlHolder.Value);
UpdateApplicationWithLdnData(application);
}
@@ -201,7 +200,7 @@ namespace Ryujinx.Ava.UI.Windows
private void UpdateApplicationWithLdnData(ApplicationData application)
{
if (application.HasControlHolder && ViewModel.LdnData.TryGetValue(application.IdString, out LdnGameData.Array ldnGameDatas))
if (application.HasControlHolder && ViewModel.UsableLdnData.TryGetValue(application.IdString, out LdnGameModel.Array ldnGameDatas))
{
application.PlayerCount = ldnGameDatas.PlayerCount;
application.GameCount = ldnGameDatas.GameCount;